



























Last month, @bastistician opened an
issue on the litedown repo
pointing out that knitr has a hook_pngquant() function for compressing PNG
plots from code chunks, but litedown lacks such a feature. He included a
reasonable workaround—calling system2("pngquant", ...) with
litedown::get_context("plot_files") in a chunk at the end of the vignette. It
shrank his vignette from 80 KB to 54 KB, which is a 33% reduction. Not bad.
The catch, of course, is that it requires pngquant to be
installed on the system. For R users, installing a system binary is more
friction than it sounds: it is brew install pngquant on macOS, a separate
package manager invocation on Linux, and hunting down a standalone executable on
Windows. If you maintain a package that others will build, you are now asking
all of them to do this—for every machine they use. By contrast,
install.packages("tinyimg") works the same way everywhere, which is the kind
of simplicity that makes a tool actually get used.
This is why I created tinyimg.
tinyimg compresses PNG files using two Rust crates bundled inside the package itself—no external tools needed:
The main function is tinypng(). At its simplest:
library(tinyimg)
tmp = tempfile(fileext = ".png")
png(tmp, width = 800, height = 600)
plot(1:100, main = "A perfectly ordinary plot")
dev.off()
tinypng(tmp) # optimize in-place
You will see output like:
/tmp/Rtmpxyz/file123.png -> /tmp/Rtmpxyz/file123.png | 52.8 KB -> 39.5 KB (-25.2%)
That is the lossless path at the default optimization level of 2. If you want to
go harder, you can set level anywhere from 0 (fast, minimal gain) to 6 (slow,
maximum compression). Level 2 usually captures most of the low-hanging fruit,
whereas level 6 typically squeezes out only a few more percentage points while
taking 10–15x longer.1
If lossless is not enough, you can enable lossy palette reduction via the
lossy argument. The value is a color-difference threshold in
CIELAB space—specifically
$\Delta E_{76}$. A threshold of 2.3 is the traditional “just noticeable
difference” (JND), meaning the color error introduced by the quantization is
theoretically imperceptible to the human eye:
tinypng(tmp, paste0(tmp, "-lossy.png"), lossy = 2.3)
In practice, for the kind of statistical graphics R produces—flat backgrounds,
solid colored points, clean lines—a lossy = 2.3 compression is nearly
indistinguishable from the original while cutting file sizes by 50–70%.
Aggressive settings like lossy = 32 can push reduction past 80%, but at that
point you may start to see banding in smooth gradients.2
To be honest, I am not sure 2.3 is a practically reasonable general-purpose threshold. From my own experiments with the benchmark plots, it feels rather conservative—the compressed images at somewhat higher values still look perfectly fine to me. Users will likely need to experiment on their own images to find the sweet spot. If you do systematic experiments and arrive at better guidance on what thresholds work well for typical R graphics, I would love to hear about your findings.
To answer the original issue: if you use litedown and want to compress all the plots in your vignette, just add a chunk like this at the end (assuming you use the PNG format for all plots):
if (requireNamespace("tinyimg"))
tinyimg::tinypng(litedown::get_context("plot_files"), lossy = 2.3)
One line. No system2(), no pngquant on the PATH, no CRAN environment
concerns. The lossy argument is optional—leave it out if you only want
lossless compression.
For knitr users, the situation is similar. You can pass the output directory
to tinypng() directly:
# if the chunk option fig.path is a dir used for all plots
tinypng(knitr::opts_chunk$get("fig.path"))
# or perhaps you can optimize the current working directory
tinypng(".")
It will recursively find and compress every PNG in the directory.
Since tinyimg uses Rust internally, building from source requires Cargo. The easiest route is to install a precompiled binary from CRAN or r-universe, which handles the Rust compilation on its end:
# CRAN version
install.packages("tinyimg", repos = "https://cloud.r-project.org")
# dev version
install.packages("tinyimg", repos = "https://yihui.r-universe.dev")
I have had a long-standing interest in Rust and the interface between Rust and R, but for years it stayed on the “someday” list. This GitHub issue gave me a good excuse to finally try it out, so tinyimg is my first real experiment with the combination. I am happy with how it turned out. The goal is not just PNG—JPEG support is on the roadmap, and the name “tinyimg” rather than “tinypng” was deliberate.
As a freelancer (currently working as a contractor) and a dad of three kids, I truly appreciate your donation to support my writing and open-source software development! Your contribution helps me cope with financial uncertainty better, so I can spend more time on producing high-quality content and software. You can make a donation through methods below.
Venmo: @yihui_xie, or Zelle: [email protected]
Paypal
If you have a Paypal account, you can follow the link https://paypal.me/YihuiXie or find me on Paypal via my email [email protected]. Please choose the payment type as “Family and Friends” (instead of “Goods and Services”) to avoid extra fees.
If you don’t have Paypal, you may donate through this link via your debit or credit card. Paypal will charge a fee on my side.
Other ways:
| WeChat Pay (微信支付:谢益辉) | Alipay (支付宝:谢益辉) |
|---|---|
![]() |
![]() |
When sending money, please be sure to add a note “gift” or “donation” if possible, so it won’t be treated as my taxable income but a genuine gift. Needless to say, donation is completely voluntary and I appreciate any amount you can give.
Please feel free to email me if you prefer a different way to give. Thank you very much!
I’ll give back a significant portion of the donations to the open-source community and charities. For the record, I received about $30,000 in total (before tax) in 2024-25, and gave back about $15,000 (after tax).
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。