v1.52.2.0 fix(make-pdf): render emoji instead of tofu (▯) on Linux (#1787)

* fix(make-pdf): emoji font fallback in print CSS

Emoji code points rendered as .notdef tofu (▯) because the body and
@top-center font stacks had no emoji family for Chromium to fall back to.
Add SANS_STACK / CJK_STACK / EMOJI_FAMILIES constants (one source of truth
per family list) and append the emoji families before the generic
sans-serif in the two stacks that can hold emoji. The @bottom-* boxes hold
counters / a fixed CONFIDENTIAL string, so they share SANS_STACK without
emoji. Non-emoji output is byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(setup): auto-install color-emoji font on Linux

macOS and Windows ship a color-emoji font; most Linux distros/containers
ship none, so make-pdf emits tofu there. ensure_emoji_font() best-effort
installs fonts-noto-color-emoji (apt, with dnf/pacman/apk fallbacks) and
refreshes the fontconfig cache. Hardened: Linux-only guard, GSTACK_SKIP_FONTS
escape hatch, fc-match color=True detection (the broad fc-list query
false-matched LastResort), sudo -n so a password prompt fails fast instead
of hanging, DEBIAN_FRONTEND=noninteractive, timeout 30 on apt update, and
fc-cache under sudo. Warns instead of failing. After a fresh install,
refresh_browse_daemon_for_fonts() runs 'browse stop' so the next render
spawns a Chromium that sees the new font (font fallback is process-cached).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(make-pdf): emoji render gate (pdffonts + pixel proof)

pdftotext is a false oracle for emoji: Skia preserves the Unicode in the
text cluster even when the glyph drew as .notdef tofu, so extraction passes
on a broken render. The gate instead asserts (1) pdffonts shows an emoji
family embedded and (2) pdftoppm rasterizes the page to color (measured
~1650 saturated pixels vs ~0 for tofu). pdfimages is not used: macOS embeds
color emoji as Type 3 fonts, so it lists nothing even on a correct render.
Adds resolvePopplerTool() (DRY resolver, returns null for clean skips) and
a fixture exercising FE0F variation-selector emoji. Skips cleanly when
poppler tools or a color-emoji font are unavailable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(make-pdf): install emoji font + run emoji gate on Ubuntu

Install fonts-noto-color-emoji before Chromium launches on the Ubuntu leg
(macOS already ships Apple Color Emoji), refresh fontconfig, and log the
fc-match result. Run the whole make-pdf/test/e2e/ dir so the emoji gate runs
alongside the combined-features copy-paste gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* harden(make-pdf): emoji gate + font install per adversarial review

Codex adversarial pass on the implementation diff flagged five robustness
gaps, all fixed here:
- emoji-gate skipped green in CI when poppler/font prerequisites were absent,
  which could let the tofu regression ship behind a green build. Missing
  prerequisites are now a HARD FAILURE when process.env.CI is set; local dev
  still skips cleanly.
- execFileSync children (make-pdf, pdffonts, pdftoppm, fc-match) had no
  timeout; a wedged binary or hostile GSTACK_*_BIN override could hang the
  job past Bun's test timeout. Each child now has a 25s ceiling.
- PPM parser trusted header tokens blindly; malformed/variant output gave a
  silently-wrong count. Now validates magic/dimensions/maxval and pixel-buffer
  length, handles header comments, throws a hard diagnostic on mismatch.
- predictable /tmp paths were collision/symlink-prone; now mkdtempSync under
  /tmp (kept under /tmp for browse's validateOutputPath allowlist).
- only apt-get update was timeout-wrapped; dnf/pacman/apk installs and apt
  install can hang on locks/mirrors. All package installs now timeout-bound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v1.52.2.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(make-pdf): document color-emoji font requirement + GSTACK_SKIP_FONTS

Extend the Linux font note to cover the color-emoji font that make-pdf
emoji rendering needs: setup auto-installs fonts-noto-color-emoji, the
print CSS falls back through Apple/Segoe/Noto emoji families, and
GSTACK_SKIP_FONTS=1 opts out. Edit the .tmpl and regenerate SKILL.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-29 18:06:19 -07:00
committed by GitHub
parent 070722ace3
commit 62024d114c
13 changed files with 625 additions and 9 deletions
+28
View File
@@ -114,6 +114,34 @@ export function resolvePdftotext(env: NodeJS.ProcessEnv = process.env): Pdftotex
].join("\n"));
}
/**
* Locate a poppler companion tool (pdffonts, pdfimages, pdftoppm) used by the
* emoji render gate. Mirrors resolvePdftotext's resolution order:
* 1. $GSTACK_<TOOL>_BIN env override (e.g. GSTACK_PDFFONTS_BIN)
* 2. PATH via Bun.which
* 3. standard POSIX locations (Homebrew + distro)
*
* Returns null (does NOT throw) when the tool is missing — the emoji gate skips
* cleanly rather than failing on a box without full poppler-utils.
*/
export function resolvePopplerTool(
tool: "pdffonts" | "pdfimages" | "pdftoppm",
env: NodeJS.ProcessEnv = process.env,
): string | null {
const override = resolveOverride(env[`GSTACK_${tool.toUpperCase()}_BIN`], env);
if (override) return override;
const PATH = env.PATH ?? env.Path ?? "";
const onPath = Bun.which(tool, { PATH });
if (onPath) return onPath;
for (const dir of ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin"]) {
const candidate = findExecutable(path.join(dir, tool));
if (candidate) return candidate;
}
return null;
}
function isExecutable(p: string): boolean {
try {
fs.accessSync(p, fs.constants.X_OK);