Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Fix annotations placed under `doc.rotate()` marking the wrong area, because `_convertRect` derived each corner's y from the already transformed x and mapped only two of the four corners, so the rectangle a viewer makes interactive did not follow the rotated content. Fixes #1153
- Add `onClick`, `onMouseDown`, `onMouseEnter`, `onMouseExit`, `onFocus` and `onBlur` options to form annotation methods, for the JavaScript a field runs on each of those events. Each accepts a string or a plain function, whose source text is written into the action
- Add an `embedFonts` option to `initForm`, embedding a complete, character-addressable copy of each custom font used in a form field. Viewers that regenerate a field's appearance from its value, such as Adobe Acrobat/Reader, need one to resolve field text; without it they fall back to a substitute font. It adds roughly the size of the font file per font, so it is off by default. Fixes #1096
- Fix standard fonts printing the wrong characters for text with decomposed accents (NFD), such as `u` followed by a combining diaeresis, because the combining mark has no WinAnsi code and its longer hex code shifted every character after it. Text is composed (NFC) before it is encoded and measured. Fixes #1661

### [v0.20.2] - 2026-08-29

Expand Down
8 changes: 6 additions & 2 deletions lib/font/standard.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ class StandardFont extends PDFFont {
}

encode(text) {
// Standard fonts use WinAnsiEncoding, which has precomposed letters like
// "ü" but no combining marks. Compose first, so that "u" followed by
// U+0308 is encoded as "ü" instead of a code the encoding cannot hold.
text = `${text}`.normalize('NFC');
const encoded = this.font.encodeText(text);
const glyphs = this.font.glyphsForString(`${text}`);
const glyphs = this.font.glyphsForString(text);
const advances = this.font.advancesForGlyphs(glyphs);
const positions = [];
for (let i = 0; i < glyphs.length; i++) {
Expand All @@ -49,7 +53,7 @@ class StandardFont extends PDFFont {
}

widthOfString(string, size) {
const glyphs = this.font.glyphsForString(`${string}`);
const glyphs = this.font.glyphsForString(`${string}`.normalize('NFC'));
const advances = this.font.advancesForGlyphs(glyphs);

let width = 0;
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/text.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,42 @@ Q
});
});

describe('text with decomposed characters (NFD)', () => {
const textStream = (text) => {
const doc = new PDFDocument({
info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) },
compress: false,
});
const docData = logData(doc);
doc.text(text, 100, 80);
doc.end();
return docData
.filter((item) => Buffer.isBuffer(item))
.map((item) => item.toString('binary'))
.find((item) => item.includes(' TJ'));
};

test('renders the same as the composed text with a standard font', () => {
const composed = 'Text f\u00fcr \u00c9t\u00e9 na\u00efve';
const decomposed = composed.normalize('NFD');
expect(decomposed).not.toBe(composed);

expect(textStream(decomposed)).toBe(textStream(composed));
// "ü", "É", "é" and "ï" as single WinAnsi bytes (fc, c9, e9, ef)
expect(textStream(decomposed)).toContain(
'[<54> 120 <65> 30 <78742066fc7220c974e9206e61ef76> 25 <65> 0] TJ',
);
});

test('measures the same width as the composed text with a standard font', () => {
// Helvetica "i" is narrower than "\u00ef", unlike most accented letters
const composed = 'na\u00efve';
expect(document.widthOfString(composed.normalize('NFD'))).toBe(
document.widthOfString(composed),
);
});
});

describe('text with structure parent links', () => {
beforeEach(() => {
document = new PDFDocument({
Expand Down
Loading