Skip to content

Bugfix: Handle TypedArrays and Buffers in deep-merge/clone#1744

Open
ykw9263 wants to merge 13 commits into
foliojs:masterfrom
ykw9263:bugfix/table-deep-merged-typedarray
Open

Bugfix: Handle TypedArrays and Buffers in deep-merge/clone#1744
ykw9263 wants to merge 13 commits into
foliojs:masterfrom
ykw9263:bugfix/table-deep-merged-typedarray

Conversation

@ykw9263

@ykw9263 ykw9263 commented Jun 25, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

This fixes #1743 where providing font buffers crashes table generation.

In lib\table\utils.js, deepMerge and deepClone() copy properties key by key including Buffer and Uint8Array without inheriting the class. This causes the merged font buffer failing to be recognised by PDFFontFactory which relies on instanceof to determine the type of font sources.

Also fixes #1746 where table style merging falsely assign font families.

This PR changes:

  • deepMerge() and deepClone() now clone TypedArrays and Buffers using .slice() and Buffer.from(buffer) respectively.

  • Table style merging is modified to copy fonts by reference.

Additional unit tests (fonts in tables, deep-merging buffers) are added for the changes.

Checklist:

  • Unit Tests
  • Documentation
  • Update CHANGELOG.md
  • Ready to be merged

TODO:

@ykw9263 ykw9263 marked this pull request as draft June 25, 2026 17:33
Comment thread lib/table/utils.js Outdated
Comment thread lib/table/utils.js Outdated
@ykw9263 ykw9263 marked this pull request as ready for review June 27, 2026 19:25

@blikblum blikblum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the font handling is not overhauled this seems a good workaround

Comment thread lib/table/utils.js Outdated
Comment thread lib/table/utils.js Outdated
ykw9263 and others added 3 commits June 28, 2026 13:49
- remove unnecessary isObject check
- rename binary data check function

Co-authored-by: Luiz Américo <camara_luiz@yahoo.com.br>
- font family name is not allowed for ttf fonts with no Variations
Comment thread lib/table/style.js
Comment on lines +126 to +132
// extract fonts
const { font: defaultFont, ...restDefaultStyle } = defaultColStyle || {};
const { font: font, ...restStyle } = colStyle || {};
const mergedFont = {
...defaultFont,
...font,
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that deepMerge was modified to handle binary data, why not use it?

@ykw9263 ykw9263 Jun 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just found a bug where merging fonts with font collections would cause problem

  doc.table({
    rowStyles: [{
        font: { src: SOME_TTC_FONT, family: FONT_FAMILY },
    }]
  })
  .row([
    { text: 'Hello World', font: { src: SOME_TTF_FONT } }
  ]);
// throws `Variations require a font with the fvar, gvar and glyf, or CFF2 tables`

I think font src should be binded with its family and not be merged with other family names across styles

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another bug, independent of table, just using font, please open another issue

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug is caused by style merging in tables propagating font families to other font options. Other than tables the same error seems only be thrown on user errors when they put unsupported families in font options.

Manually assigning font properties and avoiding deepMerge on table fonts fixes this together.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the changes ignores font: {size}

This is a rabbit hole

@hollandjake how about we keep with the following font cell | row | col def:

font: string |buffer
fontSize: number

We kill most of the complexity and AFAIK still pretty flexible

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is internal doc. Never publicized

In the final shape of this PR it introduces complexity not only in code, but in usage reasoning / documentation (must explain that font.family will not merge like other properties - is ignored without src).

In fact it breaks a valid use case:

defaultStyle: {font.src: 'fontCollection'}

cell1.font.family: 'family1'
cell2.font.family: 'family2'

IMO we should either treat font as an atomic setting or we embrace fully the current merging approach ( changing the type or not)

@ykw9263 ykw9263 Jun 29, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact it breaks a valid use case:

defaultStyle: {font.src: 'fontCollection'}

cell1.font.family: 'family1'
cell2.font.family: 'family2'

This definitely sounds complex to handle inline. May actually need a dedicated utility function to merge fonts properly without making a mess

/**
 * Merge font definitions. Front takes precedence
 * @param {Font[]} fonts 
 * @returns {Font} merged font
 */
export function mergeFonts(...fonts) {
  const mergedFont = {};
  for (const font of fonts) {
    if (!mergedFont.size) {
      mergedFont.size = font?.size;
    }
    // ignore font src / family once src is found
    if (!mergedFont.src){
      // take the first defined family
      if (!mergedFont.family){
        mergedFont.family = font?.family;
      }
      if (font?.src) {
        mergedFont.src = font.src;
      }
    }
    // return when font src & size are both defined
    if (mergedFont.src && mergedFont.size){
      return mergedFont;
    }
  }
  return mergedFont;
}

// const mergedFont = fontMerge(cell.font, rowStyle.font, colStyle.font)

edited for cleaner branches

@ykw9263 ykw9263 Jun 29, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should either treat font as an atomic setting or we embrace fully the current merging approach ( changing the type or not)

Agree that the font merging behaviour should be defined especially for partial options. If we choose the full merging approach while accounting the case in #1744 (comment), the cascading logic would need to work like

  1. The most specific src & size will apply
  2. if specificity of family >= specificity of src, family will also apply. Otherwise family is undefined.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW seems that defaultStyle.font is never merged into the final font in the original implementation.

const font = deepMerge({}, colStyle.font, rowStyle.font, cell.font);

Is this intentional?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thats because the defaults get populated during the normalisation stuff

@blikblum

Copy link
Copy Markdown
Member

There is a minor changelog conflict

Comment thread lib/table/normalize.js
Comment on lines +97 to +101
...((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,
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks the logic of merging

If you have a font: {size: 16} in row will be ignored with this last change

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now i see the size. Forgive me

Comment thread lib/table/style.js
Comment on lines +126 to +132
// extract fonts
const { font: defaultFont, ...restDefaultStyle } = defaultColStyle || {};
const { font: font, ...restStyle } = colStyle || {};
const mergedFont = {
...defaultFont,
...font,
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the changes ignores font: {size}

This is a rabbit hole

@hollandjake how about we keep with the following font cell | row | col def:

font: string |buffer
fontSize: number

We kill most of the complexity and AFAIK still pretty flexible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Table falsely propagate font families when providing fonts in column/row style PDFKit crashes when providing fonts as Buffer in table

3 participants