Skip to content

Commit 1d87296

Browse files
committed
Fix ci and tests
Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 537c7c3 commit 1d87296

File tree

3 files changed

+154
-70
lines changed

3 files changed

+154
-70
lines changed

.github/workflows/pr-validate.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ jobs:
7373
env:
7474
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7575

76+
- name: Install PDF test dependencies
77+
run: |
78+
sudo apt-get update -qq
79+
sudo apt-get install -y -qq poppler-utils qpdf fonts-dejavu-core
80+
7681
- name: Setup
7782
run: just setup
7883

@@ -137,6 +142,12 @@ jobs:
137142
env:
138143
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
139144

145+
- name: Install PDF test dependencies
146+
if: matrix.hypervisor != 'whp'
147+
run: |
148+
sudo apt-get update -qq
149+
sudo apt-get install -y -qq poppler-utils qpdf fonts-dejavu-core
150+
140151
- name: Setup
141152
run: just setup
142153

tests/pdf-fonts.test.ts

Lines changed: 118 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*
44
* Tests for TrueType font parsing, embedding, and rendering.
55
* Requires DejaVu Sans font (apt: fonts-dejavu-core).
6+
* Some tests also require poppler-utils (pdftotext, pdftoppm) and qpdf.
7+
* Skipped on Windows or when dependencies are not installed.
68
*/
79

810
import { describe, it, expect } from "vitest";
@@ -11,6 +13,52 @@ import { execSync } from "child_process";
1113

1214
const pdf: any = await import("../builtin-modules/pdf.js");
1315

16+
// ── Tool / Font Availability ─────────────────────────────────────────
17+
18+
/** Check if a command-line tool is available on this system. */
19+
function hasCommand(cmd: string): boolean {
20+
try {
21+
execSync(`which ${cmd}`, { stdio: "ignore" });
22+
return true;
23+
} catch {
24+
return false;
25+
}
26+
}
27+
28+
const IS_WINDOWS = process.platform === "win32";
29+
const HAS_PDF_TOOLS =
30+
!IS_WINDOWS &&
31+
hasCommand("pdftotext") &&
32+
hasCommand("qpdf") &&
33+
hasCommand("pdftoppm");
34+
35+
/** DejaVu Sans font paths (Linux only). */
36+
const DEJAVU_PATHS = [
37+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
38+
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
39+
];
40+
const HAS_DEJAVU = !IS_WINDOWS && DEJAVU_PATHS.some((p) => existsSync(p));
41+
42+
// ── Warn loudly on Linux if dependencies are missing ─────────────────
43+
44+
if (!IS_WINDOWS) {
45+
if (!HAS_DEJAVU) {
46+
console.warn(
47+
"\n⚠️ WARNING: fonts-dejavu-core not installed — skipping font tests." +
48+
"\n Install with: sudo apt-get install fonts-dejavu-core\n",
49+
);
50+
}
51+
if (!HAS_PDF_TOOLS) {
52+
const missing = ["pdftotext", "qpdf", "pdftoppm"]
53+
.filter((cmd) => !hasCommand(cmd))
54+
.join(", ");
55+
console.warn(
56+
`\n⚠️ WARNING: missing PDF tools (${missing}) — skipping extraction tests.` +
57+
"\n Install with: sudo apt-get install poppler-utils qpdf\n",
58+
);
59+
}
60+
}
61+
1462
// ── Helpers ──────────────────────────────────────────────────────────
1563

1664
/** Decode PDF bytes to a string for inspection. */
@@ -22,13 +70,9 @@ function pdfToString(bytes: Uint8Array): string {
2270
return s;
2371
}
2472

25-
/** Load DejaVu Sans font if available, skip test if not. */
73+
/** Load DejaVu Sans font — callers are inside skipIf blocks so this is safe. */
2674
function loadDejaVu(): Uint8Array {
27-
const paths = [
28-
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
29-
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
30-
];
31-
for (const p of paths) {
75+
for (const p of DEJAVU_PATHS) {
3276
if (existsSync(p)) {
3377
return new Uint8Array(readFileSync(p));
3478
}
@@ -38,15 +82,17 @@ function loadDejaVu(): Uint8Array {
3882

3983
// ── TTF Parser Tests ─────────────────────────────────────────────────
4084

41-
describe("TTF parser (parseTTF)", () => {
85+
describe.skipIf(!HAS_DEJAVU)("TTF parser — font loading", () => {
4286
it("should parse DejaVu Sans font tables", () => {
4387
const data = loadDejaVu();
4488
// parseTTF is internal — we test it via registerCustomFont
4589
const doc = pdf.createDocument({ debug: true });
4690
// Should not throw
4791
pdf.registerCustomFont(doc, { name: "DejaVu", data });
4892
});
93+
});
4994

95+
describe("TTF parser — rejection", () => {
5096
it("should reject non-TTF data", () => {
5197
const doc = pdf.createDocument({ debug: true });
5298
expect(() =>
@@ -67,7 +113,7 @@ describe("TTF parser (parseTTF)", () => {
67113

68114
// ── Font Registration Tests ──────────────────────────────────────────
69115

70-
describe("registerCustomFont", () => {
116+
describe.skipIf(!HAS_DEJAVU)("registerCustomFont", () => {
71117
it("should register a custom font and make it usable", () => {
72118
const data = loadDejaVu();
73119
const doc = pdf.createDocument({ debug: true });
@@ -118,7 +164,7 @@ describe("registerCustomFont", () => {
118164

119165
// ── Flow Layout with Custom Fonts ────────────────────────────────────
120166

121-
describe("custom fonts in flow layout", () => {
167+
describe.skipIf(!HAS_DEJAVU)("custom fonts in flow layout", () => {
122168
it("should work with paragraph()", () => {
123169
const data = loadDejaVu();
124170
const doc = pdf.createDocument({ debug: true });
@@ -155,7 +201,7 @@ describe("custom fonts in flow layout", () => {
155201

156202
// ── Unicode Support ──────────────────────────────────────────────────
157203

158-
describe("Unicode with custom fonts", () => {
204+
describe.skipIf(!HAS_DEJAVU)("Unicode with custom fonts", () => {
159205
it("should handle characters outside WinAnsi encoding", () => {
160206
const data = loadDejaVu();
161207
const doc = pdf.createDocument({ debug: true });
@@ -208,7 +254,7 @@ describe("Unicode with custom fonts", () => {
208254

209255
// ── PDF Structure Validity ───────────────────────────────────────────
210256

211-
describe("embedded font PDF structure", () => {
257+
describe.skipIf(!HAS_DEJAVU)("embedded font PDF structure", () => {
212258
it("should produce a valid PDF with embedded font", () => {
213259
const data = loadDejaVu();
214260
const doc = pdf.createDocument({ debug: true });
@@ -277,7 +323,7 @@ describe("embedded font PDF structure", () => {
277323

278324
// ── Subsetting (Phase 11b) ───────────────────────────────────────────
279325

280-
describe("font subsetting", () => {
326+
describe.skipIf(!HAS_DEJAVU)("font subsetting", () => {
281327
it("should track used codepoints", () => {
282328
const data = loadDejaVu();
283329
const doc = pdf.createDocument({ debug: true });
@@ -352,69 +398,72 @@ describe("font subsetting", () => {
352398

353399
// ── pdftotext Verification ───────────────────────────────────────────
354400

355-
describe("custom font text extraction", () => {
356-
it("should render custom font text that pdftotext can extract", () => {
357-
const data = loadDejaVu();
358-
// Use debug: true for uncompressed streams (easier to verify)
359-
const doc = pdf.createDocument({ debug: true });
360-
pdf.registerCustomFont(doc, { name: "DJ", data });
401+
describe.skipIf(!HAS_DEJAVU || !HAS_PDF_TOOLS)(
402+
"custom font text extraction",
403+
() => {
404+
it("should render custom font text that pdftotext can extract", () => {
405+
const data = loadDejaVu();
406+
// Use debug: true for uncompressed streams (easier to verify)
407+
const doc = pdf.createDocument({ debug: true });
408+
pdf.registerCustomFont(doc, { name: "DJ", data });
361409

362-
doc.addPage();
363-
doc.drawText("Hello World", 72, 100, { font: "DJ", fontSize: 14 });
410+
doc.addPage();
411+
doc.drawText("Hello World", 72, 100, { font: "DJ", fontSize: 14 });
412+
413+
const bytes = doc.buildPdf();
414+
const tmpPath = "/tmp/test-custom-font.pdf";
415+
writeFileSync(tmpPath, bytes);
364416

365-
const bytes = doc.buildPdf();
366-
const tmpPath = "/tmp/test-custom-font.pdf";
367-
writeFileSync(tmpPath, bytes);
368-
369-
try {
370-
const extracted = execSync(`pdftotext ${tmpPath} -`).toString().trim();
371-
// pdftotext uses the ToUnicode CMap to extract text
372-
// If CMap is correct, we get the original text back
373-
expect(extracted).toContain("Hello");
374-
} finally {
375417
try {
376-
unlinkSync(tmpPath);
377-
} catch {
378-
/* ignore */
418+
const extracted = execSync(`pdftotext ${tmpPath} -`).toString().trim();
419+
// pdftotext uses the ToUnicode CMap to extract text
420+
// If CMap is correct, we get the original text back
421+
expect(extracted).toContain("Hello");
422+
} finally {
423+
try {
424+
unlinkSync(tmpPath);
425+
} catch {
426+
/* ignore */
427+
}
379428
}
380-
}
381-
});
429+
});
382430

383-
it("should work with compressed streams (non-debug mode)", () => {
384-
const data = loadDejaVu();
385-
// Non-debug = compressed streams
386-
const doc = pdf.createDocument();
387-
pdf.registerCustomFont(doc, { name: "DJ", data });
431+
it("should work with compressed streams (non-debug mode)", () => {
432+
const data = loadDejaVu();
433+
// Non-debug = compressed streams
434+
const doc = pdf.createDocument();
435+
pdf.registerCustomFont(doc, { name: "DJ", data });
388436

389-
doc.addPage();
390-
doc.drawText("Test compressed", 72, 100, { font: "DJ", fontSize: 14 });
437+
doc.addPage();
438+
doc.drawText("Test compressed", 72, 100, { font: "DJ", fontSize: 14 });
439+
440+
const bytes = doc.buildPdf();
441+
const tmpPath = "/tmp/test-custom-font-compressed.pdf";
442+
writeFileSync(tmpPath, bytes);
391443

392-
const bytes = doc.buildPdf();
393-
const tmpPath = "/tmp/test-custom-font-compressed.pdf";
394-
writeFileSync(tmpPath, bytes);
395-
396-
try {
397-
// qpdf should pass (no stream errors)
398-
const qpdfResult = execSync(`qpdf --check ${tmpPath} 2>&1`).toString();
399-
expect(qpdfResult).toContain("No syntax or stream encoding errors");
400-
401-
// pdftoppm should render (check file exists and has size)
402-
execSync(
403-
`pdftoppm -png -r 100 -singlefile ${tmpPath} /tmp/test-font-page`,
404-
);
405-
const pngStat = readFileSync("/tmp/test-font-page.png");
406-
expect(pngStat.length).toBeGreaterThan(1000); // should be a real image
407-
} finally {
408-
try {
409-
unlinkSync(tmpPath);
410-
} catch {
411-
/* ignore */
412-
}
413444
try {
414-
unlinkSync("/tmp/test-font-page.png");
415-
} catch {
416-
/* ignore */
445+
// qpdf should pass (no stream errors)
446+
const qpdfResult = execSync(`qpdf --check ${tmpPath} 2>&1`).toString();
447+
expect(qpdfResult).toContain("No syntax or stream encoding errors");
448+
449+
// pdftoppm should render (check file exists and has size)
450+
execSync(
451+
`pdftoppm -png -r 100 -singlefile ${tmpPath} /tmp/test-font-page`,
452+
);
453+
const pngStat = readFileSync("/tmp/test-font-page.png");
454+
expect(pngStat.length).toBeGreaterThan(1000); // should be a real image
455+
} finally {
456+
try {
457+
unlinkSync(tmpPath);
458+
} catch {
459+
/* ignore */
460+
}
461+
try {
462+
unlinkSync("/tmp/test-font-page.png");
463+
} catch {
464+
/* ignore */
465+
}
417466
}
418-
}
419-
});
420-
});
467+
});
468+
},
469+
);

tests/pdf-visual.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* To regenerate: UPDATE_GOLDEN=1 npx vitest run tests/pdf-visual.test.ts
99
*
1010
* Requires: poppler-utils (pdftoppm), fonts-dejavu-core
11+
* Skipped on Windows or when poppler-utils is not installed.
1112
*/
1213

1314
import { describe, it, expect } from "vitest";
@@ -25,6 +26,29 @@ const UPDATE_GOLDEN = process.env.UPDATE_GOLDEN === "1";
2526
const PIXEL_THRESHOLD = 0.1; // per-pixel colour distance threshold
2627
const MAX_DIFF_PIXELS = 50; // fail if more than this many pixels differ
2728

29+
// ── Tool Availability ────────────────────────────────────────────────
30+
31+
/** Check if a command-line tool is available on this system. */
32+
function hasCommand(cmd: string): boolean {
33+
try {
34+
execSync(`which ${cmd}`, { stdio: "ignore" });
35+
return true;
36+
} catch {
37+
return false;
38+
}
39+
}
40+
41+
const HAS_PDFTOPPM = process.platform !== "win32" && hasCommand("pdftoppm");
42+
43+
// ── Warn loudly on Linux if pdftoppm is missing ──────────────────────
44+
45+
if (process.platform !== "win32" && !HAS_PDFTOPPM) {
46+
console.warn(
47+
"\n⚠️ WARNING: pdftoppm not installed — skipping visual regression tests." +
48+
"\n Install with: sudo apt-get install poppler-utils\n",
49+
);
50+
}
51+
2852
// ── Helpers ──────────────────────────────────────────────────────────
2953

3054
/** Ensure temp directory exists. */
@@ -211,7 +235,7 @@ const fixtures: [string, () => Uint8Array][] = [
211235
["signature-line", fixtureSignature],
212236
];
213237

214-
describe("PDF visual regression", () => {
238+
describe.skipIf(!HAS_PDFTOPPM)("PDF visual regression", () => {
215239
for (const [name, generator] of fixtures) {
216240
it(`should match golden baseline: ${name}`, () => {
217241
const pdfBytes = generator();

0 commit comments

Comments
 (0)