From 6d0ceb8aecec9753d68651d0020918694ff67252 Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:05:29 +0800 Subject: [PATCH 01/12] Clone TypedArray instead of deep merging --- lib/table/utils.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/table/utils.js b/lib/table/utils.js index 7762d6bc7..fd42cca6b 100644 --- a/lib/table/utils.js +++ b/lib/table/utils.js @@ -330,6 +330,25 @@ function isObject(item) { return item && typeof item === 'object' && !Array.isArray(item); } +/** + * TypedArray check. + * @param item + * @returns {boolean} + */ +function isTypedArray(item) { + return ( + isObject(item) && + item.length !== undefined && + (item instanceof Uint8Array || + item instanceof Uint8ClampedArray || + item instanceof Uint16Array || + item instanceof Uint32Array || + item instanceof Int8Array || + item instanceof Int16Array || + item instanceof Int32Array) + ); +} + /** * Deep merge two objects. * @@ -345,7 +364,7 @@ export function deepMerge(target, ...sources) { for (const source of sources) { if (isObject(source)) { for (const key in source) { - if (isObject(source[key])) { + if (isObject(source[key]) && !isTypedArray(source[key])) { if (!(key in target)) target[key] = {}; target[key] = deepMerge(target[key], source[key]); } else if (source[key] !== undefined) { @@ -360,7 +379,13 @@ export function deepMerge(target, ...sources) { function deepClone(obj) { let result = obj; - if (obj && typeof obj == 'object') { + if (isTypedArray(obj)) { + if (Buffer.isBuffer(obj)) { + result = Buffer.from(obj); + } else { + result = obj.slice(); + } + } else if (obj && typeof obj == 'object') { result = Array.isArray(obj) ? [] : {}; for (const key in obj) result[key] = deepClone(obj[key]); } From 4fdaf60ca3a8a6bb0a08ad8535e75014487d5e2e Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:07:37 +0800 Subject: [PATCH 02/12] Add unit tests for new deep merge behaviour --- tests/unit/table.spec.js | 129 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/unit/table.spec.js b/tests/unit/table.spec.js index 9f84938d5..11f66b7c4 100644 --- a/tests/unit/table.spec.js +++ b/tests/unit/table.spec.js @@ -1,6 +1,7 @@ import PDFDocument from '../../lib/document'; import PDFTable from '../../lib/table'; import { deepMerge } from '../../lib/table/utils'; +import fs from 'fs'; describe('table', () => { test('created', () => { @@ -14,6 +15,124 @@ describe('table', () => { table.row(['A', 'B', 'C']); expect(table._columnWidths.length).toBe(3); }); + + describe('font', () => { + test('colum standard font', () => { + const document = new PDFDocument(); + const table = document.table({ + columnStyles: { + font: { src: 'Courier' }, + }, + }); + table.row(['A']); + expect(table._columnWidths.length).toBe(1); + }); + + test('colum embeded font with path', () => { + const document = new PDFDocument(); + const table = document.table({ + columnStyles: { + font: { src: 'tests/fonts/Roboto-Regular.ttf' }, + }, + }); + table.row(['A']); + expect(table._columnWidths.length).toBe(1); + }); + + test('colum embeded font with Buffer', () => { + const document = new PDFDocument(); + const buffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); + console.log('buffer instanceof Buffer:', buffer instanceof Buffer); + const table = document.table({ + columnStyles: { + font: { + src: buffer, + }, + }, + }); + table.row(['A']); + expect(table._columnWidths.length).toBe(1); + }); + + test('row standard font', () => { + const document = new PDFDocument(); + const table = document.table({ + rowStyles: { + font: { src: 'Courier' }, + }, + }); + table.row(['A']); + expect(table._columnWidths.length).toBe(1); + }); + + test('row embeded font with path', () => { + const document = new PDFDocument(); + const table = document.table({ + rowStyles: { + font: { src: 'tests/fonts/Roboto-Regular.ttf' }, + }, + }); + table.row(['A']); + expect(table._columnWidths.length).toBe(1); + }); + + test('row embeded font with Buffer', () => { + const document = new PDFDocument(); + const buffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); + console.log('buffer instanceof Buffer:', buffer instanceof Buffer); + const table = document.table({ + rowStyles: { + font: { + src: buffer, + }, + }, + }); + table.row(['A']); + expect(table._columnWidths.length).toBe(1); + }); + + test('cell standard font', () => { + const document = new PDFDocument(); + const table = document.table(); + table.row([ + { + text: 'A', + font: { + src: 'Courier', + }, + }, + ]); + expect(table._columnWidths.length).toBe(1); + }); + + test('cell embeded font with path', () => { + const document = new PDFDocument(); + const table = document.table(); + table.row([ + { + text: 'A', + font: { + src: 'tests/fonts/Roboto-Regular.ttf', + }, + }, + ]); + expect(table._columnWidths.length).toBe(1); + }); + + test('cell embeded font with Buffer', () => { + const document = new PDFDocument(); + const table = document.table(); + table.row([ + { + text: 'A', + font: { + src: fs.readFileSync('tests/fonts/Roboto-Regular.ttf'), + }, + }, + ]); + expect(table._columnWidths.length).toBe(1); + }); + }); }); describe('utils', () => { @@ -28,6 +147,16 @@ describe('utils', () => { [1, {}, 1], [{ a: 'hello' }, { a: {} }, { a: 'hello' }], [{ a: { b: 'hello' } }, { a: { b: 'world' } }, { a: { b: 'world' } }], + [ + { a: Buffer.from([1, 2, 3]) }, + { b: Buffer.from([4, 5, 6]) }, + { a: Buffer.from([1, 2, 3]), b: Buffer.from([4, 5, 6]) }, + ], + [ + { a: new Uint8Array([1, 2, 3]) }, + { b: new Uint8Array([4, 5, 6]) }, + { a: new Uint8Array([1, 2, 3]), b: new Uint8Array([4, 5, 6]) }, + ], ])('%o -> %o', function () { const opts = Array.from(arguments); const expected = opts.splice(-1, 1)[0]; From 505b44b7e258d1234694024ed749a8ffdf993228 Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:46:58 +0800 Subject: [PATCH 03/12] Fix typos in test cases --- tests/unit/table.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/table.spec.js b/tests/unit/table.spec.js index 11f66b7c4..4bd517666 100644 --- a/tests/unit/table.spec.js +++ b/tests/unit/table.spec.js @@ -17,7 +17,7 @@ describe('table', () => { }); describe('font', () => { - test('colum standard font', () => { + test('column standard font', () => { const document = new PDFDocument(); const table = document.table({ columnStyles: { @@ -28,7 +28,7 @@ describe('table', () => { expect(table._columnWidths.length).toBe(1); }); - test('colum embeded font with path', () => { + test('column embeded font with path', () => { const document = new PDFDocument(); const table = document.table({ columnStyles: { @@ -39,7 +39,7 @@ describe('table', () => { expect(table._columnWidths.length).toBe(1); }); - test('colum embeded font with Buffer', () => { + test('column embeded font with Buffer', () => { const document = new PDFDocument(); const buffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); console.log('buffer instanceof Buffer:', buffer instanceof Buffer); From 3052cee17c209c094465a21b828b4dfd43fe89c7 Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:26:30 +0800 Subject: [PATCH 04/12] Fix table fonts to merge shallowly --- lib/table/normalize.js | 6 +++++- lib/table/style.js | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/table/normalize.js b/lib/table/normalize.js index 6ff534920..f6b500945 100644 --- a/lib/table/normalize.js +++ b/lib/table/normalize.js @@ -93,7 +93,11 @@ 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); + const font = { + ...colStyle.font, + ...rowStyle.font, + ...cell.font, + }; const customFont = Object.values(font).filter((v) => v != null).length > 0; const doc = this.document; diff --git a/lib/table/style.js b/lib/table/style.js index 17ac24d1a..75fd99961 100644 --- a/lib/table/style.js +++ b/lib/table/style.js @@ -58,8 +58,17 @@ export function normalizedRowStyle(defaultRowStyle, rowStyleInternal, i) { rowStyle.borderColor = normalizeSides(rowStyle.borderColor); rowStyle.align = normalizeAlignment(rowStyle.align); + // extract fonts + const { font: defaultFont, ...restDefaultStyle } = defaultRowStyle || {}; + const { font: font, ...restStyle } = rowStyle || {}; + const mergedFont = { + ...defaultFont, + ...font, + }; + // Merge defaults - rowStyle = deepMerge(defaultRowStyle, rowStyle); + rowStyle = deepMerge(restDefaultStyle, restStyle); + rowStyle.font = mergedFont; const document = this.document; const page = document.page; @@ -114,8 +123,17 @@ export function normalizedColumnStyle(defaultColStyle, colStyleInternal, i) { colStyle.borderColor = normalizeSides(colStyle.borderColor); colStyle.align = normalizeAlignment(colStyle.align); + // extract fonts + const { font: defaultFont, ...restDefaultStyle } = defaultColStyle || {}; + const { font: font, ...restStyle } = colStyle || {}; + const mergedFont = { + ...defaultFont, + ...font, + }; + // Merge defaults - colStyle = deepMerge(defaultColStyle, colStyle); + colStyle = deepMerge(restDefaultStyle, restStyle); + colStyle.font = mergedFont; if (colStyle.width == null || colStyle.width === '*') { colStyle.width = '*'; From f5ccc929c10df74c4695c502f997cf625ec9d0a2 Mon Sep 17 00:00:00 2001 From: ykw9263 Date: Fri, 26 Jun 2026 16:42:36 +0800 Subject: [PATCH 05/12] Rewrite table font unit test --- tests/unit/table.spec.js | 244 +++++++++++++++++++++++++-------------- 1 file changed, 159 insertions(+), 85 deletions(-) diff --git a/tests/unit/table.spec.js b/tests/unit/table.spec.js index 4bd517666..d809de63f 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'; @@ -17,120 +18,193 @@ describe('table', () => { }); describe('font', () => { - test('column standard font', () => { + test('column font', () => { + const standardFont = 'Courier'; + const fontPath = 'tests/fonts/Roboto-Regular.ttf'; + const fontBuffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); const document = new PDFDocument(); - const table = document.table({ - columnStyles: { - font: { src: 'Courier' }, - }, - }); - table.row(['A']); - expect(table._columnWidths.length).toBe(1); - }); + const fontSpy = vi.spyOn(document, 'font'); - test('column embeded font with path', () => { - const document = new PDFDocument(); const table = document.table({ - columnStyles: { - font: { src: 'tests/fonts/Roboto-Regular.ttf' }, - }, + columnStyles: [ + { font: { src: standardFont } }, + { font: { src: fontPath } }, + { font: { src: fontBuffer } }, + ], }); - table.row(['A']); - expect(table._columnWidths.length).toBe(1); + table.row(['A', 'B', 'C']); + expect(fontSpy).toHaveBeenCalledWith( + standardFont, + expect.toSatisfy(() => true), + ); + expect(fontSpy).toHaveBeenCalledWith( + fontPath, + expect.toSatisfy(() => true), + ); + expect(fontSpy).toHaveBeenCalledWith( + fontBuffer, + expect.toSatisfy(() => true), + ); }); - test('column embeded font with Buffer', () => { + test('row font', () => { + const standardFont = 'Courier'; + const fontPath = 'tests/fonts/Roboto-Regular.ttf'; + const fontBuffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); const document = new PDFDocument(); - const buffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); - console.log('buffer instanceof Buffer:', buffer instanceof Buffer); - const table = document.table({ - columnStyles: { - font: { - src: buffer, - }, - }, - }); - table.row(['A']); - expect(table._columnWidths.length).toBe(1); - }); + const fontSpy = vi.spyOn(document, 'font'); - test('row standard font', () => { - const document = new PDFDocument(); const table = document.table({ - rowStyles: { - font: { src: 'Courier' }, - }, + rowStyles: [ + { font: { src: standardFont } }, + { font: { src: fontPath } }, + { font: { src: fontBuffer } }, + ], }); table.row(['A']); - expect(table._columnWidths.length).toBe(1); + table.row(['B']); + table.row(['C']); + expect(fontSpy).toHaveBeenCalledWith( + standardFont, + expect.toSatisfy(() => true), + ); + expect(fontSpy).toHaveBeenCalledWith( + fontPath, + expect.toSatisfy(() => true), + ); + expect(fontSpy).toHaveBeenCalledWith( + fontBuffer, + expect.toSatisfy(() => true), + ); }); - test('row embeded font with path', () => { + test('cell font', () => { + const standardFont = 'Courier'; + const fontPath = 'tests/fonts/Roboto-Regular.ttf'; + const fontBuffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); const document = new PDFDocument(); - const table = document.table({ - rowStyles: { - font: { src: 'tests/fonts/Roboto-Regular.ttf' }, - }, - }); - table.row(['A']); - expect(table._columnWidths.length).toBe(1); + const fontSpy = vi.spyOn(document, 'font'); + + const table = document.table(); + table.row([ + { text: 'A', font: { src: standardFont } }, + { text: 'B', font: { src: fontPath } }, + { text: 'C', font: { src: fontBuffer } }, + ]); + expect(fontSpy).toHaveBeenCalledWith( + standardFont, + expect.toSatisfy(() => true), + ); + expect(fontSpy).toHaveBeenCalledWith( + fontPath, + expect.toSatisfy(() => true), + ); + expect(fontSpy).toHaveBeenCalledWith( + fontBuffer, + expect.toSatisfy(() => true), + ); }); - test('row embeded font with Buffer', () => { + test('merge table font', () => { + const colFamily = 'family1'; + const rowFamily = 'family2'; + const cellFamily = 'family3'; + const fontSrcs = { + colStandardFont: 'Courier', + colFontPath: 'tests/fonts/Roboto-Regular.ttf', + colFontBuffer: fs.readFileSync('tests/fonts/Roboto-Regular.ttf'), + rowStandardFont: 'Courier-Bold', + rowFontPath: 'tests/fonts/Roboto-Medium.ttf', + rowFontBuffer: fs.readFileSync('tests/fonts/Roboto-Medium.ttf'), + cellStandardFont: 'Courier-Oblique', + cellFontPath: 'tests/fonts/Roboto-MediumItalic.ttf', + cellFontBuffer: fs.readFileSync('tests/fonts/Roboto-MediumItalic.ttf'), + }; + const fontSrcSet = Object.values(fontSrcs); + + /** + * Check whether given spy has been called with specified allowed fonts + * and not other fonts within concerned font set + * @param {*} fontSpy + * @param {import('../../lib/table/utils').Font[]} allowedFonts + */ + function expectFonts( + fontSpy, + allowedFonts = [], + testedFonts = fontSrcSet, + ) { + const allowedFontSrc = allowedFonts.map((font) => { + expect(fontSpy).toHaveBeenCalledWith(font.src, font.family); + return font.src; + }); + testedFonts.forEach((fontSrc) => { + if (!allowedFontSrc.includes(fontSrc)) { + expect(fontSpy).not.toHaveBeenCalledWith( + fontSrc, + expect.toSatisfy(() => true), + ); + } + }); + } const document = new PDFDocument(); - const buffer = fs.readFileSync('tests/fonts/Roboto-Regular.ttf'); - console.log('buffer instanceof Buffer:', buffer instanceof Buffer); + const fontSpy = vi.spyOn(document, 'font'); + const table = document.table({ - rowStyles: { - font: { - src: buffer, - }, - }, + columnStyles: [ + { font: { src: fontSrcs.colStandardFont, family: colFamily } }, + { font: { src: fontSrcs.colFontPath, family: colFamily } }, + { font: { src: fontSrcs.colFontBuffer, family: colFamily } }, + ], + rowStyles: [ + {}, + { font: { src: fontSrcs.rowStandardFont, family: rowFamily } }, + { font: { src: fontSrcs.rowFontPath, family: rowFamily } }, + { font: { src: fontSrcs.rowFontBuffer, family: rowFamily } }, + { font: { src: fontSrcs.rowFontBuffer, family: rowFamily } }, + ], }); - table.row(['A']); - expect(table._columnWidths.length).toBe(1); - }); + // fonts in column styles + fontSpy.mockClear(); + table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); + expectFonts(fontSpy, [ + { src: fontSrcs.colStandardFont, family: colFamily }, + { src: fontSrcs.colFontPath, family: colFamily }, + { src: fontSrcs.colFontBuffer, family: colFamily }, + ]); - test('cell standard font', () => { - const document = new PDFDocument(); - const table = document.table(); - table.row([ - { - text: 'A', - font: { - src: 'Courier', - }, - }, + // fonts in column + row styles + fontSpy.mockClear(); + table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); + expectFonts(fontSpy, [ + { src: fontSrcs.rowStandardFont, family: rowFamily }, + ]); + fontSpy.mockClear(); + table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); + expectFonts(fontSpy, [{ src: fontSrcs.rowFontPath, family: rowFamily }]); + fontSpy.mockClear(); + table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); + expectFonts(fontSpy, [ + { src: fontSrcs.rowFontBuffer, family: rowFamily }, ]); - expect(table._columnWidths.length).toBe(1); - }); - test('cell embeded font with path', () => { - const document = new PDFDocument(); - const table = document.table(); + // fonts in column + row + cell style + fontSpy.mockClear(); table.row([ { text: 'A', - font: { - src: 'tests/fonts/Roboto-Regular.ttf', - }, + font: { src: fontSrcs.cellStandardFont, family: cellFamily }, }, - ]); - expect(table._columnWidths.length).toBe(1); - }); - - test('cell embeded font with Buffer', () => { - const document = new PDFDocument(); - const table = document.table(); - table.row([ + { text: 'B', font: { src: fontSrcs.cellFontPath, family: cellFamily } }, { - text: 'A', - font: { - src: fs.readFileSync('tests/fonts/Roboto-Regular.ttf'), - }, + text: 'C', + font: { src: fontSrcs.cellFontBuffer, family: cellFamily }, }, ]); - expect(table._columnWidths.length).toBe(1); + expectFonts(fontSpy, [ + { src: fontSrcs.cellStandardFont, family: cellFamily }, + { src: fontSrcs.cellFontPath, family: cellFamily }, + { src: fontSrcs.cellFontBuffer, family: cellFamily }, + ]); }); }); }); From 05f624a7876fff2580ee88a963aef4e99d500ab7 Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:51:38 +0800 Subject: [PATCH 06/12] Remove unneeded type checks / clone --- lib/table/utils.js | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/lib/table/utils.js b/lib/table/utils.js index fd42cca6b..379726ea9 100644 --- a/lib/table/utils.js +++ b/lib/table/utils.js @@ -331,21 +331,14 @@ function isObject(item) { } /** - * TypedArray check. + * buffer check. * @param item * @returns {boolean} */ -function isTypedArray(item) { +function isBuffer(item) { return ( isObject(item) && - item.length !== undefined && - (item instanceof Uint8Array || - item instanceof Uint8ClampedArray || - item instanceof Uint16Array || - item instanceof Uint32Array || - item instanceof Int8Array || - item instanceof Int16Array || - item instanceof Int32Array) + (item instanceof Uint8Array || item instanceof ArrayBuffer) ); } @@ -364,7 +357,7 @@ export function deepMerge(target, ...sources) { for (const source of sources) { if (isObject(source)) { for (const key in source) { - if (isObject(source[key]) && !isTypedArray(source[key])) { + if (isObject(source[key]) && !isBuffer(source[key])) { if (!(key in target)) target[key] = {}; target[key] = deepMerge(target[key], source[key]); } else if (source[key] !== undefined) { @@ -379,12 +372,8 @@ export function deepMerge(target, ...sources) { function deepClone(obj) { let result = obj; - if (isTypedArray(obj)) { - if (Buffer.isBuffer(obj)) { - result = Buffer.from(obj); - } else { - result = obj.slice(); - } + if (isBuffer(obj)) { + result = obj; } else if (obj && typeof obj == 'object') { result = Array.isArray(obj) ? [] : {}; for (const key in obj) result[key] = deepClone(obj[key]); From fbc34f13c38b6e47f7b4175ed4e55f924e1d2002 Mon Sep 17 00:00:00 2001 From: ykw9263 Date: Sun, 28 Jun 2026 03:21:14 +0800 Subject: [PATCH 07/12] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ece09fbc4..f0f6b071b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Unreleased +- Fix Table style merging crashes when passing fonts as buffer (#1743) + ### [v0.19.1] - 2026-06-10 - Fix RGB JPEG embedded as DeviceGray (0.19.0 regression) (#1734) From d724450c25a4d2d0ef30d3142bf0c09776728380 Mon Sep 17 00:00:00 2001 From: ykw9263 Date: Sun, 28 Jun 2026 13:49:03 +0800 Subject: [PATCH 08/12] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove unnecessary isObject check - rename binary data check function Co-authored-by: Luiz Américo --- lib/table/utils.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/table/utils.js b/lib/table/utils.js index 379726ea9..75d0208df 100644 --- a/lib/table/utils.js +++ b/lib/table/utils.js @@ -335,9 +335,8 @@ function isObject(item) { * @param item * @returns {boolean} */ -function isBuffer(item) { +function isBinaryData(item) { return ( - isObject(item) && (item instanceof Uint8Array || item instanceof ArrayBuffer) ); } From 731a4c12fc25e3160d63ea78d9f00396a9de1ef9 Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Sun, 28 Jun 2026 13:51:03 +0800 Subject: [PATCH 09/12] Update function name and Lint --- lib/table/utils.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/table/utils.js b/lib/table/utils.js index 75d0208df..9c9fcdf21 100644 --- a/lib/table/utils.js +++ b/lib/table/utils.js @@ -336,9 +336,7 @@ function isObject(item) { * @returns {boolean} */ function isBinaryData(item) { - return ( - (item instanceof Uint8Array || item instanceof ArrayBuffer) - ); + return item instanceof Uint8Array || item instanceof ArrayBuffer; } /** @@ -356,7 +354,7 @@ export function deepMerge(target, ...sources) { for (const source of sources) { if (isObject(source)) { for (const key in source) { - if (isObject(source[key]) && !isBuffer(source[key])) { + if (isObject(source[key]) && !isBinaryData(source[key])) { if (!(key in target)) target[key] = {}; target[key] = deepMerge(target[key], source[key]); } else if (source[key] !== undefined) { @@ -371,7 +369,7 @@ export function deepMerge(target, ...sources) { function deepClone(obj) { let result = obj; - if (isBuffer(obj)) { + if (isBinaryData(obj)) { result = obj; } else if (obj && typeof obj == 'object') { result = Array.isArray(obj) ? [] : {}; From 4b84c3076155d75406dd1b12c086c01403d2e414 Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:46:29 +0800 Subject: [PATCH 10/12] Remove family name in table fonts unit test - font family name is not allowed for ttf fonts with no Variations --- tests/unit/table.spec.js | 45 +++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/unit/table.spec.js b/tests/unit/table.spec.js index d809de63f..a765aabd2 100644 --- a/tests/unit/table.spec.js +++ b/tests/unit/table.spec.js @@ -106,9 +106,6 @@ describe('table', () => { }); test('merge table font', () => { - const colFamily = 'family1'; - const rowFamily = 'family2'; - const cellFamily = 'family3'; const fontSrcs = { colStandardFont: 'Courier', colFontPath: 'tests/fonts/Roboto-Regular.ttf', @@ -151,59 +148,55 @@ describe('table', () => { const table = document.table({ columnStyles: [ - { font: { src: fontSrcs.colStandardFont, family: colFamily } }, - { font: { src: fontSrcs.colFontPath, family: colFamily } }, - { font: { src: fontSrcs.colFontBuffer, family: colFamily } }, + { font: { src: fontSrcs.colStandardFont } }, + { font: { src: fontSrcs.colFontPath } }, + { font: { src: fontSrcs.colFontBuffer } }, ], rowStyles: [ {}, - { font: { src: fontSrcs.rowStandardFont, family: rowFamily } }, - { font: { src: fontSrcs.rowFontPath, family: rowFamily } }, - { font: { src: fontSrcs.rowFontBuffer, family: rowFamily } }, - { font: { src: fontSrcs.rowFontBuffer, family: rowFamily } }, + { font: { src: fontSrcs.rowStandardFont } }, + { font: { src: fontSrcs.rowFontPath } }, + { font: { src: fontSrcs.rowFontBuffer } }, + { font: { src: fontSrcs.rowFontBuffer } }, ], }); // fonts in column styles fontSpy.mockClear(); table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); expectFonts(fontSpy, [ - { src: fontSrcs.colStandardFont, family: colFamily }, - { src: fontSrcs.colFontPath, family: colFamily }, - { src: fontSrcs.colFontBuffer, family: colFamily }, + { src: fontSrcs.colStandardFont }, + { src: fontSrcs.colFontPath }, + { src: fontSrcs.colFontBuffer }, ]); // fonts in column + row styles fontSpy.mockClear(); table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); - expectFonts(fontSpy, [ - { src: fontSrcs.rowStandardFont, family: rowFamily }, - ]); + expectFonts(fontSpy, [{ src: fontSrcs.rowStandardFont }]); fontSpy.mockClear(); table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); - expectFonts(fontSpy, [{ src: fontSrcs.rowFontPath, family: rowFamily }]); + expectFonts(fontSpy, [{ src: fontSrcs.rowFontPath }]); fontSpy.mockClear(); table.row([{ text: 'A' }, { text: 'B' }, { text: 'C' }]); - expectFonts(fontSpy, [ - { src: fontSrcs.rowFontBuffer, family: rowFamily }, - ]); + expectFonts(fontSpy, [{ src: fontSrcs.rowFontBuffer }]); // fonts in column + row + cell style fontSpy.mockClear(); table.row([ { text: 'A', - font: { src: fontSrcs.cellStandardFont, family: cellFamily }, + font: { src: fontSrcs.cellStandardFont }, }, - { text: 'B', font: { src: fontSrcs.cellFontPath, family: cellFamily } }, + { text: 'B', font: { src: fontSrcs.cellFontPath } }, { text: 'C', - font: { src: fontSrcs.cellFontBuffer, family: cellFamily }, + font: { src: fontSrcs.cellFontBuffer }, }, ]); expectFonts(fontSpy, [ - { src: fontSrcs.cellStandardFont, family: cellFamily }, - { src: fontSrcs.cellFontPath, family: cellFamily }, - { src: fontSrcs.cellFontBuffer, family: cellFamily }, + { src: fontSrcs.cellStandardFont }, + { src: fontSrcs.cellFontPath }, + { src: fontSrcs.cellFontBuffer }, ]); }); }); From 84cb1a152ba02ddbea2b78aae6e2e10630fd701e Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:34:31 +0800 Subject: [PATCH 11/12] Bind font src and family in style merge --- lib/table/normalize.js | 5 ++--- lib/table/style.js | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/table/normalize.js b/lib/table/normalize.js index f6b500945..422b391dc 100644 --- a/lib/table/normalize.js +++ b/lib/table/normalize.js @@ -94,9 +94,8 @@ export function normalizeCell(cell, rowIndex, colIndex) { let rowStyle = this._rowStyle(rowIndex); const font = { - ...colStyle.font, - ...rowStyle.font, - ...cell.font, + ...(cell.font || rowStyle.font || colStyle.font), + size: cell.font?.size || rowStyle.font?.size || colStyle.font?.size, }; const customFont = Object.values(font).filter((v) => v != null).length > 0; const doc = this.document; diff --git a/lib/table/style.js b/lib/table/style.js index 75fd99961..d9a61ed2c 100644 --- a/lib/table/style.js +++ b/lib/table/style.js @@ -62,8 +62,8 @@ export function normalizedRowStyle(defaultRowStyle, rowStyleInternal, i) { const { font: defaultFont, ...restDefaultStyle } = defaultRowStyle || {}; const { font: font, ...restStyle } = rowStyle || {}; const mergedFont = { - ...defaultFont, - ...font, + ...(font || defaultFont), + size: font?.size || defaultFont?.size, }; // Merge defaults @@ -127,8 +127,8 @@ export function normalizedColumnStyle(defaultColStyle, colStyleInternal, i) { const { font: defaultFont, ...restDefaultStyle } = defaultColStyle || {}; const { font: font, ...restStyle } = colStyle || {}; const mergedFont = { - ...defaultFont, - ...font, + ...(font || defaultFont), + size: font?.size || defaultFont?.size, }; // Merge defaults From b8ca3a9f99aea5b93ea6015edd7f04d3e7ee9dc8 Mon Sep 17 00:00:00 2001 From: ykw9263 <180926524+ykw9263@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:54:23 +0800 Subject: [PATCH 12/12] Merge font only when font has src defined --- lib/table/normalize.js | 4 +++- lib/table/style.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/table/normalize.js b/lib/table/normalize.js index 422b391dc..c937591fd 100644 --- a/lib/table/normalize.js +++ b/lib/table/normalize.js @@ -94,7 +94,9 @@ export function normalizeCell(cell, rowIndex, colIndex) { let rowStyle = this._rowStyle(rowIndex); const font = { - ...(cell.font || rowStyle.font || colStyle.font), + ...((cell.font?.src && cell.font) || + (rowStyle.font?.src && rowStyle.font) || + (colStyle.font?.src && colStyle.font)), size: cell.font?.size || rowStyle.font?.size || colStyle.font?.size, }; const customFont = Object.values(font).filter((v) => v != null).length > 0; diff --git a/lib/table/style.js b/lib/table/style.js index d9a61ed2c..9439de506 100644 --- a/lib/table/style.js +++ b/lib/table/style.js @@ -62,7 +62,7 @@ export function normalizedRowStyle(defaultRowStyle, rowStyleInternal, i) { const { font: defaultFont, ...restDefaultStyle } = defaultRowStyle || {}; const { font: font, ...restStyle } = rowStyle || {}; const mergedFont = { - ...(font || defaultFont), + ...((font?.src && font) || (defaultFont?.src && defaultFont)), size: font?.size || defaultFont?.size, }; @@ -127,7 +127,7 @@ export function normalizedColumnStyle(defaultColStyle, colStyleInternal, i) { const { font: defaultFont, ...restDefaultStyle } = defaultColStyle || {}; const { font: font, ...restStyle } = colStyle || {}; const mergedFont = { - ...(font || defaultFont), + ...((font?.src && font) || (defaultFont?.src && defaultFont)), size: font?.size || defaultFont?.size, };