mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
fix(make-pdf): Liberation Sans font fallback for Linux rendering
On Linux (Docker, CI, servers), neither Helvetica nor Arial exist. Our CSS stacks were falling through to DejaVu Sans — wider letterforms that look like Verdana, not the intended Helvetica/Faber look. Liberation Sans is the standard metric-compatible Arial clone (SIL OFL 1.1, apt package fonts-liberation). - print-css.ts: all four font stacks (body + @top-center + @bottom-center + @bottom-right CONFIDENTIAL) gain "Liberation Sans" between Helvetica and Arial. File-header docblock updated to reflect the new stack. - .github/docker/Dockerfile.ci: explicit apt-get install fonts-liberation + fontconfig with retry, fc-cache -f, and a verify step that fails the build loud if the font disappears. Playwright's install-deps happens to pull this in today but the dep is implicit and could silently regress. - SKILL.md.tmpl: one-sentence note pointing Linux users at fonts-liberation. - SKILL.md: regenerated via bun run gen:skill-docs --host all (only make-pdf's generated file changed — verified clean diff scope). - render.test.ts: 2 assertions — Liberation Sans in body stack AND in at least one @page margin-box rule (proves all four intended stacks got touched, not just one). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -470,6 +470,10 @@ left-aligned body, Helvetica throughout, curly quotes and em dashes, optional
|
||||
cover page and clickable TOC, diagonal DRAFT watermark when you need it.
|
||||
Copy-paste from the PDF produces clean words, never "S a i l i n g".
|
||||
|
||||
On Linux, install `fonts-liberation` for correct rendering — Helvetica and Arial
|
||||
aren't present by default, and Liberation Sans is the standard metric-compatible
|
||||
fallback. CI and Docker builds install it automatically via Dockerfile.ci.
|
||||
|
||||
## MAKE-PDF SETUP (run this check BEFORE any make-pdf command)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -39,6 +39,10 @@ left-aligned body, Helvetica throughout, curly quotes and em dashes, optional
|
||||
cover page and clickable TOC, diagonal DRAFT watermark when you need it.
|
||||
Copy-paste from the PDF produces clean words, never "S a i l i n g".
|
||||
|
||||
On Linux, install `fonts-liberation` for correct rendering — Helvetica and Arial
|
||||
aren't present by default, and Liberation Sans is the standard metric-compatible
|
||||
fallback. CI and Docker builds install it automatically via Dockerfile.ci.
|
||||
|
||||
{{MAKE_PDF_SETUP}}
|
||||
|
||||
## Core patterns
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
* Mirror those CSS rules here. The HTML references were approved via
|
||||
* /plan-design-review with explicit design decisions locked in the plan:
|
||||
*
|
||||
* - Helvetica only (system font, no bundled webfonts — dodges the
|
||||
* per-glyph Tj bug that breaks copy-paste extraction).
|
||||
* - Helvetica first, with Liberation Sans as a metric-compatible Linux
|
||||
* fallback (Helvetica and Arial aren't installed on most Linux distros;
|
||||
* Liberation Sans ships via the fonts-liberation package and Playwright's
|
||||
* install-deps). No bundled webfonts — dodges the per-glyph Tj bug that
|
||||
* breaks copy-paste extraction.
|
||||
* - All paragraphs flush-left. No first-line indent, no justify, no
|
||||
* p+p indent. text-align: left everywhere. 12pt margin-bottom.
|
||||
* - Cover page has the same 1in margins as every other page. No flexbox
|
||||
@@ -15,8 +18,8 @@
|
||||
* - `@page :first` suppresses running header/footer but does NOT override
|
||||
* the 1in margin.
|
||||
* - No <link>, no external CSS/fonts — everything inlined.
|
||||
* - CJK fallback: Helvetica, Arial, Hiragino Kaku Gothic ProN, Noto Sans
|
||||
* CJK JP, Microsoft YaHei, sans-serif.
|
||||
* - CJK fallback: Helvetica, Liberation Sans, Arial, Hiragino Kaku Gothic
|
||||
* ProN, Noto Sans CJK JP, Microsoft YaHei, sans-serif.
|
||||
*/
|
||||
|
||||
export interface PrintCssOptions {
|
||||
@@ -81,13 +84,13 @@ function pageRules(size: string, margin: string, opts: PrintCssOptions): string
|
||||
` size: ${size};`,
|
||||
` margin: ${margin};`,
|
||||
runningHeader
|
||||
? ` @top-center { content: "${runningHeader}"; font-family: Helvetica, Arial, sans-serif; font-size: 9pt; color: #666; }`
|
||||
? ` @top-center { content: "${runningHeader}"; font-family: Helvetica, "Liberation Sans", Arial, sans-serif; font-size: 9pt; color: #666; }`
|
||||
: ``,
|
||||
showPageNumbers
|
||||
? ` @bottom-center { content: counter(page) " of " counter(pages); font-family: Helvetica, Arial, sans-serif; font-size: 9pt; color: #666; }`
|
||||
? ` @bottom-center { content: counter(page) " of " counter(pages); font-family: Helvetica, "Liberation Sans", Arial, sans-serif; font-size: 9pt; color: #666; }`
|
||||
: ``,
|
||||
showConfidential
|
||||
? ` @bottom-right { content: "CONFIDENTIAL"; font-family: Helvetica, Arial, sans-serif; font-size: 8pt; color: #aaa; letter-spacing: 0.05em; }`
|
||||
? ` @bottom-right { content: "CONFIDENTIAL"; font-family: Helvetica, "Liberation Sans", Arial, sans-serif; font-size: 8pt; color: #aaa; letter-spacing: 0.05em; }`
|
||||
: ``,
|
||||
`}`,
|
||||
``,
|
||||
@@ -104,7 +107,7 @@ function rootTypography(): string {
|
||||
return [
|
||||
`html { lang: en; }`,
|
||||
`body {`,
|
||||
` font-family: Helvetica, Arial, "Hiragino Kaku Gothic ProN", "Noto Sans CJK JP", "Microsoft YaHei", sans-serif;`,
|
||||
` font-family: Helvetica, "Liberation Sans", Arial, "Hiragino Kaku Gothic ProN", "Noto Sans CJK JP", "Microsoft YaHei", sans-serif;`,
|
||||
` font-size: 11pt;`,
|
||||
` line-height: 1.5;`,
|
||||
` color: #111;`,
|
||||
|
||||
@@ -326,6 +326,23 @@ describe("printCss", () => {
|
||||
const css = printCss({ pageNumbers: true });
|
||||
expect(css).toMatch(/@bottom-center\s*\{\s*content:\s*counter\(page\)/);
|
||||
});
|
||||
|
||||
test("font stacks include Liberation Sans adjacent to Helvetica", () => {
|
||||
const css = printCss({ confidential: true });
|
||||
// Body stack
|
||||
expect(css).toMatch(/font-family:\s*Helvetica,\s*"Liberation Sans",\s*Arial/);
|
||||
// At least one @page margin box (running header / page number / CONFIDENTIAL)
|
||||
// should also have the updated stack.
|
||||
const marginBoxStacks = css.match(/@(top|bottom)-(center|right)\s*\{[^}]*Liberation Sans/g) ?? [];
|
||||
expect(marginBoxStacks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("all four original Helvetica stacks now include Liberation Sans", () => {
|
||||
const css = printCss({ runningHeader: "Running Title", confidential: true });
|
||||
// Count: body (1) + running header (1) + page numbers (1) + confidential (1) = 4
|
||||
const occurrences = (css.match(/"Liberation Sans"/g) ?? []).length;
|
||||
expect(occurrences).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── render() — pageNumbers / footerTemplate data flow ───────────────
|
||||
|
||||
Reference in New Issue
Block a user