Add fabric print variant with black replaced by dark grey
When printing on light fabric, solid black bleeds out, making QR codes
hard to scan and text fuzzy. ci/pdf-lighten-black rewrites all vector
pure black in a finished PDF to a dark grey (default 15%, configurable)
via a qpdf QDF round-trip: text and vector graphics stay vector and
embedded raster images are untouched. The CI now additionally produces
target/skeleton_fabric.pdf.
Ported from the DeLiKo@DNB poster project.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change-Id: I594a1a813284e8d4f9d6c3a97f0d3d56c36162a6
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 63cd7ee..fc45c35 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -145,7 +145,7 @@
- start_section install_linux_packages "Installing missing Linux packages"
- mkdir -pv $APT_CACHE_DIR ccache $R_LIBS_USER
- apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 update
- - apt-get install --no-install-recommends -o dir::cache::archives="$APT_CACHE_DIR" -y libasound2t64 npm curl ccache libvulkan1 libu2f-udev fonts-liberation build-essential libglpk40 libcurl4-gnutls-dev libxml2-dev libsodium-dev libsecret-1-dev libfontconfig1-dev libssl-dev libxt6 libpq-dev imagemagick ghostscript inkscape scour poppler-utils fonts-stix
+ - apt-get install --no-install-recommends -o dir::cache::archives="$APT_CACHE_DIR" -y libasound2t64 npm curl ccache libvulkan1 libu2f-udev fonts-liberation build-essential libglpk40 libcurl4-gnutls-dev libxml2-dev libsodium-dev libsecret-1-dev libfontconfig1-dev libssl-dev libxt6 libpq-dev imagemagick ghostscript inkscape scour poppler-utils qpdf fonts-stix
# The distro Node (18, EOL) is too old for current tooling. Install the
# current Node.js LTS (24.x) from the official tarball into /usr/local, which
# precedes /usr/bin on PATH so `node`/`npx` resolve to it. We deliberately do
@@ -197,6 +197,9 @@
- export HTML=target/$(basename $SOURCE_FILE .Rmd).html
- export PDF=target/$(basename $SOURCE_FILE .Rmd).pdf
- ./ci/html2pdf $HTML $PDF 1
+ # Variant for printing on fabric, where solid black bleeds out: replace all
+ # vector black by 15% grey (keeps QR codes scannable and text crisp).
+ - ./ci/pdf-lighten-black $PDF target/$(basename $SOURCE_FILE .Rmd)_fabric.pdf
- gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sPAPERSIZE=a1 -dFIXEDMEDIA -dPDFFitPage -sOutputFile=target/$(basename $SOURCE_FILE .Rmd)_a1.pdf $PDF
- inkscape -b white --pdf-poppler -o $SVG $PDF || true
- scour -i $SVG -o target/skeleton_optimized.svg --enable-viewboxing --enable-id-stripping --shorten-ids --indent=none
diff --git a/README.md b/README.md
index 22e528b..2fbdb3b 100644
--- a/README.md
+++ b/README.md
@@ -91,3 +91,20 @@
```
View the resulting `poster.html` in your browser and print it as a PDF (don´t forget to tick print backgrounds and possibly select only the first page).
+
+Alternatively, [ci/html2pdf](./ci/html2pdf) converts the HTML to an A0 PDF with exact dimensions and automatic hyphenation:
+
+```bash
+ci/html2pdf poster.html poster.pdf
+```
+
+## Printing on fabric
+
+When printing on light fabric, solid black tends to bleed out, making QR codes hard to scan and text fuzzy. [ci/pdf-lighten-black](./ci/pdf-lighten-black) produces a variant of a finished PDF in which all vector black is replaced by a dark grey (default 15%, configurable), while text and graphics stay vector and embedded raster images are untouched:
+
+```bash
+ci/pdf-lighten-black poster.pdf poster_fabric.pdf # 15% grey
+ci/pdf-lighten-black poster.pdf poster_fabric.pdf 0.25 # lighter, if black still bleeds
+```
+
+The CI workflow builds this variant automatically as `target/skeleton_fabric.pdf`.
diff --git a/ci/pdf-lighten-black b/ci/pdf-lighten-black
new file mode 100755
index 0000000..e24b565
--- /dev/null
+++ b/ci/pdf-lighten-black
@@ -0,0 +1,41 @@
+#!/bin/sh -eu
+# pdf-lighten-black IN.pdf [OUT.pdf] [GREY]
+#
+# Produce a copy of IN.pdf in which all *vector* pure black is replaced by a
+# dark grey (default 15% = #262626). Intended for printing on fabric, where
+# solid black ink bleeds out, making QR codes hard to scan and text fuzzy.
+#
+# Works on the PDF content streams directly (qpdf QDF round-trip), so text
+# stays text and vector graphics stay vector -- nothing is rasterized and the
+# file works at any print resolution. Colors inside embedded raster images
+# (e.g. screenshots) are not touched.
+#
+# GREY is the target grey level in [0,1) per RGB channel (0.15 = 15% = #262626).
+
+in=$1
+out=${2:-${in%.pdf}_fabric.pdf}
+grey=${3:-0.15}
+
+k=$(awk -v g="$grey" 'BEGIN { printf "%.4g", 1 - g }')
+
+tmpdir=$(mktemp -d)
+trap 'rm -rf "$tmpdir"' EXIT
+
+# QDF mode writes an editable, uncompressed PDF whose stream lengths can be
+# repaired by fix-qdf after editing.
+qpdf --qdf --object-streams=disable "$in" "$tmpdir/in.qdf"
+
+# Rewrite the color-setting operators for pure black:
+# `0 0 0 rg/RG` (DeviceRGB), `0 g/G` (DeviceGray), `0 0 0 sc/scn` variants,
+# and `0 0 0 1 k/K` (DeviceCMYK, mapped to (1-GREY) K).
+# The lookarounds keep numbers like `10 g` or names like `/GS0 gs` intact.
+perl -pe "
+ s/(?<![\\d.])0 0 0 (rg|RG|scn|sc|SCN|SC)(?![\\w])/$grey $grey $grey \$1/g;
+ s/(?<![\\d.])0 (g|G)(?![\\w])/$grey \$1/g;
+ s/(?<![\\d.])0 0 0 1 (k|K)(?![\\w])/0 0 0 $k \$1/g;
+" "$tmpdir/in.qdf" | fix-qdf > "$tmpdir/grey.qdf"
+
+# Rewrite as a normal, compressed PDF.
+qpdf "$tmpdir/grey.qdf" "$out"
+
+echo "pdf-lighten-black: wrote $out (black -> ${grey} grey)" >&2