├── configure.win ├── .github ├── .gitignore ├── dependabot.yaml ├── ISSUE_TEMPLATE │ ├── other.md │ ├── bug_report.md │ └── feature-request.md └── workflows │ ├── pkgdown.yaml │ ├── R-CMD-check.yaml │ ├── test-coverage.yaml │ └── rhub.yaml ├── .covrignore ├── src ├── .gitignore ├── unigd_version.h ├── sysdep_tests │ ├── sysdep_cairo.c │ └── sysdep_libtiff.cpp ├── Makevars.in ├── uuid.h ├── base_64.h ├── compress.h ├── debug_print.h ├── unigd_commons.h ├── renderer_meta.h ├── renderer_meta.cpp ├── plot_history.h ├── Makevars.win ├── geom.h ├── uuid.cpp ├── renderer_tikz.h ├── Makevars.ucrt ├── renderer_json.h ├── renderer_strings.h ├── renderers.h ├── r_thread.h ├── renderer_strings.cpp ├── unigd_external.h ├── r_thread_posix.cpp ├── compress.cpp ├── plot_history.cpp ├── page_store.h ├── renderer_svg.h ├── renderer_cairo.h ├── async_utils.h ├── draw_data.cpp ├── r_thread_win32.cpp ├── renderers.cpp ├── base_64.cpp ├── unigd_dev.h ├── cpp11.cpp ├── lib │ └── svglite_utils.h ├── renderer_json.cpp ├── unigd.cpp ├── unigd_external.cpp ├── draw_data.h └── page_store.cpp ├── vignettes ├── .gitignore ├── a00_installation.Rmd └── b00_guide.Rmd ├── cleanup ├── man ├── figures │ └── logo.png ├── ugd_test_pattern.Rd ├── ugd_clear.Rd ├── ugd_info.Rd ├── ugd_renderers.Rd ├── ugd_close.Rd ├── ugd_remove.Rd ├── ugd_state.Rd ├── ugd_render.Rd ├── ugd_id.Rd ├── ugd_save.Rd ├── ugd_render_inline.Rd ├── ugd_save_inline.Rd ├── ugd.Rd └── unigd-package.Rd ├── _pkgdown.yml ├── .clang-format ├── tests ├── testthat │ ├── test-device.R │ ├── test-svglite-raster.R │ ├── test-png.R │ ├── test-tiff.R │ ├── test-svg.R │ ├── test-svglite-colour.R │ ├── helper-svglite.R │ ├── test-svglite-clip.R │ ├── test-svglite-rect.R │ ├── test-svglite-output.R │ ├── test-svglite-scale.R │ ├── test-svglite-devSVG.R │ ├── test-svglite-points.R │ ├── test-svglite-path.R │ ├── test-history.R │ ├── test-svglite-text.R │ ├── test-svglite-text-fonts.R │ └── test-svglite-lines.R └── testthat.R ├── .gitignore ├── codecov.yml ├── .Rbuildignore ├── tools ├── winlibs.R └── wincairo.R ├── valgrind.dockerfile ├── R ├── unigd-package.R ├── cpp11.R ├── utils.R ├── fonts.R └── testgraphic.R ├── unigd.Rproj ├── NAMESPACE ├── cran-comments.md ├── NEWS.md ├── inst ├── include │ ├── unigd_api.h │ ├── unigd_api_v1.h │ └── fmt │ │ └── ostream.h └── licenses │ └── fmt-MIT.txt ├── README.md ├── DESCRIPTION └── configure /configure.win: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.covrignore: -------------------------------------------------------------------------------- 1 | src/lib 2 | inst/include 3 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /cleanup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -f src/Makevars configure.log -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nx10/unigd/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://nx10.github.io/unigd/ 2 | template: 3 | bootstrap: 5 4 | 5 | -------------------------------------------------------------------------------- /src/unigd_version.h: -------------------------------------------------------------------------------- 1 | #ifndef UNIGD_VERSION 2 | #define UNIGD_VERSION "0.1.0.9000" 3 | #endif -------------------------------------------------------------------------------- /src/sysdep_tests/sysdep_cairo.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | int main() { return 0; } -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | BreakBeforeBraces: Allman 4 | ColumnLimit: '90' 5 | SortIncludes: 'true' 6 | 7 | ... 8 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /tests/testthat/test-device.R: -------------------------------------------------------------------------------- 1 | test_that("Device capabilities are reported", { 2 | ugd() 3 | expect_error(dev.capabilities(), regexp = NA) # Expect no error 4 | dev.off() 5 | }) 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: Something other than a bug report or feature request 4 | title: '' 5 | labels: 6 | assignees: '' 7 | 8 | --- 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .Rdata 4 | .httr-oauth 5 | .DS_Store 6 | .vscode 7 | .idea 8 | CMakeLists.txt 9 | cmake-build-debug 10 | compile_commands.json 11 | windows 12 | docs 13 | inst/doc 14 | -------------------------------------------------------------------------------- /src/sysdep_tests/sysdep_libtiff.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | int main() { 4 | std::ostringstream tiff_ostream; 5 | TIFF *tiff = TIFFStreamOpen("memory", &tiff_ostream); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/Makevars.in: -------------------------------------------------------------------------------- 1 | PKG_CPPFLAGS = @cflags@ -Ilib -I../inst/include -DFMT_HEADER_ONLY 2 | 3 | #PKG_CXXFLAGS=$(C_VISIBILITY) 4 | 5 | PKG_LIBS = @libs@ -lpng -lz 6 | 7 | all: clean 8 | 9 | clean: 10 | rm -f $(SHLIB) $(OBJECTS) 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-raster.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("raster exists", { 3 | x <- xmlSVG({ 4 | image(matrix(runif(64), nrow = 8), useRaster = TRUE) 5 | }) 6 | ns <- xml2::xml_ns(x) 7 | 8 | img <- xml2::xml_attr(xml2::xml_find_all(x, ".//d1:image", ns = ns), "xlink:href", ns = ns) 9 | expect_gt(nchar(img), 1000) 10 | }) -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^unigd\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.github$ 4 | ^\.vscode$ 5 | ^\.idea$ 6 | ^CMakeLists.txt$ 7 | ^cmake-build-debug$ 8 | ^windows$ 9 | ^codecov\.yml$ 10 | ^_pkgdown\.yml$ 11 | ^docs$ 12 | ^pkgdown$ 13 | ^\.covrignore$ 14 | ^.clang-format$ 15 | ^valgrind.dockerfile$ 16 | ^CRAN-SUBMISSION$ 17 | ^cran-comments\.md$ 18 | -------------------------------------------------------------------------------- /src/uuid.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_UUID_H__ 2 | #define __UNIGD_UUID_H__ 3 | 4 | #include 5 | 6 | namespace unigd 7 | { 8 | // Can not use R's RNG for this for security reasons. 9 | // (Seed could be predicted) 10 | namespace uuid 11 | { 12 | std::string uuid(); 13 | } // namespace uuid 14 | } // namespace unigd 15 | 16 | #endif /* __UNIGD_UUID_H__ */ 17 | -------------------------------------------------------------------------------- /tests/testthat/test-png.R: -------------------------------------------------------------------------------- 1 | test_that("PNG file signature", { 2 | skip_if_not("png" %in% ugd_renderers()$id, "PNG renderer not installed") 3 | 4 | png_magic <- as.raw(c(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A)) 5 | ugd_magic <- ugd_render_inline({ 6 | plot(1) 7 | }, as = "png")[seq_along(png_magic)] 8 | 9 | expect_equal(png_magic, ugd_magic) 10 | }) 11 | -------------------------------------------------------------------------------- /src/base_64.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_BASE_64_H__ 2 | #define __UNIGD_BASE_64_H__ 3 | 4 | #include 5 | 6 | #include "draw_data.h" 7 | 8 | namespace unigd 9 | { 10 | std::string base64_encode(const std::uint8_t *buffer, size_t size); 11 | std::string raster_base64(const renderers::Raster &t_raster); 12 | 13 | } // namespace unigd 14 | 15 | #endif /* __UNIGD_BASE_64_H__ */ 16 | -------------------------------------------------------------------------------- /tools/winlibs.R: -------------------------------------------------------------------------------- 1 | VERSION <- commandArgs(TRUE) 2 | if(!file.exists(sprintf("../windows/harfbuzz-%s/include/png.h", VERSION))){ 3 | if(getRversion() < "3.3.0") setInternet2() 4 | download.file(sprintf("https://github.com/rwinlib/harfbuzz/archive/v%s.zip", VERSION), "lib.zip", quiet = TRUE) 5 | dir.create("../windows", showWarnings = FALSE) 6 | unzip("lib.zip", exdir = "../windows") 7 | unlink("lib.zip") 8 | } 9 | -------------------------------------------------------------------------------- /valgrind.dockerfile: -------------------------------------------------------------------------------- 1 | FROM rocker/r-devel-san:latest 2 | 3 | WORKDIR /app 4 | COPY . . 5 | 6 | RUN R -e "install.packages('pak', repos = 'http://cran.us.r-project.org')" 7 | RUN apt-get install -y --no-install-recommends libxml2-dev libssl-dev 8 | RUN R -e "pak::pkg_install('.', dependencies=TRUE)" 9 | 10 | ENTRYPOINT ["/bin/sh"] 11 | 12 | # Use this: cd tests && R -d "valgrind --leak-check=full" -f testthat.R -------------------------------------------------------------------------------- /tools/wincairo.R: -------------------------------------------------------------------------------- 1 | VERSION <- commandArgs(TRUE) 2 | if(!file.exists(sprintf("../windows/cairo-%s/include/cairo/cairo.h", VERSION))){ 3 | if(getRversion() < "3.3.0") setInternet2() 4 | download.file(sprintf("https://github.com/rwinlib/cairo/archive/v%s.zip", VERSION), "libcairo.zip", quiet = TRUE) 5 | dir.create("../windows", showWarnings = FALSE) 6 | unzip("libcairo.zip", exdir = "../windows") 7 | unlink("libcairo.zip") 8 | } -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/tests.html 7 | # * https://testthat.r-lib.org/reference/test_package.html#special-files 8 | 9 | library(testthat) 10 | library(unigd) 11 | 12 | test_check("unigd") 13 | -------------------------------------------------------------------------------- /src/compress.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_COMPRESS_H__ 2 | #define __UNIGD_COMPRESS_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace unigd 9 | { 10 | namespace compr 11 | { 12 | std::vector compress(const uint8_t *input, size_t input_size); 13 | 14 | std::vector compress_str(const std::string &s); 15 | 16 | } // namespace compr 17 | } // namespace unigd 18 | 19 | #endif /* __UNIGD_COMPRESS_H__ */ 20 | -------------------------------------------------------------------------------- /R/unigd-package.R: -------------------------------------------------------------------------------- 1 | #' unigd: Universal graphics device 2 | #' 3 | #' Universal graphics device 4 | #' 5 | #' @name unigd-package 6 | #' @useDynLib unigd, .registration=TRUE 7 | "_PACKAGE" 8 | 9 | .onLoad <- function(libname, pkgname) { 10 | unigd_ipc_open_() 11 | } 12 | 13 | #' @importFrom grDevices dev.list dev.off 14 | .onUnload <- function (libpath) { 15 | ugd_close(all = TRUE) 16 | unigd_ipc_close_() 17 | library.dynam.unload("unigd", libpath) 18 | } 19 | -------------------------------------------------------------------------------- /src/debug_print.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_DEBUG_PRINT_H__ 2 | #define __UNIGD_DEBUG_PRINT_H__ 3 | 4 | #ifndef UNIGD_DEBUG 5 | #define debug_print(...) 6 | #define debug_println(...) 7 | #else 8 | #define debug_print(fmt, ...) \ 9 | Rprintf("%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__); 10 | #define debug_println(fmt, ...) \ 11 | Rprintf("%s:%d:%s(): " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__); 12 | #endif 13 | 14 | #endif /* __UNIGD_DEBUG_PRINT_H__ */ 15 | -------------------------------------------------------------------------------- /src/unigd_commons.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef __UNIGD_UNIGD_COMMONS_H__ 3 | #define __UNIGD_UNIGD_COMMONS_H__ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace unigd 10 | { 11 | // safely increases numbers (wraps to 0) 12 | template 13 | T incwrap(T t_value) 14 | { 15 | T v = t_value; 16 | if (v == std::numeric_limits::max()) 17 | { 18 | return static_cast(0); 19 | } 20 | return v + 1; 21 | } 22 | 23 | } // namespace unigd 24 | 25 | #endif /* __UNIGD_UNIGD_COMMONS_H__ */ 26 | -------------------------------------------------------------------------------- /man/ugd_test_pattern.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testgraphic.R 3 | \name{ugd_test_pattern} 4 | \alias{ugd_test_pattern} 5 | \title{Plot a test pattern that can be used to evaluate and compare graphics 6 | devices.} 7 | \usage{ 8 | ugd_test_pattern() 9 | } 10 | \value{ 11 | Nothing, but a plot is generated. 12 | } 13 | \description{ 14 | Plot a test pattern that can be used to evaluate and compare graphics 15 | devices. 16 | } 17 | \examples{ 18 | \dontrun{ 19 | 20 | ugd_test_pattern() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/testthat/test-tiff.R: -------------------------------------------------------------------------------- 1 | test_that("TIFF file signature", { 2 | skip_if_not("tiff" %in% ugd_renderers()$id, "TIFF renderer not installed") 3 | 4 | # TIFF has a different signature depending on endianness 5 | file_magic_le <- as.raw(c(0x49, 0x49, 0x2A, 0x00)) 6 | file_magic_be <- as.raw(c(0x4D, 0x4D, 0x00, 0x2A)) 7 | 8 | ugd_magic <- ugd_render_inline({ 9 | plot(1) 10 | }, as = "tiff")[seq_along(file_magic_le)] 11 | 12 | expect_true( 13 | all.equal(file_magic_le, ugd_magic) || 14 | all.equal(file_magic_be, ugd_magic) 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /unigd.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | ProjectId: 79fe1cd7-9e93-4983-9dc7-ede77a4210d6 3 | 4 | RestoreWorkspace: No 5 | SaveWorkspace: No 6 | AlwaysSaveHistory: Default 7 | 8 | EnableCodeIndexing: Yes 9 | UseSpacesForTab: Yes 10 | NumSpacesForTab: 2 11 | Encoding: UTF-8 12 | 13 | RnwWeave: Sweave 14 | LaTeX: pdfLaTeX 15 | 16 | AutoAppendNewline: Yes 17 | StripTrailingWhitespace: Yes 18 | LineEndingConversion: Posix 19 | 20 | BuildType: Package 21 | PackageUseDevtools: Yes 22 | PackageInstallArgs: --no-multiarch --with-keep.source 23 | PackageRoxygenize: rd,collate,namespace 24 | -------------------------------------------------------------------------------- /man/ugd_clear.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_clear} 4 | \alias{ugd_clear} 5 | \title{Clear all unigd plot pages.} 6 | \usage{ 7 | ugd_clear(which = dev.cur()) 8 | } 9 | \arguments{ 10 | \item{which}{Which device (ID).} 11 | } 12 | \value{ 13 | Whether there were any pages to remove. 14 | } 15 | \description{ 16 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 17 | } 18 | \examples{ 19 | ugd() 20 | plot(1, 1) 21 | hist(rnorm(100)) 22 | ugd_clear() # Clear all previous plots 23 | hist(rnorm(100)) 24 | 25 | dev.off() 26 | } 27 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,unigd_pid) 4 | export(ugd) 5 | export(ugd_clear) 6 | export(ugd_close) 7 | export(ugd_id) 8 | export(ugd_info) 9 | export(ugd_remove) 10 | export(ugd_render) 11 | export(ugd_render_inline) 12 | export(ugd_renderers) 13 | export(ugd_save) 14 | export(ugd_save_inline) 15 | export(ugd_state) 16 | export(ugd_test_pattern) 17 | importFrom(grDevices,dev.cur) 18 | importFrom(grDevices,dev.list) 19 | importFrom(grDevices,dev.off) 20 | importFrom(systemfonts,font_info) 21 | importFrom(systemfonts,match_font) 22 | importFrom(tools,file_ext) 23 | useDynLib(unigd, .registration=TRUE) 24 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | - GitHub rlib/actions 3 | - R-hub2 actions 4 | 5 | ## R CMD check results 6 | There were no ERRORs or WARNINGs. 7 | 8 | > checking C++ specification ... NOTE 9 | Specified C++14: please drop specification unless essential 10 | 11 | C++14 is essential for this package. 12 | 13 | > checking installed package size ... NOTE 14 | installed size is 9.6Mb 15 | sub-directories of 1Mb or more: 16 | libs 8.4Mb 17 | 18 | This package uses larger libraries. 19 | 20 | ## GCC 15 error 21 | 22 | The missing standard library include has been added. 23 | Package successfully builds with the current GCC 15 snapshot. 24 | -------------------------------------------------------------------------------- /tests/testthat/test-svg.R: -------------------------------------------------------------------------------- 1 | test_that("SVG contains plot text", { 2 | ugd() 3 | plot.new() 4 | teststr <- "Some text abc123" 5 | text(0, 0, teststr) 6 | svg <- ugd_render() 7 | dev.off() 8 | expect_true(grepl(teststr, svg, fixed = TRUE)) 9 | }) 10 | 11 | test_that("boxplot returns valid SVG", { 12 | expect_warning(xmlSVG({ 13 | boxplot(rnorm(10)) 14 | }), regexp = NA) 15 | }) 16 | 17 | #test_that("Append CSS with extra_css", { 18 | # testcss <- ".httpgd polyline { stroke: green; }" 19 | # ugd(webserver=F, extra_css = testcss) 20 | # plot(1) 21 | # svg <- ugd_render() 22 | # dev.off() 23 | # expect_true(grepl(testcss, svg, fixed = TRUE)) 24 | #}) -------------------------------------------------------------------------------- /src/renderer_meta.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_RENDERER_META_H__ 2 | #define __UNIGD_RENDERER_META_H__ 3 | 4 | #include 5 | 6 | #include "renderers.h" 7 | 8 | namespace unigd 9 | { 10 | namespace renderers 11 | { 12 | class RendererMeta : public render_target 13 | { 14 | public: 15 | void render(const Page &t_page, double t_scale) override; 16 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 17 | 18 | // Renderer 19 | void page(const Page &t_page); 20 | 21 | private: 22 | fmt::memory_buffer os; 23 | double m_scale; 24 | }; 25 | 26 | } // namespace renderers 27 | } // namespace unigd 28 | 29 | #endif /* __UNIGD_RENDERER_META_H__ */ 30 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-colour.R: -------------------------------------------------------------------------------- 1 | test_that("transparent blacks are written", { 2 | x <- xmlSVG({ 3 | plot.new() 4 | points(0.5, 0.5, col = rgb(0, 0, 0, 0.25)) 5 | points(0.5, 0.5, col = rgb(0, 0, 0, 0.50)) 6 | points(0.5, 0.5, col = rgb(0, 0, 0, 0.75)) 7 | }) 8 | 9 | circle <- xml2::xml_find_all(x, ".//d1:circle") 10 | 11 | expect_equal(style_attr(circle, "stroke"), rep("#000000", 3)) 12 | expect_equal(style_attr(circle, "stroke-opacity"), c("0.25", "0.50", "0.75")) 13 | }) 14 | 15 | test_that("transparent colours are not written", { 16 | x <- xmlSVG({ 17 | plot.new() 18 | points(0.5, 0.5, col = NA) 19 | }) 20 | 21 | circle <- xml2::xml_find_all(x, ".//d1:circle") 22 | expect_length(circle, 0) 23 | }) -------------------------------------------------------------------------------- /man/ugd_info.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_info} 4 | \alias{ugd_info} 5 | \title{unigd device information.} 6 | \usage{ 7 | ugd_info(which = dev.cur()) 8 | } 9 | \arguments{ 10 | \item{which}{Which device (ID).} 11 | } 12 | \value{ 13 | List of status variables with the following named items: 14 | \verb{$id}: Server unique ID, 15 | \verb{$version}: unigd and library versions. 16 | } 17 | \description{ 18 | Access general information of a unigd graphics device. 19 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 20 | } 21 | \examples{ 22 | ugd() # Initialize graphics device 23 | ugd_info() # Get device information 24 | dev.off() # Close device 25 | } 26 | -------------------------------------------------------------------------------- /man/ugd_renderers.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_renderers} 4 | \alias{ugd_renderers} 5 | \title{unigd device renderers.} 6 | \usage{ 7 | ugd_renderers() 8 | } 9 | \value{ 10 | List of renderers with the following named items: 11 | \verb{$id}: Renderer ID, 12 | \verb{$mime}: File mime type, 13 | \verb{$ext}: File extension, 14 | \verb{$name}: Human readable name, 15 | \verb{$type}: Renderer type (currently either \code{plot} or \code{other}), 16 | \verb{$bin}: Is the file a binary blob or text. 17 | } 18 | \description{ 19 | Get a list of available renderers. 20 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 21 | } 22 | \examples{ 23 | 24 | ugd_renderers() 25 | 26 | } 27 | -------------------------------------------------------------------------------- /man/ugd_close.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_close} 4 | \alias{ugd_close} 5 | \title{Close unigd device.} 6 | \usage{ 7 | ugd_close(which = dev.cur(), all = FALSE) 8 | } 9 | \arguments{ 10 | \item{which}{Which device (ID).} 11 | 12 | \item{all}{Should all running unigd devices be closed.} 13 | } 14 | \value{ 15 | Number and name of the new active device (after the specified device 16 | has been shut down). 17 | } 18 | \description{ 19 | This achieves the same effect as \code{\link[grDevices:dev]{grDevices::dev.off()}}, 20 | but will only close the device if it has the unigd type. 21 | } 22 | \examples{ 23 | ugd() 24 | hist(rnorm(100)) 25 | ugd_close() # Equvalent to dev.off() 26 | 27 | ugd() 28 | ugd() 29 | ugd() 30 | ugd_close(all = TRUE) 31 | } 32 | -------------------------------------------------------------------------------- /tests/testthat/helper-svglite.R: -------------------------------------------------------------------------------- 1 | # httpgd equivalent to svglite::xmlSVG 2 | xmlSVG <- function(code, ...) { 3 | xml2::read_xml(ugd_render_inline(code, ...)) 4 | } 5 | 6 | # Helper 7 | # https://github.com/r-lib/svglite/blob/master/tests/testthat/helper-style.R 8 | 9 | style_attr <- function(nodes, attr) { 10 | style <- xml2::xml_attr(nodes, "style") 11 | ifelse( 12 | grepl(sprintf("%s: [^;]*;", attr), style), 13 | gsub(sprintf(".*%s: ([^;]*);.*", attr), "\\1", style), 14 | NA_character_ 15 | ) 16 | } 17 | 18 | #mini_plot <- function(...) graphics::plot(..., axes = FALSE, xlab = "", ylab = "") 19 | 20 | dash_array <- function(...) { 21 | x <- xmlSVG(mini_plot(1:3, ..., type = "l")) 22 | dash <- style_attr(xml2::xml_find_first(x, "//d1:polyline"), "stroke-dasharray") 23 | as.numeric(strsplit(dash, ",")[[1]]) 24 | } -------------------------------------------------------------------------------- /tests/testthat/test-svglite-clip.R: -------------------------------------------------------------------------------- 1 | # todo (both are manual) 2 | 3 | #test_that("regression test for no clipping", { 4 | # svglite("test-no-clip.svg", 4, 4, user_fonts = bitstream) 5 | # on.exit(dev.off()) 6 | 7 | # mini_plot(c(-1, 1), c(-1, 1), asp = 1, type = "n") 8 | # rect(-0.5, -0.5, 0.5, 0.5, col = "blue") 9 | # text(0, 0.5, "Clipping", cex = 2, srt = 30) 10 | # abline(h = 0.5, col = "red") 11 | #}) 12 | 13 | #test_that("regression test for clipping", { 14 | # svglite("test-clip.svg", 4, 4, user_fonts = bitstream) 15 | # on.exit(dev.off()) 16 | 17 | # mini_plot(c(-1, 1), c(-1, 1), asp = 1, type = "n") 18 | # clip(-1, 0, -1, 0) 19 | # rect(-0.5, -0.5, 0.5, 0.5, col = "blue") 20 | # clip(0, 1, 0, 1) 21 | # text(0, 0.5, "Clipping", cex = 2, srt = 30) 22 | # clip(-1, 0, 0, 1) 23 | # abline(h = 0.5, col = "red") 24 | #}) -------------------------------------------------------------------------------- /src/renderer_meta.cpp: -------------------------------------------------------------------------------- 1 | #include "renderer_meta.h" 2 | 3 | namespace unigd 4 | { 5 | namespace renderers 6 | { 7 | void RendererMeta::render(const Page &t_page, double t_scale) 8 | { 9 | m_scale = t_scale; 10 | page(t_page); 11 | } 12 | 13 | void RendererMeta::get_data(const uint8_t **t_buf, size_t *t_size) const 14 | { 15 | *t_buf = reinterpret_cast(os.begin()); 16 | *t_size = os.size(); 17 | } 18 | 19 | void RendererMeta::page(const Page &t_page) 20 | { 21 | fmt::format_to( 22 | std::back_inserter(os), 23 | "{{\n " 24 | R""("id": "{}", "w": {:.2f}, "h": {:.2f}, "scale": {:.2f}, clips: {}, draw_calls: {})"" 25 | "\n}}", 26 | t_page.id, t_page.size.x, t_page.size.y, m_scale, t_page.cps.size(), 27 | t_page.dcs.size()); 28 | } 29 | 30 | } // namespace renderers 31 | } // namespace unigd 32 | -------------------------------------------------------------------------------- /man/ugd_remove.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_remove} 4 | \alias{ugd_remove} 5 | \title{Remove a unigd plot page.} 6 | \usage{ 7 | ugd_remove(page = 0, which = dev.cur()) 8 | } 9 | \arguments{ 10 | \item{page}{Plot page to remove. If this is set to \code{0}, the last page will 11 | be selected. Can be set to a numeric plot index or plot ID 12 | (see \code{\link[=ugd_id]{ugd_id()}}).} 13 | 14 | \item{which}{Which device (ID).} 15 | } 16 | \value{ 17 | Whether the page existed (and thereby was successfully removed). 18 | } 19 | \description{ 20 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 21 | } 22 | \examples{ 23 | ugd() 24 | plot(1, 1) # page 1 25 | hist(rnorm(100)) # page 2 26 | ugd_remove(page = 1) # remove page 1 27 | 28 | dev.off() 29 | } 30 | -------------------------------------------------------------------------------- /man/ugd_state.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_state} 4 | \alias{ugd_state} 5 | \title{unigd device status.} 6 | \usage{ 7 | ugd_state(which = dev.cur()) 8 | } 9 | \arguments{ 10 | \item{which}{Which device (ID).} 11 | } 12 | \value{ 13 | List of status variables with the following named items: 14 | \verb{$hsize}: Plot history size (how many plots are accessible), 15 | \verb{$upid}: Update ID (changes when the device has received new information), 16 | \verb{$active}: Is the device the currently activated device. 17 | } 18 | \description{ 19 | Access status information of a unigd graphics device. 20 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 21 | } 22 | \examples{ 23 | ugd() 24 | ugd_state() 25 | plot(1, 1) 26 | ugd_state() 27 | 28 | dev.off() 29 | } 30 | -------------------------------------------------------------------------------- /src/plot_history.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_PLOT_HISTORY_H__ 2 | #define __UNIGD_PLOT_HISTORY_H__ 3 | 4 | #include 5 | #define R_NO_REMAP 6 | #include 7 | 8 | #include 9 | 10 | namespace unigd 11 | { 12 | class PlotHistory 13 | { 14 | public: 15 | // Replay the current graphics device state 16 | static bool replay_current(pDevDesc dd); 17 | 18 | PlotHistory(); 19 | 20 | void put(R_xlen_t index, SEXP snapshot); 21 | bool put_current(R_xlen_t index, pDevDesc dd); 22 | void put_last(R_xlen_t index, pDevDesc dd); 23 | bool get(R_xlen_t index, SEXP *snapshot); 24 | 25 | bool remove(R_xlen_t index); 26 | 27 | void clear(); 28 | bool play(R_xlen_t index, pDevDesc dd); 29 | 30 | private: 31 | cpp11::writable::list m_items; 32 | }; 33 | 34 | } // namespace unigd 35 | #endif /* __UNIGD_PLOT_HISTORY_H__ */ 36 | -------------------------------------------------------------------------------- /src/Makevars.win: -------------------------------------------------------------------------------- 1 | VERSION_HARFBUZZ = 2.7.4 2 | VERSION_CAIRO = 1.16.0 3 | 4 | RWINLIB_HARFBUZZ = ../windows/harfbuzz-${VERSION_HARFBUZZ} 5 | RWINLIB_CAIRO = ../windows/cairo-${VERSION_CAIRO} 6 | 7 | PKG_CPPFLAGS = -Ilib -I../inst/include -I${RWINLIB_HARFBUZZ}/include \ 8 | -I${RWINLIB_CAIRO}/include/cairo \ 9 | -I${RWINLIB_CAIRO}/include/freetype2 \ 10 | -DFMT_HEADER_ONLY 11 | 12 | PKG_LIBS = -L${RWINLIB_HARFBUZZ}/lib${R_ARCH}${CRT} -lpng -lz \ 13 | -L${RWINLIB_CAIRO}/lib${R_ARCH}${CRT} -lcairo -lfreetype -lpixman-1 -lharfbuzz -lbz2 -liconv -lgdi32 \ 14 | -ltiff -ltiffxx -ljpeg -lzstd -lwebp -lsharpyuv -llzma 15 | 16 | all: winlibs 17 | 18 | winlibs: 19 | "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" "../tools/winlibs.R" ${VERSION_HARFBUZZ} & \ 20 | "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" "../tools/wincairo.R" ${VERSION_CAIRO} 21 | 22 | clean: 23 | rm -f $(OBJECTS) 24 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # unigd 0.1.3 2 | 3 | - Added missing standard library include for GCC 15. 4 | - Drop 'C++14' requirement. 5 | 6 | # unigd 0.1.2 7 | 8 | - Fixed an issue that made unigd crash when rendering without any plots in the history on some platforms. 9 | - Update installation instructions (thanks @huangyxi). 10 | - Minor internal improvements. 11 | 12 | # unigd 0.1.1 13 | 14 | - Fix issues with 'libtiff'. (Thanks @benz0li) 15 | - Update linking on Windows for upcoming version of 'Rtools'. (Thanks @kalibera) 16 | 17 | # unigd 0.1.0 18 | 19 | - Split graphics rendering and R interface from 'httpgd'. 20 | - Large refactoring and rewrite. 21 | - Add async C client API. 22 | - Add custom inter process communication layer. 23 | - Add TIFF renderer. 24 | - Add Base64 PNG renderer. 25 | - Fix crash when querying capabilities on R 4.2. 26 | - Improve testing. 27 | - Many small fixes and improvements. 28 | -------------------------------------------------------------------------------- /src/geom.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_GEOM_H__ 2 | #define __UNIGD_GEOM_H__ 3 | 4 | #include 5 | #include 6 | 7 | namespace unigd 8 | { 9 | 10 | using color_t = int; 11 | 12 | template 13 | struct gvertex 14 | { 15 | T x, y; 16 | }; 17 | 18 | template 19 | struct grect 20 | { 21 | T x, y, width, height; 22 | }; 23 | 24 | template 25 | grect normalize_rect(T x0, T y0, T x1, T y1) 26 | { 27 | return {std::min(x0, x1), std::min(y0, y1), std::fabs(x0 - x1), std::fabs(y0 - y1)}; 28 | } 29 | 30 | template 31 | bool rect_equals(const grect &r0, const grect &r1, T eps) 32 | { 33 | return (std::fabs(r0.x - r1.x) < eps) && (std::fabs(r0.y - r1.y) < eps) && 34 | (std::fabs(r0.width - r1.width) < eps) && 35 | (std::fabs(r0.height - r1.height) < eps); 36 | } 37 | 38 | } // namespace unigd 39 | 40 | #endif /* __UNIGD_GEOM_H__ */ 41 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-rect.R: -------------------------------------------------------------------------------- 1 | # https://github.com/r-lib/svglite/blob/master/tests/testthat/test-rect.R 2 | 3 | test_that("rects equivalent regardless of direction", { 4 | ugd() 5 | plot.new() 6 | rect(0.2, 0.2, 0.8, 0.8) 7 | x1 <- xml2::read_xml(ugd_render()) 8 | plot.new() 9 | rect(0.8, 0.8, 0.2, 0.2) 10 | x2 <- xml2::read_xml(ugd_render()) 11 | dev.off() 12 | 13 | rect1 <- xml2::xml_attrs(xml2::xml_find_all(x1, "./d1:g/d1:rect")[[1]]) 14 | rect2 <- xml2::xml_attrs(xml2::xml_find_all(x2, "./d1:g/d1:rect")[[1]]) 15 | 16 | expect_equal(rect1, rect2) 17 | }) 18 | 19 | test_that("fill and stroke colors", { 20 | 21 | x <- xmlSVG({ 22 | plot.new() 23 | rect(0.2, 0.2, 0.8, 0.8, col = "blue", border = "red") 24 | }) 25 | 26 | rectangle <- xml2::xml_find_all(x, "./d1:g/d1:rect")[[1]] 27 | expect_equal(style_attr(rectangle, "fill"), rgb(0, 0, 1)) 28 | expect_equal(style_attr(rectangle, "stroke"), rgb(1, 0, 0)) 29 | }) -------------------------------------------------------------------------------- /src/uuid.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "uuid.h" 3 | 4 | #include 5 | #include 6 | 7 | namespace unigd 8 | { 9 | namespace uuid 10 | { 11 | static std::random_device rd; 12 | static std::mt19937 gen(rd()); 13 | static std::uniform_int_distribution<> dis(0, 15); 14 | static std::uniform_int_distribution<> dis2(8, 11); 15 | 16 | std::string uuid() 17 | { 18 | std::stringstream ss; 19 | int i; 20 | ss << std::hex; 21 | for (i = 0; i < 8; i++) 22 | { 23 | ss << dis(gen); 24 | } 25 | ss << "-"; 26 | for (i = 0; i < 4; i++) 27 | { 28 | ss << dis(gen); 29 | } 30 | ss << "-4"; 31 | for (i = 0; i < 3; i++) 32 | { 33 | ss << dis(gen); 34 | } 35 | ss << "-"; 36 | ss << dis2(gen); 37 | for (i = 0; i < 3; i++) 38 | { 39 | ss << dis(gen); 40 | } 41 | ss << "-"; 42 | for (i = 0; i < 12; i++) 43 | { 44 | ss << dis(gen); 45 | }; 46 | return ss.str(); 47 | } 48 | } // namespace uuid 49 | 50 | } // namespace unigd 51 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-output.R: -------------------------------------------------------------------------------- 1 | # https://github.com/r-lib/svglite/blob/master/tests/testthat/test-output.R 2 | 3 | test_that("different string and file output produce identical svg", { 4 | ugd() 5 | 6 | ## 1. Write to a file 7 | f1 <- tempfile() 8 | plot(1:5) 9 | ugd_save(file=f1, as="svg") 10 | dev.off() 11 | 12 | out1 <- readLines(f1, warn = FALSE) 13 | 14 | ## 2. Write to a string stream 15 | ugd() 16 | plot(1:5) 17 | s <- ugd_render() 18 | dev.off() 19 | out2 <- strsplit(s, "\n")[[1]] 20 | 21 | expect_equal(out1, out2) 22 | }) 23 | 24 | test_that("intermediate outputs are always valid svg", { 25 | ugd() 26 | 27 | expect_valid_svg <- function() { 28 | expect_error(xml2::read_xml(ugd_render()), NA) 29 | } 30 | 31 | mini_plot(1:10) 32 | expect_valid_svg() 33 | 34 | rect(2, 2, 3, 3) 35 | expect_valid_svg() 36 | 37 | segments(5, 5, 6, 6) 38 | expect_valid_svg() 39 | 40 | dev.off() 41 | }) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | 14 | 15 | (Write your answer here.) 16 | 17 | ### To Reproduce 18 | 21 | 22 | (Write your answer here.) 23 | 24 | ### Expected behavior 25 | 28 | 29 | (Write your answer here.) 30 | 31 | ### Screenshots 32 | 35 | 36 | (Write your answer here.) 37 | 38 | ### Environment 39 | - OS: 40 | - R version: 41 | - unigd version: 42 | 43 | ### Additional context 44 | 47 | 48 | (Write your answer here.) 49 | -------------------------------------------------------------------------------- /inst/include/unigd_api.h: -------------------------------------------------------------------------------- 1 | #ifndef UNIGD_EXTERNAL_API_H 2 | #define UNIGD_EXTERNAL_API_H 3 | 4 | #ifndef R_NO_REMAP 5 | #define R_NO_REMAP 6 | #endif 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | static inline int unigd_api_v1_create(unigd_api_v1 **api) 17 | { 18 | static int(*ptr_api_v1_create)(unigd_api_v1 **) = NULL; 19 | if (ptr_api_v1_create == NULL) { 20 | ptr_api_v1_create = (int(*)(unigd_api_v1 **)) R_GetCCallable("unigd", "api_v1_create"); 21 | } 22 | return ptr_api_v1_create(api); 23 | } 24 | 25 | static inline int unigd_api_v1_destroy(unigd_api_v1 *api) 26 | { 27 | static int(*ptr_api_v1_destroy)(unigd_api_v1 *) = NULL; 28 | if (ptr_api_v1_destroy == NULL) { 29 | ptr_api_v1_destroy = (int(*)(unigd_api_v1 *)) R_GetCCallable("unigd", "api_v1_destroy"); 30 | } 31 | return ptr_api_v1_destroy(api); 32 | } 33 | 34 | #endif // UNIGD_EXTERNAL_API_H 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your proposal related to a problem? 11 | 12 | 16 | 17 | (Write your answer here.) 18 | 19 | ### Describe the solution you'd like 20 | 21 | 24 | 25 | (Describe your proposed solution here.) 26 | 27 | ### Describe alternatives you've considered 28 | 29 | 32 | 33 | (Write your answer here.) 34 | 35 | ### Additional context 36 | 37 | 41 | 42 | (Write your answer here.) 43 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-scale.R: -------------------------------------------------------------------------------- 1 | library("grid") 2 | 3 | # manual test 4 | #test_that("text has correct dimensions", { 5 | # ttf <- fontquiver::font("Liberation", "Sans", "Regular")$ttf 6 | # w <- systemfonts::string_width("foobar", path = ttf, index = 0L, res = 1e4) * 72 / 1e4 7 | # h <- max(vapply(systemfonts::glyph_info("foobar", path = ttf, index = 0L, res = 1e4)$bbox, `[[`, numeric(1), "ymax")) * 72 / 1e4 8 | # 9 | # ugd(width = w, height = h, 10 | # user_fonts = fontquiver::font_families("Liberation")) 11 | # 12 | # grid.newpage() 13 | # grid.rect(0, 1, width = unit(w, "bigpts"), height = unit(h, "bigpts"), 14 | # hjust = 0, vjust = 1, gp = gpar(col = "red", lwd = 1)) 15 | # grid.text("foobar", 0, 1, hjust = 0, vjust = 1, gp = gpar(fontsize = 12)) 16 | # pushViewport(viewport()) 17 | #}) 18 | 19 | test_that("lwd has correct dimensions", { 20 | x <- xmlSVG({ 21 | plot.new() 22 | segments(0, 1, 0, 0, lwd = 96 / 72) 23 | }) 24 | line <- xml2::xml_find_all(x, "//d1:line") 25 | expect_equal(xml2::xml_attr(line, "style"), "stroke-width: 1.00;") 26 | }) -------------------------------------------------------------------------------- /src/renderer_tikz.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_RENDERER_TIKZ_H__ 2 | #define __UNIGD_RENDERER_TIKZ_H__ 3 | 4 | #include 5 | 6 | #include "renderers.h" 7 | 8 | namespace unigd 9 | { 10 | namespace renderers 11 | { 12 | class RendererTikZ : public render_target, public draw_call_visitor 13 | { 14 | public: 15 | void render(const Page &t_page, double t_scale) override; 16 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 17 | 18 | // Renderer 19 | void page(const Page &t_page); 20 | void visit(const Rect *t_rect) override; 21 | void visit(const Text *t_text) override; 22 | void visit(const Circle *t_circle) override; 23 | void visit(const Line *t_line) override; 24 | void visit(const Polyline *t_polyline) override; 25 | void visit(const Polygon *t_polygon) override; 26 | void visit(const Path *t_path) override; 27 | void visit(const Raster *t_raster) override; 28 | 29 | private: 30 | fmt::memory_buffer os; 31 | double m_scale; 32 | }; 33 | 34 | } // namespace renderers 35 | } // namespace unigd 36 | 37 | #endif /* __UNIGD_RENDERER_TIKZ_H__ */ 38 | -------------------------------------------------------------------------------- /src/Makevars.ucrt: -------------------------------------------------------------------------------- 1 | PKG_CPPFLAGS = -Ilib \ 2 | -DFMT_HEADER_ONLY \ 3 | -I../inst/include 4 | 5 | ifeq (,$(shell pkg-config --version 2>/dev/null)) 6 | LIBDEFLATE = $(or $(and $(wildcard $(R_TOOLS_SOFT)/lib/libdeflate.a),-ldeflate),) 7 | LIBLERC = $(or $(and $(wildcard $(R_TOOLS_SOFT)/lib/liblerc.a),-llerc),) 8 | LIBBROTLI = $(or $(and $(wildcard $(R_TOOLS_SOFT)/lib/libbrotlidec.a),-lbrotlidec -lbrotlicommon),) 9 | LIBSHARPYUV = $(or $(and $(wildcard $(R_TOOLS_SOFT)/lib/libsharpyuv.a),-lsharpyuv),) 10 | PKG_LIBS = -lcairo -lpixman-1 -lfontconfig -lncrypt -lksecdd -lbcrypt -lexpat -lharfbuzz_too -lfreetype_too -lharfbuzz -lfreetype $(LIBBROTLI) -lpng16 -lpng -lbz2 -lgdi32 -lintl -liconv -lz -lcfitsio -ltiff -ltiffxx $(LIBDEFLATE) $(LIBLERC) -ljpeg -lzstd -lwebp $(LIBSHARPYUV) -llzma -luuid -lole32 11 | PKG_CPPFLAGS += -I$(R_TOOLS_SOFT)/include/cairo -DCAIRO_WIN32_STATIC_BUILD 12 | else 13 | PKG_LIBS = -ltiffxx $(shell pkg-config --libs cairo libtiff-4) 14 | PKG_CPPFLAGS += $(shell pkg-config --cflags cairo libtiff-4) 15 | endif 16 | 17 | all: clean 18 | 19 | clean: 20 | rm -f $(OBJECTS) 21 | -------------------------------------------------------------------------------- /src/renderer_json.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_RENDERER_JSON_H__ 2 | #define __UNIGD_RENDERER_JSON_H__ 3 | 4 | #include 5 | 6 | #include "renderers.h" 7 | 8 | namespace unigd 9 | { 10 | namespace renderers 11 | { 12 | class RendererJSON : public render_target, public draw_call_visitor 13 | { 14 | public: 15 | void render(const Page &t_page, double t_scale) override; 16 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 17 | 18 | // Renderer 19 | void page(const Page &t_page); 20 | 21 | void visit(const Rect *t_rect) override; 22 | void visit(const Text *t_text) override; 23 | void visit(const Circle *t_circle) override; 24 | void visit(const Line *t_line) override; 25 | void visit(const Polyline *t_polyline) override; 26 | void visit(const Polygon *t_polygon) override; 27 | void visit(const Path *t_path) override; 28 | void visit(const Raster *t_raster) override; 29 | 30 | private: 31 | fmt::memory_buffer os; 32 | double m_scale; 33 | }; 34 | 35 | } // namespace renderers 36 | } // namespace unigd 37 | 38 | #endif /* __UNIGD_RENDERER_JSON_H__ */ 39 | -------------------------------------------------------------------------------- /src/renderer_strings.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_RENDERER_STRINGS_H__ 2 | #define __UNIGD_RENDERER_STRINGS_H__ 3 | 4 | #include 5 | 6 | #include "renderers.h" 7 | 8 | namespace unigd 9 | { 10 | namespace renderers 11 | { 12 | class RendererStrings : public render_target, public draw_call_visitor 13 | { 14 | public: 15 | void render(const Page &t_page, double t_scale) override; 16 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 17 | 18 | // Renderer 19 | void page(const Page &t_page); 20 | void visit(const Rect *t_rect) override; 21 | void visit(const Text *t_text) override; 22 | void visit(const Circle *t_circle) override; 23 | void visit(const Line *t_line) override; 24 | void visit(const Polyline *t_polyline) override; 25 | void visit(const Polygon *t_polygon) override; 26 | void visit(const Path *t_path) override; 27 | void visit(const Raster *t_raster) override; 28 | 29 | private: 30 | fmt::memory_buffer os; 31 | size_t string_count; 32 | }; 33 | 34 | } // namespace renderers 35 | } // namespace unigd 36 | 37 | #endif /* __UNIGD_RENDERER_STRINGS_H__ */ 38 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-devSVG.R: -------------------------------------------------------------------------------- 1 | test_that("adds default background", { 2 | x <- xmlSVG(plot.new()) 3 | expect_equal(style_attr(xml2::xml_find_first(x, "./d1:rect"), "fill"), "#FFFFFF") 4 | }) 5 | 6 | test_that("adds background set by device driver", { 7 | x <- xmlSVG(plot.new(), bg = "red") 8 | expect_equal(style_attr(xml2::xml_find_first(x, "./d1:rect"), "fill"), rgb(1, 0, 0)) 9 | }) 10 | 11 | test_that("default background respects par", { 12 | x <- xmlSVG({ 13 | par(bg = "red") 14 | plot.new() 15 | }) 16 | expect_equal(style_attr(xml2::xml_find_first(x, "./d1:rect"), "fill"), rgb(1, 0, 0)) 17 | }) 18 | 19 | test_that("if bg is transparent in par(), use device driver background", { 20 | x <- xmlSVG({ 21 | par(bg = NA) 22 | plot.new() 23 | }, bg = "blue") 24 | style <- xml2::xml_text(xml2::xml_find_first(x, "//d1:style")) 25 | expect_match(style, "fill: none;") 26 | expect_equal(style_attr(xml2::xml_find_first(x, "./d1:rect"), "fill"), rgb(0, 0, 1)) 27 | }) 28 | 29 | # not applicable: 30 | #test_that("creating multiple pages is identical to creating multiple individual svgs", { 31 | -------------------------------------------------------------------------------- /src/renderers.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_RENDERERS_H__ 2 | #define __UNIGD_RENDERERS_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "draw_data.h" 10 | #include "unigd_external.h" 11 | 12 | namespace unigd 13 | { 14 | namespace renderers 15 | { 16 | // regex for refactoring: 17 | // ([Rr]ect|[Tt]ext|[Cc]ircle|[Ll]ine|[Pp]olyline|[Pp]olygon|[Pp]ath|[Rr]aster) 18 | 19 | class render_target : public ex::render_data 20 | { 21 | public: 22 | virtual void render(const Page &t_page, double t_scale) = 0; 23 | }; 24 | 25 | using renderer_gen = std::function()>; 26 | struct renderer_map_entry 27 | { 28 | unigd_renderer_info info; 29 | renderer_gen generator; 30 | }; 31 | 32 | bool find(const std::string &id, renderer_map_entry *renderer); 33 | bool find_generator(const std::string &id, renderer_gen *renderer); 34 | bool find_info(const std::string &id, unigd_renderer_info *renderer); 35 | const std::unordered_map *renderers(); 36 | } // namespace renderers 37 | 38 | } // namespace unigd 39 | 40 | #endif /* __UNIGD_RENDERERS_H__ */ 41 | -------------------------------------------------------------------------------- /src/r_thread.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_R_THREAD_H__ 2 | #define __UNIGD_R_THREAD_H__ 3 | 4 | #include 5 | #include 6 | #include // for std::invoke_result/result_of 7 | #include "async_utils.h" 8 | 9 | namespace unigd 10 | { 11 | namespace async 12 | { 13 | void ipc_open(); 14 | void ipc_close(); 15 | 16 | void r_thread_impl(function_wrapper &&f); 17 | 18 | template 19 | #if defined(__cplusplus) && __cplusplus >= 201703L 20 | std::future> r_thread(FunctionType f) 21 | #else 22 | std::future::type> r_thread(FunctionType f) 23 | #endif 24 | { 25 | #if defined(__cplusplus) && __cplusplus >= 201703L 26 | typedef typename std::invoke_result_t result_type; 27 | #else 28 | typedef typename std::result_of::type result_type; 29 | #endif 30 | std::packaged_task task(std::move(f)); 31 | std::future res(task.get_future()); 32 | r_thread_impl(std::move(task)); 33 | return res; 34 | } 35 | 36 | } // namespace async 37 | } // namespace unigd 38 | 39 | #endif /* __UNIGD_R_THREAD_H__ */ 40 | -------------------------------------------------------------------------------- /src/renderer_strings.cpp: -------------------------------------------------------------------------------- 1 | #include "renderer_strings.h" 2 | 3 | namespace unigd 4 | { 5 | namespace renderers 6 | { 7 | 8 | void RendererStrings::render(const Page &t_page, double t_scale) { page(t_page); } 9 | 10 | void RendererStrings::get_data(const uint8_t **t_buf, size_t *t_size) const 11 | { 12 | *t_buf = reinterpret_cast(os.begin()); 13 | *t_size = os.size(); 14 | } 15 | 16 | void RendererStrings::page(const Page &t_page) 17 | { 18 | string_count = 0; 19 | for (auto it = t_page.dcs.begin(); it != t_page.dcs.end(); ++it) 20 | { 21 | (*it)->visit(this); 22 | } 23 | } 24 | 25 | void RendererStrings::visit(const Rect *t_rect) {} 26 | 27 | void RendererStrings::visit(const Text *t_text) 28 | { 29 | if (string_count++ > 0) 30 | { 31 | fmt::format_to(std::back_inserter(os), "\n"); 32 | } 33 | fmt::format_to(std::back_inserter(os), "{}", t_text->str); 34 | } 35 | 36 | void RendererStrings::visit(const Circle *t_circle) {} 37 | 38 | void RendererStrings::visit(const Line *t_line) {} 39 | 40 | void RendererStrings::visit(const Polyline *t_polyline) {} 41 | 42 | void RendererStrings::visit(const Polygon *t_polygon) {} 43 | 44 | void RendererStrings::visit(const Path *t_path) {} 45 | 46 | void RendererStrings::visit(const Raster *t_raster) {} 47 | 48 | } // namespace renderers 49 | } // namespace unigd 50 | -------------------------------------------------------------------------------- /man/ugd_render.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_render} 4 | \alias{ugd_render} 5 | \title{Render unigd plot and return it.} 6 | \usage{ 7 | ugd_render( 8 | page = 0, 9 | width = -1, 10 | height = -1, 11 | zoom = 1, 12 | as = "svg", 13 | which = dev.cur() 14 | ) 15 | } 16 | \arguments{ 17 | \item{page}{Plot page to render. If this is set to \code{0}, the last page will 18 | be selected. Can be set to a numeric plot index or plot ID 19 | (see \code{\link[=ugd_id]{ugd_id()}}).} 20 | 21 | \item{width}{Width of the plot. If this is set to \code{-1}, the last width will 22 | be selected.} 23 | 24 | \item{height}{Height of the plot. If this is set to \code{-1}, the last height 25 | will be selected.} 26 | 27 | \item{zoom}{Zoom level. (For example: \code{2} corresponds to 200\%, \code{0.5} would 28 | be 50\%.)} 29 | 30 | \item{as}{Renderer.} 31 | 32 | \item{which}{Which device (ID).} 33 | } 34 | \value{ 35 | Rendered plot. Text renderers return strings, binary renderers 36 | return byte arrays. 37 | } 38 | \description{ 39 | See \code{\link[=ugd_save]{ugd_save()}} for saving rendered plots as files. 40 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 41 | } 42 | \examples{ 43 | ugd() 44 | plot(1, 1) 45 | ugd_render(width = 600, height = 400, as = "svg") 46 | dev.off() 47 | } 48 | -------------------------------------------------------------------------------- /R/cpp11.R: -------------------------------------------------------------------------------- 1 | # Generated by cpp11: do not edit by hand 2 | 3 | unigd_ugd_ <- function(bg, width, height, pointsize, aliases, reset_par) { 4 | .Call(`_unigd_unigd_ugd_`, bg, width, height, pointsize, aliases, reset_par) 5 | } 6 | 7 | unigd_state_ <- function(devnum) { 8 | .Call(`_unigd_unigd_state_`, devnum) 9 | } 10 | 11 | unigd_info_ <- function(devnum) { 12 | .Call(`_unigd_unigd_info_`, devnum) 13 | } 14 | 15 | unigd_renderers_ <- function() { 16 | .Call(`_unigd_unigd_renderers_`) 17 | } 18 | 19 | unigd_plot_find_ <- function(devnum, plot_id) { 20 | .Call(`_unigd_unigd_plot_find_`, devnum, plot_id) 21 | } 22 | 23 | unigd_render_ <- function(devnum, page, width, height, zoom, renderer_id) { 24 | .Call(`_unigd_unigd_render_`, devnum, page, width, height, zoom, renderer_id) 25 | } 26 | 27 | unigd_remove_ <- function(devnum, page) { 28 | .Call(`_unigd_unigd_remove_`, devnum, page) 29 | } 30 | 31 | unigd_remove_id_ <- function(devnum, plot_id) { 32 | .Call(`_unigd_unigd_remove_id_`, devnum, plot_id) 33 | } 34 | 35 | unigd_id_ <- function(devnum, page, limit) { 36 | .Call(`_unigd_unigd_id_`, devnum, page, limit) 37 | } 38 | 39 | unigd_clear_ <- function(devnum) { 40 | .Call(`_unigd_unigd_clear_`, devnum) 41 | } 42 | 43 | unigd_ipc_open_ <- function() { 44 | invisible(.Call(`_unigd_unigd_ipc_open_`)) 45 | } 46 | 47 | unigd_ipc_close_ <- function() { 48 | invisible(.Call(`_unigd_unigd_ipc_close_`)) 49 | } 50 | -------------------------------------------------------------------------------- /src/unigd_external.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_UNIGD_EXTERNAL_H__ 2 | #define __UNIGD_UNIGD_EXTERNAL_H__ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace unigd 13 | { 14 | namespace ex 15 | { 16 | using plot_index_t = UNIGD_PLOT_INDEX; 17 | using plot_relative_t = UNIGD_PLOT_RELATIVE; 18 | using plot_id_t = UNIGD_PLOT_ID; 19 | using renderer_id_t = UNIGD_RENDERER_ID; 20 | 21 | using graphics_client = unigd_graphics_client; 22 | 23 | inline char *copy_c_str(std::string str) 24 | { 25 | const auto size = str.size(); 26 | char *buffer = new char[size + 1]; 27 | memcpy(buffer, str.c_str(), size + 1); 28 | return buffer; 29 | } 30 | 31 | inline void destroy_c_str(char *str) { delete str; } 32 | 33 | template 34 | struct unigd_handle 35 | { 36 | std::shared_ptr device; 37 | }; 38 | 39 | using device_state = unigd_device_state; 40 | 41 | struct find_results 42 | { 43 | unigd_device_state state; 44 | std::vector ids; 45 | 46 | unigd_find_results c_repr(); 47 | }; 48 | 49 | class render_data 50 | { 51 | public: 52 | render_data() = default; 53 | virtual ~render_data() = default; 54 | 55 | virtual void get_data(const uint8_t **t_buf, size_t *t_size) const = 0; 56 | }; 57 | } // namespace ex 58 | 59 | } // namespace unigd 60 | 61 | #endif /* __UNIGD_UNIGD_EXTERNAL_H__ */ 62 | -------------------------------------------------------------------------------- /inst/licenses/fmt-MIT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 - present, Victor Zverovich 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | --- Optional exception to the license --- 23 | 24 | As an exception, if, as a result of your compiling your source code, portions 25 | of this Software are embedded into a machine-executable object form of such 26 | source code, you may redistribute such embedded portions in such object form 27 | without including the above copyright and permission notices. 28 | -------------------------------------------------------------------------------- /man/ugd_id.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_id} 4 | \alias{ugd_id} 5 | \title{Query unigd plot IDs} 6 | \usage{ 7 | ugd_id(index = 0, limit = 1, which = dev.cur(), state = FALSE) 8 | } 9 | \arguments{ 10 | \item{index}{Plot index. If this is set to \code{0}, the last page will be 11 | selected.} 12 | 13 | \item{limit}{Limit the number of returned IDs. If this is set to a 14 | value > 1 the returned type is a list if IDs. Set to \code{0} for all.} 15 | 16 | \item{which}{Which device (ID).} 17 | 18 | \item{state}{Include the current device state in the returned result 19 | (see also: \code{\link[=ugd_state]{ugd_state()}}).} 20 | } 21 | \value{ 22 | List containing static plot IDs. 23 | } 24 | \description{ 25 | Query unigd graphics device static plot IDs. 26 | Available plot IDs starting from \code{index} will be returned. 27 | \code{limit} specifies the number of plots. 28 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 29 | } 30 | \examples{ 31 | ugd() # Initialize graphics device 32 | 33 | # Page 1 34 | plot.new() 35 | text(.5, .5, "#1") 36 | 37 | # Page 2 38 | plot.new() 39 | text(.5, .5, "#2") 40 | 41 | # Page 3 42 | plot.new() 43 | text(.5, .5, "#3") 44 | 45 | third <- ugd_id() # Get ID of page 3 (last page) 46 | second <- ugd_id(2) # Get ID of page 2 47 | all <- ugd_id(1, limit = Inf) # Get all IDs 48 | 49 | ugd_remove(1) # Remove page 1 50 | ugd_render(second) # Render page 2 51 | 52 | dev.off() # Close device 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | pkgdown: 18 | runs-on: ubuntu-latest 19 | # Only restrict concurrency for non-PR jobs 20 | concurrency: 21 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 22 | env: 23 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 24 | permissions: 25 | contents: write 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: r-lib/actions/setup-pandoc@v2 30 | 31 | - uses: r-lib/actions/setup-r@v2 32 | with: 33 | use-public-rspm: true 34 | 35 | - uses: r-lib/actions/setup-r-dependencies@v2 36 | with: 37 | extra-packages: any::pkgdown, local::. 38 | needs: website 39 | 40 | - name: Build site 41 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 42 | shell: Rscript {0} 43 | 44 | - name: Deploy to GitHub pages 🚀 45 | if: github.event_name != 'pull_request' 46 | uses: JamesIves/github-pages-deploy-action@v4.6.3 47 | with: 48 | clean: false 49 | branch: gh-pages 50 | folder: docs -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: [push, pull_request] 4 | 5 | name: R-CMD-check 6 | 7 | permissions: read-all 8 | 9 | jobs: 10 | R-CMD-check: 11 | runs-on: ${{ matrix.config.os }} 12 | 13 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | config: 19 | - {os: macos-latest, r: 'release'} 20 | - {os: windows-latest, r: 'release'} 21 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 22 | - {os: ubuntu-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'oldrel-1'} 24 | 25 | env: 26 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 27 | R_KEEP_PKG_SOURCE: yes 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - uses: r-lib/actions/setup-pandoc@v2 33 | 34 | - uses: r-lib/actions/setup-r@v2 35 | with: 36 | r-version: ${{ matrix.config.r }} 37 | http-user-agent: ${{ matrix.config.http-user-agent }} 38 | use-public-rspm: true 39 | 40 | - uses: r-lib/actions/setup-r-dependencies@v2 41 | with: 42 | extra-packages: any::rcmdcheck 43 | needs: check 44 | 45 | - uses: r-lib/actions/check-r-package@v2 46 | with: 47 | upload-snapshots: true 48 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' -------------------------------------------------------------------------------- /man/ugd_save.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_save} 4 | \alias{ugd_save} 5 | \title{Render unigd plot to a file.} 6 | \usage{ 7 | ugd_save( 8 | file, 9 | page = 0, 10 | width = -1, 11 | height = -1, 12 | zoom = 1, 13 | as = "auto", 14 | which = dev.cur() 15 | ) 16 | } 17 | \arguments{ 18 | \item{file}{Filepath to save plot.} 19 | 20 | \item{page}{Plot page to render. If this is set to \code{0}, the last page will 21 | be selected. Can be set to a numeric plot index or plot ID 22 | (see \code{\link[=ugd_id]{ugd_id()}}).} 23 | 24 | \item{width}{Width of the plot. If this is set to \code{-1}, the last width will 25 | be selected.} 26 | 27 | \item{height}{Height of the plot. If this is set to \code{-1}, the last height 28 | will be selected.} 29 | 30 | \item{zoom}{Zoom level. (For example: \code{2} corresponds to 200\%, \code{0.5} would 31 | be 50\%.)} 32 | 33 | \item{as}{Renderer. When set to \code{"auto"} renderer is inferred from the file 34 | extension.} 35 | 36 | \item{which}{Which device (ID).} 37 | } 38 | \value{ 39 | No return value. Plot will be saved to file. 40 | } 41 | \description{ 42 | See \code{\link[=ugd_render]{ugd_render()}} for accessing plot data directly in memory without 43 | saving as a file. 44 | This function will only work after starting a device with \code{\link[=ugd]{ugd()}}. 45 | } 46 | \examples{ 47 | ugd() 48 | 49 | plot(1, 1) 50 | 51 | tf <- tempfile() 52 | on.exit(unlink(tf)) 53 | 54 | ugd_save(file = tf, width = 600, height = 400, as = "png") 55 | 56 | dev.off() 57 | } 58 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-points.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("radius is not given in points", { 3 | x <- xmlSVG({ 4 | plot.new() 5 | points(0.5, 0.5, cex = 20) 6 | text(0.5, 0.5, cex = 20) 7 | }) 8 | circle <- xml2::xml_find_all(x, ".//d1:circle") 9 | expect_equal(xml2::xml_attr(circle, "r"), "54.00") 10 | }) 11 | 12 | test_that("points are given stroke and fill", { 13 | x <- xmlSVG({ 14 | plot.new() 15 | points(0.5, 0.5, pch = 21, col = "red", bg = "blue", cex = 20) 16 | }) 17 | circle <- xml2::xml_find_all(x, ".//d1:circle") 18 | expect_equal(style_attr(circle, "stroke"), rgb(1, 0, 0)) 19 | expect_equal(style_attr(circle, "fill"), rgb(0, 0, 1)) 20 | }) 21 | 22 | test_that("points get alpha stroke and fill given stroke and fill", { 23 | x <- xmlSVG({ 24 | plot.new() 25 | points(0.5, 0.5, pch = 21, col = rgb(1, 0, 0, 0.1), bg = rgb(0, 0, 1, 0.1), cex = 20) 26 | }) 27 | circle <- xml2::xml_find_all(x, ".//d1:circle") 28 | expect_equal(style_attr(circle, "stroke"), rgb(1, 0, 0)) 29 | expect_equal(style_attr(circle, "stroke-opacity"), "0.10") 30 | expect_equal(style_attr(circle, "fill"), rgb(0, 0, 1)) 31 | expect_equal(style_attr(circle, "fill-opacity"), "0.10") 32 | }) 33 | 34 | test_that("points are given stroke and fill (none)", { 35 | x <- xmlSVG({ 36 | plot.new() 37 | points(0.5, 0.5, pch = 21, col = "red", bg = NA, cex = 20) 38 | }) 39 | style <- xml2::xml_text(xml2::xml_find_first(x, "//d1:style")) 40 | expect_match(style, "fill: none;") 41 | 42 | circle <- xml2::xml_find_all(x, ".//d1:circle") 43 | expect_equal(style_attr(circle, "fill"), NA_character_) 44 | }) -------------------------------------------------------------------------------- /man/ugd_render_inline.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_render_inline} 4 | \alias{ugd_render_inline} 5 | \title{Inline plot rendering.} 6 | \usage{ 7 | ugd_render_inline( 8 | code, 9 | page = 0, 10 | width = getOption("unigd.width", 720), 11 | height = getOption("unigd.height", 576), 12 | zoom = 1, 13 | as = "svg", 14 | ... 15 | ) 16 | } 17 | \arguments{ 18 | \item{code}{Plotting code. See examples for more information.} 19 | 20 | \item{page}{Plot page to render. If this is set to \code{0}, the last page will 21 | be selected. Can be set to a numeric plot index or plot ID 22 | (see \code{\link[=ugd_id]{ugd_id()}}).} 23 | 24 | \item{width}{Width of the plot.} 25 | 26 | \item{height}{Height of the plot.} 27 | 28 | \item{zoom}{Zoom level. (For example: \code{2} corresponds to 200\%, \code{0.5} would 29 | be 50\%.)} 30 | 31 | \item{as}{Renderer.} 32 | 33 | \item{...}{Additional parameters passed to \code{ugd(...)}} 34 | } 35 | \value{ 36 | Rendered plot. Text renderers return strings, binary renderers 37 | return byte arrays. 38 | } 39 | \description{ 40 | Convenience function for quick inline plot rendering. 41 | This is similar to \code{\link[=ugd_render]{ugd_render()}} but the plotting code 42 | is specified inline and an unigd graphics device is managed 43 | (created and closed) automatically. Starting a device with \code{\link[=ugd]{ugd()}} is 44 | therefore not necessary. 45 | } 46 | \examples{ 47 | ugd_render_inline({ 48 | hist(rnorm(100)) 49 | }, as = "svgz") 50 | 51 | s <- ugd_render_inline({ 52 | plot.new() 53 | lines(c(0.5, 1, 0.5), c(0.5, 1, 1)) 54 | }) 55 | cat(s) 56 | } 57 | -------------------------------------------------------------------------------- /man/ugd_save_inline.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd_save_inline} 4 | \alias{ugd_save_inline} 5 | \title{Inline plot rendering to a file.} 6 | \usage{ 7 | ugd_save_inline( 8 | code, 9 | file, 10 | page = 0, 11 | width = getOption("unigd.width", 720), 12 | height = getOption("unigd.height", 576), 13 | zoom = 1, 14 | as = "auto", 15 | ... 16 | ) 17 | } 18 | \arguments{ 19 | \item{code}{Plotting code. See examples for more information.} 20 | 21 | \item{file}{Filepath to save plot.} 22 | 23 | \item{page}{Plot page to render. If this is set to \code{0}, the last page will 24 | be selected. Can be set to a numeric plot index or plot ID 25 | (see \code{\link[=ugd_id]{ugd_id()}}).} 26 | 27 | \item{width}{Width of the plot.} 28 | 29 | \item{height}{Height of the plot.} 30 | 31 | \item{zoom}{Zoom level. (For example: \code{2} corresponds to 200\%, \code{0.5} would 32 | be 50\%.)} 33 | 34 | \item{as}{Renderer.} 35 | 36 | \item{...}{Additional parameters passed to \code{ugd(...)}} 37 | } 38 | \value{ 39 | No return value. Plot will be saved to file. 40 | } 41 | \description{ 42 | Convenience function for quick inline plot rendering. 43 | This is similar to \code{\link[=ugd_save]{ugd_save()}} but the plotting code 44 | is specified inline and an unigd graphics device is managed 45 | (created and closed) automatically. Starting a device with \code{\link[=ugd]{ugd()}} is 46 | therefore not necessary. 47 | } 48 | \examples{ 49 | tf <- tempfile(fileext=".svg") 50 | on.exit(unlink(tf)) 51 | 52 | ugd_save_inline({ 53 | plot.new() 54 | lines(c(0.5, 1, 0.5), c(0.5, 1, 1)) 55 | }, file = tf) 56 | } 57 | -------------------------------------------------------------------------------- /src/r_thread_posix.cpp: -------------------------------------------------------------------------------- 1 | #ifndef _WIN32 2 | #include // for addInputHandler() 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "r_thread.h" 10 | 11 | namespace unigd 12 | { 13 | namespace async 14 | { 15 | namespace 16 | { 17 | const int UNIGD_ACTIVITY_ID = 513; 18 | const size_t UNIGD_PIPE_BUFFER_SIZE = 32; 19 | threadsafe_queue work_queue; 20 | int message_fd[2]; 21 | char message_buf[UNIGD_PIPE_BUFFER_SIZE]; 22 | InputHandler* message_input_handle; 23 | 24 | inline void r_print_error(const char* message) 25 | { 26 | REprintf("Error (httpgd IPC): %s\n", message); 27 | } 28 | 29 | inline void process_tasks() 30 | { 31 | function_wrapper task; 32 | while (work_queue.try_pop(task)) 33 | { 34 | task.call(); 35 | } 36 | } 37 | 38 | inline void notify_work() 39 | { 40 | if (write(message_fd[1], "h", 1) == -1) 41 | { 42 | r_print_error("Could not write to pipe"); 43 | } 44 | } 45 | 46 | inline void empty_pipe() 47 | { 48 | if (read(message_fd[0], message_buf, UNIGD_PIPE_BUFFER_SIZE) == -1) 49 | { 50 | r_print_error("Could not read from pipe"); 51 | } 52 | } 53 | 54 | void input_handler(void* userData) 55 | { 56 | empty_pipe(); 57 | process_tasks(); 58 | } 59 | } // namespace 60 | 61 | void ipc_open() 62 | { 63 | if (pipe(message_fd) == -1) 64 | { 65 | r_print_error("Could not create pipe"); 66 | } 67 | 68 | message_input_handle = 69 | addInputHandler(R_InputHandlers, message_fd[0], input_handler, UNIGD_ACTIVITY_ID); 70 | } 71 | 72 | void ipc_close() 73 | { 74 | removeInputHandler(&R_InputHandlers, message_input_handle); 75 | close(message_fd[0]); 76 | close(message_fd[1]); 77 | } 78 | 79 | void r_thread_impl(function_wrapper&& task) 80 | { 81 | work_queue.push(std::move(task)); 82 | notify_work(); 83 | } 84 | } // namespace async 85 | } // namespace unigd 86 | 87 | #endif -------------------------------------------------------------------------------- /src/compress.cpp: -------------------------------------------------------------------------------- 1 | #include "compress.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace unigd 9 | { 10 | namespace compr 11 | { 12 | template 13 | inline std::vector compressToGzip(const charTypeIn *input, size_t inputSize) 14 | { 15 | static_assert(sizeof(charTypeIn) == 1, "input not a char type"); 16 | static_assert(sizeof(charTypeOut) == 1, "output not a char type vector"); 17 | 18 | z_stream zs; 19 | zs.zalloc = Z_NULL; 20 | zs.zfree = Z_NULL; 21 | zs.opaque = Z_NULL; 22 | zs.avail_in = static_cast(inputSize); 23 | zs.next_in = (Bytef *)input; 24 | 25 | int ret = deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, 26 | Z_DEFAULT_STRATEGY); 27 | if (ret != Z_OK) 28 | { 29 | return std::vector{}; 30 | } 31 | 32 | std::vector buffer; 33 | 34 | for (;;) 35 | { 36 | const int chunk_size = 16384; 37 | const size_t old_size = buffer.size(); 38 | buffer.resize(buffer.size() + chunk_size); 39 | 40 | zs.avail_out = chunk_size; 41 | zs.next_out = reinterpret_cast(&buffer[old_size]); 42 | ret = deflate(&zs, Z_FINISH); 43 | if (ret == Z_STREAM_ERROR) 44 | { 45 | deflateEnd(&zs); 46 | return std::vector{}; 47 | } 48 | buffer.resize(old_size + (chunk_size - zs.avail_out)); 49 | 50 | if (zs.avail_out != 0) 51 | { 52 | break; 53 | } 54 | } 55 | deflateEnd(&zs); 56 | return buffer; 57 | } 58 | 59 | std::vector compress(const uint8_t *input, size_t input_size) 60 | { 61 | return compressToGzip(input, input_size); 62 | } 63 | 64 | std::vector compress_str(const std::string &s) 65 | { 66 | return compressToGzip(s.c_str(), s.size()); 67 | } 68 | 69 | } // namespace compr 70 | 71 | } // namespace unigd 72 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | test-coverage: 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: r-lib/actions/setup-r@v2 23 | with: 24 | use-public-rspm: true 25 | 26 | - uses: r-lib/actions/setup-r-dependencies@v2 27 | with: 28 | extra-packages: any::covr, any::xml2 29 | needs: coverage 30 | 31 | - name: Test coverage 32 | run: | 33 | cov <- covr::package_coverage( 34 | quiet = FALSE, 35 | clean = FALSE, 36 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 37 | ) 38 | covr::to_cobertura(cov) 39 | shell: Rscript {0} 40 | 41 | - uses: codecov/codecov-action@v4 42 | with: 43 | fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }} 44 | file: ./cobertura.xml 45 | plugin: noop 46 | disable_search: true 47 | token: ${{ secrets.CODECOV_TOKEN }} 48 | 49 | - name: Show testthat output 50 | if: always() 51 | run: | 52 | ## -------------------------------------------------------------------- 53 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 54 | shell: bash 55 | 56 | - name: Upload test results 57 | if: failure() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: coverage-test-failures 61 | path: ${{ runner.temp }}/package -------------------------------------------------------------------------------- /tests/testthat/test-svglite-path.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("paths with winding fill mode", { 3 | x <- xmlSVG({ 4 | plot.new() 5 | polypath(c(.1, .1, .9, .9, NA, .2, .2, .8, .8), 6 | c(.1, .9, .9, .1, NA, .2, .8, .8, .2), 7 | col = rgb(0.5, 0.5, 0.5, 0.3), border = rgb(1, 0, 0, 0.3), 8 | rule = "winding") 9 | }) 10 | path <- xml2::xml_find_first(x, ".//d1:path") 11 | expect_equal(style_attr(path, "fill-rule"), "nonzero") 12 | expect_equal(style_attr(path, "fill"), rgb(0.5, 0.5, 0.5)) 13 | expect_equal(style_attr(path, "fill-opacity"), "0.30") 14 | expect_equal(style_attr(path, "stroke"), rgb(1, 0, 0)) 15 | expect_equal(style_attr(path, "stroke-opacity"), "0.30") 16 | }) 17 | 18 | test_that("paths with evenodd fill mode", { 19 | x <- xmlSVG({ 20 | plot.new() 21 | polypath(c(.1, .1, .9, .9, NA, .2, .2, .8, .8), 22 | c(.1, .9, .9, .1, NA, .2, .8, .8, .2), 23 | col = rgb(0.5, 0.5, 0.5, 0.3), border = rgb(1, 0, 0, 0.3), 24 | rule = "evenodd") 25 | }) 26 | path <- xml2::xml_find_first(x, ".//d1:path") 27 | expect_equal(style_attr(path, "fill-rule"), "evenodd") 28 | expect_equal(style_attr(path, "fill"), rgb(0.5, 0.5, 0.5)) 29 | expect_equal(style_attr(path, "fill-opacity"), "0.30") 30 | expect_equal(style_attr(path, "stroke"), rgb(1, 0, 0)) 31 | expect_equal(style_attr(path, "stroke-opacity"), "0.30") 32 | }) 33 | 34 | test_that("paths with no filling color", { 35 | x <- xmlSVG({ 36 | plot.new() 37 | polypath(c(.1, .1, .9, .9, NA, .2, .2, .8, .8), 38 | c(.1, .9, .9, .1, NA, .2, .8, .8, .2), 39 | col = NA, border = rgb(1, 0, 0, 0.3), 40 | rule = "winding") 41 | }) 42 | style <- xml2::xml_text(xml2::xml_find_first(x, "//d1:style")) 43 | expect_match(style, "fill: none;") 44 | 45 | path <- xml2::xml_find_first(x, ".//d1:path") 46 | expect_equal(style_attr(path, "fill-rule"), "nonzero") 47 | expect_equal(style_attr(path, "fill"), NA_character_) 48 | expect_equal(style_attr(path, "stroke"), rgb(1, 0, 0)) 49 | expect_equal(style_attr(path, "stroke-opacity"), "0.30") 50 | }) -------------------------------------------------------------------------------- /man/ugd.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd.R 3 | \name{ugd} 4 | \alias{ugd} 5 | \title{A unified R graphics backend.} 6 | \usage{ 7 | ugd( 8 | width = getOption("unigd.width", 720), 9 | height = getOption("unigd.height", 576), 10 | bg = getOption("unigd.bg", "white"), 11 | pointsize = getOption("unigd.pointsize", 12), 12 | system_fonts = getOption("unigd.system_fonts", list()), 13 | user_fonts = getOption("unigd.user_fonts", list()), 14 | reset_par = getOption("unigd.reset_par", FALSE) 15 | ) 16 | } 17 | \arguments{ 18 | \item{width}{Graphics device width (pixels).} 19 | 20 | \item{height}{Graphics device height (pixels).} 21 | 22 | \item{bg}{Background color.} 23 | 24 | \item{pointsize}{Graphics device point size.} 25 | 26 | \item{system_fonts}{Named list of font names to be aliased with 27 | fonts installed on your system. If unspecified, the R default 28 | families \code{sans}, \code{serif}, \code{mono} and \code{symbol} 29 | are aliased to the family returned by 30 | \code{\link[systemfonts:font_info]{systemfonts::font_info()}}.} 31 | 32 | \item{user_fonts}{Named list of fonts to be aliased with font files 33 | provided by the user rather than fonts properly installed on the 34 | system. The aliases can be fonts from the fontquiver package, 35 | strings containing a path to a font file, or a list containing 36 | \code{name} and \code{file} elements with \code{name} indicating 37 | the font alias in the SVG output and \code{file} the path to a 38 | font file.} 39 | 40 | \item{reset_par}{If set to \code{TRUE}, global graphics parameters will be saved 41 | on device start and reset every time \code{\link[=ugd_clear]{ugd_clear()}} is called (see 42 | \code{\link[graphics:par]{graphics::par()}}).} 43 | } 44 | \value{ 45 | No return value, called to initialize graphics device. 46 | } 47 | \description{ 48 | This function initializes a unigd graphics device. 49 | } 50 | \details{ 51 | All font settings and descriptions are adopted from the excellent 52 | 'svglite' package. 53 | } 54 | \examples{ 55 | ugd() # Initialize graphics device 56 | 57 | # Plot something 58 | x <- seq(0, 3 * pi, by = 0.1) 59 | plot(x, sin(x), type = "l") 60 | 61 | # Render plot as SVG 62 | ugd_render(width = 600, height = 400, as = "svg") 63 | 64 | dev.off() # alternatively: ugd_close() 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `unigd` 2 | 3 | 4 | 5 | [![R-CMD-check](https://github.com/nx10/unigd/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/nx10/unigd/actions/workflows/R-CMD-check.yaml) 6 | [![CRAN](https://www.r-pkg.org/badges/version/unigd)](https://CRAN.R-project.org/package=unigd) 7 | ![downloads](https://cranlogs.r-pkg.org/badges/grand-total/unigd) 8 | [![Codecov test coverage](https://codecov.io/gh/nx10/unigd/branch/master/graph/badge.svg)](https://app.codecov.io/gh/nx10/unigd?branch=master) 9 | 10 | 11 | A unified R graphics backend. Render R graphics fast and easy to many common file formats. 12 | 13 | This package provides a thread-safe interface to power interactive graphics clients such as [`httpgd`](https://github.com/nx10/httpgd). 14 | 15 | ## Features 16 | 17 | * Fast plotting 18 | * Plot resizing and history 19 | * Render to various image formats (SVG, PNG, PDF, EPS, ...) 20 | * In-memory access to rendered graphics 21 | * Platform independent 22 | * Stateless thread-safe C client API for package developers 23 | 24 | ## Installation 25 | 26 | Install `unigd` from CRAN: 27 | 28 | ```R 29 | install.packages("unigd") 30 | ``` 31 | 32 | Or get the latest development version from GitHub: 33 | 34 | ```R 35 | remotes::install_github("nx10/unigd") 36 | ``` 37 | 38 | See [system requirements](https://nx10.github.io/unigd/articles/a00_installation.html#system-requirements) for troubleshooting. 39 | 40 | ## Getting started 41 | 42 | See [the guide](https://nx10.github.io/unigd/articles/b00_guide.html) for more details. 43 | 44 | ## Contributions welcome! 45 | 46 | `unigd` is mostly written in C++, but contributions to the tests (in R) or the documentation are also always welcome! 47 | 48 | ## About & License 49 | 50 | Depends on `cpp11` and `systemfonts`. 51 | 52 | Much of the font handling and SVG rendering code is modified code from the excellent [`svglite`]() package. 53 | 54 | This project is licensed GPL v2.0. 55 | 56 | It includes parts of [`svglite`]() (GPL ≥ 2) and [`fmt`](https://github.com/fmtlib/fmt) (MIT). 57 | 58 | Full copies of the license agreements used by these components are included in [`./inst/licenses`](https://github.com/nx10/unigd/tree/master/inst/licenses). 59 | -------------------------------------------------------------------------------- /tests/testthat/test-history.R: -------------------------------------------------------------------------------- 1 | test_that("Create pages", { 2 | ugd() 3 | pnum <- 10 4 | for (i in 1:pnum) { 5 | plot.new() 6 | } 7 | hs <- ugd_state() 8 | dev.off() 9 | expect_equal(hs$hsize, pnum) 10 | }) 11 | 12 | test_that("Delete pages", { 13 | ugd() 14 | pnum <- 10 15 | dnum <- 3 16 | for (i in 1:pnum) { 17 | plot.new() 18 | } 19 | for (i in 1:dnum) { 20 | ugd_remove() 21 | } 22 | hs <- ugd_state() 23 | dev.off() 24 | expect_equal(hs$hsize, pnum - dnum) 25 | }) 26 | 27 | test_that("Get page by index", { 28 | ugd() 29 | pnum <- 10 30 | dnum <- 3 31 | for (i in seq_len(pnum)) { 32 | plot.new() 33 | teststr <- paste0("123abc_plot_", i) 34 | text(0, 0, teststr) 35 | } 36 | json_out_4 <- ugd_render(page = 4, as = "json") 37 | json_out_4neg <- ugd_render(page = -4, as = "json") 38 | json_out_1 <- ugd_render(page = 1, as = "json") 39 | json_out_0 <- ugd_render(page = 0, as = "json") 40 | json_out_10 <- ugd_render(page = 10, as = "json") 41 | json_out_9neg <- ugd_render(page = -9, as = "json") 42 | expect_error(ugd_render(page = 11, as = "json")) 43 | expect_error(ugd_render(page = -10, as = "json")) 44 | dev.off() 45 | expect_true(grepl("123abc_plot_4", json_out_4, fixed = TRUE)) 46 | expect_true(grepl("123abc_plot_6", json_out_4neg, fixed = TRUE)) 47 | expect_true(grepl("123abc_plot_10", json_out_0, fixed = TRUE)) 48 | expect_true(grepl("123abc_plot_1", json_out_1, fixed = TRUE)) 49 | expect_true(grepl("123abc_plot_10", json_out_10, fixed = TRUE)) 50 | expect_true(grepl("123abc_plot_1", json_out_9neg, fixed = TRUE)) 51 | }) 52 | 53 | test_that("Delete page by index", { 54 | ugd() 55 | pnum <- 10 56 | dnum <- 3 57 | for (i in 1:pnum) { 58 | plot.new() 59 | teststr <- paste0("123abc_plot_", i) 60 | text(0, 0, teststr) 61 | } 62 | ugd_remove(page = 4) 63 | hs <- ugd_state() 64 | svgs <- rep(NA, hs$hsize) 65 | for (i in 1:hs$hsize) { 66 | svgs[i] <- ugd_render(page = i, as = "json") 67 | } 68 | dev.off() 69 | expect_true(grepl("123abc_plot_3", svgs[3], fixed = TRUE)) 70 | expect_true(grepl("123abc_plot_5", svgs[4], fixed = TRUE)) 71 | }) 72 | 73 | test_that("Clear pages", { 74 | ugd() 75 | pnum <- 10 76 | for (i in 1:pnum) { 77 | plot.new() 78 | } 79 | ugd_clear() 80 | hs <- ugd_state() 81 | dev.off() 82 | expect_equal(hs$hsize, 0) 83 | }) 84 | -------------------------------------------------------------------------------- /man/unigd-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/unigd-package.R 3 | \docType{package} 4 | \name{unigd-package} 5 | \alias{unigd} 6 | \alias{unigd-package} 7 | \title{unigd: Universal graphics device} 8 | \description{ 9 | Universal graphics device 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://github.com/nx10/unigd} 15 | \item \url{https://nx10.github.io/unigd/} 16 | \item Report bugs at \url{https://github.com/nx10/unigd/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Florian Rupprecht \email{floruppr@gmail.com} (\href{https://orcid.org/0000-0002-1795-8624}{ORCID}) 22 | 23 | Other contributors: 24 | \itemize{ 25 | \item Kun Ren \email{mail@renkun.me} [contributor] 26 | \item Tatsuya Shima \email{ts1s1andn@gmail.com} [contributor] 27 | \item Jeroen Ooms \email{jeroen@berkeley.edu} (\href{https://orcid.org/0000-0002-4035-0289}{ORCID}) [contributor] 28 | \item Hadley Wickham \email{hadley@rstudio.com} (Author of included svglite code) [copyright holder] 29 | \item Lionel Henry \email{lionel@rstudio.com} (Author of included svglite code) [copyright holder] 30 | \item Thomas Lin Pedersen \email{thomas.pedersen@rstudio.com} (Author and creator of included svglite code) [copyright holder] 31 | \item T Jake Luciani \email{jake@apache.org} (Author of included svglite code) [copyright holder] 32 | \item Matthieu Decorde \email{matthieu.decorde@ens-lyon.fr} (Author of included svglite code) [copyright holder] 33 | \item Vaudor Lise \email{lise.vaudor@ens-lyon.fr} (Author of included svglite code) [copyright holder] 34 | \item Tony Plate (Contributor to included svglite code) [copyright holder] 35 | \item David Gohel (Contributor to included svglite code) [copyright holder] 36 | \item Yixuan Qiu (Contributor to included svglite code) [copyright holder] 37 | \item Håkon Malmedal (Contributor to included svglite code) [copyright holder] 38 | \item RStudio (Copyright holder of included svglite code) [copyright holder] 39 | \item Brett Robinson (Author of included belle library) [copyright holder] 40 | \item Google (Copyright holder of included material design icons) [copyright holder] 41 | \item Victor Zverovich (Author of included fmt library) [copyright holder] 42 | \item Andrzej Krzemienski (Author of included std::experimental::optional library) [copyright holder] 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | # utils.R 2 | # from: https://github.com/r-lib/svglite/blob/master/R/utils.R 3 | 4 | mini_plot <- function(...) graphics::plot(..., axes = FALSE, xlab = "", ylab = "") 5 | 6 | plot_dim <- function(dim = c(NA, NA)) { 7 | if (any(is.na(dim))) { 8 | if (length(grDevices::dev.list()) == 0) { 9 | default_dim <- c(10, 8) 10 | } else { 11 | default_dim <- grDevices::dev.size() 12 | } 13 | 14 | dim[is.na(dim)] <- default_dim[is.na(dim)] 15 | dim_f <- prettyNum(dim, digits = 3) 16 | 17 | message("Saving ", dim_f[1], "\" x ", dim_f[2], "\" image") 18 | } 19 | 20 | dim 21 | } 22 | 23 | vapply_chr <- function(.x, .f, ...) { 24 | vapply(.x, .f, character(1), ...) 25 | } 26 | vapply_lgl <- function(.x, .f, ...) { 27 | vapply(.x, .f, logical(1), ...) 28 | } 29 | lapply_if <- function(.x, .p, .f, ...) { 30 | if (!is.logical(.p)) { 31 | .p <- vapply_lgl(.x, .p) 32 | } 33 | .x[.p] <- lapply(.x[.p], .f, ...) 34 | .x 35 | } 36 | keep <- function(.x, .p, ...) { 37 | .x[vapply_lgl(.x, .p, ...)] 38 | } 39 | compact <- function(x) { 40 | Filter(length, x) 41 | } 42 | `%||%` <- function(x, y) { 43 | if (is.null(x)) y else x 44 | } 45 | is_scalar_character <- function(x) { 46 | is.character(x) && length(x) == 1 47 | } 48 | names2 <- function(x) { 49 | names(x) %||% rep("", length(x)) 50 | } 51 | ilapply <- function(.x, .f, ...) { 52 | idx <- names(.x) %||% seq_along(.x) 53 | out <- Map(.f, names(.x), .x, ...) 54 | names(out) <- names(.x) 55 | out 56 | } 57 | ilapply_if <- function(.x, .p, .f, ...) { 58 | if (!is.logical(.p)) { 59 | .p <- vapply_lgl(.x, .p) 60 | } 61 | .x[.p] <- ilapply(.x[.p], .f, ...) 62 | .x 63 | } 64 | set_names <- function(x, nm = x) { 65 | stats::setNames(x, nm) 66 | } 67 | zip <- function(.l) { 68 | fields <- set_names(names(.l[[1]])) 69 | lapply(fields, function(i) { 70 | lapply(.l, .subset2, i) 71 | }) 72 | } 73 | 74 | invalid_filename <- function(filename) { 75 | if (!is.character(filename) || length(filename) != 1) { 76 | return(TRUE) 77 | } 78 | 79 | # strip double occurences of % 80 | stripped_file <- gsub("%{2}", "", filename) 81 | # filename is fine if there are no % left 82 | if (!grepl("%", stripped_file)) { 83 | return(FALSE) 84 | } 85 | # remove first allowed pattern, % followed by digits followed by [diouxX] 86 | stripped_file <- sub("%[#0 ,+-]*[0-9.]*[diouxX]", "", stripped_file) 87 | # matching leftover % indicates multiple patterns or a single incorrect pattern (e.g., %s) 88 | return(grepl("%", stripped_file)) 89 | } 90 | -------------------------------------------------------------------------------- /vignettes/a00_installation.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Installation} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | 18 | 19 | You can install the development version of `unigd` from GitHub with: 20 | 21 | ``` r 22 | # install.packages("remotes") 23 | remotes::install_github("nx10/unigd") 24 | ``` 25 | 26 | See [system requirements](#System-requirements) for troubleshooting. 27 | 28 | ## System requirements 29 | 30 | `libpng` and X11 are **required** on unix like systems (e.g. Linux, macOS). 31 | `Cairo` is optional on unix like systems to enable PNG, PDF, EPS and PS renderers. 32 | `libtiff` is required on unix like systems to enable TIFF renderers. 33 | 34 | ### macOS 35 | 36 | If `libpng` is missing install it via: 37 | 38 | ```sh 39 | brew install libpng 40 | ``` 41 | 42 | If `Cairo` is missing install it via: 43 | 44 | ```sh 45 | brew install cairo 46 | ``` 47 | 48 | If `libtiff` is missing install it via: 49 | 50 | ```sh 51 | brew install libtiff 52 | ``` 53 | 54 | If `X11` is missing the error message will include the text: 55 | 56 | ```sh 57 | unable to load shared object [...] systemfonts/libs/systemfonts.so [...] 58 | ``` 59 | 60 | Install [`XQuartz`](https://www.xquartz.org/). 61 | (see: ) 62 | 63 | ### Linux 64 | 65 | For source installation on Linux, the fontconfig freetype2 library is required to install the `{systemfonts}` package, which is a dependency of `unigd`. 66 | 67 | #### Debian, Ubuntu, etc. 68 | 69 | ```sh 70 | apt install libfontconfig1-dev 71 | ``` 72 | 73 | #### Fedora, CentOS, RHEL, etc. 74 | 75 | ```sh 76 | dnf install fontconfig-devel 77 | ``` 78 | 79 | To support additional plot file formats (PDF, EPS, PS) optionally, the `Cairo` library is required. 80 | 81 | #### Debian, Ubuntu, etc. 82 | 83 | ```sh 84 | apt install libcairo2-dev 85 | ``` 86 | 87 | #### Fedora, CentOS, RHEL, etc. 88 | 89 | ```sh 90 | dnf install cairo-devel 91 | ``` 92 | 93 | To support additional TIFF formats optionally, the `libtiff` library is required. 94 | 95 | #### Debian, Ubuntu, etc. 96 | 97 | ```sh 98 | apt install libtiff-dev 99 | ``` 100 | 101 | #### Fedora, EPEL, etc. 102 | 103 | ```sh 104 | dnf install libtiff-devel 105 | ``` 106 | -------------------------------------------------------------------------------- /src/plot_history.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | #include 5 | 6 | // clang-format off 7 | #define R_NO_REMAP 8 | #include 9 | #include 10 | // clang-format on 11 | 12 | #include 13 | 14 | #include "debug_print.h" 15 | #include "plot_history.h" 16 | 17 | namespace unigd 18 | { 19 | bool PlotHistory::replay_current(pDevDesc dd) 20 | { 21 | pGEDevDesc gdd = desc2GEDesc(dd); 22 | if (gdd->dirty) 23 | { // avoid trying to replay list if there has been no drawing 24 | try 25 | { 26 | cpp11::safe[GEplayDisplayList](gdd); 27 | } 28 | catch (...) 29 | { 30 | debug_print("GEplayDisplayList error\n"); 31 | return false; 32 | } 33 | } 34 | return true; 35 | } 36 | 37 | PlotHistory::PlotHistory() : m_items() {} 38 | void PlotHistory::put(R_xlen_t t_index, SEXP t_snapshot) 39 | { 40 | if (m_items.size() <= t_index) 41 | { 42 | m_items.resize(t_index + 1); 43 | } 44 | m_items[t_index] = t_snapshot; 45 | } 46 | bool PlotHistory::put_current(R_xlen_t t_index, pDevDesc dd) 47 | { 48 | pGEDevDesc gdd = desc2GEDesc(dd); 49 | if (gdd->displayList != R_NilValue) 50 | { 51 | try 52 | { 53 | put(t_index, cpp11::safe[GEcreateSnapshot](gdd)); 54 | } 55 | catch (...) 56 | { 57 | debug_print("GEcreateSnapshot error\n"); 58 | return false; 59 | } 60 | return true; 61 | } 62 | return false; 63 | } 64 | void PlotHistory::put_last(R_xlen_t t_index, pDevDesc dd) 65 | { 66 | put(t_index, desc2GEDesc(dd)->savedSnapshot); 67 | } 68 | void PlotHistory::clear() { m_items.clear(); } 69 | bool PlotHistory::play(R_xlen_t t_index, pDevDesc dd) 70 | { 71 | SEXP snap = R_NilValue; 72 | if (get(t_index, &snap)) 73 | { 74 | try 75 | { 76 | cpp11::safe[GEplaySnapshot](snap, desc2GEDesc(dd)); 77 | } 78 | catch (...) 79 | { 80 | debug_print("GEplaySnapshot error\n"); 81 | return false; 82 | } 83 | return true; 84 | } 85 | return false; 86 | } 87 | bool PlotHistory::get(R_xlen_t t_index, SEXP *t_snapshot) 88 | { 89 | if (m_items.size() <= t_index) 90 | { 91 | *t_snapshot = R_NilValue; 92 | return false; 93 | } 94 | *t_snapshot = m_items[t_index]; 95 | return *t_snapshot != R_NilValue; 96 | } 97 | 98 | bool PlotHistory::remove(R_xlen_t t_index) 99 | { 100 | if (m_items.size() <= t_index) 101 | { 102 | return false; 103 | } 104 | m_items.erase(t_index); 105 | return true; 106 | } 107 | 108 | } // namespace unigd -------------------------------------------------------------------------------- /src/page_store.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_PAGE_STORE_H__ 2 | #define __UNIGD_PAGE_STORE_H__ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "geom.h" 15 | #include "renderers.h" 16 | #include "unigd_external.h" 17 | 18 | namespace unigd 19 | { 20 | class page_store 21 | { 22 | public: 23 | page_store() = default; 24 | 25 | page_store(const page_store &) = delete; 26 | page_store &operator=(page_store &) = delete; 27 | page_store &operator=(const page_store &) = delete; 28 | 29 | page_store(page_store &&) = delete; 30 | page_store &operator=(page_store &&) = delete; 31 | 32 | std::experimental::optional find_index(ex::plot_id_t t_id); 33 | std::experimental::optional normalize_index( 34 | ex::plot_relative_t t_index); 35 | 36 | bool render(ex::plot_relative_t t_index, renderers::render_target *t_renderer, 37 | double t_scale); 38 | bool render_if_size(ex::plot_relative_t t_index, renderers::render_target *t_renderer, 39 | double t_scale, gvertex t_target_size); 40 | 41 | ex::plot_index_t append(gvertex t_size); 42 | void clear(ex::plot_relative_t t_index, bool t_silent); 43 | bool remove(ex::plot_relative_t t_index, bool t_silent); 44 | bool remove_all(); 45 | void resize(ex::plot_relative_t t_index, gvertex t_size); 46 | gvertex size(ex::plot_relative_t t_index); 47 | 48 | void fill(ex::plot_relative_t t_index, color_t t_fill); 49 | void add_dc(ex::plot_relative_t t_index, std::unique_ptr &&t_dc, 50 | bool t_silent); 51 | void add_dc(ex::plot_relative_t t_index, 52 | std::vector> &&t_dcs, bool t_silent); 53 | void clip(ex::plot_relative_t t_index, grect t_rect); 54 | 55 | ex::device_state state(); 56 | void set_device_active(bool t_active); 57 | 58 | ex::find_results query(ex::plot_relative_t t_offset, ex::plot_index_t t_limit); 59 | 60 | void extra_css(std::experimental::optional t_extra_css); 61 | 62 | private: 63 | std::shared_timed_mutex m_store_mutex; 64 | 65 | ex::plot_id_t m_id_counter = 0; 66 | std::vector m_pages{}; 67 | int m_upid = 0; 68 | bool m_device_active = true; 69 | 70 | std::experimental::optional m_extra_css{}; 71 | 72 | void m_inc_upid(); 73 | 74 | inline bool m_valid_index(ex::plot_relative_t t_index); 75 | inline size_t m_index_to_pos(ex::plot_relative_t t_index); 76 | }; 77 | 78 | } // namespace unigd 79 | 80 | #endif /* __UNIGD_PAGE_STORE_H__ */ 81 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-text.R: -------------------------------------------------------------------------------- 1 | test_that("par(cex) affects strwidth", { 2 | xmlSVG({ 3 | plot.new() 4 | w1 <- strwidth("X") 5 | par(cex = 4) 6 | w4 <- strwidth("X") 7 | }) 8 | expect_equal(w4 / w1, 4, tolerance = 1e-3) 9 | }) 10 | 11 | test_that("cex affects strwidth", { 12 | ugd_render_inline(height = 7 * 72, width = 7 * 72, { 13 | plot.new() 14 | w1 <- strwidth("X") 15 | w4 <- strwidth("X", cex = 4) 16 | }) 17 | expect_equal(w4 / w1, 4, tolerance = 1e-3) 18 | }) 19 | 20 | test_that("special characters are escaped", { 21 | x <- xmlSVG({ 22 | plot.new() 23 | text(0.5, 0.5, "<&>") 24 | }) 25 | # xml_text unescapes for us - this still tests that the 26 | # file parses, which it wouldn't otherwise 27 | expect_equal(xml2::xml_text(xml2::xml_find_first(x, ".//d1:text")), "<&>") 28 | }) 29 | 30 | test_that("utf-8 characters are preserved", { 31 | skip_on_os("windows") # skip because of xml2 buglet 32 | skip_if_not(l10n_info()$`UTF-8`) 33 | 34 | x <- xmlSVG({ 35 | plot.new() 36 | text(0.5, 0.5, "\u00b5") 37 | }) 38 | # xml_text unescapes for us - this still tests that the 39 | # file parses, which it wouldn't otherwise 40 | expect_equal(xml2::xml_text(xml2::xml_find_first(x, ".//d1:text")), "\u00b5") 41 | }) 42 | 43 | test_that("special characters are escaped", { 44 | x <- xmlSVG({ 45 | plot.new() 46 | text(0.5, 0.5, "a", col = "#113399") 47 | }) 48 | # xml_text unescapes for us - this still tests that the 49 | # file parses, which it wouldn't otherwise 50 | expect_equal(style_attr(xml2::xml_find_first(x, ".//d1:text"), "fill"), "#113399") 51 | }) 52 | 53 | test_that("default point size is 12", { 54 | x <- xmlSVG({ 55 | plot.new() 56 | text(0.5, 0.5, "a") 57 | }) 58 | expect_equal(style_attr(xml2::xml_find_first(x, ".//d1:text"), "font-size"), "12.00px") 59 | }) 60 | 61 | test_that("cex generates fractional font sizes", { 62 | x <- xmlSVG({ 63 | plot.new() 64 | text(0.5, 0.5, "a", cex = 0.1) 65 | }) 66 | expect_equal(style_attr(xml2::xml_find_first(x, ".//d1:text"), "font-size"), "1.20px") 67 | }) 68 | 69 | test_that("a symbol has width greater than 0", { 70 | xmlSVG({ 71 | plot.new() 72 | strw <- strwidth(expression(symbol("\042"))) 73 | }) 74 | expect_lt(.Machine$double.eps, strw) 75 | }) 76 | 77 | 78 | # manual test 79 | #test_that("strwidth and height correctly computed", { 80 | # hgd(width=4 * 72, height=4 * 72, user_fonts = fontquiver::font_families("Bitstream Vera")) 81 | # 82 | # plot.new() 83 | # str <- "This is a string" 84 | # text(0.5, 0.5, str) 85 | # 86 | # h <- strheight(str) 87 | # w <- strwidth(str) 88 | # 89 | # rect(0.5 - w / 2, 0.5 - h / 2, 0.5 + w / 2, 0.5 + h / 2) 90 | #}) 91 | 92 | test_that("strwidth has fallback for unknown glyphs", { 93 | xmlSVG(user_fonts = fontquiver::font_families("Bitstream Vera"), { 94 | plot.new() 95 | w <- strwidth("正規分布") 96 | }) 97 | expect_true(w > 0) 98 | }) -------------------------------------------------------------------------------- /src/renderer_svg.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_RENDERER_SVG_H__ 2 | #define __UNIGD_RENDERER_SVG_H__ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "renderers.h" 10 | 11 | namespace unigd 12 | { 13 | namespace renderers 14 | { 15 | class RendererSVG : public render_target, public draw_call_visitor 16 | { 17 | public: 18 | explicit RendererSVG(std::experimental::optional t_extra_css); 19 | void render(const Page &t_page, double t_scale) override; 20 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 21 | 22 | // Renderer 23 | void page(const Page &t_page); 24 | void visit(const Rect *t_rect) override; 25 | void visit(const Text *t_text) override; 26 | void visit(const Circle *t_circle) override; 27 | void visit(const Line *t_line) override; 28 | void visit(const Polyline *t_polyline) override; 29 | void visit(const Polygon *t_polygon) override; 30 | void visit(const Path *t_path) override; 31 | void visit(const Raster *t_raster) override; 32 | 33 | private: 34 | fmt::memory_buffer os; 35 | std::experimental::optional m_extra_css; 36 | double m_scale; 37 | }; 38 | 39 | /** 40 | * Produces SVG that can directly be embedded in HTML documents 41 | * without causing ID conflicts at the expense of larger file size. 42 | * - Does not use style tags or CDATA embedded CSS. 43 | * - Appends random UUID to document-wide (clipPath) IDs. 44 | */ 45 | class RendererSVGPortable : public render_target, public draw_call_visitor 46 | { 47 | public: 48 | RendererSVGPortable(); 49 | void render(const Page &t_page, double t_scale) override; 50 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 51 | 52 | // Renderer 53 | void page(const Page &t_page); 54 | void visit(const Rect *t_rect) override; 55 | void visit(const Text *t_text) override; 56 | void visit(const Circle *t_circle) override; 57 | void visit(const Line *t_line) override; 58 | void visit(const Polyline *t_polyline) override; 59 | void visit(const Polygon *t_polygon) override; 60 | void visit(const Path *t_path) override; 61 | void visit(const Raster *t_raster) override; 62 | 63 | private: 64 | fmt::memory_buffer os; 65 | double m_scale; 66 | std::string m_unique_id; 67 | }; 68 | 69 | class RendererSVGZ : public RendererSVG 70 | { 71 | public: 72 | explicit RendererSVGZ(std::experimental::optional t_extra_css); 73 | void render(const Page &t_page, double t_scale) override; 74 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 75 | 76 | private: 77 | std::vector m_compressed; 78 | }; 79 | 80 | class RendererSVGZPortable : public RendererSVGPortable 81 | { 82 | public: 83 | RendererSVGZPortable(); 84 | void render(const Page &t_page, double t_scale) override; 85 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 86 | 87 | private: 88 | std::vector m_compressed; 89 | }; 90 | 91 | } // namespace renderers 92 | } // namespace unigd 93 | 94 | #endif /* __UNIGD_RENDERER_SVG_H__ */ -------------------------------------------------------------------------------- /src/renderer_cairo.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_RENDERER_CAIRO_H__ 2 | #define __UNIGD_RENDERER_CAIRO_H__ 3 | 4 | #ifndef UNIGD_NO_CAIRO 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "draw_data.h" 12 | #include "renderers.h" 13 | 14 | namespace unigd 15 | { 16 | namespace renderers 17 | { 18 | class RendererCairo : public draw_call_visitor 19 | { 20 | public: 21 | void visit(const Rect *t_rect) override; 22 | void visit(const Text *t_text) override; 23 | void visit(const Circle *t_circle) override; 24 | void visit(const Line *t_line) override; 25 | void visit(const Polyline *t_polyline) override; 26 | void visit(const Polygon *t_polygon) override; 27 | void visit(const Path *t_path) override; 28 | void visit(const Raster *t_raster) override; 29 | 30 | void render_page(const Page *t_page); 31 | 32 | protected: 33 | cairo_surface_t *surface = nullptr; 34 | cairo_t *cr = nullptr; 35 | }; 36 | 37 | class RendererCairoPng : public render_target, public RendererCairo 38 | { 39 | public: 40 | void render(const Page &t_page, double t_scale) override; 41 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 42 | 43 | private: 44 | std::vector m_render_data{}; 45 | }; 46 | 47 | class RendererCairoPngBase64 : public render_target, public RendererCairo 48 | { 49 | public: 50 | void render(const Page &t_page, double t_scale) override; 51 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 52 | 53 | private: 54 | std::string m_buf; 55 | }; 56 | 57 | class RendererCairoPdf : public render_target, public RendererCairo 58 | { 59 | public: 60 | void render(const Page &t_page, double t_scale) override; 61 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 62 | 63 | private: 64 | std::vector m_render_data{}; 65 | }; 66 | 67 | class RendererCairoPs : public render_target, public RendererCairo 68 | { 69 | public: 70 | void render(const Page &t_page, double t_scale) override; 71 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 72 | 73 | private: 74 | fmt::memory_buffer m_os; 75 | }; 76 | 77 | class RendererCairoEps : public render_target, public RendererCairo 78 | { 79 | public: 80 | void render(const Page &t_page, double t_scale) override; 81 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 82 | 83 | private: 84 | fmt::memory_buffer m_os; 85 | }; 86 | 87 | #ifndef UNIGD_NO_TIFF 88 | 89 | class RendererCairoTiff : public render_target, public RendererCairo 90 | { 91 | public: 92 | void render(const Page &t_page, double t_scale) override; 93 | void get_data(const uint8_t **t_buf, size_t *t_size) const override; 94 | 95 | private: 96 | std::vector m_render_data{}; 97 | }; 98 | 99 | #endif /* UNIGD_NO_TIFF */ 100 | 101 | } // namespace renderers 102 | } // namespace unigd 103 | 104 | #endif /* UNIGD_NO_CAIRO */ 105 | 106 | #endif /* __UNIGD_RENDERER_CAIRO_H__ */ 107 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: unigd 2 | Type: Package 3 | Title: Universal Graphics Device 4 | Version: 0.1.3 5 | Authors@R: 6 | c( 7 | person(given = "Florian", family = "Rupprecht", email = "floruppr@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-1795-8624")), 8 | person(given = "Kun", family = "Ren", role = "ctb", email = "mail@renkun.me"), 9 | person(given = "Tatsuya", family = "Shima", role = "ctb", email = "ts1s1andn@gmail.com"), 10 | person("Jeroen", "Ooms", role = c("ctb"), email = "jeroen@berkeley.edu", comment = c(ORCID = "0000-0002-4035-0289")), 11 | person("Hadley", "Wickham", email = "hadley@rstudio.com", role = "cph", comment = "Author of included svglite code"), 12 | person("Lionel", "Henry", email = "lionel@rstudio.com", role = "cph", comment = "Author of included svglite code"), 13 | person("Thomas Lin", "Pedersen", email = "thomas.pedersen@rstudio.com", role = "cph", comment = "Author and creator of included svglite code"), 14 | person("T Jake", "Luciani", email = "jake@apache.org", role = "cph", comment = "Author of included svglite code"), 15 | person("Matthieu", "Decorde", email = "matthieu.decorde@ens-lyon.fr", role = "cph", comment = "Author of included svglite code"), 16 | person("Vaudor", "Lise", email = "lise.vaudor@ens-lyon.fr", role = "cph", comment = "Author of included svglite code"), 17 | person("Tony", "Plate", role = "cph", comment = "Contributor to included svglite code"), 18 | person("David", "Gohel", role = "cph", comment = "Contributor to included svglite code"), 19 | person("Yixuan", "Qiu", role = "cph", comment = "Contributor to included svglite code"), 20 | person("Håkon", "Malmedal", role = "cph", comment = "Contributor to included svglite code"), 21 | person("RStudio", role = "cph", comment = "Copyright holder of included svglite code"), 22 | person("Brett", "Robinson", role = "cph", comment = "Author of included belle library"), 23 | person("Google", role = "cph", comment = "Copyright holder of included material design icons"), 24 | person("Victor", "Zverovich", role = "cph", comment = "Author of included fmt library"), 25 | person("Andrzej", "Krzemienski", role = "cph", comment = "Author of included std::experimental::optional library") 26 | ) 27 | Description: A unified R graphics backend. Render R graphics fast and easy to many common file formats. 28 | Provides a thread safe 'C' interface for asynchronous rendering of R graphics. 29 | License: GPL (>= 2) 30 | Depends: 31 | R (>= 3.2.0) 32 | Imports: 33 | systemfonts (>= 1.0.0) 34 | LinkingTo: 35 | cpp11 (>= 0.2.4), 36 | systemfonts 37 | Encoding: UTF-8 38 | SystemRequirements: 39 | libpng, 40 | cairo, 41 | freetype2, 42 | fontconfig 43 | Roxygen: list(markdown = TRUE) 44 | RoxygenNote: 7.3.2 45 | URL: https://github.com/nx10/unigd, 46 | https://nx10.github.io/unigd/ 47 | BugReports: https://github.com/nx10/unigd/issues 48 | Suggests: 49 | testthat (>= 3.0.0), 50 | xml2 (>= 1.0.0), 51 | fontquiver (>= 0.2.0), 52 | covr, 53 | knitr, 54 | rmarkdown 55 | Config/testthat/edition: 3 56 | Config/Needs/website: tidyverse/tidytemplate 57 | VignetteBuilder: knitr 58 | -------------------------------------------------------------------------------- /src/async_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_ASYNC_UTILS_H__ 2 | #define __UNIGD_ASYNC_UTILS_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace unigd 11 | { 12 | 13 | namespace async 14 | { 15 | 16 | template 17 | class threadsafe_queue 18 | { 19 | private: 20 | mutable std::mutex mut; 21 | std::queue data_queue; 22 | std::condition_variable data_cond; 23 | 24 | public: 25 | threadsafe_queue() {} 26 | threadsafe_queue(threadsafe_queue const &other) 27 | { 28 | std::lock_guard lk(other.mut); 29 | data_queue = other.data_queue; 30 | } 31 | void push(T &&new_value) 32 | { 33 | std::lock_guard lk(mut); 34 | data_queue.push(std::move(new_value)); 35 | data_cond.notify_one(); 36 | } 37 | void wait_and_pop(T &value) 38 | { 39 | std::unique_lock lk(mut); 40 | data_cond.wait(lk, [this] { return !data_queue.empty(); }); 41 | value = std::move(data_queue.front()); 42 | data_queue.pop(); 43 | } 44 | std::shared_ptr wait_and_pop() 45 | { 46 | std::unique_lock lk(mut); 47 | data_cond.wait(lk, [this] { return !data_queue.empty(); }); 48 | std::shared_ptr res(std::make_shared(data_queue.front())); 49 | data_queue.pop(); 50 | return res; 51 | } 52 | bool try_pop(T &value) 53 | { 54 | std::lock_guard lk(mut); 55 | if (data_queue.empty()) return false; 56 | value = std::move(data_queue.front()); 57 | data_queue.pop(); 58 | return true; 59 | } 60 | std::shared_ptr try_pop() 61 | { 62 | std::lock_guard lk(mut); 63 | if (data_queue.empty()) return std::shared_ptr(); 64 | std::shared_ptr res(std::make_shared(data_queue.front())); 65 | data_queue.pop(); 66 | return res; 67 | } 68 | bool empty() const 69 | { 70 | std::lock_guard lk(mut); 71 | return data_queue.empty(); 72 | } 73 | }; 74 | 75 | class function_wrapper 76 | { 77 | struct impl_base 78 | { 79 | virtual void call() = 0; 80 | virtual ~impl_base() {} 81 | }; 82 | std::unique_ptr impl; 83 | template 84 | struct impl_type : impl_base 85 | { 86 | F f; 87 | impl_type(F &&f_) : f(std::move(f_)) {} 88 | void call() { f(); } 89 | }; 90 | 91 | public: 92 | function_wrapper() = default; 93 | 94 | template 95 | function_wrapper(F &&f) : impl(new impl_type(std::move(f))) 96 | { 97 | } 98 | 99 | void call() { impl->call(); } 100 | 101 | function_wrapper(function_wrapper &&other) : impl(std::move(other.impl)) {} 102 | 103 | function_wrapper &operator=(function_wrapper &&other) 104 | { 105 | impl = std::move(other.impl); 106 | return *this; 107 | } 108 | 109 | function_wrapper(const function_wrapper &) = delete; 110 | function_wrapper(function_wrapper &) = delete; 111 | function_wrapper &operator=(const function_wrapper &) = delete; 112 | }; 113 | } // namespace async 114 | 115 | } // namespace unigd 116 | 117 | #endif /* __UNIGD_ASYNC_UTILS_H__ */ 118 | -------------------------------------------------------------------------------- /.github/workflows/rhub.yaml: -------------------------------------------------------------------------------- 1 | # R-hub's generic GitHub Actions workflow file. It's canonical location is at 2 | # https://github.com/r-hub/actions/blob/v1/workflows/rhub.yaml 3 | # You can update this file to a newer version using the rhub2 package: 4 | # 5 | # rhub::rhub_setup() 6 | # 7 | # It is unlikely that you need to modify this file manually. 8 | 9 | name: R-hub 10 | run-name: "${{ github.event.inputs.id }}: ${{ github.event.inputs.name || format('Manually run by {0}', github.triggering_actor) }}" 11 | 12 | on: 13 | workflow_dispatch: 14 | inputs: 15 | config: 16 | description: 'A comma separated list of R-hub platforms to use.' 17 | type: string 18 | default: 'linux,windows,macos' 19 | name: 20 | description: 'Run name. You can leave this empty now.' 21 | type: string 22 | id: 23 | description: 'Unique ID. You can leave this empty now.' 24 | type: string 25 | 26 | jobs: 27 | 28 | setup: 29 | runs-on: ubuntu-latest 30 | outputs: 31 | containers: ${{ steps.rhub-setup.outputs.containers }} 32 | platforms: ${{ steps.rhub-setup.outputs.platforms }} 33 | 34 | steps: 35 | # NO NEED TO CHECKOUT HERE 36 | - uses: r-hub/actions/setup@v1 37 | with: 38 | config: ${{ github.event.inputs.config }} 39 | id: rhub-setup 40 | 41 | linux-containers: 42 | needs: setup 43 | if: ${{ needs.setup.outputs.containers != '[]' }} 44 | runs-on: ubuntu-latest 45 | name: ${{ matrix.config.label }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | config: ${{ fromJson(needs.setup.outputs.containers) }} 50 | container: 51 | image: ${{ matrix.config.container }} 52 | 53 | steps: 54 | - uses: r-hub/actions/checkout@v1 55 | - uses: r-hub/actions/platform-info@v1 56 | with: 57 | token: ${{ secrets.RHUB_TOKEN }} 58 | job-config: ${{ matrix.config.job-config }} 59 | - uses: r-hub/actions/setup-deps@v1 60 | with: 61 | token: ${{ secrets.RHUB_TOKEN }} 62 | job-config: ${{ matrix.config.job-config }} 63 | - uses: r-hub/actions/run-check@v1 64 | with: 65 | token: ${{ secrets.RHUB_TOKEN }} 66 | job-config: ${{ matrix.config.job-config }} 67 | 68 | other-platforms: 69 | needs: setup 70 | if: ${{ needs.setup.outputs.platforms != '[]' }} 71 | runs-on: ${{ matrix.config.os }} 72 | name: ${{ matrix.config.label }} 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | config: ${{ fromJson(needs.setup.outputs.platforms) }} 77 | 78 | steps: 79 | - uses: r-hub/actions/checkout@v1 80 | - uses: r-hub/actions/setup-r@v1 81 | with: 82 | job-config: ${{ matrix.config.job-config }} 83 | token: ${{ secrets.RHUB_TOKEN }} 84 | - uses: r-hub/actions/platform-info@v1 85 | with: 86 | token: ${{ secrets.RHUB_TOKEN }} 87 | job-config: ${{ matrix.config.job-config }} 88 | - uses: r-hub/actions/setup-deps@v1 89 | with: 90 | job-config: ${{ matrix.config.job-config }} 91 | token: ${{ secrets.RHUB_TOKEN }} 92 | - uses: r-hub/actions/run-check@v1 93 | with: 94 | job-config: ${{ matrix.config.job-config }} 95 | token: ${{ secrets.RHUB_TOKEN }} 96 | -------------------------------------------------------------------------------- /src/draw_data.cpp: -------------------------------------------------------------------------------- 1 | #include "draw_data.h" 2 | 3 | #include 4 | 5 | namespace unigd 6 | { 7 | namespace renderers 8 | { 9 | 10 | Text::Text(color_t t_col, gvertex t_pos, std::string &&t_str, double t_rot, 11 | double t_hadj, TextInfo &&t_text) 12 | : col(t_col), pos(t_pos), rot(t_rot), hadj(t_hadj), str(t_str), text(t_text) 13 | { 14 | } 15 | Circle::Circle(LineInfo &&t_line, color_t t_fill, gvertex t_pos, double t_radius) 16 | : line(t_line), fill(t_fill), pos(t_pos), radius(t_radius) 17 | { 18 | } 19 | Line::Line(LineInfo &&t_line, gvertex t_orig, gvertex t_dest) 20 | : line(t_line), orig(t_orig), dest(t_dest) 21 | { 22 | } 23 | Rect::Rect(LineInfo &&t_line, color_t t_fill, grect t_rect) 24 | : line(t_line), fill(t_fill), rect(t_rect) 25 | { 26 | } 27 | Polyline::Polyline(LineInfo &&t_line, std::vector> &&t_points) 28 | : line(t_line), points(t_points) 29 | { 30 | } 31 | Polygon::Polygon(LineInfo &&t_line, color_t t_fill, 32 | std::vector> &&t_points) 33 | : line(t_line), fill(t_fill), points(t_points) 34 | { 35 | } 36 | Path::Path(LineInfo &&t_line, color_t t_fill, std::vector> &&t_points, 37 | std::vector &&t_nper, bool t_winding) 38 | : line(t_line), fill(t_fill), points(t_points), nper(t_nper), winding(t_winding) 39 | { 40 | } 41 | Raster::Raster(std::vector &&t_raster, gvertex t_wh, 42 | grect t_rect, double t_rot, bool t_interpolate) 43 | : raster(t_raster), wh(t_wh), rect(t_rect), rot(t_rot), interpolate(t_interpolate) 44 | { 45 | } 46 | 47 | void Text::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 48 | 49 | void Circle::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 50 | 51 | void Line::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 52 | 53 | void Rect::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 54 | 55 | void Polyline::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 56 | 57 | void Polygon::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 58 | 59 | void Path::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 60 | 61 | void Raster::visit(draw_call_visitor *t_visitor) const { t_visitor->visit(this); } 62 | 63 | Page::Page(page_id_t t_id, gvertex t_size) : id(t_id), size(t_size), dcs(), cps() 64 | { 65 | clip({0, 0, size.x, size.y}); 66 | } 67 | void Page::put(std::unique_ptr &&t_dc) 68 | { 69 | t_dc->clip_id = cps.back().id; 70 | dcs.emplace_back(std::move(t_dc)); 71 | } 72 | void Page::put(std::vector> &&t_dcs) 73 | { 74 | for (auto &cp : t_dcs) 75 | { 76 | cp->clip_id = cps.back().id; 77 | } 78 | dcs.insert(dcs.end(), std::make_move_iterator(t_dcs.begin()), 79 | std::make_move_iterator(t_dcs.end())); 80 | } 81 | void Page::clear() 82 | { 83 | dcs.clear(); 84 | cps.clear(); 85 | clip({0, 0, size.x, size.y}); 86 | } 87 | void Page::clip(grect t_rect) 88 | { 89 | const auto cps_count = cps.size(); 90 | if (cps_count == 0 || !cps.back().equals(t_rect)) 91 | { 92 | cps.emplace_back(Clip{(int)cps_count, t_rect}); 93 | } 94 | } 95 | 96 | } // namespace renderers 97 | 98 | } // namespace unigd 99 | -------------------------------------------------------------------------------- /R/fonts.R: -------------------------------------------------------------------------------- 1 | # fonts.r 2 | # from: https://github.com/r-lib/svglite/blob/master/R/fonts.R 3 | 4 | r_font_families <- c("sans", "serif", "mono", "symbol") 5 | r_font_faces <- c("plain", "bold", "italic", "bolditalic", "symbol") 6 | 7 | alias_lookup <- function() { 8 | if (.Platform$OS.type == "windows") { 9 | serif_font <- "Times New Roman" 10 | symbol_font <- "Standard Symbols L" 11 | } else { 12 | serif_font <- "Times" 13 | symbol_font <- "Symbol" 14 | } 15 | c( 16 | sans = "Arial", 17 | serif = serif_font, 18 | mono = "Courier", 19 | symbol = symbol_font 20 | ) 21 | } 22 | 23 | #' @importFrom systemfonts font_info 24 | match_family <- function(font, bold = FALSE, italic = FALSE) { 25 | font_info(font, bold = bold, italic = italic)$family[1] 26 | } 27 | 28 | validate_aliases <- function(system_fonts, user_fonts) { 29 | system_fonts <- compact(lapply(system_fonts, compact)) 30 | user_fonts <- compact(lapply(user_fonts, compact)) 31 | 32 | system_fonts <- lapply(system_fonts, validate_system_alias) 33 | user_fonts <- ilapply(user_fonts, validate_user_alias) 34 | 35 | aliases <- c(names(system_fonts), names(user_fonts)) 36 | if (any(duplicated(aliases))) { 37 | stop("Cannot supply both system and font alias", call. = FALSE) 38 | } 39 | 40 | # Add missing system fonts for base families 41 | missing_aliases <- setdiff(r_font_families, aliases) 42 | system_fonts[missing_aliases] <- 43 | lapply(alias_lookup()[missing_aliases], match_family) 44 | 45 | list( 46 | system = system_fonts, 47 | user = user_fonts 48 | ) 49 | } 50 | 51 | validate_system_alias <- function(alias) { 52 | if (!is_scalar_character(alias)) { 53 | stop("System fonts must be scalar character vector", call. = FALSE) 54 | } 55 | 56 | matched <- match_family(alias) 57 | if (alias != matched) { 58 | warning( 59 | call. = FALSE, 60 | "System font `", 61 | alias, 62 | "` not found. ", 63 | "Closest match: `", 64 | matched, 65 | "`" 66 | ) 67 | } 68 | matched 69 | } 70 | 71 | is_user_alias <- function(x) { 72 | is.list(x) && 73 | (is_scalar_character(x$file) || is_scalar_character(x$ttf)) && 74 | (is_scalar_character(x$alias) || is_scalar_character(x$name)) 75 | } 76 | 77 | validate_user_alias <- function(default_name, family) { 78 | if (!all(names(family) %in% r_font_faces)) { 79 | stop( 80 | "Faces must contain only: ", 81 | paste(sprintf("`%s`", r_font_faces), collapse = ", "), 82 | call. = FALSE 83 | ) 84 | } 85 | 86 | is_alias_object <- vapply_lgl(family, is_user_alias) 87 | is_alias_plain <- vapply_lgl(family, is_scalar_character) 88 | 89 | is_valid_alias <- is_alias_object | is_alias_plain 90 | if (any(!is_valid_alias)) { 91 | stop( 92 | call. = FALSE, 93 | "The following faces are invalid for `", 94 | default_name, 95 | "`: ", 96 | paste0(names(family)[!is_valid_alias], collapse = ", ") 97 | ) 98 | } 99 | 100 | names <- ifelse(is_alias_plain, default_name, family) 101 | names <- lapply_if(names, is_alias_object, function(obj) { 102 | obj$alias %||% obj$name 103 | }) 104 | files <- lapply_if(family, is_alias_object, function(obj) { 105 | obj$file %||% obj$ttf 106 | }) 107 | 108 | file_exists <- vapply_lgl(files, file.exists) 109 | if (any(!file_exists)) { 110 | missing <- unlist(files)[!file_exists] 111 | stop( 112 | call. = FALSE, 113 | "Could not find font file: ", 114 | paste0(missing, collapse = ", ") 115 | ) 116 | } 117 | 118 | zip(list(name = names, file = files)) 119 | } 120 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-text-fonts.R: -------------------------------------------------------------------------------- 1 | test_that("font sets weight/style", { 2 | if (.Platform$OS.type == "windows") { 3 | skip_on_cran() 4 | } 5 | x <- xmlSVG({ 6 | plot.new() 7 | text(0.5, seq(0.9, 0.1, length = 4), "a", font = 1:4) 8 | }) 9 | text <- xml2::xml_find_all(x, ".//d1:text") 10 | expect_equal(style_attr(text, "font-weight"), c(NA, "bold", NA, "bold")) 11 | expect_equal(style_attr(text, "font-style"), c(NA, NA, "italic", "italic")) 12 | }) 13 | 14 | test_that("metrics are computed for different weight/style", { 15 | if (.Platform$OS.type == "windows") { 16 | skip_on_cran() 17 | } 18 | x <- xmlSVG(user_fonts = fontquiver::font_families("Bitstream Vera"), { 19 | plot.new() 20 | text(1, 1, "text") 21 | text(1, 1, "text", font = 2) 22 | text(1, 1, "text", font = 4) 23 | }) 24 | text <- xml2::xml_find_all(x, ".//d1:text") 25 | x <- xml2::xml_attr(text, "textLength") 26 | expect_false(any(x[2:3] == x[1])) 27 | }) 28 | 29 | test_that("symbol font family is 'Symbol'", { 30 | if (.Platform$OS.type == "windows") { 31 | skip_on_cran() 32 | } 33 | symbol_font <- alias_lookup()["symbol"] 34 | matched_symbol_font <- match_family(symbol_font) 35 | 36 | x <- xmlSVG({ 37 | plot(c(0,2), c(0,2), type = "n", axes = FALSE, xlab = "", ylab = "") 38 | text(1, 1, expression(symbol("\042"))) 39 | }) 40 | text <- xml2::xml_find_all(x, ".//d1:text") 41 | expect_equal(style_attr(text, "font-family"), matched_symbol_font) 42 | }) 43 | 44 | test_that("throw on malformed alias", { 45 | if (.Platform$OS.type == "windows") { 46 | skip_on_cran() 47 | } 48 | expect_error(validate_aliases(list(mono = letters), list()), "must be scalar") 49 | expect_warning(validate_aliases(list(sans = "foobar"), list()), "not found") 50 | }) 51 | 52 | test_that("fonts are aliased", { 53 | if (.Platform$OS.type == "windows") { 54 | skip_on_cran() 55 | } 56 | matched <- match_family("cursive") 57 | x <- xmlSVG( 58 | system_fonts = list(sans = matched), 59 | user_fonts = list(mono = fontquiver::font_faces("Bitstream Vera", "Mono")), 60 | { 61 | plot.new() 62 | text(0.5, 0.1, "a", family = "serif") 63 | text(0.5, 0.5, "a", family = "sans") 64 | text(0.5, 0.9, "a", family = "mono") 65 | }) 66 | text <- xml2::xml_find_all(x, ".//d1:text") 67 | families <- style_attr(text, "font-family") 68 | 69 | expect_false(families[[1]] == "serif") 70 | expect_true(all(families[2:3] == c(matched, "Bitstream Vera Sans Mono"))) 71 | }) 72 | 73 | test_that("metrics are computed for different fonts", { 74 | if (.Platform$OS.type == "windows") { 75 | skip_on_cran() 76 | } 77 | aliases <- fontquiver::font_families("Bitstream Vera") 78 | x <- xmlSVG(user_fonts = aliases, { 79 | plot.new() 80 | text(0.5, 0.9, "a", family = "serif") 81 | text(0.5, 0.9, "a", family = "mono") 82 | }) 83 | text <- xml2::xml_find_all(x, ".//d1:text") 84 | x_attr <- xml2::xml_attr(text, "textLength") 85 | y_attr <- xml2::xml_attr(text, "y") 86 | 87 | expect_false(x_attr[[1]] == x_attr[[2]]) 88 | expect_false(y_attr[[1]] == y_attr[[2]]) 89 | }) 90 | 91 | test_that("unicode characters in plotmath are handled", { 92 | if (.Platform$OS.type == "windows") { 93 | skip_on_cran() 94 | } 95 | rho <- suppressWarnings(as.name("\u03c1")) 96 | expr <- call("*", rho, rho) 97 | 98 | x <- xmlSVG({ 99 | plot.new() 100 | text(0.5, 0.5, as.expression(expr)) 101 | }) 102 | text <- xml2::xml_find_all(x, ".//d1:text") 103 | x_attr <- as.double(xml2::xml_attr(text, "x")) 104 | 105 | expect_true(x_attr[2] - x_attr[1] > 0) 106 | }) -------------------------------------------------------------------------------- /src/r_thread_win32.cpp: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | #include "r_thread.h" 3 | 4 | #ifndef WIN32_LEAN_AND_MEAN 5 | #define WIN32_LEAN_AND_MEAN 6 | #endif 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | namespace unigd 14 | { 15 | namespace async 16 | { 17 | namespace 18 | { 19 | const auto *UNIGD_WINDOW_CLASS_NAME = TEXT("unigd_window_class"); 20 | const UINT UNIGD_MESSAGE_ID = WM_USER + 217; 21 | threadsafe_queue work_queue; 22 | bool ipc_initialized{false}; 23 | HWND message_hwind; 24 | 25 | inline void r_print_error(const char *message) 26 | { 27 | REprintf("Error (unigd IPC): %s\n", message); 28 | } 29 | 30 | inline void process_tasks() 31 | { 32 | function_wrapper task; 33 | while (work_queue.try_pop(task)) 34 | { 35 | task.call(); 36 | } 37 | } 38 | 39 | LRESULT CALLBACK window_callback(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 40 | { 41 | switch (message) 42 | { 43 | case UNIGD_MESSAGE_ID: 44 | process_tasks(); 45 | return 0; 46 | default: 47 | return DefWindowProc(hWnd, message, wParam, lParam); 48 | } 49 | } 50 | 51 | inline bool window_class_exists() 52 | { 53 | WNDCLASSEX wc{}; 54 | return GetClassInfoEx(GetModuleHandle(NULL), UNIGD_WINDOW_CLASS_NAME, &wc) != 0; 55 | } 56 | 57 | inline bool register_window_class() 58 | { 59 | WNDCLASSEX wc{}; 60 | wc.cbSize = sizeof(WNDCLASSEX); 61 | wc.lpfnWndProc = window_callback; 62 | wc.hInstance = GetModuleHandle(NULL); 63 | wc.lpszClassName = UNIGD_WINDOW_CLASS_NAME; 64 | return RegisterClassEx(&wc) != 0; 65 | } 66 | 67 | inline HWND create_message_window() 68 | { 69 | return CreateWindowEx(0, UNIGD_WINDOW_CLASS_NAME, TEXT("unigd"), 0, 0, 0, 0, 0, 70 | HWND_MESSAGE, NULL, NULL, NULL); 71 | } 72 | 73 | inline void notify() { PostMessage(message_hwind, UNIGD_MESSAGE_ID, 0, 0); } 74 | } // namespace 75 | 76 | void ipc_open() 77 | { 78 | if (ipc_initialized) return; 79 | 80 | if (!window_class_exists()) 81 | { 82 | // If there is already another version of unigd loaded. (E.g. with pkgload::load_all, 83 | // which, unfortunately does not call .onUnload), 84 | // we have to re-use the (unique) window class, but should not reuse the message 85 | // window. Otherwise we could not destroy the window while it is in use. As the 86 | // callback lives inside the window class, messages be received only once. 87 | 88 | // message_hwind = FindWindowEx(NULL, NULL, UNIGD_WINDOW_CLASS_NAME, TEXT("unigd")); 89 | 90 | if (!register_window_class()) 91 | { 92 | r_print_error("unigd: Failed to register window class."); 93 | } 94 | } 95 | 96 | message_hwind = create_message_window(); 97 | if (!message_hwind) 98 | { 99 | r_print_error("unigd: Failed to create message window."); 100 | return; 101 | } 102 | ipc_initialized = true; 103 | } 104 | 105 | void ipc_close() 106 | { 107 | if (!ipc_initialized) return; 108 | 109 | if (DestroyWindow(message_hwind) == 0) 110 | { 111 | r_print_error("unigd: Failed to destroy message window."); 112 | } 113 | 114 | // We can not be sure if there is another dll instance loaded with pkgload::load_all 115 | // so we cannot unregister the window class. 116 | 117 | // if (UnregisterClass(UNIGD_WINDOW_CLASS_NAME, NULL) == 0) { 118 | // r_print_error("unigd: Failed to unregister window class."); 119 | // } 120 | 121 | ipc_initialized = false; 122 | } 123 | 124 | void r_thread_impl(function_wrapper &&task) 125 | { 126 | work_queue.push(std::move(task)); 127 | notify(); 128 | } 129 | 130 | } // namespace async 131 | } // namespace unigd 132 | 133 | #endif -------------------------------------------------------------------------------- /R/testgraphic.R: -------------------------------------------------------------------------------- 1 | 2 | #' Plot a test pattern that can be used to evaluate and compare graphics 3 | #' devices. 4 | #' 5 | #' @return Nothing, but a plot is generated. 6 | #' 7 | #' @export 8 | #' 9 | #' @examples 10 | #' \dontrun{ 11 | #' 12 | #' ugd_test_pattern() 13 | #' } 14 | ugd_test_pattern <- function() { 15 | graphics::plot.new() 16 | graphics::plot.window(xlim = c(0, 100), ylim = c(0, 100)) 17 | 18 | bx <- c(0, 100, 0, 100) 19 | by <- c(0, 0, 100, 100) 20 | 21 | graphics::points(bx, by, pch = 3) 22 | graphics::text(8 + bx * 0.84, by, sprintf("%i,%i", bx, by)) 23 | graphics::text(50, 24 | 100, 25 | "httpgd test graphics 0.1", 26 | cex = 0.8, 27 | col = "darkgray" 28 | ) 29 | 30 | graphics::rect(5, 5, 25, 20, border = "blue", col = "lightblue") 31 | graphics::text(5, 25, "rect") 32 | 33 | graphics::polygon(30 + c(0, 10, 15, 2, 8), 34 | 22 + c(0, 0, 10, 5, 2), 35 | border = "darkgreen", 36 | col = "aquamarine" 37 | ) 38 | graphics::text(35, 35, "polygon") 39 | 40 | x <- 1:6 41 | graphics::points(30 + x * 3, 5 + (x %% 2) * 5) 42 | graphics::lines(30 + x * 3, 5 + (x %% 2) * 5, col = "red") 43 | graphics::text(35, 15, "polyline") 44 | 45 | graphics::points(15, 36, col = "chocolate") 46 | graphics::points( 47 | 15, 48 | 36, 49 | pch = 21, 50 | cex = 5, 51 | col = "darkviolet", 52 | bg = "coral" 53 | ) 54 | graphics::text(15, 45, "circle") 55 | 56 | image <- grDevices::as.raster(matrix(rep(0:1, length.out = 15), ncol = 5, nrow = 3)) 57 | graphics::rasterImage(image, 60, 30, 65, 35, interpolate = FALSE) 58 | graphics::rasterImage(image, 60, 40, 65, 45) 59 | graphics::rasterImage(image, 70, 25, 70 + graphics::xinch(.5), 25 + graphics::yinch(.3), 60 | interpolate = FALSE 61 | ) 62 | graphics::rasterImage(image, 63 | 70, 64 | 40, 65 | 75, 66 | 45, 67 | angle = 15, 68 | interpolate = FALSE 69 | ) 70 | 71 | image <- grDevices::as.raster(matrix(seq(0, 1, 1 / 3), ncol = 2)) 72 | 73 | graphics::rasterImage(image, 60, 10, 70, 20, interpolate = FALSE) 74 | 75 | graphics::text(65, 55, "raster") 76 | 77 | graphics::text(10, 90, "text") 78 | graphics::text(5, 78, expression(bar(x) == sum(frac(x[i], n), i == 1, n))) 79 | 80 | graphics::text(26, 87, "A", cex = 0.5) 81 | graphics::text(30, 87, "A") 82 | graphics::text(34, 87, "A", cex = 1.5) 83 | 84 | graphics::text(6, 85 | 68 - 1:4 * 4, 86 | c("plain", "bold", "italic", "bold-italic"), 87 | font = 1:4 88 | ) 89 | 90 | graphics::text(32, 74, "\u00E9\u00E8 \u00F8\u00D8 \u00E5\u00C5 \u00E6\u00C6") 91 | 92 | graphics::text(25, 80, "0", srt = 0) 93 | graphics::text(30, 80, "25", srt = 25) 94 | graphics::text(35, 80, "90", srt = 90) 95 | graphics::text(40, 80, "180", srt = 180) 96 | 97 | graphics::points(20, 98 | 62, 99 | pch = 3, 100 | cex = 4, 101 | col = "red" 102 | ) 103 | graphics::text(20, 62, "UR", adj = c(0, 0)) 104 | graphics::text(20, 62, "UL", adj = c(1, 0)) 105 | graphics::text(20, 62, "BR", adj = c(0, 1)) 106 | graphics::text(20, 62, "BL", adj = c(1, 1)) 107 | 108 | graphics::points(32, 109 | 62, 110 | pch = 3, 111 | cex = 4, 112 | col = "red" 113 | ) 114 | graphics::text(32, 62, "B", adj = c(0.5, 1)) 115 | graphics::text(32, 62, "R", adj = c(0, 0.5)) 116 | graphics::text(32, 62, "L", adj = c(1, 0.5)) 117 | graphics::text(32, 62, "U", adj = c(0.5, 0)) 118 | 119 | graphics::points(44, 120 | 62, 121 | pch = 3, 122 | cex = 4, 123 | col = "red" 124 | ) 125 | graphics::text(44, 62, "C") 126 | 127 | graphics::text(70, 90, "lines") 128 | 129 | graphics::segments(60 + 2.5 * 4, 85, 60 + 0:5 * 4, 70, lty = 0:6) 130 | graphics::points(60 + 0:5 * 4, rep(70, 6)) 131 | 132 | graphics::segments(85, 90 - 0:4 * 5, 90, 90 - 0:4 * 5, lwd = 1:5 * 3) 133 | 134 | sapply(0:2, function(x) { 135 | graphics::lines( 136 | rep(60 + x * 8, 3) + 0:2 * 2, 137 | c(60, 63, 60), 138 | lwd = 8, 139 | lend = x, 140 | ljoin = x 141 | ) 142 | }) 143 | 144 | 145 | graphics::polypath( 146 | c(0, 1, 1, 0, NA, c(0, 1, 1, 0) + 2) * 6 + 24, 147 | c(1.5, 1, 0, 0.5, NA, 1, 1.5, 0.5, 0) * 6 + 40, 148 | col = "violet", border = "purple" 149 | ) 150 | graphics::text(30, 52, "path") 151 | 152 | graphics::points(rep(98, 26), 90 - 0:25 * 3, pch = 0:25, bg = "red") 153 | } 154 | -------------------------------------------------------------------------------- /inst/include/unigd_api_v1.h: -------------------------------------------------------------------------------- 1 | #ifndef UNIGD_EXTERNAL_API_V1_H 2 | #define UNIGD_EXTERNAL_API_V1_H 3 | 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" 8 | { 9 | #endif 10 | 11 | #include 12 | 13 | typedef void *UNIGD_HANDLE; 14 | typedef void *UNIGD_RENDER_HANDLE; 15 | typedef void *UNIGD_RENDERERS_HANDLE; 16 | typedef void *UNIGD_RENDERERS_ENTRY_HANDLE; 17 | typedef void *UNIGD_FIND_HANDLE; 18 | typedef const char *UNIGD_RENDERER_ID; 19 | typedef uint32_t UNIGD_PLOT_ID; 20 | typedef uint32_t UNIGD_PLOT_INDEX; 21 | typedef int32_t UNIGD_PLOT_RELATIVE; 22 | typedef uint32_t UNIGD_CLIENT_ID; 23 | 24 | struct unigd_graphics_client 25 | { 26 | void (*start)(void *); 27 | void (*close)(void *); 28 | void (*state_change)(void *); 29 | const char *(*info)(void *); 30 | }; 31 | 32 | struct unigd_renderer_info 33 | { 34 | UNIGD_RENDERER_ID id; 35 | const char *mime; 36 | const char *fileext; 37 | const char *name; 38 | const char *type; 39 | const char *description; 40 | bool text; 41 | }; 42 | 43 | struct unigd_renderers_list 44 | { 45 | const unigd_renderer_info *entries; 46 | uint64_t size; 47 | }; 48 | 49 | struct unigd_device_state 50 | { 51 | int upid; 52 | UNIGD_PLOT_INDEX hsize; 53 | bool active; 54 | }; 55 | 56 | struct unigd_render_args 57 | { 58 | double width; 59 | double height; 60 | double scale; 61 | }; 62 | 63 | struct unigd_render_access 64 | { 65 | const uint8_t *buffer; 66 | uint64_t size; 67 | }; 68 | 69 | struct unigd_find_results 70 | { 71 | unigd_device_state state; 72 | UNIGD_PLOT_INDEX size; 73 | UNIGD_PLOT_ID *ids; 74 | }; 75 | 76 | // unigd API access version 1 77 | struct unigd_api_v1 78 | { 79 | // GENERAL 80 | 81 | // Send a log message to R (thread safe). 82 | void (*log)(const char *t_message); 83 | 84 | // Info about unigd installation. 85 | const char *(*info)(); 86 | 87 | // DEVICE 88 | 89 | // Get a new unused client ID. 90 | UNIGD_CLIENT_ID (*register_client_id)(); 91 | 92 | // Attach a client to the unigd device. 93 | UNIGD_HANDLE(*device_attach) 94 | (int devnum, unigd_graphics_client *client, UNIGD_CLIENT_ID client_id, void *); 95 | 96 | // Get client. 97 | void *(*device_get)(int devnum, UNIGD_CLIENT_ID client_id); 98 | 99 | // Destroy device handle. 100 | void (*device_destroy)(UNIGD_HANDLE); 101 | 102 | // Get the current unigd device state. 103 | unigd_device_state (*device_state)(UNIGD_HANDLE); 104 | 105 | // RENDERING 106 | 107 | // Render a plot. 108 | UNIGD_RENDER_HANDLE(*device_render_create) 109 | (UNIGD_HANDLE, UNIGD_RENDERER_ID, UNIGD_PLOT_ID, unigd_render_args, unigd_render_access *); 110 | 111 | // Free render memory. 112 | void (*device_render_destroy)(UNIGD_RENDER_HANDLE); 113 | 114 | // HISTORY 115 | 116 | // Remove a plot from history. 117 | bool (*device_plots_remove)(UNIGD_HANDLE, UNIGD_PLOT_ID); 118 | 119 | // Clear plot history. 120 | bool (*device_plots_clear)(UNIGD_HANDLE); 121 | 122 | // Plot ID lookup. 123 | UNIGD_FIND_HANDLE(*device_plots_find) 124 | (UNIGD_HANDLE, UNIGD_PLOT_RELATIVE offset, UNIGD_PLOT_INDEX limit, unigd_find_results *results); 125 | 126 | // Free plot ID lookup memory. 127 | void (*device_plots_find_destroy)(UNIGD_FIND_HANDLE); 128 | 129 | // RENDERERS 130 | 131 | // Get full list of available renderers. 132 | UNIGD_RENDERERS_HANDLE(*renderers) 133 | (unigd_renderers_list *renderer); 134 | 135 | // Free memory of renderers list. 136 | void (*renderers_destroy)(UNIGD_RENDERERS_HANDLE); 137 | 138 | // Renderer ID lookup. 139 | UNIGD_RENDERERS_ENTRY_HANDLE(*renderers_find) 140 | (UNIGD_RENDERER_ID, unigd_renderer_info *renderer); 141 | 142 | // Free memory of renderer lookup. 143 | void (*renderers_find_destroy)(UNIGD_RENDERERS_ENTRY_HANDLE); 144 | }; 145 | 146 | #ifdef __cplusplus 147 | } 148 | #endif 149 | 150 | #endif // UNIGD_EXTERNAL_API_V1_H 151 | -------------------------------------------------------------------------------- /tests/testthat/test-svglite-lines.R: -------------------------------------------------------------------------------- 1 | test_that("segments don't have fill", { 2 | x <- xmlSVG({ 3 | plot.new() 4 | segments(0.5, 0.5, 1, 1) 5 | }) 6 | style <- xml2::xml_text(xml2::xml_find_first(x, "//d1:style")) 7 | expect_match(style, "fill: none;") 8 | expect_equal(style_attr(xml2::xml_find_first(x, ".//d1:line"), "fill"), NA_character_) 9 | }) 10 | 11 | test_that("lines don't have fill", { 12 | x <- xmlSVG({ 13 | plot.new() 14 | lines(c(0.5, 1, 0.5), c(0.5, 1, 1)) 15 | }) 16 | expect_equal(style_attr(xml2::xml_find_first(x, ".//d1:polyline"), "fill"), NA_character_) 17 | }) 18 | 19 | test_that("polygons do have fill", { 20 | x <- xmlSVG({ 21 | plot.new() 22 | polygon(c(0.5, 1, 0.5), c(0.5, 1, 1), col = "red", border = "blue") 23 | }) 24 | polygon <- xml2::xml_find_first(x, ".//d1:polygon") 25 | expect_equal(style_attr(polygon, "fill"), rgb(1, 0, 0)) 26 | expect_equal(style_attr(polygon, "stroke"), rgb(0, 0, 1)) 27 | }) 28 | 29 | test_that("polygons without border", { 30 | x <- xmlSVG({ 31 | plot.new() 32 | polygon(c(0.5, 1, 0.5), c(0.5, 1, 1), col = "red", border = NA) 33 | }) 34 | polygon <- xml2::xml_find_first(x, ".//d1:polygon") 35 | expect_equal(style_attr(polygon, "fill"), rgb(1, 0, 0)) 36 | expect_equal(style_attr(polygon, "stroke"), "none") 37 | }) 38 | 39 | test_that("blank lines are omitted", { 40 | x <- xmlSVG(mini_plot(1:3, lty = "blank", type = "l")) 41 | expect_equal(length(xml2::xml_find_all(x, "//d1:polygon")), 0) 42 | }) 43 | 44 | test_that("lines lty becomes stroke-dasharray", { 45 | expect_equal(dash_array(lty = 1), NA_integer_) 46 | expect_equal(dash_array(lty = 2), c(4, 4)) 47 | expect_equal(dash_array(lty = 3), c(1, 3)) 48 | expect_equal(dash_array(lty = 4), c(1, 3, 4, 3)) 49 | expect_equal(dash_array(lty = 5), c(7, 3)) 50 | expect_equal(dash_array(lty = 6), c(2, 2, 6, 2)) 51 | expect_equal(dash_array(lty = "1F"), c(1, 15)) 52 | expect_equal(dash_array(lty = "1234"), c(1, 2, 3, 4)) 53 | }) 54 | 55 | test_that("stroke-dasharray scales with lwd > 1", { 56 | expect_equal(dash_array(lty = 2, lwd = 1), c(4, 4)) 57 | expect_equal(dash_array(lty = 2, lwd = 1/2), c(4, 4)) 58 | expect_equal(dash_array(lty = 2, lwd = 1.1), c(4.4, 4.4)) 59 | expect_equal(dash_array(lty = 2, lwd = 2), c(8, 8)) 60 | }) 61 | 62 | test_that("line end shapes", { 63 | x1 <- xmlSVG({ 64 | plot.new() 65 | lines(c(0.3, 0.7), c(0.5, 0.5), lwd = 15, lend = "round") 66 | }) 67 | x2 <- xmlSVG({ 68 | plot.new() 69 | lines(c(0.3, 0.7), c(0.5, 0.5), lwd = 15, lend = "butt") 70 | }) 71 | x3 <- xmlSVG({ 72 | plot.new() 73 | lines(c(0.3, 0.7), c(0.5, 0.5), lwd = 15, lend = "square") 74 | }) 75 | style <- xml2::xml_text(xml2::xml_find_first(x1, "//d1:style")) 76 | expect_match(style, "stroke-linecap: round;") 77 | expect_equal(style_attr(xml2::xml_find_first(x1, ".//d1:polyline"), "stroke-linecap"), NA_character_) 78 | expect_equal(style_attr(xml2::xml_find_first(x2, ".//d1:polyline"), "stroke-linecap"), "butt") 79 | expect_equal(style_attr(xml2::xml_find_first(x3, ".//d1:polyline"), "stroke-linecap"), "square") 80 | }) 81 | 82 | test_that("line join shapes", { 83 | x1 <- xmlSVG({ 84 | plot.new() 85 | lines(c(0.3, 0.5, 0.7), c(0.1, 0.9, 0.1), lwd = 15, ljoin = "round") 86 | }) 87 | x2 <- xmlSVG({ 88 | plot.new() 89 | lines(c(0.3, 0.5, 0.7), c(0.1, 0.9, 0.1), lwd = 15, ljoin = "mitre", lmitre = 10) 90 | }) 91 | x3 <- xmlSVG({ 92 | plot.new() 93 | lines(c(0.3, 0.5, 0.7), c(0.1, 0.9, 0.1), lwd = 15, ljoin = "mitre", lmitre = 4) 94 | }) 95 | x4 <- xmlSVG({ 96 | plot.new() 97 | lines(c(0.3, 0.5, 0.7), c(0.1, 0.9, 0.1), lwd = 15, ljoin = "bevel") 98 | }) 99 | style <- xml2::xml_text(xml2::xml_find_first(x1, "//d1:style")) 100 | expect_match(style, "stroke-linejoin: round;") 101 | expect_match(style, "stroke-miterlimit: 10.00;") 102 | expect_equal(style_attr(xml2::xml_find_all(x1, ".//d1:polyline"), "stroke-linejoin"), NA_character_) 103 | expect_equal(style_attr(xml2::xml_find_all(x2, ".//d1:polyline"), "stroke-linejoin"), "miter") 104 | expect_equal(style_attr(xml2::xml_find_all(x2, ".//d1:polyline"), "stroke-miterlimit"), NA_character_) 105 | expect_equal(style_attr(xml2::xml_find_all(x3, ".//d1:polyline"), "stroke-linejoin"), "miter") 106 | expect_equal(style_attr(xml2::xml_find_all(x3, ".//d1:polyline"), "stroke-miterlimit"), "4.00") 107 | expect_equal(style_attr(xml2::xml_find_all(x4, ".//d1:polyline"), "stroke-linejoin"), "bevel") 108 | }) -------------------------------------------------------------------------------- /src/renderers.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "renderers.h" 3 | 4 | #include "renderer_cairo.h" 5 | #include "renderer_json.h" 6 | #include "renderer_meta.h" 7 | #include "renderer_strings.h" 8 | #include "renderer_svg.h" 9 | #include "renderer_tikz.h" 10 | 11 | namespace unigd 12 | { 13 | namespace renderers 14 | { 15 | 16 | static std::unordered_map renderer_map = { 17 | {"svg", 18 | {{"svg", "image/svg+xml", ".svg", "SVG", "plot", "Scalable Vector Graphics (SVG).", 19 | true}, 20 | []() 21 | { return std::make_unique(std::experimental::nullopt); }}}, 22 | {"svgp", 23 | {{"svgp", "image/svg+xml", ".svg", "Portable SVG", "plot", 24 | "Version of the SVG renderer that produces portable SVGs.", true}, 25 | []() { return std::make_unique(); }}}, 26 | {"json", 27 | {{"json", "application/json", ".json", "JSON", "plot", 28 | "Plot data serialized to JSON format.", true}, 29 | []() { return std::make_unique(); }}}, 30 | {"tikz", 31 | {{"tikz", "text/plain", ".tex", "TikZ", "plot", "LaTeX TikZ code.", true}, 32 | []() { return std::make_unique(); }}}, 33 | {"strings", 34 | {{"strings", "text/plain", ".txt", "Strings", "data", 35 | "List of strings contained in plot.", true}, 36 | []() { return std::make_unique(); }}}, 37 | {"meta", 38 | {{"meta", "application/json", ".json", "Meta", "data", "Plot meta information.", 39 | true}, 40 | []() { return std::make_unique(); }}}, 41 | {"svgz", 42 | {{"svgz", "image/svg+xml", ".svgz", "SVGZ", "plot", 43 | "Compressed Scalable Vector Graphics (SVGZ).", false}, 44 | []() 45 | { return std::make_unique(std::experimental::nullopt); }}}, 46 | {"svgzp", 47 | {{"svgzp", "image/svg+xml", ".svgz", "Portable SVGZ", "plot", 48 | "Version of the SVG renderer that produces portable SVGZs.", false}, 49 | []() { return std::make_unique(); }}} 50 | 51 | #ifndef UNIGD_NO_CAIRO 52 | , 53 | {"ps", 54 | { 55 | {"ps", "application/postscript", ".ps", "PS", "plot", "PostScript (PS).", true}, 56 | []() { return std::make_unique(); }, 57 | }}, 58 | {"eps", 59 | {{"eps", "application/postscript", ".eps", "EPS", "plot", 60 | "Encapsulated PostScript (EPS).", true}, 61 | []() { return std::make_unique(); }}}, 62 | 63 | {"png", 64 | {{"png", "image/png", ".png", "PNG", "plot", "Portable Network Graphics (PNG).", 65 | false}, 66 | []() { return std::make_unique(); }}}, 67 | 68 | {"png-base64", 69 | {{"png-base64", "text/plain", ".txt", "Base64 PNG", "plot", 70 | "Base64 encoded Portable Network Graphics (PNG).", true}, 71 | []() { return std::make_unique(); }}}, 72 | 73 | {"pdf", 74 | {{"pdf", "application/pdf", ".pdf", "PDF", "plot", 75 | "Adobe Portable Document Format (PDF).", false}, 76 | []() { return std::make_unique(); }}}, 77 | 78 | #ifndef UNIGD_NO_TIFF 79 | {"tiff", 80 | {{"tiff", "image/tiff", ".tiff", "TIFF", "plot", "Tagged Image File Format (TIFF).", 81 | false}, 82 | []() { return std::make_unique(); }}} 83 | #endif /* UNIGD_NO_TIFF */ 84 | 85 | #endif /* UNIGD_NO_CAIRO */ 86 | }; 87 | 88 | bool find(const std::string &id, renderer_map_entry *renderer) 89 | { 90 | const auto it = renderer_map.find(id); 91 | if (it != renderer_map.end()) 92 | { 93 | *renderer = it->second; 94 | return true; 95 | } 96 | return false; 97 | } 98 | 99 | bool find_generator(const std::string &id, renderer_gen *renderer) 100 | { 101 | renderer_map_entry renderer_str; 102 | if (find(id, &renderer_str)) 103 | { 104 | *renderer = renderer_str.generator; 105 | return true; 106 | } 107 | return false; 108 | } 109 | 110 | bool find_info(const std::string &id, unigd_renderer_info *renderer) 111 | { 112 | renderer_map_entry renderer_str; 113 | if (find(id, &renderer_str)) 114 | { 115 | *renderer = renderer_str.info; 116 | return true; 117 | } 118 | return false; 119 | } 120 | 121 | const std::unordered_map *renderers() 122 | { 123 | return &renderer_map; 124 | } 125 | 126 | } // namespace renderers 127 | } // namespace unigd -------------------------------------------------------------------------------- /src/base_64.cpp: -------------------------------------------------------------------------------- 1 | #include "base_64.h" 2 | 3 | #include 4 | 5 | extern "C" 6 | { 7 | #include 8 | } 9 | 10 | namespace unigd 11 | { 12 | const static char encode_lookup[] = 13 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 14 | const static char pad_character = '='; 15 | std::string base64_encode(const std::uint8_t *buffer, size_t size) 16 | { 17 | std::string encoded_string; 18 | encoded_string.reserve(((size / 3) + (size % 3 > 0)) * 4); 19 | std::uint32_t temp{}; 20 | int index = 0; 21 | for (size_t idx = 0; idx < size / 3; idx++) 22 | { 23 | temp = buffer[index++] << 16; // Convert to big endian 24 | temp += buffer[index++] << 8; 25 | temp += buffer[index++]; 26 | encoded_string.append(1, encode_lookup[(temp & 0x00FC0000) >> 18]); 27 | encoded_string.append(1, encode_lookup[(temp & 0x0003F000) >> 12]); 28 | encoded_string.append(1, encode_lookup[(temp & 0x00000FC0) >> 6]); 29 | encoded_string.append(1, encode_lookup[(temp & 0x0000003F)]); 30 | } 31 | switch (size % 3) 32 | { 33 | case 1: 34 | temp = buffer[index++] << 16; // Convert to big endian 35 | encoded_string.append(1, encode_lookup[(temp & 0x00FC0000) >> 18]); 36 | encoded_string.append(1, encode_lookup[(temp & 0x0003F000) >> 12]); 37 | encoded_string.append(2, pad_character); 38 | break; 39 | case 2: 40 | temp = buffer[index++] << 16; // Convert to big endian 41 | temp += buffer[index++] << 8; 42 | encoded_string.append(1, encode_lookup[(temp & 0x00FC0000) >> 18]); 43 | encoded_string.append(1, encode_lookup[(temp & 0x0003F000) >> 12]); 44 | encoded_string.append(1, encode_lookup[(temp & 0x00000FC0) >> 6]); 45 | encoded_string.append(1, pad_character); 46 | break; 47 | } 48 | return encoded_string; 49 | } 50 | 51 | static void png_memory_write(png_structp png_ptr, png_bytep data, png_size_t length) 52 | { 53 | std::vector *p = (std::vector *)png_get_io_ptr(png_ptr); 54 | p->insert(p->end(), data, data + length); 55 | } 56 | inline std::string raster_to_string(std::vector raster_, int w, int h, 57 | double width, double height, bool interpolate) 58 | { 59 | unsigned int *raster = raster_.data(); 60 | 61 | h = h < 0 ? -h : h; 62 | w = w < 0 ? -w : w; 63 | bool resize = false; 64 | int w_fac = 1, h_fac = 1; 65 | std::vector raster_resize; 66 | 67 | if (!interpolate && double(w) < width) 68 | { 69 | resize = true; 70 | w_fac = std::ceil(width / w); 71 | } 72 | if (!interpolate && double(h) < height) 73 | { 74 | resize = true; 75 | h_fac = std::ceil(height / h); 76 | } 77 | 78 | if (resize) 79 | { 80 | int w_new = w * w_fac; 81 | int h_new = h * h_fac; 82 | raster_resize.reserve(w_new * h_new); 83 | for (int i = 0; i < h; ++i) 84 | { 85 | for (int j = 0; j < w; ++j) 86 | { 87 | unsigned int val = raster[i * w + j]; 88 | for (int wrep = 0; wrep < w_fac; ++wrep) 89 | { 90 | raster_resize.push_back(val); 91 | } 92 | } 93 | for (int hrep = 1; hrep < h_fac; ++hrep) 94 | { 95 | raster_resize.insert(raster_resize.end(), raster_resize.end() - w_new, 96 | raster_resize.end()); 97 | } 98 | } 99 | raster = raster_resize.data(); 100 | w = w_new; 101 | h = h_new; 102 | } 103 | 104 | png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 105 | if (!png) 106 | { 107 | return ""; 108 | } 109 | png_infop info = png_create_info_struct(png); 110 | if (!info) 111 | { 112 | png_destroy_write_struct(&png, (png_infopp)NULL); 113 | return ""; 114 | } 115 | if (setjmp(png_jmpbuf(png))) 116 | { 117 | png_destroy_write_struct(&png, &info); 118 | return ""; 119 | } 120 | png_set_IHDR(png, info, w, h, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, 121 | PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); 122 | std::vector rows(h); 123 | for (int y = 0; y < h; ++y) 124 | { 125 | rows[y] = (uint8_t *)raster + y * w * 4; 126 | } 127 | 128 | std::vector buffer; 129 | png_set_rows(png, info, rows.data()); 130 | png_set_write_fn(png, &buffer, png_memory_write, NULL); 131 | png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL); 132 | png_destroy_write_struct(&png, &info); 133 | 134 | return base64_encode(buffer.data(), buffer.size()); 135 | } 136 | 137 | std::string raster_base64(const renderers::Raster &t_raster) 138 | { 139 | return raster_to_string(t_raster.raster, t_raster.wh.x, t_raster.wh.y, 140 | t_raster.rect.width, t_raster.rect.height, 141 | t_raster.interpolate); 142 | } 143 | 144 | } // namespace unigd -------------------------------------------------------------------------------- /src/unigd_dev.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_UNIGD_DEV_H__ 2 | #define __UNIGD_UNIGD_DEV_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "generic_dev.h" 11 | #include "page_store.h" 12 | #include "plot_history.h" 13 | #include "unigd_commons.h" 14 | #include "unigd_external.h" 15 | 16 | namespace unigd 17 | { 18 | struct device_params 19 | { 20 | int bg; 21 | double width; 22 | double height; 23 | double pointsize; 24 | cpp11::list aliases; 25 | bool reset_par; 26 | }; 27 | 28 | class DeviceTarget 29 | { 30 | public: 31 | int get_index() const; 32 | void set_index(int index); 33 | int get_newest_index() const; 34 | void set_newest_index(int index); 35 | bool is_void() const; 36 | void set_void(); 37 | 38 | private: 39 | int m_index{-1}; // current draw target 40 | int m_newest_index{-1}; // open draw target 41 | bool m_void{true}; 42 | }; 43 | 44 | class unigd_device : public generic_dev 45 | { 46 | public: 47 | // Font handling 48 | cpp11::list system_aliases; 49 | cpp11::list user_aliases; 50 | 51 | unigd_device(const device_params &t_params); 52 | 53 | unigd_device(const unigd_device &) = delete; 54 | unigd_device &operator=(unigd_device &) = delete; 55 | unigd_device &operator=(const unigd_device &) = delete; 56 | 57 | unigd_device(unigd_device &&) = delete; 58 | unigd_device &operator=(unigd_device &&) = delete; 59 | 60 | bool attach_client(ex::graphics_client *t_client, UNIGD_CLIENT_ID t_client_id, 61 | void *t_client_data); 62 | bool get_client(ex::graphics_client **t_client, UNIGD_CLIENT_ID t_client_id, 63 | void **t_client_data); 64 | bool get_client_anonymous(ex::graphics_client **t_client, void **t_client_data); 65 | bool remove_client(); 66 | 67 | // Synchronous access 68 | 69 | void plt_prerender(int index, double width, double height); 70 | bool plt_remove(int index); 71 | bool plt_clear(); 72 | bool plt_render(int index, double width, double height, 73 | renderers::render_target *t_renderer, double t_scale); 74 | 75 | // Datastore only access 76 | 77 | ex::device_state plt_state(); 78 | ex::find_results plt_query(int offset, int limit); 79 | int plt_index(int32_t id); 80 | 81 | // Asynchronous access 82 | 83 | std::unique_ptr api_render(ex::renderer_id_t t_renderer_id, 84 | int32_t t_plot_id, double t_width, 85 | double t_height, double t_scale); 86 | bool api_remove(int32_t t_id); 87 | bool api_clear(); 88 | 89 | protected: 90 | // Device callbacks 91 | 92 | void dev_activate(pDevDesc dd) override; 93 | void dev_deactivate(pDevDesc dd) override; 94 | void dev_close(pDevDesc dd) override; 95 | void dev_clip(double x0, double x1, double y0, double y1, pDevDesc dd) override; 96 | void dev_size(double *left, double *right, double *bottom, double *top, 97 | pDevDesc dd) override; 98 | void dev_newPage(pGEcontext gc, pDevDesc dd) override; 99 | void dev_line(double x1, double y1, double x2, double y2, pGEcontext gc, 100 | pDevDesc dd) override; 101 | void dev_text(double x, double y, const char *str, double rot, double hadj, 102 | pGEcontext gc, pDevDesc dd) override; 103 | double dev_strWidth(const char *str, pGEcontext gc, pDevDesc dd) override; 104 | void dev_rect(double x0, double y0, double x1, double y1, pGEcontext gc, 105 | pDevDesc dd) override; 106 | void dev_circle(double x, double y, double r, pGEcontext gc, pDevDesc dd) override; 107 | void dev_polygon(int n, double *x, double *y, pGEcontext gc, pDevDesc dd) override; 108 | void dev_polyline(int n, double *x, double *y, pGEcontext gc, pDevDesc dd) override; 109 | void dev_path(double *x, double *y, int npoly, int *nper, Rboolean winding, 110 | pGEcontext gc, pDevDesc dd) override; 111 | void dev_mode(int mode, pDevDesc dd) override; 112 | void dev_metricInfo(int c, pGEcontext gc, double *ascent, double *descent, 113 | double *width, pDevDesc dd) override; 114 | void dev_raster(unsigned int *raster, int w, int h, double x, double y, double width, 115 | double height, double rot, Rboolean interpolate, pGEcontext gc, 116 | pDevDesc dd) override; 117 | 118 | private: 119 | PlotHistory m_history; 120 | std::shared_ptr m_data_store; 121 | 122 | ex::graphics_client *m_client{nullptr}; 123 | UNIGD_CLIENT_ID m_client_id = 0; 124 | void *m_client_data{nullptr}; 125 | 126 | bool replaying{false}; // Is the device replaying 127 | DeviceTarget m_target; 128 | 129 | bool m_initialized{false}; 130 | 131 | void put(std::unique_ptr &&t_dc); 132 | 133 | // set device size 134 | void resize_device_to_page(pDevDesc dd); 135 | 136 | // graphical parameters for reseting 137 | cpp11::list m_reset_par; 138 | 139 | std::vector> m_dc_buffer{}; 140 | }; 141 | 142 | } // namespace unigd 143 | 144 | #endif /* __UNIGD_UNIGD_DEV_H__ */ 145 | -------------------------------------------------------------------------------- /src/cpp11.cpp: -------------------------------------------------------------------------------- 1 | // Generated by cpp11: do not edit by hand 2 | // clang-format off 3 | 4 | 5 | #include "cpp11/declarations.hpp" 6 | #include 7 | 8 | // unigd.cpp 9 | int unigd_ugd_(std::string bg, double width, double height, double pointsize, cpp11::list aliases, bool reset_par); 10 | extern "C" SEXP _unigd_unigd_ugd_(SEXP bg, SEXP width, SEXP height, SEXP pointsize, SEXP aliases, SEXP reset_par) { 11 | BEGIN_CPP11 12 | return cpp11::as_sexp(unigd_ugd_(cpp11::as_cpp>(bg), cpp11::as_cpp>(width), cpp11::as_cpp>(height), cpp11::as_cpp>(pointsize), cpp11::as_cpp>(aliases), cpp11::as_cpp>(reset_par))); 13 | END_CPP11 14 | } 15 | // unigd.cpp 16 | cpp11::list unigd_state_(int devnum); 17 | extern "C" SEXP _unigd_unigd_state_(SEXP devnum) { 18 | BEGIN_CPP11 19 | return cpp11::as_sexp(unigd_state_(cpp11::as_cpp>(devnum))); 20 | END_CPP11 21 | } 22 | // unigd.cpp 23 | cpp11::list unigd_info_(int devnum); 24 | extern "C" SEXP _unigd_unigd_info_(SEXP devnum) { 25 | BEGIN_CPP11 26 | return cpp11::as_sexp(unigd_info_(cpp11::as_cpp>(devnum))); 27 | END_CPP11 28 | } 29 | // unigd.cpp 30 | cpp11::data_frame unigd_renderers_(); 31 | extern "C" SEXP _unigd_unigd_renderers_() { 32 | BEGIN_CPP11 33 | return cpp11::as_sexp(unigd_renderers_()); 34 | END_CPP11 35 | } 36 | // unigd.cpp 37 | int unigd_plot_find_(int devnum, int plot_id); 38 | extern "C" SEXP _unigd_unigd_plot_find_(SEXP devnum, SEXP plot_id) { 39 | BEGIN_CPP11 40 | return cpp11::as_sexp(unigd_plot_find_(cpp11::as_cpp>(devnum), cpp11::as_cpp>(plot_id))); 41 | END_CPP11 42 | } 43 | // unigd.cpp 44 | SEXP unigd_render_(int devnum, int page, double width, double height, double zoom, std::string renderer_id); 45 | extern "C" SEXP _unigd_unigd_render_(SEXP devnum, SEXP page, SEXP width, SEXP height, SEXP zoom, SEXP renderer_id) { 46 | BEGIN_CPP11 47 | return cpp11::as_sexp(unigd_render_(cpp11::as_cpp>(devnum), cpp11::as_cpp>(page), cpp11::as_cpp>(width), cpp11::as_cpp>(height), cpp11::as_cpp>(zoom), cpp11::as_cpp>(renderer_id))); 48 | END_CPP11 49 | } 50 | // unigd.cpp 51 | bool unigd_remove_(int devnum, int page); 52 | extern "C" SEXP _unigd_unigd_remove_(SEXP devnum, SEXP page) { 53 | BEGIN_CPP11 54 | return cpp11::as_sexp(unigd_remove_(cpp11::as_cpp>(devnum), cpp11::as_cpp>(page))); 55 | END_CPP11 56 | } 57 | // unigd.cpp 58 | bool unigd_remove_id_(int devnum, int plot_id); 59 | extern "C" SEXP _unigd_unigd_remove_id_(SEXP devnum, SEXP plot_id) { 60 | BEGIN_CPP11 61 | return cpp11::as_sexp(unigd_remove_id_(cpp11::as_cpp>(devnum), cpp11::as_cpp>(plot_id))); 62 | END_CPP11 63 | } 64 | // unigd.cpp 65 | cpp11::writable::list unigd_id_(int devnum, int page, int limit); 66 | extern "C" SEXP _unigd_unigd_id_(SEXP devnum, SEXP page, SEXP limit) { 67 | BEGIN_CPP11 68 | return cpp11::as_sexp(unigd_id_(cpp11::as_cpp>(devnum), cpp11::as_cpp>(page), cpp11::as_cpp>(limit))); 69 | END_CPP11 70 | } 71 | // unigd.cpp 72 | bool unigd_clear_(int devnum); 73 | extern "C" SEXP _unigd_unigd_clear_(SEXP devnum) { 74 | BEGIN_CPP11 75 | return cpp11::as_sexp(unigd_clear_(cpp11::as_cpp>(devnum))); 76 | END_CPP11 77 | } 78 | // unigd.cpp 79 | void unigd_ipc_open_(); 80 | extern "C" SEXP _unigd_unigd_ipc_open_() { 81 | BEGIN_CPP11 82 | unigd_ipc_open_(); 83 | return R_NilValue; 84 | END_CPP11 85 | } 86 | // unigd.cpp 87 | void unigd_ipc_close_(); 88 | extern "C" SEXP _unigd_unigd_ipc_close_() { 89 | BEGIN_CPP11 90 | unigd_ipc_close_(); 91 | return R_NilValue; 92 | END_CPP11 93 | } 94 | 95 | extern "C" { 96 | static const R_CallMethodDef CallEntries[] = { 97 | {"_unigd_unigd_clear_", (DL_FUNC) &_unigd_unigd_clear_, 1}, 98 | {"_unigd_unigd_id_", (DL_FUNC) &_unigd_unigd_id_, 3}, 99 | {"_unigd_unigd_info_", (DL_FUNC) &_unigd_unigd_info_, 1}, 100 | {"_unigd_unigd_ipc_close_", (DL_FUNC) &_unigd_unigd_ipc_close_, 0}, 101 | {"_unigd_unigd_ipc_open_", (DL_FUNC) &_unigd_unigd_ipc_open_, 0}, 102 | {"_unigd_unigd_plot_find_", (DL_FUNC) &_unigd_unigd_plot_find_, 2}, 103 | {"_unigd_unigd_remove_", (DL_FUNC) &_unigd_unigd_remove_, 2}, 104 | {"_unigd_unigd_remove_id_", (DL_FUNC) &_unigd_unigd_remove_id_, 2}, 105 | {"_unigd_unigd_render_", (DL_FUNC) &_unigd_unigd_render_, 6}, 106 | {"_unigd_unigd_renderers_", (DL_FUNC) &_unigd_unigd_renderers_, 0}, 107 | {"_unigd_unigd_state_", (DL_FUNC) &_unigd_unigd_state_, 1}, 108 | {"_unigd_unigd_ugd_", (DL_FUNC) &_unigd_unigd_ugd_, 6}, 109 | {NULL, NULL, 0} 110 | }; 111 | } 112 | 113 | void export_api(DllInfo* dll); 114 | 115 | extern "C" attribute_visible void R_init_unigd(DllInfo* dll){ 116 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 117 | R_useDynamicSymbols(dll, FALSE); 118 | export_api(dll); 119 | R_forceSymbols(dll, TRUE); 120 | } 121 | -------------------------------------------------------------------------------- /src/lib/svglite_utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // Collection of functions for font metrics and raster image encoding. 3 | // Extrected from: https://github.com/r-lib/svglite/blob/master/src/devSVG.cpp 4 | // (2020-06-21T15:26:43+00:00) 5 | // 6 | // 7 | // (C) 2002 T Jake Luciani: SVG device, based on PicTex device 8 | // (C) 2008 Tony Plate: Line type support from RSVGTipsDevice package 9 | // (C) 2012 Matthieu Decorde: UTF-8 support, XML reserved characters and XML header 10 | // (C) 2015 RStudio (Hadley Wickham): modernisation & refactoring 11 | // 12 | // This program is free software; you can redistribute it and/or modify 13 | // it under the terms of the GNU General Public License as published by 14 | // the Free Software Foundation; either version 2 of the License, or 15 | // (at your option) any later version. 16 | // 17 | // This program is distributed in the hope that it will be useful, 18 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | // GNU General Public License for more details. 21 | // 22 | // You should have received a copy of the GNU General Public License 23 | // along with this program; if not, write to the Free Software 24 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 25 | 26 | #ifndef __UNIGD_SVGLITE_UTILS_H__ 27 | #define __UNIGD_SVGLITE_UTILS_H__ 28 | 29 | #include 30 | #include 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | namespace unigd 38 | { 39 | // Font handling 40 | 41 | inline bool is_bold(int face) { return face == 2 || face == 4; } 42 | inline bool is_italic(int face) { return face == 3 || face == 4; } 43 | inline bool is_bolditalic(int face) { return face == 4; } 44 | inline bool is_symbol(int face) { return face == 5; } 45 | 46 | inline std::string find_alias_field(std::string family, cpp11::list &alias, 47 | const char *face, const char *field) 48 | { 49 | if (alias[face] != R_NilValue) 50 | { 51 | cpp11::list font(alias[face]); 52 | if (font[field] != R_NilValue) return cpp11::as_cpp(font[field]); 53 | } 54 | return std::string(); 55 | } 56 | 57 | inline std::string find_user_alias(std::string family, cpp11::list const &aliases, 58 | int face, const char *field) 59 | { 60 | std::string out; 61 | if (aliases[family.c_str()] != R_NilValue) 62 | { 63 | cpp11::list alias(aliases[family.c_str()]); 64 | if (is_bolditalic(face)) 65 | out = find_alias_field(family, alias, "bolditalic", field); 66 | else if (is_bold(face)) 67 | out = find_alias_field(family, alias, "bold", field); 68 | else if (is_italic(face)) 69 | out = find_alias_field(family, alias, "italic", field); 70 | else if (is_symbol(face)) 71 | out = find_alias_field(family, alias, "symbol", field); 72 | else 73 | out = find_alias_field(family, alias, "plain", field); 74 | } 75 | return out; 76 | } 77 | 78 | inline std::string fontfile(const char *family_, int face, cpp11::list user_aliases) 79 | { 80 | std::string family(family_); 81 | if (face == 5) 82 | family = "symbol"; 83 | else if (family == "") 84 | family = "sans"; 85 | 86 | return find_user_alias(family, user_aliases, face, "file"); 87 | } 88 | 89 | inline FontSettings get_font_file(const char *family, int face, cpp11::list user_aliases) 90 | { 91 | const char *fontfamily = family; 92 | if (is_symbol(face)) 93 | { 94 | fontfamily = "symbol"; 95 | } 96 | else if (strcmp(family, "") == 0) 97 | { 98 | fontfamily = "sans"; 99 | } 100 | std::string alias = fontfile(family, face, user_aliases); 101 | if (alias.size() > 0) 102 | { 103 | FontSettings result = {}; 104 | std::strncpy(result.file, alias.c_str(), PATH_MAX); 105 | result.index = 0; 106 | result.n_features = 0; 107 | return result; 108 | } 109 | 110 | return locate_font_with_features(fontfamily, is_italic(face), is_bold(face)); 111 | } 112 | 113 | inline std::string find_system_alias(std::string family, cpp11::list const &aliases) 114 | { 115 | std::string out; 116 | if (aliases[family.c_str()] != R_NilValue) 117 | { 118 | cpp11::sexp alias = aliases[family.c_str()]; 119 | if (TYPEOF(alias) == STRSXP && Rf_length(alias) == 1) 120 | out = cpp11::as_cpp(alias); 121 | } 122 | return out; 123 | } 124 | 125 | inline std::string fontname(const char *family_, int face, 126 | cpp11::list const &system_aliases, 127 | cpp11::list const &user_aliases, FontSettings &font) 128 | { 129 | std::string family(family_); 130 | if (face == 5) 131 | family = "symbol"; 132 | else if (family == "") 133 | family = "sans"; 134 | 135 | std::string alias = find_system_alias(family, system_aliases); 136 | if (!alias.size()) 137 | { 138 | alias = find_user_alias(family, user_aliases, face, "name"); 139 | } 140 | 141 | if (alias.size()) 142 | { 143 | return alias; 144 | } 145 | 146 | const size_t MAX_FONT_FAMILY_LEN = 100; 147 | char family_name[MAX_FONT_FAMILY_LEN]; 148 | if (get_font_family(font.file, font.index, family_name, MAX_FONT_FAMILY_LEN)) 149 | { 150 | return std::string(family_name, strnlen(family_name, MAX_FONT_FAMILY_LEN)); 151 | } 152 | return family; 153 | } 154 | 155 | } // namespace unigd 156 | 157 | #endif /* __UNIGD_SVGLITE_UTILS_H__ */ 158 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | # Based on davidgohel/gdtools package source with minimal modifications: 2 | # https://github.com/davidgohel/gdtools/blob/master/configure 3 | 4 | # Anticonf script by Jeroen Ooms (2020) 5 | # The script will try 'pkg-config' to find required cflags and ldflags. 6 | # Make sure this executable is in PATH when installing the package. 7 | # Alternatively, you can set INCLUDE_DIR and LIB_DIR manually: 8 | # R CMD INSTALL --configure-vars='INCLUDE_DIR=/.../include LIB_DIR=/.../lib' 9 | 10 | # Library settings 11 | PKG_CONFIG_NAME="cairo freetype2" 12 | PKG_DEB_NAME="libcairo2-dev" 13 | PKG_RPM_NAME="cairo-devel" 14 | PKG_CSW_NAME="libcairo_dev" 15 | PKG_BREW_NAME="cairo" 16 | PKG_LIBS="-lcairo -lfreetype" 17 | PKG_TEST_FILE="src/sysdep_tests/sysdep_cairo.c" 18 | 19 | # Prefer static linking on MacOS 20 | if [ `uname` = "Darwin" ]; then 21 | PKG_CONFIG_NAME="--static $PKG_CONFIG_NAME" 22 | if [ `arch` = "i386" ]; then 23 | export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/libffi/lib/pkgconfig" 24 | fi 25 | fi 26 | 27 | # Use pkg-config if available 28 | if [ `command -v pkg-config` ]; then 29 | PKGCONFIG_CFLAGS=`pkg-config --cflags ${PKG_CONFIG_NAME}` 30 | PKGCONFIG_LIBS=`pkg-config --libs ${PKG_CONFIG_NAME}` 31 | fi 32 | 33 | # On CRAN do not use pkgconfig cairo (which depends on XQuartz) 34 | if [ -d "/Builds/CRAN-QA-Simon" ] || [ -d "/Volumes/Builds" ]; then 35 | unset PKGCONFIG_CFLAGS 36 | unset PKGCONFIG_LIBS 37 | fi 38 | 39 | # Note that cflags may be empty in case of success 40 | if [ "$INCLUDE_DIR" ] || [ "$LIB_DIR" ]; then 41 | echo "Found INCLUDE_DIR and/or LIB_DIR!" 42 | PKG_CFLAGS="-I$INCLUDE_DIR $PKG_CFLAGS" 43 | PKG_LIBS="-L$LIB_DIR $PKG_LIBS" 44 | elif [ "$PKGCONFIG_CFLAGS" ] || [ "$PKGCONFIG_LIBS" ]; then 45 | echo "Found pkg-config cflags and libs!" 46 | PKG_CFLAGS=${PKGCONFIG_CFLAGS} 47 | PKG_LIBS=${PKGCONFIG_LIBS} 48 | elif [ `uname` = "Darwin" ]; then 49 | brew --version 2>/dev/null 50 | if [ $? -eq 0 ]; then 51 | BREWDIR=`brew --prefix` 52 | PKG_CFLAGS="-I$BREWDIR/include/cairo -I$BREWDIR/include/fontconfig -I$BREWDIR/include/freetype2" 53 | PKG_LIBS="-L$BREWDIR/lib $PKG_LIBS" 54 | else 55 | curl -sfL "https://autobrew.github.io/scripts/cairo" > autobrew 56 | . autobrew 57 | fi 58 | fi 59 | 60 | # Find compiler 61 | CC=`${R_HOME}/bin/R CMD config CC` 62 | CFLAGS=`${R_HOME}/bin/R CMD config CFLAGS` 63 | CPPFLAGS=`${R_HOME}/bin/R CMD config CPPFLAGS` 64 | 65 | # For debugging 66 | echo "Using PKG_CFLAGS=$PKG_CFLAGS" 67 | echo "Using PKG_LIBS=$PKG_LIBS" 68 | 69 | # Test configuration 70 | ${CC} ${CPPFLAGS} ${PKG_CFLAGS} ${CFLAGS} -E ${PKG_TEST_FILE} >/dev/null 2>configure.log 71 | if [ $? -ne 0 ]; then 72 | echo "------------------------------[ CAIRO ]---------------------------------" 73 | echo "Info: Configuration failed to find cairo system library." 74 | echo "httpgd is fully functional without cairo, but some plot file formats" 75 | echo "will not be available." 76 | echo "If you want to install cairo see below error for troubleshooting." 77 | echo "-----------------------------[ ANTICONF ]-------------------------------" 78 | echo "Configuration failed to find libraries. Try installing:" 79 | echo " * deb: $PKG_DEB_NAME (Debian, Ubuntu)" 80 | echo " * rpm: $PKG_RPM_NAME (Fedora, CentOS, RHEL)" 81 | echo " * csw: $PKG_CSW_NAME (Solaris)" 82 | echo " * brew: $PKG_BREW_NAME (OSX)" 83 | echo "If $PKG_CONFIG_NAME are already installed, check that 'pkg-config' is in your" 84 | echo "PATH and PKG_CONFIG_PATH contains a $PKG_CONFIG_NAME.pc file. If pkg-config" 85 | echo "is unavailable you can set INCLUDE_DIR and LIB_DIR manually via:" 86 | echo "R CMD INSTALL --configure-vars='INCLUDE_DIR=... LIB_DIR=...'" 87 | echo "---------------------------[ ERROR MESSAGE ]----------------------------" 88 | cat configure.log 89 | echo "------------------------------------------------------------------------" 90 | PKG_CFLAGS="-DUNIGD_NO_CAIRO" 91 | PKG_LIBS="" 92 | fi 93 | 94 | # Library settings 95 | PKG_LIBTIFF_CONFIG_NAME="libtiff-4" 96 | PKG_LIBTIFF_DEB_NAME="libtiff-dev" 97 | PKG_LIBTIFF_RPM_NAME="libtiff-devel" 98 | PKG_LIBTIFF_BREW_NAME="libtiff" 99 | PKG_LIBTIFF_TEST_FILE="src/sysdep_tests/sysdep_libtiff.cpp" 100 | PKG_LIBTIFF_LIBS="-ltiff -ljpeg" 101 | 102 | # Use pkg-config if available 103 | if [ $(command -v pkg-config) ]; then 104 | PKG_LIBTIFF_CFLAGS=$(pkg-config --cflags --silence-errors $PKG_LIBTIFF_CONFIG_NAME) 105 | PKG_LIBTIFF_LIBS="$(pkg-config --libs $PKG_LIBTIFF_CONFIG_NAME) -ltiffxx " 106 | fi 107 | 108 | echo "Using PKG_LIBTIFF_CFLAGS=$PKG_LIBTIFF_CFLAGS" 109 | echo "Using PKG_LIBTIFF_LIBS=$PKG_LIBTIFF_LIBS" 110 | 111 | # Find compiler 112 | CXX=`${R_HOME}/bin/R CMD config CXX` 113 | CXXFLAGS=`${R_HOME}/bin/R CMD config CXXFLAGS` 114 | CPPFLAGS=`${R_HOME}/bin/R CMD config CPPFLAGS` 115 | 116 | # Test configuration 117 | ${CXX} ${CPPFLAGS} ${PKG_LIBTIFF_CFLAGS} ${CFLAGS} ${PKG_LIBTIFF_TEST_FILE} ${PKG_LIBTIFF_LIBS} -o tmp_libtiff_test >/dev/null 2>configure.log 118 | # Customize the error 119 | if [ $? -ne 0 ]; then 120 | echo "------------------------------[ WARNING ]---------------------------" 121 | echo "Info: Configuration failed to find $PKG_LIBTIFF_CONFIG_NAME system library." 122 | echo "httpgd is fully functional without $PKG_LIBTIFF_CONFIG_NAME, but some plot file formats" 123 | echo "will not be available." 124 | echo "If you want to install $PKG_LIBTIFF_CONFIG_NAME see below error for troubleshooting." 125 | echo "------------------------- ANTICONF ERROR ---------------------------" 126 | echo "Configuration failed because $PKG_LIBTIFF_CONFIG_NAME was not found. Try installing:" 127 | echo " * deb: $PKG_LIBTIFF_DEB_NAME (Debian, Ubuntu, etc)" 128 | echo " * rpm: $PKG_LIBTIFF_RPM_NAME (Fedora, EPEL)" 129 | echo " * brew: $PKG_LIBTIFF_BREW_NAME (OSX)" 130 | echo "If $PKG_LIBTIFF_CONFIG_NAME is already installed, check that 'pkg-config' is in your" 131 | echo "PATH and PKG_CONFIG_PATH contains a $PKG_LIBTIFF_CONFIG_NAME.pc file. If pkg-config" 132 | echo "is unavailable you can set INCLUDE_DIR and LIB_DIR manually via:" 133 | echo "R CMD INSTALL --configure-vars='INCLUDE_DIR=... LIB_DIR=...'" 134 | echo "--------------------------------------------------------------------" 135 | PKG_LIBTIFF_CFLAGS="-DUNIGD_NO_TIFF" 136 | PKG_LIBTIFF_LIBS="" 137 | fi 138 | # remove test file 139 | rm -f tmp_libtiff_test 140 | 141 | # Write to Makevars 142 | sed -e "s|@cflags@|$PKG_CFLAGS $PKG_LIBTIFF_CFLAGS|" -e "s|@libs@|$PKG_LIBS $PKG_LIBTIFF_LIBS|" src/Makevars.in > src/Makevars 143 | 144 | # Success 145 | exit 0 146 | -------------------------------------------------------------------------------- /src/renderer_json.cpp: -------------------------------------------------------------------------------- 1 | #include "renderer_json.h" 2 | 3 | #include "base_64.h" 4 | 5 | namespace unigd 6 | { 7 | namespace renderers 8 | { 9 | 10 | static inline std::string hexcol(color_t t_color) 11 | { 12 | return fmt::format("#{:02X}{:02X}{:02X}", color::red(t_color), color::green(t_color), 13 | color::blue(t_color)); 14 | } 15 | 16 | static inline std::string json_lineinfo(const LineInfo &t_line) 17 | { 18 | return fmt::format( 19 | R""({{ "col": "{}", "lwd": {:.2f}, "lty": {}, "lend": {}, "ljoin": {}, "lmitre": {} }})"", 20 | hexcol(t_line.col), t_line.lwd, t_line.lty, static_cast(t_line.lend), static_cast(t_line.ljoin), 21 | static_cast(t_line.lmitre)); 22 | } 23 | 24 | static inline void json_verts(fmt::memory_buffer &os, 25 | const std::vector> &t_verts) 26 | { 27 | fmt::format_to(std::back_inserter(os), "["); 28 | for (auto it = t_verts.begin(); it != t_verts.end(); ++it) 29 | { 30 | if (it != t_verts.begin()) 31 | { 32 | fmt::format_to(std::back_inserter(os), ", "); 33 | } 34 | fmt::format_to(std::back_inserter(os), "[ {:.2f}, {:.2f} ]", it->x, it->y); 35 | } 36 | fmt::format_to(std::back_inserter(os), "]"); 37 | } 38 | 39 | void RendererJSON::render(const Page &t_page, double t_scale) 40 | { 41 | m_scale = t_scale; 42 | page(t_page); 43 | } 44 | 45 | void RendererJSON::get_data(const uint8_t **t_buf, size_t *t_size) const 46 | { 47 | *t_buf = reinterpret_cast(os.begin()); 48 | *t_size = os.size(); 49 | } 50 | 51 | void RendererJSON::page(const Page &t_page) 52 | { 53 | fmt::format_to( 54 | std::back_inserter(os), 55 | "{{\n " 56 | R""("id": "{}", "w": {:.2f}, "h": {:.2f}, "scale": {:.2f}, "fill": "{}",)"" 57 | "\n", 58 | t_page.id, t_page.size.x, t_page.size.y, m_scale, hexcol(t_page.fill)); 59 | fmt::format_to(std::back_inserter(os), " \"clips\": [\n "); 60 | for (auto it = t_page.cps.begin(); it != t_page.cps.end(); ++it) 61 | { 62 | if (it != t_page.cps.begin()) 63 | { 64 | fmt::format_to(std::back_inserter(os), ",\n "); 65 | } 66 | fmt::format_to( 67 | std::back_inserter(os), 68 | R""({{ "id": {}, "x": {:.2f}, "y": {:.2f}, "w": {:.2f}, "h": {:.2f} }})"", it->id, 69 | it->rect.x, it->rect.y, it->rect.width, it->rect.height); 70 | } 71 | 72 | fmt::format_to(std::back_inserter(os), "\n ],\n \"draw_calls\": [\n "); 73 | for (auto it = t_page.dcs.begin(); it != t_page.dcs.end(); ++it) 74 | { 75 | if (it != t_page.dcs.begin()) 76 | { 77 | fmt::format_to(std::back_inserter(os), ",\n "); 78 | } 79 | fmt::format_to(std::back_inserter(os), "{{ "); 80 | (*it)->visit(this); 81 | fmt::format_to(std::back_inserter(os), " }}"); 82 | } 83 | fmt::format_to(std::back_inserter(os), "\n ]\n}}"); 84 | } 85 | 86 | void RendererJSON::visit(const Rect *t_rect) 87 | { 88 | fmt::format_to( 89 | std::back_inserter(os), 90 | R""("type": "rect", "clip_id": {}, "x": {:.2f}, "y": {:.2f}, "w": {:.2f}, "h": {:.2f}, "line": {})"", 91 | t_rect->clip_id, t_rect->rect.x, t_rect->rect.y, t_rect->rect.width, 92 | t_rect->rect.height, json_lineinfo(t_rect->line)); 93 | } 94 | 95 | void RendererJSON::visit(const Text *t_text) 96 | { 97 | fmt::format_to( 98 | std::back_inserter(os), 99 | R""("type": "text", "clip_id": {}, "x": {:.2f}, "y": {:.2f}, "rot": {:.2f}, "hadj": {:.2f}, "col": "{}", "str": "{}", )"" 100 | R""("weight": {}, "features": "{}", "font_family": "{}", "fontsize": {:.2f}, "italic": {}, "txtwidth_px": {:.2f})"", 101 | t_text->clip_id, t_text->pos.x, t_text->pos.y, t_text->rot, t_text->hadj, 102 | hexcol(t_text->col), t_text->str, t_text->text.weight, t_text->text.features, 103 | t_text->text.font_family, t_text->text.fontsize, t_text->text.italic, 104 | t_text->text.txtwidth_px); 105 | } 106 | 107 | void RendererJSON::visit(const Circle *t_circle) 108 | { 109 | fmt::format_to( 110 | std::back_inserter(os), 111 | R""("type": "circle", "clip_id": {}, "x": {:.2f}, "y": {:.2f}, "r": {:.2f}, "fill": "{}", "line": {})"", 112 | t_circle->clip_id, t_circle->pos.x, t_circle->pos.y, t_circle->radius, 113 | hexcol(t_circle->fill), json_lineinfo(t_circle->line)); 114 | } 115 | 116 | void RendererJSON::visit(const Line *t_line) 117 | { 118 | fmt::format_to( 119 | std::back_inserter(os), 120 | R""("type": "line", "clip_id": {}, "x0": {:.2f}, "y0": {:.2f}, "x1": {:.2f}, "y1": {:.2f}, "line": {})"", 121 | t_line->clip_id, t_line->orig.x, t_line->orig.y, t_line->dest.x, t_line->dest.y, 122 | json_lineinfo(t_line->line)); 123 | } 124 | 125 | void RendererJSON::visit(const Polyline *t_polyline) 126 | { 127 | fmt::format_to(std::back_inserter(os), 128 | R""("type": "polyline", "clip_id": {}, "line": {}, "points": )"", 129 | t_polyline->clip_id, json_lineinfo(t_polyline->line)); 130 | json_verts(os, t_polyline->points); 131 | } 132 | 133 | void RendererJSON::visit(const Polygon *t_polygon) 134 | { 135 | fmt::format_to( 136 | std::back_inserter(os), 137 | R""("type": "polygon", "clip_id": {}, "fill": "{}", "line": {}, "points": )"", 138 | t_polygon->clip_id, hexcol(t_polygon->fill), json_lineinfo(t_polygon->line)); 139 | json_verts(os, t_polygon->points); 140 | } 141 | 142 | void RendererJSON::visit(const Path *t_path) 143 | { 144 | fmt::format_to(std::back_inserter(os), 145 | R""("type": "path", "clip_id": {}, "fill": "{}", "line": {}, "nper": )"", 146 | t_path->clip_id, hexcol(t_path->fill), json_lineinfo(t_path->line)); 147 | 148 | fmt::format_to(std::back_inserter(os), "["); 149 | for (auto it = t_path->nper.begin(); it != t_path->nper.end(); ++it) 150 | { 151 | if (it != t_path->nper.begin()) 152 | { 153 | fmt::format_to(std::back_inserter(os), ", "); 154 | } 155 | fmt::format_to(std::back_inserter(os), "{}", *it); 156 | } 157 | fmt::format_to(std::back_inserter(os), R""(], "points": )""); 158 | json_verts(os, t_path->points); 159 | } 160 | 161 | void RendererJSON::visit(const Raster *t_raster) 162 | { 163 | fmt::format_to( 164 | std::back_inserter(os), 165 | R""("type": "raster", "clip_id": {}, "x": {:.2f}, "y": {:.2f}, "w": {:.2f}, "h": {:.2f}, "rot": {:.2f}, "raster": {{ "w": {}, "h": {}, "data": "{}" }})"", 166 | t_raster->clip_id, t_raster->rect.x, t_raster->rect.y, t_raster->rect.width, 167 | t_raster->rect.height, t_raster->rot, t_raster->wh.x, t_raster->wh.y, 168 | raster_base64(*t_raster)); 169 | } 170 | 171 | } // namespace renderers 172 | } // namespace unigd 173 | -------------------------------------------------------------------------------- /src/unigd.cpp: -------------------------------------------------------------------------------- 1 | #include // std::max 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "debug_print.h" 15 | #include "generic_dev.h" 16 | #include "r_thread.h" 17 | #include "renderer_svg.h" 18 | #include "renderers.h" 19 | #include "unigd_dev.h" 20 | #include "unigd_version.h" 21 | #include "uuid.h" 22 | 23 | namespace 24 | { 25 | inline std::shared_ptr validate_unigddev(int devnum) 26 | { 27 | auto a = unigd::unigd_device::from_device_number(devnum); 28 | if (a == nullptr) 29 | { 30 | cpp11::stop("Not a valid device number"); 31 | } 32 | return a; 33 | } 34 | 35 | } // namespace 36 | 37 | [[cpp11::register]] int unigd_ugd_(std::string bg, double width, double height, 38 | double pointsize, cpp11::list aliases, bool reset_par) 39 | { 40 | int ibg = R_GE_str2col(bg.c_str()); 41 | 42 | const unigd::device_params dparams{ibg, width, height, pointsize, aliases, reset_par}; 43 | 44 | return std::make_shared(dparams)->create("unigd"); 45 | } 46 | 47 | [[cpp11::register]] cpp11::list unigd_state_(int devnum) 48 | { 49 | auto dev = validate_unigddev(devnum); 50 | 51 | const auto state = dev->plt_state(); 52 | 53 | SEXP client_info; 54 | unigd::ex::graphics_client *client; 55 | void *client_data; 56 | if (dev->get_client_anonymous(&client, &client_data)) 57 | { 58 | client_info = cpp11::writable::strings({std::string(client->info(client_data))}); 59 | } 60 | else 61 | { 62 | client_info = R_NilValue; 63 | } 64 | 65 | using namespace cpp11::literals; 66 | return cpp11::writable::list{"hsize"_nm = state.hsize, "upid"_nm = state.upid, 67 | "active"_nm = state.active, "client"_nm = client_info}; 68 | } 69 | 70 | [[cpp11::register]] cpp11::list unigd_info_(int devnum) 71 | { 72 | /*auto dev = validate_unigddev(devnum);*/ 73 | 74 | using namespace cpp11::literals; 75 | return cpp11::writable::list{"version"_nm = 76 | cpp11::writable::list{"unigd"_nm = UNIGD_VERSION}}; 77 | } 78 | 79 | [[cpp11::register]] cpp11::data_frame unigd_renderers_() 80 | { 81 | using namespace cpp11::literals; 82 | 83 | const auto renderers = unigd::renderers::renderers(); 84 | 85 | cpp11::writable::list rens{static_cast(renderers->size())}; 86 | 87 | const R_xlen_t nren = renderers->size(); 88 | cpp11::writable::strings ren_id{nren}; 89 | cpp11::writable::strings ren_mime{nren}; 90 | cpp11::writable::strings ren_ext{nren}; 91 | cpp11::writable::strings ren_name{nren}; 92 | cpp11::writable::strings ren_type{nren}; 93 | cpp11::writable::logicals ren_text; 94 | ren_text.resize(nren); // R cpp11 bug? 95 | cpp11::writable::strings ren_descr{nren}; 96 | 97 | R_xlen_t i = 0; 98 | for (auto it = renderers->begin(); it != renderers->end(); it++) 99 | { 100 | ren_id[i] = it->second.info.id; 101 | ren_mime[i] = it->second.info.mime; 102 | ren_ext[i] = it->second.info.fileext; 103 | ren_name[i] = it->second.info.name; 104 | ren_type[i] = it->second.info.type; 105 | ren_text[i] = it->second.info.text; 106 | ren_descr[i] = it->second.info.description; 107 | i++; 108 | } 109 | 110 | return cpp11::writable::data_frame( 111 | {"id"_nm = ren_id, "mime"_nm = ren_mime, "ext"_nm = ren_ext, "name"_nm = ren_name, 112 | "type"_nm = ren_type, "text"_nm = ren_text, "descr"_nm = ren_descr}); 113 | } 114 | 115 | [[cpp11::register]] int unigd_plot_find_(int devnum, int plot_id) 116 | { 117 | auto dev = validate_unigddev(devnum); 118 | auto page = dev->plt_index(plot_id); 119 | if (page == -1) 120 | { 121 | cpp11::stop("Not a valid plot ID."); 122 | } 123 | return page; 124 | } 125 | 126 | [[cpp11::register]] SEXP unigd_render_(int devnum, int page, double width, double height, 127 | double zoom, std::string renderer_id) 128 | { 129 | auto dev = validate_unigddev(devnum); 130 | 131 | if (width < 0 || height < 0) 132 | { 133 | zoom = 1; 134 | } 135 | 136 | unigd::renderers::renderer_map_entry ren; 137 | auto fi_renderer = unigd::renderers::find(renderer_id, &ren); 138 | if (!fi_renderer) 139 | { 140 | cpp11::stop("Not a valid renderer ID."); 141 | } 142 | auto renderer = ren.generator(); 143 | if (!dev->plt_render(page, width / zoom, height / zoom, renderer.get(), zoom)) 144 | { 145 | cpp11::stop("Plot does not exist."); 146 | } 147 | 148 | const uint8_t *buf; 149 | size_t buf_size; 150 | renderer->get_data(&buf, &buf_size); 151 | 152 | if (ren.info.text) 153 | { 154 | return cpp11::writable::strings({cpp11::r_string(std::string(buf, buf + buf_size))}); 155 | } 156 | else 157 | { 158 | return cpp11::writable::raws(buf, buf + buf_size); 159 | } 160 | } 161 | 162 | [[cpp11::register]] bool unigd_remove_(int devnum, int page) 163 | { 164 | auto dev = validate_unigddev(devnum); 165 | return dev->plt_remove(page); 166 | } 167 | 168 | [[cpp11::register]] bool unigd_remove_id_(int devnum, int plot_id) 169 | { 170 | auto dev = validate_unigddev(devnum); 171 | auto page = dev->plt_index(plot_id); 172 | if (page == -1) 173 | { 174 | cpp11::stop("Not a valid plot ID."); 175 | } 176 | 177 | return dev->plt_remove(page); 178 | } 179 | 180 | [[cpp11::register]] cpp11::writable::list unigd_id_(int devnum, int page, int limit) 181 | { 182 | auto dev = validate_unigddev(devnum); 183 | unigd::ex::find_results res; 184 | 185 | limit = std::max(limit, 0); 186 | 187 | res = dev->plt_query(page, limit); 188 | 189 | using namespace cpp11::literals; 190 | cpp11::writable::list state{"hsize"_nm = res.state.hsize, "upid"_nm = res.state.upid, 191 | "active"_nm = res.state.active}; 192 | 193 | cpp11::writable::list plots{static_cast(res.ids.size())}; 194 | 195 | for (std::size_t i = 0; i < res.ids.size(); ++i) 196 | { 197 | cpp11::writable::list p{"id"_nm = res.ids[i]}; 198 | p.attr("class") = "unigd_pid"; 199 | plots[i] = p; 200 | } 201 | 202 | return {"state"_nm = state, "plots"_nm = plots}; 203 | } 204 | 205 | [[cpp11::register]] bool unigd_clear_(int devnum) 206 | { 207 | auto dev = validate_unigddev(devnum); 208 | return dev->plt_clear(); 209 | } 210 | 211 | [[cpp11::register]] void unigd_ipc_open_() { unigd::async::ipc_open(); } 212 | 213 | [[cpp11::register]] void unigd_ipc_close_() { unigd::async::ipc_close(); } 214 | -------------------------------------------------------------------------------- /src/unigd_external.cpp: -------------------------------------------------------------------------------- 1 | #include "unigd_external.h" 2 | 3 | #include "r_thread.h" 4 | #include "renderers.h" 5 | #include "unigd_dev.h" 6 | #include "unigd_version.h" 7 | 8 | namespace unigd 9 | { 10 | namespace ex 11 | { 12 | using unigd_handle_t = unigd_handle; 13 | 14 | unigd_find_results find_results::c_repr() 15 | { 16 | return {state, static_cast(ids.size()), ids.data()}; 17 | } 18 | 19 | int api_test_fun() { return 7; } 20 | 21 | void api_log(const char *t_message) 22 | { 23 | std::string msg(t_message); 24 | async::r_thread([=]() { Rprintf("unigd client: %s\n", msg.c_str()); }); 25 | } 26 | 27 | const char *api_info() { return "unigd " UNIGD_VERSION; } 28 | 29 | UNIGD_CLIENT_ID api_register_client_id() 30 | { 31 | static UNIGD_CLIENT_ID client_id_counter = 0; 32 | return client_id_counter++; // todo: handle overflow 33 | } 34 | 35 | UNIGD_HANDLE api_device_attach(int devnum, unigd_graphics_client *client, 36 | UNIGD_CLIENT_ID client_id, void *client_data) 37 | { 38 | auto dev = unigd_device::from_device_number(devnum); 39 | if (!dev) 40 | { 41 | return nullptr; 42 | } 43 | if (dev->attach_client(client, client_id, client_data)) 44 | { 45 | return new unigd_handle_t{dev}; 46 | } 47 | return nullptr; 48 | } 49 | 50 | void *api_device_get(int devnum, UNIGD_CLIENT_ID client_id) 51 | { 52 | auto dev = unigd_device::from_device_number(devnum); 53 | if (!dev) 54 | { 55 | return nullptr; 56 | } 57 | graphics_client *client; 58 | void *client_data; 59 | if (!dev->get_client(&client, client_id, &client_data)) 60 | { 61 | return nullptr; 62 | } 63 | 64 | return client_data; 65 | } 66 | 67 | void api_device_destroy(UNIGD_HANDLE handle) 68 | { 69 | delete static_cast(handle); 70 | } 71 | 72 | unigd_device_state api_device_state(UNIGD_HANDLE ugd_handle) 73 | { 74 | const auto ugd = static_cast(ugd_handle); 75 | return ugd->device->plt_state(); 76 | } 77 | 78 | bool api_plots_clear(UNIGD_HANDLE ugd_handle) 79 | { 80 | const auto ugd = static_cast(ugd_handle); 81 | return ugd->device->api_clear(); 82 | } 83 | 84 | bool api_plots_remove(UNIGD_HANDLE ugd_handle, UNIGD_PLOT_ID id) 85 | { 86 | const auto ugd = static_cast(ugd_handle); 87 | return ugd->device->api_remove(id); 88 | } 89 | 90 | UNIGD_RENDER_HANDLE api_render_create(UNIGD_HANDLE ugd_handle, 91 | UNIGD_RENDERER_ID renderer_id, 92 | UNIGD_PLOT_ID plot_id, 93 | unigd_render_args render_args, 94 | unigd_render_access *render_access) 95 | { 96 | const auto ugd = static_cast(ugd_handle); 97 | auto handle = ugd->device 98 | ->api_render(renderer_id, plot_id, render_args.width, 99 | render_args.height, render_args.scale) 100 | .release(); 101 | if (handle) 102 | { 103 | size_t buf_size; 104 | handle->get_data(&render_access->buffer, &buf_size); 105 | render_access->size = buf_size; 106 | } 107 | else 108 | { 109 | render_access->buffer = nullptr; 110 | render_access->size = 0; 111 | } 112 | return handle; 113 | } 114 | 115 | void api_render_destroy(UNIGD_RENDER_HANDLE handle) 116 | { 117 | delete static_cast(handle); 118 | } 119 | 120 | UNIGD_FIND_HANDLE api_plots_find(UNIGD_HANDLE ugd_handle, UNIGD_PLOT_RELATIVE offset, 121 | UNIGD_PLOT_INDEX limit, unigd_find_results *results) 122 | { 123 | const auto ugd = static_cast(ugd_handle); 124 | 125 | auto *re = new find_results{}; 126 | *re = ugd->device->plt_query(offset, limit); 127 | *results = re->c_repr(); 128 | return re; 129 | } 130 | 131 | void api_plots_find_destroy(UNIGD_FIND_HANDLE handle) 132 | { 133 | delete static_cast(handle); 134 | } 135 | 136 | UNIGD_RENDERERS_ENTRY_HANDLE api_renderers_find(UNIGD_RENDERER_ID id, 137 | unigd_renderer_info *renderer) 138 | { 139 | if (!renderers::find_info(id, renderer)) 140 | { 141 | return nullptr; 142 | } 143 | static int ok_return = 1; 144 | return static_cast(&ok_return); 145 | } 146 | 147 | void api_renderers_find_destroy(UNIGD_RENDERERS_ENTRY_HANDLE handle) 148 | { 149 | // Placeholder in case a renderer lookup ever needs to alloc (e.g. for dynamic renderer 150 | // adding/removing) 151 | } 152 | 153 | UNIGD_RENDERERS_HANDLE api_renderers(unigd_renderers_list *renderer) 154 | { 155 | const auto rs = renderers::renderers(); 156 | 157 | auto *re = new std::vector; 158 | 159 | re->reserve(rs->size()); 160 | 161 | for (auto &it : *rs) 162 | { 163 | re->emplace_back(it.second.info); 164 | } 165 | 166 | *renderer = {(*re).data(), re->size()}; 167 | 168 | return re; 169 | } 170 | 171 | void api_renderers_destroy(UNIGD_RENDERERS_HANDLE handle) 172 | { 173 | delete static_cast *>(handle); 174 | } 175 | 176 | int api_v1_create(unigd_api_v1 **api_) 177 | { 178 | auto api = new unigd_api_v1(); 179 | 180 | api->log = api_log; 181 | api->info = api_info; 182 | 183 | api->register_client_id = api_register_client_id; 184 | 185 | api->device_attach = api_device_attach; 186 | api->device_get = api_device_get; 187 | api->device_destroy = api_device_destroy; 188 | 189 | api->device_state = api_device_state; 190 | 191 | api->device_plots_clear = api_plots_clear; 192 | api->device_plots_remove = api_plots_remove; 193 | 194 | api->device_render_create = api_render_create; 195 | api->device_render_destroy = api_render_destroy; 196 | 197 | api->device_plots_find = api_plots_find; 198 | api->device_plots_find_destroy = api_plots_find_destroy; 199 | 200 | api->renderers = api_renderers; 201 | api->renderers_destroy = api_renderers_destroy; 202 | api->renderers_find = api_renderers_find; 203 | api->renderers_find_destroy = api_renderers_find_destroy; 204 | 205 | *api_ = api; 206 | return 0; 207 | } 208 | 209 | int api_v1_destroy(unigd_api_v1 *api_) 210 | { 211 | delete api_; 212 | 213 | return 0; 214 | } 215 | 216 | } // namespace ex 217 | } // namespace unigd 218 | 219 | // There is a bug in cpp11 / decor where pointer types are not detected when the asterisk 220 | // is right-aligned see: https://github.com/r-lib/decor/pull/11 221 | // clang-format off 222 | [[cpp11::init]] void export_api(DllInfo* dll) 223 | // clang-format on 224 | { 225 | R_RegisterCCallable("unigd", "api_v1_create", 226 | reinterpret_cast(unigd::ex::api_v1_create)); 227 | R_RegisterCCallable("unigd", "api_v1_destroy", 228 | reinterpret_cast(unigd::ex::api_v1_destroy)); 229 | } 230 | -------------------------------------------------------------------------------- /src/draw_data.h: -------------------------------------------------------------------------------- 1 | #ifndef __UNIGD_DRAW_DATA_H__ 2 | #define __UNIGD_DRAW_DATA_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "geom.h" 10 | 11 | // Do not include any R headers here ! 12 | 13 | namespace unigd 14 | { 15 | namespace renderers 16 | { 17 | 18 | namespace color 19 | { 20 | constexpr color_t red_offset{0}; 21 | constexpr color_t green_offset{8}; 22 | constexpr color_t blue_offset{16}; 23 | constexpr color_t alpha_offset{24}; 24 | 25 | constexpr color_t byte_mask{0xFF}; 26 | constexpr color_t blue_mask{byte_mask << blue_offset}; 27 | constexpr color_t green_mask{byte_mask << green_offset}; 28 | constexpr color_t red_mask{byte_mask << red_offset}; 29 | constexpr color_t alpha_mask{byte_mask << alpha_offset}; 30 | 31 | constexpr color_t rgb(color_t r, color_t g, color_t b) 32 | { 33 | return ((r << red_offset) | (g << green_offset) | (b << blue_offset) | alpha_mask); 34 | } 35 | constexpr color_t rgba(color_t r, color_t g, color_t b, color_t a) 36 | { 37 | return ((r << red_offset) | (g << green_offset) | (b << blue_offset) | 38 | (a << alpha_offset)); 39 | } 40 | constexpr color_t red(color_t x) { return (x >> red_offset) & byte_mask; } 41 | constexpr color_t green(color_t x) { return (x >> green_offset) & byte_mask; } 42 | constexpr color_t blue(color_t x) { return (x >> blue_offset) & byte_mask; } 43 | constexpr color_t alpha(color_t x) { return (x >> alpha_offset) & byte_mask; } 44 | constexpr bool opaque(color_t x) { return alpha(x) == byte_mask; } 45 | constexpr bool transparent(color_t x) { return alpha(x) == 0; } 46 | constexpr bool tranwhite(color_t x) 47 | { 48 | return x == rgba(byte_mask, byte_mask, byte_mask, 0); 49 | } 50 | 51 | constexpr double byte_frac(color_t x) { return x / static_cast(byte_mask); } 52 | constexpr double red_frac(color_t x) { return byte_frac(red(x)); } 53 | constexpr double green_frac(color_t x) { return byte_frac(green(x)); } 54 | constexpr double blue_frac(color_t x) { return byte_frac(blue(x)); } 55 | constexpr double alpha_frac(color_t x) { return byte_frac(alpha(x)); } 56 | } // namespace color 57 | 58 | using clip_id_t = int; 59 | using page_id_t = uint32_t; 60 | 61 | // Data 62 | 63 | struct LineInfo 64 | { 65 | static const int TY_BLANK = 0; 66 | static const int TY_SOLID = -1; 67 | 68 | enum GC_lineend 69 | { 70 | GC_ROUND_CAP = 1, 71 | GC_BUTT_CAP = 2, 72 | GC_SQUARE_CAP = 3 73 | }; 74 | 75 | enum GC_linejoin 76 | { 77 | GC_ROUND_JOIN = 1, 78 | GC_MITRE_JOIN = 2, 79 | GC_BEVEL_JOIN = 3 80 | }; 81 | 82 | enum LTY 83 | { 84 | BLANK = -1, 85 | SOLID = 0, 86 | DASHED = 4 + (4 << 4), 87 | DOTTED = 1 + (3 << 4), 88 | DOTDASH = 1 + (3 << 4) + (4 << 8) + (3 << 12), 89 | LONGDASH = 7 + (3 << 4), 90 | TWODASH = 2 + (2 << 4) + (6 << 8) + (2 << 12) 91 | }; 92 | 93 | color_t col; 94 | double lwd; 95 | int lty; 96 | GC_lineend lend; 97 | GC_linejoin ljoin; 98 | double lmitre; 99 | }; 100 | 101 | struct TextInfo 102 | { 103 | int weight; 104 | std::string features; 105 | std::string font_family; 106 | double fontsize; 107 | bool italic; 108 | double txtwidth_px; 109 | }; 110 | 111 | // Draw calls 112 | 113 | class Page; 114 | class DrawCall; 115 | class Rect; 116 | class Text; 117 | class Circle; 118 | class Line; 119 | class Polyline; 120 | class Polygon; 121 | class Path; 122 | class Raster; 123 | 124 | struct draw_call_visitor 125 | { 126 | virtual void visit(const Rect *t_rect) = 0; 127 | virtual void visit(const Text *t_text) = 0; 128 | virtual void visit(const Circle *t_circle) = 0; 129 | virtual void visit(const Line *t_line) = 0; 130 | virtual void visit(const Polyline *t_polyline) = 0; 131 | virtual void visit(const Polygon *t_polygon) = 0; 132 | virtual void visit(const Path *t_path) = 0; 133 | virtual void visit(const Raster *t_raster) = 0; 134 | }; 135 | 136 | class DrawCall 137 | { 138 | public: 139 | virtual ~DrawCall() = default; 140 | virtual void visit(draw_call_visitor *t_visitor) const = 0; 141 | 142 | clip_id_t clip_id = 0; 143 | }; 144 | 145 | class Text : public DrawCall 146 | { 147 | public: 148 | Text(color_t t_col, gvertex t_pos, std::string &&t_str, double t_rot, 149 | double t_hadj, TextInfo &&t_text); 150 | void visit(draw_call_visitor *t_visitor) const override; 151 | 152 | color_t col; 153 | gvertex pos; 154 | double rot, hadj; 155 | std::string str; 156 | TextInfo text; 157 | }; 158 | 159 | class Circle : public DrawCall 160 | { 161 | public: 162 | Circle(LineInfo &&t_line, color_t t_fill, gvertex t_pos, double t_radius); 163 | void visit(draw_call_visitor *t_visitor) const override; 164 | 165 | LineInfo line; 166 | color_t fill; 167 | gvertex pos; 168 | double radius; 169 | }; 170 | 171 | class Line : public DrawCall 172 | { 173 | public: 174 | Line(LineInfo &&t_line, gvertex t_orig, gvertex t_dest); 175 | void visit(draw_call_visitor *t_visitor) const override; 176 | 177 | LineInfo line; 178 | gvertex orig, dest; 179 | }; 180 | 181 | class Rect : public DrawCall 182 | { 183 | public: 184 | Rect(LineInfo &&t_line, color_t t_fill, grect t_rect); 185 | void visit(draw_call_visitor *t_visitor) const override; 186 | 187 | LineInfo line; 188 | color_t fill; 189 | grect rect; 190 | }; 191 | 192 | class Polyline : public DrawCall 193 | { 194 | public: 195 | Polyline(LineInfo &&t_line, std::vector> &&t_points); 196 | void visit(draw_call_visitor *t_visitor) const override; 197 | 198 | LineInfo line; 199 | std::vector> points; 200 | }; 201 | class Polygon : public DrawCall 202 | { 203 | public: 204 | Polygon(LineInfo &&t_line, color_t t_fill, std::vector> &&t_points); 205 | void visit(draw_call_visitor *t_visitor) const override; 206 | 207 | LineInfo line; 208 | color_t fill; 209 | std::vector> points; 210 | }; 211 | class Path : public DrawCall 212 | { 213 | public: 214 | Path(LineInfo &&t_line, color_t t_fill, std::vector> &&t_points, 215 | std::vector &&t_nper, bool t_winding); 216 | void visit(draw_call_visitor *t_visitor) const override; 217 | 218 | LineInfo line; 219 | color_t fill; 220 | std::vector> points; 221 | std::vector nper; 222 | bool winding; 223 | }; 224 | 225 | class Raster : public DrawCall 226 | { 227 | public: 228 | Raster(std::vector &&t_raster, gvertex t_wh, grect t_rect, 229 | double t_rot, bool t_interpolate); 230 | void visit(draw_call_visitor *t_visitor) const override; 231 | 232 | std::vector raster; 233 | gvertex wh; 234 | grect rect; 235 | double rot; 236 | bool interpolate; 237 | }; 238 | 239 | class Clip 240 | { 241 | public: 242 | inline bool equals(const grect &t_rect) const 243 | { 244 | return rect_equals(t_rect, rect, 0.01); 245 | } 246 | 247 | clip_id_t id; 248 | grect rect; 249 | }; 250 | 251 | class Page 252 | { 253 | public: 254 | Page(page_id_t t_id, gvertex t_size); 255 | 256 | Page(const Page &) = delete; 257 | Page &operator=(Page &) = delete; 258 | Page &operator=(const Page &) = delete; 259 | 260 | Page(Page &&) = default; 261 | Page &operator=(Page &&) = default; 262 | 263 | void put(std::unique_ptr &&t_dc); 264 | void put(std::vector> &&t_dcs); 265 | void clear(); 266 | void clip(grect t_rect); 267 | 268 | page_id_t id; 269 | gvertex size; 270 | color_t fill; 271 | 272 | std::vector> dcs; 273 | std::vector cps; 274 | }; 275 | 276 | } // namespace renderers 277 | 278 | } // namespace unigd 279 | 280 | #endif /* __UNIGD_DRAW_DATA_H__ */ 281 | -------------------------------------------------------------------------------- /src/page_store.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "page_store.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "unigd_commons.h" 8 | 9 | // Do not include any R headers here! 10 | 11 | namespace unigd 12 | { 13 | inline bool page_store::m_valid_index(ex::plot_relative_t t_index) 14 | { 15 | const auto psize = static_cast(m_pages.size()); 16 | return (psize > 0 && (t_index >= -psize && t_index < psize)); 17 | } 18 | inline std::size_t page_store::m_index_to_pos(ex::plot_relative_t t_index) 19 | { 20 | return (t_index < 0 ? (m_pages.size() + t_index) : t_index); 21 | } 22 | 23 | std::experimental::optional page_store::normalize_index( 24 | ex::plot_relative_t t_index) 25 | { 26 | const std::shared_lock r_lock(m_store_mutex); 27 | if (!m_valid_index(t_index)) 28 | { 29 | return std::experimental::nullopt; 30 | } 31 | return m_index_to_pos(t_index); 32 | } 33 | 34 | ex::plot_index_t page_store::append(gvertex t_size) 35 | { 36 | const std::unique_lock w_lock(m_store_mutex); 37 | m_pages.emplace_back(unigd::renderers::Page{m_id_counter, t_size}); 38 | 39 | m_id_counter = incwrap(m_id_counter); 40 | 41 | return m_pages.size() - 1; 42 | } 43 | void page_store::add_dc(ex::plot_relative_t t_index, 44 | std::unique_ptr &&t_dc, bool t_silent) 45 | { 46 | const std::unique_lock w_lock(m_store_mutex); 47 | if (!m_valid_index(t_index)) 48 | { 49 | return; 50 | } 51 | auto index = m_index_to_pos(t_index); 52 | m_pages[index].put(std::move(t_dc)); 53 | if (!t_silent) 54 | { 55 | m_inc_upid(); 56 | } 57 | } 58 | 59 | void page_store::add_dc(ex::plot_relative_t t_index, 60 | std::vector> &&t_dcs, 61 | bool t_silent) 62 | { 63 | const std::unique_lock w_lock(m_store_mutex); 64 | if (!m_valid_index(t_index)) 65 | { 66 | return; 67 | } 68 | auto index = m_index_to_pos(t_index); 69 | 70 | m_pages[index].put(std::move(t_dcs)); 71 | if (!t_silent) 72 | { 73 | m_inc_upid(); 74 | } 75 | } 76 | void page_store::clear(ex::plot_relative_t t_index, bool t_silent) 77 | { 78 | const std::unique_lock w_lock(m_store_mutex); 79 | if (!m_valid_index(t_index)) 80 | { 81 | return; 82 | } 83 | auto index = m_index_to_pos(t_index); 84 | m_pages[index].clear(); 85 | if (!t_silent) 86 | { 87 | m_inc_upid(); 88 | } 89 | } 90 | bool page_store::remove(ex::plot_relative_t t_index, bool t_silent) 91 | { 92 | const std::unique_lock w_lock(m_store_mutex); 93 | 94 | if (!m_valid_index(t_index)) 95 | { 96 | return false; 97 | } 98 | auto index = m_index_to_pos(t_index); 99 | 100 | m_pages.erase(m_pages.begin() + index); 101 | if (!t_silent) // if it was the last page 102 | { 103 | m_inc_upid(); 104 | } 105 | return true; 106 | } 107 | bool page_store::remove_all() 108 | { 109 | const std::unique_lock w_lock(m_store_mutex); 110 | 111 | if (m_pages.empty()) 112 | { 113 | return false; 114 | } 115 | /*for (auto &p : m_pages) 116 | { 117 | p.clear(); 118 | }*/ 119 | m_pages.clear(); 120 | m_inc_upid(); 121 | return true; 122 | } 123 | void page_store::fill(ex::plot_relative_t t_index, color_t t_fill) 124 | { 125 | const std::unique_lock w_lock(m_store_mutex); 126 | if (!m_valid_index(t_index)) 127 | { 128 | return; 129 | } 130 | auto index = m_index_to_pos(t_index); 131 | m_pages[index].fill = t_fill; 132 | } 133 | void page_store::resize(ex::plot_relative_t t_index, gvertex t_size) 134 | { 135 | const std::unique_lock w_lock(m_store_mutex); 136 | if (!m_valid_index(t_index)) 137 | { 138 | return; 139 | } 140 | auto index = m_index_to_pos(t_index); 141 | m_pages[index].size = t_size; 142 | m_pages[index].clear(); 143 | } 144 | unigd::gvertex page_store::size(ex::plot_relative_t t_index) 145 | { 146 | const std::shared_lock r_lock(m_store_mutex); 147 | if (!m_valid_index(t_index)) 148 | { 149 | return {10, 10}; 150 | } 151 | auto index = m_index_to_pos(t_index); 152 | return m_pages[index].size; 153 | } 154 | void page_store::clip(ex::plot_relative_t t_index, grect t_rect) 155 | { 156 | const std::unique_lock w_lock(m_store_mutex); 157 | if (!m_valid_index(t_index)) 158 | { 159 | return; 160 | } 161 | auto index = m_index_to_pos(t_index); 162 | m_pages[index].clip(t_rect); 163 | } 164 | 165 | bool page_store::render(ex::plot_relative_t t_index, renderers::render_target *t_renderer, 166 | double t_scale) 167 | { 168 | const std::unique_lock w_lock(m_store_mutex); 169 | if (!m_valid_index(t_index)) 170 | { 171 | return false; 172 | } 173 | auto index = m_index_to_pos(t_index); 174 | t_renderer->render(m_pages[index], std::fabs(t_scale)); 175 | return true; 176 | } 177 | 178 | bool page_store::render_if_size(ex::plot_relative_t t_index, 179 | renderers::render_target *t_renderer, double t_scale, 180 | gvertex t_target_size) 181 | { 182 | const std::shared_lock r_lock(m_store_mutex); 183 | if (!m_valid_index(t_index)) 184 | { 185 | return false; 186 | } 187 | auto index = m_index_to_pos(t_index); 188 | 189 | // get current state 190 | gvertex old_size = m_pages[index].size; 191 | 192 | if (t_target_size.x < 0.1) 193 | { 194 | t_target_size.x = old_size.x; 195 | } 196 | if (t_target_size.y < 0.1) 197 | { 198 | t_target_size.y = old_size.y; 199 | } 200 | 201 | // Check if replay needed 202 | if (std::fabs(t_target_size.x - old_size.x) > 0.1 || 203 | std::fabs(t_target_size.y - old_size.y) > 0.1) 204 | { 205 | return false; 206 | } 207 | 208 | t_renderer->render(m_pages[index], std::fabs(t_scale)); 209 | return true; 210 | } 211 | 212 | std::experimental::optional page_store::find_index(ex::plot_id_t t_id) 213 | { 214 | const std::shared_lock r_lock(m_store_mutex); 215 | for (std::size_t i = 0; i != m_pages.size(); i++) 216 | { 217 | if (m_pages[i].id == t_id) 218 | { 219 | return static_cast(i); 220 | } 221 | } 222 | return std::experimental::nullopt; 223 | } 224 | 225 | void page_store::m_inc_upid() { m_upid = incwrap(m_upid); } 226 | unigd_device_state page_store::state() 227 | { 228 | const std::shared_lock r_lock(m_store_mutex); 229 | return {m_upid, static_cast(m_pages.size()), m_device_active}; 230 | } 231 | 232 | void page_store::set_device_active(bool t_active) 233 | { 234 | const std::unique_lock w_lock(m_store_mutex); 235 | m_device_active = t_active; 236 | } 237 | 238 | ex::find_results page_store::query(ex::plot_relative_t t_offset, ex::plot_id_t t_limit) 239 | { 240 | const std::shared_lock r_lock(m_store_mutex); 241 | 242 | if (!m_valid_index(t_offset)) 243 | { 244 | return {{m_upid, static_cast(m_pages.size()), m_device_active}, {}}; 245 | } 246 | auto index = m_index_to_pos(t_offset); 247 | if (t_limit <= 0) 248 | { 249 | t_limit = m_pages.size(); 250 | } 251 | auto end = std::min(m_pages.size(), index + static_cast(t_limit)); 252 | 253 | std::vector res(end - index); 254 | for (std::size_t i = index; i != end; i++) 255 | { 256 | res[i - index] = m_pages[i].id; 257 | } 258 | return {{m_upid, static_cast(m_pages.size()), m_device_active}, res}; 259 | } 260 | 261 | void page_store::extra_css(std::experimental::optional t_extra_css) 262 | { 263 | const std::unique_lock w_lock(m_store_mutex); 264 | m_extra_css = t_extra_css; 265 | } 266 | 267 | } // namespace unigd 268 | -------------------------------------------------------------------------------- /inst/include/fmt/ostream.h: -------------------------------------------------------------------------------- 1 | // Formatting library for C++ - std::ostream support 2 | // 3 | // Copyright (c) 2012 - present, Victor Zverovich 4 | // All rights reserved. 5 | // 6 | // For the license information refer to format.h. 7 | 8 | #ifndef FMT_OSTREAM_H_ 9 | #define FMT_OSTREAM_H_ 10 | 11 | #include // std::filebuf 12 | 13 | #ifdef _WIN32 14 | # ifdef __GLIBCXX__ 15 | # include 16 | # include 17 | # endif 18 | # include 19 | #endif 20 | 21 | #include "format.h" 22 | 23 | FMT_BEGIN_NAMESPACE 24 | namespace detail { 25 | 26 | template class formatbuf : public Streambuf { 27 | private: 28 | using char_type = typename Streambuf::char_type; 29 | using streamsize = decltype(std::declval().sputn(nullptr, 0)); 30 | using int_type = typename Streambuf::int_type; 31 | using traits_type = typename Streambuf::traits_type; 32 | 33 | buffer& buffer_; 34 | 35 | public: 36 | explicit formatbuf(buffer& buf) : buffer_(buf) {} 37 | 38 | protected: 39 | // The put area is always empty. This makes the implementation simpler and has 40 | // the advantage that the streambuf and the buffer are always in sync and 41 | // sputc never writes into uninitialized memory. A disadvantage is that each 42 | // call to sputc always results in a (virtual) call to overflow. There is no 43 | // disadvantage here for sputn since this always results in a call to xsputn. 44 | 45 | auto overflow(int_type ch) -> int_type override { 46 | if (!traits_type::eq_int_type(ch, traits_type::eof())) 47 | buffer_.push_back(static_cast(ch)); 48 | return ch; 49 | } 50 | 51 | auto xsputn(const char_type* s, streamsize count) -> streamsize override { 52 | buffer_.append(s, s + count); 53 | return count; 54 | } 55 | }; 56 | 57 | // Generate a unique explicit instantion in every translation unit using a tag 58 | // type in an anonymous namespace. 59 | namespace { 60 | struct file_access_tag {}; 61 | } // namespace 62 | template 63 | class file_access { 64 | friend auto get_file(BufType& obj) -> FILE* { return obj.*FileMemberPtr; } 65 | }; 66 | 67 | #if FMT_MSC_VERSION 68 | template class file_access; 70 | auto get_file(std::filebuf&) -> FILE*; 71 | #endif 72 | 73 | inline auto write_ostream_unicode(std::ostream& os, fmt::string_view data) 74 | -> bool { 75 | FILE* f = nullptr; 76 | #if FMT_MSC_VERSION 77 | if (auto* buf = dynamic_cast(os.rdbuf())) 78 | f = get_file(*buf); 79 | else 80 | return false; 81 | #elif defined(_WIN32) && defined(__GLIBCXX__) 82 | auto* rdbuf = os.rdbuf(); 83 | if (auto* sfbuf = dynamic_cast<__gnu_cxx::stdio_sync_filebuf*>(rdbuf)) 84 | f = sfbuf->file(); 85 | else if (auto* fbuf = dynamic_cast<__gnu_cxx::stdio_filebuf*>(rdbuf)) 86 | f = fbuf->file(); 87 | else 88 | return false; 89 | #else 90 | ignore_unused(os, data, f); 91 | #endif 92 | #ifdef _WIN32 93 | if (f) { 94 | int fd = _fileno(f); 95 | if (_isatty(fd)) { 96 | os.flush(); 97 | return write_console(fd, data); 98 | } 99 | } 100 | #endif 101 | return false; 102 | } 103 | inline auto write_ostream_unicode(std::wostream&, 104 | fmt::basic_string_view) -> bool { 105 | return false; 106 | } 107 | 108 | // Write the content of buf to os. 109 | // It is a separate function rather than a part of vprint to simplify testing. 110 | template 111 | void write_buffer(std::basic_ostream& os, buffer& buf) { 112 | const Char* buf_data = buf.data(); 113 | using unsigned_streamsize = std::make_unsigned::type; 114 | unsigned_streamsize size = buf.size(); 115 | unsigned_streamsize max_size = to_unsigned(max_value()); 116 | do { 117 | unsigned_streamsize n = size <= max_size ? size : max_size; 118 | os.write(buf_data, static_cast(n)); 119 | buf_data += n; 120 | size -= n; 121 | } while (size != 0); 122 | } 123 | 124 | template 125 | void format_value(buffer& buf, const T& value) { 126 | auto&& format_buf = formatbuf>(buf); 127 | auto&& output = std::basic_ostream(&format_buf); 128 | #if !defined(FMT_STATIC_THOUSANDS_SEPARATOR) 129 | output.imbue(std::locale::classic()); // The default is always unlocalized. 130 | #endif 131 | output << value; 132 | output.exceptions(std::ios_base::failbit | std::ios_base::badbit); 133 | } 134 | 135 | template struct streamed_view { 136 | const T& value; 137 | }; 138 | 139 | } // namespace detail 140 | 141 | // Formats an object of type T that has an overloaded ostream operator<<. 142 | template 143 | struct basic_ostream_formatter : formatter, Char> { 144 | void set_debug_format() = delete; 145 | 146 | template 147 | auto format(const T& value, basic_format_context& ctx) const 148 | -> OutputIt { 149 | auto buffer = basic_memory_buffer(); 150 | detail::format_value(buffer, value); 151 | return formatter, Char>::format( 152 | {buffer.data(), buffer.size()}, ctx); 153 | } 154 | }; 155 | 156 | using ostream_formatter = basic_ostream_formatter; 157 | 158 | template 159 | struct formatter, Char> 160 | : basic_ostream_formatter { 161 | template 162 | auto format(detail::streamed_view view, 163 | basic_format_context& ctx) const -> OutputIt { 164 | return basic_ostream_formatter::format(view.value, ctx); 165 | } 166 | }; 167 | 168 | /** 169 | \rst 170 | Returns a view that formats `value` via an ostream ``operator<<``. 171 | 172 | **Example**:: 173 | 174 | fmt::print("Current thread id: {}\n", 175 | fmt::streamed(std::this_thread::get_id())); 176 | \endrst 177 | */ 178 | template 179 | constexpr auto streamed(const T& value) -> detail::streamed_view { 180 | return {value}; 181 | } 182 | 183 | namespace detail { 184 | 185 | inline void vprint_directly(std::ostream& os, string_view format_str, 186 | format_args args) { 187 | auto buffer = memory_buffer(); 188 | detail::vformat_to(buffer, format_str, args); 189 | detail::write_buffer(os, buffer); 190 | } 191 | 192 | } // namespace detail 193 | 194 | FMT_EXPORT template 195 | void vprint(std::basic_ostream& os, 196 | basic_string_view> format_str, 197 | basic_format_args>> args) { 198 | auto buffer = basic_memory_buffer(); 199 | detail::vformat_to(buffer, format_str, args); 200 | if (detail::write_ostream_unicode(os, {buffer.data(), buffer.size()})) return; 201 | detail::write_buffer(os, buffer); 202 | } 203 | 204 | /** 205 | \rst 206 | Prints formatted data to the stream *os*. 207 | 208 | **Example**:: 209 | 210 | fmt::print(cerr, "Don't {}!", "panic"); 211 | \endrst 212 | */ 213 | FMT_EXPORT template 214 | void print(std::ostream& os, format_string fmt, T&&... args) { 215 | const auto& vargs = fmt::make_format_args(args...); 216 | if (detail::is_utf8()) 217 | vprint(os, fmt, vargs); 218 | else 219 | detail::vprint_directly(os, fmt, vargs); 220 | } 221 | 222 | FMT_EXPORT 223 | template 224 | void print(std::wostream& os, 225 | basic_format_string...> fmt, 226 | Args&&... args) { 227 | vprint(os, fmt, fmt::make_format_args>(args...)); 228 | } 229 | 230 | FMT_EXPORT template 231 | void println(std::ostream& os, format_string fmt, T&&... args) { 232 | fmt::print(os, "{}\n", fmt::format(fmt, std::forward(args)...)); 233 | } 234 | 235 | FMT_EXPORT 236 | template 237 | void println(std::wostream& os, 238 | basic_format_string...> fmt, 239 | Args&&... args) { 240 | print(os, L"{}\n", fmt::format(fmt, std::forward(args)...)); 241 | } 242 | 243 | FMT_END_NAMESPACE 244 | 245 | #endif // FMT_OSTREAM_H_ 246 | -------------------------------------------------------------------------------- /vignettes/b00_guide.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Plotting with unigd" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Plotting with unigd} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include=FALSE} 11 | library(unigd) 12 | temp <- airquality$Temp 13 | ``` 14 | 15 | The following guide walks through the basic features of `unigd` and compares them with the plot rendering methods in base R. 16 | 17 | ## Plot rendering in base R 18 | 19 | Rendering a plot in base R is done by (1) starting a graphics device, (2) calling some plot functions and subsequently (3) closing the device: 20 | 21 | ```r 22 | temp <- airquality$Temp # Fetch some data 23 | 24 | png(file="my_plot1.png", width=600, height=400) # (1) Start the 'png' device 25 | hist(temp, col="darkblue") # (2) Plot a histogram 26 | dev.off() # (3) Close the device 27 | ``` 28 | 29 | Note that this has some unfortunate constraints: 30 | 31 | - Rendering information must be specified _before_ the plot is created 32 | - File format (`png()`, `pdf()`, `svg()`, ...) 33 | - Filepath (i.e.: `file="my_plot1.png"`) 34 | - Dimensions (i.e.: `width=600, height=400`) 35 | - There is no way (without re-running the plotting code) to render the plot... 36 | - ...in multiple formats. 37 | - ...in multiple dimensions. 38 | - No easy way to access the plotting data without writing to a file first. 39 | - Closing the device with `dev.off()` must be called every time. 40 | 41 | `unigd` solves these issues by employing a different graphics device architecture. 42 | 43 | ## Plot rendering with `unigd` 44 | 45 | Let's see how the same render can be created using `unigd`: 46 | 47 | ```r 48 | library(unigd) 49 | temp <- airquality$Temp # Fetch some data 50 | 51 | ugd() # (1) Start the 'ugd' device 52 | hist(temp, col="darkblue") # (2) Plot a histogram 53 | ugd_save(file="my_plot1.png", width=600, height=400) # Render 600*400 PNG file 54 | dev.off() # (3) Close the device 55 | ``` 56 | 57 | Notice how rendering is an explicit instruction _after_ plotting when using unigd. 58 | This way we can also render the same plot to multiple formats and/or dimensions: 59 | 60 | ```r 61 | # ... 62 | hist(temp, col="darkblue") 63 | ugd_save(file="my_plot1.png", width=600, height=400) # Render 600*400 PNG file 64 | ugd_save(file="my_plot2.pdf", width=300, height=300) # Render 300*300 PDF file 65 | # ... 66 | ``` 67 | 68 | Starting and closing a device can be cumbersome, especially if the plotting code aborts after an error and leaves the device open. 69 | For this reason `unigd` comes with a set of functions called `ugd_*_inline`: 70 | 71 | ```r 72 | library(unigd) 73 | temp <- airquality$Temp # Fetch some data 74 | 75 | ugd_save_inline({ 76 | hist(temp, col="darkblue") 77 | }, file="my_plot1.png", width=600, height=400) 78 | ``` 79 | 80 | Plotting this way keeps you from having to create and close a device manually. 81 | Depending on your personal preference this may also be considered as more 'readable' code. 82 | 83 | You can obtain the full list of included renderers with `ugd_renderers()`. (It's growing with every `unigd` update!) 84 | 85 | The next section will illustrate how to access the render data directly without having to create a file. 86 | 87 | ## In-memory render access 88 | 89 | For some applications, you might want to access the rendered data directly. 90 | Example use-cases for this might be report generation, web services or interactive applications. 91 | While you can most likely think of workarounds for this issue, this `unigd` 92 | feature will certainly lower code complexity and increase performance. 93 | 94 | Rendering in-memory is done by simply calling `ugd_render(...)` instead of `ugd_save(...)`: 95 | 96 | ```r 97 | temp <- airquality$Temp 98 | 99 | ugd() 100 | hist(temp, col="darkblue") 101 | my_svg <- ugd_render(as="svg") 102 | dev.off() 103 | 104 | cat(my_svg) # Print the SVG as a string 105 | ``` 106 | 107 | Of course there is also a inline function for this: 108 | 109 | ```r 110 | temp <- airquality$Temp 111 | 112 | my_svg <- ugd_render_inline({ 113 | hist(temp, col="darkblue") 114 | }, as="svg") 115 | 116 | cat(my_svg) # Print the SVG as a string 117 | ``` 118 | 119 | ## More `unigd` features 120 | 121 | `unigd` offers a number of features which go beyond the base R graphics devices. 122 | 123 | ### Zoom 124 | 125 | All rendering function in `unigd` offer a `zoom` parameter. This parameter can be 126 | used to increase (or decrease) the size of objects inside a plot (independently 127 | of plot dimensions). For example `zoom=2` will increase the size of all objects 128 | to 200%, `zoom=0.5` will decrease them to 50%. 129 | 130 | ```{r} 131 | my_svg_1_0 <- ugd_render_inline({ 132 | hist(temp, col="darkblue", main = "Zoom 1.0") 133 | }, as="png-base64", width=300, height=300, zoom=1.0) 134 | 135 | my_svg_1_5 <- ugd_render_inline({ 136 | hist(temp, col="darkblue", main = "Zoom 1.5") 137 | }, as="png-base64", width=300, height=300, zoom=1.5) 138 | 139 | my_svg_0_5 <- ugd_render_inline({ 140 | hist(temp, col="darkblue", main = "Zoom 0.5") 141 | }, as="png-base64", width=300, height=300, zoom=0.5) 142 | 143 | # (Output directly in this RMarkdown document) 144 | knitr::raw_html(paste0(sprintf("", c(my_svg_1_0, my_svg_1_5, my_svg_0_5)))) 145 | ``` 146 | 147 | ### Paging (by index) 148 | 149 | The `page` parameter lets you select which plot should from the history should be rendered. 150 | By default this is set to `0` which will use the last created plot. Set this to any number 151 | ≥ 1 to select a plot by it's index (oldest first). Use numbers ≤ 0 to select plots 152 | newest-first: 153 | 154 | 155 | ```r 156 | ugd() 157 | for (i in 1:10) { 158 | plot(1, main=paste0("Plot #", i)) 159 | } 160 | 161 | ugd_save(file="plot.png", page = 3) # Plot #3 162 | ugd_save(file="plot.png") # Plot #10 163 | ugd_save(file="plot.png", page = -1) # Plot #9 164 | 165 | dev.off() 166 | ``` 167 | 168 | Note that plots can be deleted from the history the same way: 169 | 170 | ```r 171 | # ... 172 | ugd_remove() # Remove last 173 | ugd_remove(page = -1) # Remove second-to-last 174 | ugd_clear() # Remove all 175 | # ... 176 | ``` 177 | 178 | Instead of keeping track of the plot index, which might change 179 | when plots are added and removed, static plot IDs can be obtained. 180 | 181 | ### Plot IDs 182 | 183 | If you want to render a plot at a later point without having to keep 184 | track of its index, you can obtain its ID at any point after it's 185 | creation. 186 | 187 | The following example extensively demonstrates how this can be used: 188 | 189 | ```r 190 | ugd() 191 | 192 | plot(rnorm(50)) # A 193 | 194 | first_plot_id <- ugd_id() # Get last ID (A at this point) 195 | 196 | hist(rnorm(50)) # B 197 | 198 | plot(sin((1:100)/3)) # C 199 | 200 | other_id <- ugd_id(-1) # Get the second-to-last ID (B at this point) 201 | 202 | hist(runif(100)) # D 203 | 204 | ugd_remove(3) # Remove 3rd plot (C) 205 | 206 | first_again <- ugd_id(1) # Get the first ID (A) 207 | 208 | ugd_save(file="plot_1.png", page = first_plot_id) 209 | ugd_save(file="plot_2.png", page = other_id) 210 | ugd_save(file="plot_3.png", page = first_again) 211 | 212 | dev.off() 213 | ``` 214 | 215 | Note that a typical use-case would be much simpler, and just be getting 216 | the last ID after each plot by calling `ugd_id()` subsequently. 217 | 218 | ### (Special) renderers 219 | 220 | `unigd` also ships with a number of 'special' renderers. This guide will 221 | not go into too much detail about this topic but here are some noteworthy mentions: 222 | 223 | 224 | - `"strings"`-renderer 225 | - All text elements inside a plot 226 | - Linebreak separated plain text format 227 | - Could be used to e.g. 'search' through plots 228 | - `"meta"`-renderer 229 | - Meta information about the plot 230 | - Guaranteed to have a render time of O(1) regardless of number of objects 231 | - Includes complexity (number of draw calls and clipping planes) 232 | - JSON format 233 | - `"json"`-renderer 234 | - Contains _all_ information `unigd` has about one plot 235 | - JSON format 236 | 237 | ## Performance considerations 238 | 239 | While `unigd` aims to provide the best performance in any case, there are some 240 | considerations you can make when optimizing graphics rendering. 241 | 242 | > At this point it should be mentioned that for most user applications readability 243 | > should be prioritized over performance and, unless graphics rendering is 244 | > bottlenecking your R script, you can most likely ignore this section in good 245 | > conscience. 246 | 247 | When optimizing rendering code, it is fundamental to understand in what 248 | cases `unigd` needs to call into the R graphics engine to let a plot be re-drawn: 249 | 250 | Rendering is done after drawing. The last drawn dimensions of a plot are cached. 251 | We can derive a few simple rules from this: 252 | 253 | - Rendering the same plot in different formats: Fast. 254 | - Rendering the same plot in different dimensions: Slow(er). 255 | 256 | This means ordering the rendering calls will result in faster execution: 257 | 258 | ```r 259 | # SLOWER: 260 | ugd_save(file="my_plot1.png", width=600, height=400) 261 | ugd_save(file="my_plot2.pdf", width=300, height=300) # re-draw 1 262 | ugd_save(file="my_plot3.pdf", width=600, height=400) # re-draw 2 263 | 264 | # FASTER: 265 | ugd_save(file="my_plot1.png", width=600, height=400) 266 | ugd_save(file="my_plot3.pdf", width=600, height=400) 267 | ugd_save(file="my_plot2.pdf", width=300, height=300) # re-draw 1 268 | ``` 269 | 270 | And, while `unigd` gives you the _choice_ of specifying your render dimension 271 | _after_ plotting, you can hint them at device creation time to achieve the 272 | best performance: 273 | 274 | ```r 275 | # SLOWER: 276 | ugd() # default dimensions: 720 * 576 277 | # ... 278 | ugd_save(file="my_plot1.png", width=300, height=300) # re-draw 279 | 280 | # FASTER: 281 | ugd(width=300, height=300) 282 | # ... 283 | ugd_save(file="my_plot1.png", width=300, height=300) 284 | ``` 285 | 286 | If the dimensions are omitted when calling rendering functions, the last known 287 | dimensions will be used and rendering is guaranteed to be fast: 288 | 289 | ```r 290 | ugd_save(file="my_plot1.png") 291 | ``` 292 | 293 | Any use of `ugd_*_inline` functions is also guaranteed to be fast. 294 | 295 | Note that width and height also interact with the `zoom` parameter. (i.e.: Cached width = width / zoom). 296 | --------------------------------------------------------------------------------