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
810import { describe , it , expect } from "vitest" ;
@@ -11,6 +13,52 @@ import { execSync } from "child_process";
1113
1214const 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 . */
2674function 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+ ) ;
0 commit comments