From 37da46c10c4d2d1a4775b3491704cfab01442043 Mon Sep 17 00:00:00 2001 From: Mahathir Mohammad Shuvo Date: Wed, 19 Aug 2026 18:23:38 +0600 Subject: [PATCH 1/2] Fix table cell font src inheriting an unrelated font family --- lib/table/normalize.js | 20 +++++++++++++++++++- tests/unit/table.spec.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/table/normalize.js b/lib/table/normalize.js index 6ff534920..a528a1812 100644 --- a/lib/table/normalize.js +++ b/lib/table/normalize.js @@ -93,7 +93,25 @@ export function normalizeCell(cell, rowIndex, colIndex) { const colStyle = this._colStyle(colIndex); let rowStyle = this._rowStyle(rowIndex); - const font = deepMerge({}, colStyle.font, rowStyle.font, cell.font); + // `src` and `family` identify one font together: `family` names a + // subfamily/variation *of that src*. Merging the levels key by key let a + // `family` from a less specific level survive next to a more specific `src`, + // producing a pair that was never configured - which either throws (the + // family is not a variation of the new src) or silently resolves to the wrong + // font, because the family cache already maps that name to the old src. + // So a level that sets `src` also resets `family`, while a level that sets + // only `family` still refines the inherited `src`. `size` is independent. + const font = {}; + for (const level of [colStyle.font, rowStyle.font, cell.font]) { + if (level == null) continue; + if (level.src != null) { + font.src = level.src; + font.family = level.family; + } else if (level.family != null) { + font.family = level.family; + } + if (level.size != null) font.size = level.size; + } const customFont = Object.values(font).filter((v) => v != null).length > 0; const doc = this.document; diff --git a/tests/unit/table.spec.js b/tests/unit/table.spec.js index 9f84938d5..c3c1fff37 100644 --- a/tests/unit/table.spec.js +++ b/tests/unit/table.spec.js @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import PDFDocument from '../../lib/document'; import PDFTable from '../../lib/table'; import { deepMerge } from '../../lib/table/utils'; @@ -14,6 +15,42 @@ describe('table', () => { table.row(['A', 'B', 'C']); expect(table._columnWidths.length).toBe(3); }); + + describe('font resolution across style levels', () => { + const REGULAR = 'tests/fonts/Roboto-Regular.ttf'; + const MEDIUM = 'tests/fonts/Roboto-Medium.ttf'; + + test('a cell overriding only src does not inherit the row family', () => { + // `family` names a subfamily/variation inside the src, so carrying the + // row's family over to a different src produced a pair that was never + // configured. With a real font that threw "Variations require a font with + // the fvar, gvar and glyf, or CFF2 tables". + const document = new PDFDocument({ font: REGULAR }); + const spy = vi.spyOn(document, 'font'); + + expect(() => + document + .table({ rowStyles: [{ font: { src: REGULAR, family: 'Roboto' } }] }) + .row([{ text: 'x', font: { src: MEDIUM } }]), + ).not.toThrow(); + + expect(spy).toHaveBeenCalledWith(MEDIUM, undefined); + expect(spy).not.toHaveBeenCalledWith(MEDIUM, 'Roboto'); + }); + + test('a cell overriding only family still refines the inherited src', () => { + const document = new PDFDocument({ font: REGULAR }); + // Resolution only - the font is stubbed so that an arbitrary family name + // does not have to exist inside the file. + const spy = vi.spyOn(document, 'font').mockReturnThis(); + + document + .table({ rowStyles: [{ font: { src: REGULAR } }] }) + .row([{ text: 'x', font: { family: 'Condensed' } }]); + + expect(spy).toHaveBeenCalledWith(REGULAR, 'Condensed'); + }); + }); }); describe('utils', () => { From c675f7dad6766466d0cad6e23e47f2604b6494c5 Mon Sep 17 00:00:00 2001 From: Mahathir Mohammad Shuvo Date: Wed, 19 Aug 2026 19:06:48 +0600 Subject: [PATCH 2/2] Cover binary font sources in table cells --- tests/unit/table.spec.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/table.spec.js b/tests/unit/table.spec.js index c3c1fff37..b7e3d7e5b 100644 --- a/tests/unit/table.spec.js +++ b/tests/unit/table.spec.js @@ -1,3 +1,4 @@ +import fs from 'fs'; import { vi } from 'vitest'; import PDFDocument from '../../lib/document'; import PDFTable from '../../lib/table'; @@ -38,6 +39,23 @@ describe('table', () => { expect(spy).not.toHaveBeenCalledWith(MEDIUM, 'Roboto'); }); + test('a binary font src is passed through untouched', () => { + // deepMerge deep-cloned the src, turning a Buffer/Uint8Array into a plain + // object of numeric keys, so the font no longer looked like a font: + // "Not a supported font format or standard PDF font." It also made that + // merge walk every byte of the file. + const document = new PDFDocument({ font: REGULAR }); + const spy = vi.spyOn(document, 'font'); + const buffer = fs.readFileSync(REGULAR); + + expect(() => + document.table().row([{ text: 'x', font: { src: buffer } }]), + ).not.toThrow(); + + // the very same object, not a copy of it + expect(spy.mock.calls.some(([src]) => src === buffer)).toBe(true); + }); + test('a cell overriding only family still refines the inherited src', () => { const document = new PDFDocument({ font: REGULAR }); // Resolution only - the font is stubbed so that an arbitrary family name