Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion lib/table/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
55 changes: 55 additions & 0 deletions tests/unit/table.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'fs';
import { vi } from 'vitest';
import PDFDocument from '../../lib/document';
import PDFTable from '../../lib/table';
import { deepMerge } from '../../lib/table/utils';
Expand All @@ -14,6 +16,59 @@ 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 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
// 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', () => {
Expand Down
Loading