--- title: "Choosing an output format" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Choosing an output format} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r} #| include: false knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(vellum) ``` vellum now writes enough formats that the choice is a real one. This article is the capability matrix, and — more usefully — where each format *degrades*. ## Still pictures ```{r} #| eval: false render(scene, "figure.png") # raster render(scene, "figure.svg") # vector, web render(scene, "figure.pdf") # vector, print ``` | | PNG | SVG | PDF | |---|---|---|---| | Resolution-independent | ✗ | ✓ | ✓ | | Gradients, patterns | ✓ | ✓ | ✓ | | Hatching (`vl_hatch()`) | ✓ | ✓ (as ``) | ✓ | | Selectable / searchable text | ✗ | ✓ (`text = "native"`) | ✓ | | Screen-reader structure | ✗ | ✓ (`role`, `data-*`) | ✓ (tagged) | | Masks | ✓ | ✓ | ✓ | | Tiling patterns | ✓ | ✓ | degrades | | Faithful without the author's fonts | ✓ | only with `text = "outline"` | ✓ (embedded) | Two rows deserve expansion. **SVG text is a choice, not a default you can ignore.** `text = "native"` emits real `` — selectable, searchable, readable by a screen reader — but it references system fonts, so it is only faithful on a machine that has them. `text = "outline"` fills the same glyph outlines the raster backend does, so it is identical everywhere and not selectable. There is no third option yet; embedding a subset font would give both, and is blocked on tooling (see `vignette("accessible-output")`). **PDF embeds its fonts**, which is why it is the format to hand someone when fidelity matters more than editability. Anything a backend cannot honour is reported as a warning at render time rather than silently dropped, so you do not have to consult this table to find out. ## Animation ```{r} #| eval: false vl_render_animation(keyframes, seg, frac, "a.gif", format = "gif") vl_render_animation(keyframes, seg, frac, "a.png", format = "apng") vl_render_animation(keyframes, seg, frac, "a.svg", format = "svg") vl_render_animation(keyframes, seg, frac, "frames/", format = "frames") ``` | | GIF | APNG | animated SVG | frames | |---|---|---|---|---| | Colour fidelity | 256/frame | lossless | exact (vector) | lossless | | Resolution-independent | ✗ | ✗ | ✓ | ✗ | | Size on line art | medium | large | **small** | n/a | | Size on dense marks | **medium** | large | very large | n/a | | Universally viewable | ✓ | ✓ | in a browser | n/a | | Honours reduced-motion | ✗ | ✗ | ✓ | n/a | The size row is the one that decides it, and it is not a preference — the animated SVG emits **every frame in full**, so its size grows with scene complexity times frame count, while a raster format's does not. Measured on a 30-frame scatter animation, gzipped (which is how a browser fetches it): | marks | animated SVG (gzipped) | GIF | |---|---|---| | 20 | 20 KB | 61 KB | | 200 | 80 KB | 296 KB | | 2000 | **720 KB** | **124 KB** | So: **line art → SVG, dense marks → GIF or APNG.** An explanatory animation of a few moving marks is the case animated SVG was worth building for, and there it wins by a wide margin *and* stays crisp at any size. A 2000-point cloud is 24 MB uncompressed; vellum warns above 5 MB rather than letting you find out later. `prefers-reduced-motion` is worth noting as more than a checkbox: a reader who has asked their system not to animate gets the first frame, held, instead of nothing. ## Documents and batches ```{r} #| eval: false pdf_pages(list_of_scenes, "report.pdf") # one document, many pages render_all(named_scenes, "figures/") # many files, across cores ``` `pdf_pages()` writes a document rather than a page: a report's figures, one facet per page, an animation as a contact sheet. Pages may differ in size, and each page's tagging survives — every tagged page becomes a top-level figure in the document's structure tree. `render_all()` renders independent scenes across cores. It is embarrassingly parallel — one whole scene per worker, nothing shared — which is exactly why it exists while tiling a *single* raster across threads does not: that needs synchronised access to one pixmap, and `PERFORMANCE.md` declines it on those grounds. The saving is worth having when the scenes are substantial and there are several of them (about 3× on four report figures here). For a handful of small figures the overhead dominates; `workers = 1` gets you the sequential path without changing the call. Parallelism never changes a pixel — the outputs are byte-identical either way, and that is asserted in the test suite. ## Formats vellum does not write Named because "can it?" is a reasonable question with a definite answer. **PowerPoint / OOXML DrawingML.** The gap with the clearest audience: `rvg` and `officer` are widely used because people need *editable* shapes in a deck, not a picture of a plot. A resolved scene maps onto DrawingML about as directly as onto SVG. It is not here because it needs a new backend and a zip container writer — a phase of its own rather than a tail item, and worth doing properly. **EPS / PostScript.** Still demanded by some journals, and a hard blocker for those who need it. Also a new backend: krilla writes PDF and nothing else, so this is a PostScript writer from scratch for a shrinking audience. **JPEG / WebP.** No one has asked. PNG is lossless and the right default for plots, which are flat colour and hard edges — exactly what JPEG is worst at. Straightforward to add if a need appears. ## Where to go next - `vignette("accessible-output")`: tagged PDF, and why SVG cannot yet be both faithful and selectable. - `vignette("scene-and-paint")`: the paint features whose degradation this table summarises. - `vignette("scenes-as-values")`: rendering is not the only thing to do with a finished scene.