pdf-lite 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,15 @@
1
1
  import type { PdfDefaultAppearance } from '../fields/pdf-default-appearance.js';
2
2
  import { PdfFont } from '../../fonts/pdf-font.js';
3
+ /**
4
+ * Resource names of font variants to use when rendering styled markdown segments.
5
+ * When a variant is provided, the appearance stream switches fonts instead of
6
+ * simulating bold/italic via stroke width or matrix shear.
7
+ */
8
+ export interface FontVariantNames {
9
+ bold?: string;
10
+ italic?: string;
11
+ boldItalic?: string;
12
+ }
3
13
  /**
4
14
  * Lightweight builder for PDF content streams.
5
15
  * Chains PDF operators via a fluent API and emits the final stream with build().
@@ -9,9 +19,11 @@ export declare class PdfGraphics {
9
19
  private lines;
10
20
  private resolvedFonts?;
11
21
  private defaultAppearance?;
22
+ private fontVariantNames?;
12
23
  constructor(options?: {
13
24
  resolvedFonts?: Map<string, PdfFont>;
14
25
  defaultAppearance?: PdfDefaultAppearance;
26
+ fontVariantNames?: FontVariantNames;
15
27
  });
16
28
  save(): this;
17
29
  restore(): this;
@@ -33,6 +45,40 @@ export declare class PdfGraphics {
33
45
  fill(): this;
34
46
  stroke(): this;
35
47
  closePath(): this;
48
+ static readonly ITALIC_SHEAR = 0.267;
49
+ static readonly BOLD_STROKE_RATIO = 0.04;
50
+ /**
51
+ * Re-attributes styled segments to wrapped lines by tracking character
52
+ * positions in the flat plain-text. One whitespace character is consumed
53
+ * at each line boundary (space from word-wrap or newline from paragraph).
54
+ */
55
+ private static splitSegmentsToLines;
56
+ /**
57
+ * Returns the resource name of the best-matching font variant for the given
58
+ * bold/italic flags, or undefined if no variant fonts are configured.
59
+ */
60
+ private resolveVariantFontName;
61
+ /**
62
+ * Measures text width using a specific font from resolvedFonts by resource
63
+ * name, falling back to measureTextWidth (regular font) if not found.
64
+ */
65
+ measureTextWidthWithFont(text: string, fontName: string | undefined, fontSize: number): number;
66
+ /**
67
+ * Emits styled text segments into the current BT…ET block.
68
+ * When font variants are configured, switches fonts mid-stream for true
69
+ * bold/italic rendering. Falls back to stroke simulation / Tm shear when
70
+ * no variant fonts are provided (backward compatible).
71
+ * Each segment is positioned with an absolute Tm so no prior Td is needed.
72
+ */
73
+ private showSegments;
74
+ /**
75
+ * Parses a markdown string and renders the styled segments into the current
76
+ * BT…ET block. Pass `multiline` to wrap across multiple lines.
77
+ */
78
+ showMarkdown(markdown: string, isUnicode: boolean, reverseEncodingMap: Map<string, number> | undefined, x: number, y: number, fontSize: number, multiline?: {
79
+ availableWidth: number;
80
+ lineHeight: number;
81
+ }): this;
36
82
  build(): string;
37
83
  private get currentFont();
38
84
  /**
@@ -41,6 +87,8 @@ export declare class PdfGraphics {
41
87
  measureTextWidth(text: string, fontSize?: number): number;
42
88
  /**
43
89
  * Wrap text to fit within the specified width, breaking at word boundaries.
90
+ * When a bold font variant is configured, uses its metrics conservatively
91
+ * to prevent bold glyphs from overflowing field bounds.
44
92
  */
45
93
  wrapTextToLines(text: string, maxWidth: number, fontSize?: number): string[];
46
94
  /**
@@ -1,5 +1,6 @@
1
1
  import { encodePdfText } from '../../utils/encodePdfText.js';
2
2
  import { PdfFont } from '../../fonts/pdf-font.js';
3
+ import { parseMarkdownSegments, } from '../../utils/parse-markdown-segments.js';
3
4
  /**
4
5
  * Lightweight builder for PDF content streams.
5
6
  * Chains PDF operators via a fluent API and emits the final stream with build().
@@ -9,9 +10,11 @@ export class PdfGraphics {
9
10
  lines = [];
10
11
  resolvedFonts;
11
12
  defaultAppearance;
13
+ fontVariantNames;
12
14
  constructor(options) {
13
15
  this.resolvedFonts = options?.resolvedFonts;
14
16
  this.defaultAppearance = options?.defaultAppearance;
17
+ this.fontVariantNames = options?.fontVariantNames;
15
18
  }
16
19
  save() {
17
20
  this.lines.push('q');
@@ -94,6 +97,160 @@ export class PdfGraphics {
94
97
  this.lines.push('h');
95
98
  return this;
96
99
  }
100
+ // Italic shear: ≈ tan(15°), applied as the c component of the Tm matrix.
101
+ static ITALIC_SHEAR = 0.267;
102
+ // Bold stroke width as a fraction of font size (0.04 × 12pt = 0.48pt stroke).
103
+ static BOLD_STROKE_RATIO = 0.04;
104
+ /**
105
+ * Re-attributes styled segments to wrapped lines by tracking character
106
+ * positions in the flat plain-text. One whitespace character is consumed
107
+ * at each line boundary (space from word-wrap or newline from paragraph).
108
+ */
109
+ static splitSegmentsToLines(segments, lines) {
110
+ const chars = [];
111
+ for (const seg of segments) {
112
+ for (const char of seg.text) {
113
+ chars.push({ char, bold: seg.bold, italic: seg.italic });
114
+ }
115
+ }
116
+ const result = [];
117
+ let pos = 0;
118
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
119
+ if (lineIdx > 0 && pos < chars.length) {
120
+ const c = chars[pos].char;
121
+ if (c === ' ' || c === '\n' || c === '\r')
122
+ pos++;
123
+ }
124
+ const lineSegs = [];
125
+ let curText = '';
126
+ let curBold = false;
127
+ let curItalic = false;
128
+ const lineLen = lines[lineIdx].replace(/\r/g, '').length;
129
+ for (let j = 0; j < lineLen && pos < chars.length; j++, pos++) {
130
+ const { char, bold, italic } = chars[pos];
131
+ if (curText === '') {
132
+ curText = char;
133
+ curBold = bold;
134
+ curItalic = italic;
135
+ }
136
+ else if (bold !== curBold || italic !== curItalic) {
137
+ lineSegs.push({
138
+ text: curText,
139
+ bold: curBold,
140
+ italic: curItalic,
141
+ });
142
+ curText = char;
143
+ curBold = bold;
144
+ curItalic = italic;
145
+ }
146
+ else {
147
+ curText += char;
148
+ }
149
+ }
150
+ if (curText)
151
+ lineSegs.push({
152
+ text: curText,
153
+ bold: curBold,
154
+ italic: curItalic,
155
+ });
156
+ result.push(lineSegs);
157
+ }
158
+ return result;
159
+ }
160
+ /**
161
+ * Returns the resource name of the best-matching font variant for the given
162
+ * bold/italic flags, or undefined if no variant fonts are configured.
163
+ */
164
+ resolveVariantFontName(bold, italic) {
165
+ if (bold && italic && this.fontVariantNames?.boldItalic)
166
+ return this.fontVariantNames.boldItalic;
167
+ if (bold && this.fontVariantNames?.bold)
168
+ return this.fontVariantNames.bold;
169
+ if (italic && this.fontVariantNames?.italic)
170
+ return this.fontVariantNames.italic;
171
+ return undefined;
172
+ }
173
+ /**
174
+ * Measures text width using a specific font from resolvedFonts by resource
175
+ * name, falling back to measureTextWidth (regular font) if not found.
176
+ */
177
+ measureTextWidthWithFont(text, fontName, fontSize) {
178
+ if (!fontName)
179
+ return this.measureTextWidth(text, fontSize);
180
+ const font = this.resolvedFonts?.get(fontName);
181
+ if (!font)
182
+ return this.measureTextWidth(text, fontSize);
183
+ let width = 0;
184
+ for (const char of text) {
185
+ const w = font.getCharacterWidth(char.charCodeAt(0), fontSize);
186
+ width += w !== null ? w : fontSize * 0.6;
187
+ }
188
+ return width;
189
+ }
190
+ /**
191
+ * Emits styled text segments into the current BT…ET block.
192
+ * When font variants are configured, switches fonts mid-stream for true
193
+ * bold/italic rendering. Falls back to stroke simulation / Tm shear when
194
+ * no variant fonts are provided (backward compatible).
195
+ * Each segment is positioned with an absolute Tm so no prior Td is needed.
196
+ */
197
+ showSegments(lineSegs, isUnicode, reverseEncodingMap, startX, startY, fontSize) {
198
+ const regularFontName = this.defaultAppearance.fontName;
199
+ let x = startX;
200
+ for (const seg of lineSegs) {
201
+ const variantName = this.resolveVariantFontName(seg.bold, seg.italic);
202
+ if (variantName) {
203
+ // True font variant — switch font, no simulation needed
204
+ const variantFont = this.resolvedFonts?.get(variantName);
205
+ const segIsUnicode = variantFont?.isUnicode ?? isUnicode;
206
+ const segEncMap = variantFont?.reverseEncodingMap ?? reverseEncodingMap;
207
+ this.raw(`1 0 0 1 ${x.toFixed(3)} ${startY.toFixed(3)} Tm`);
208
+ this.raw(`/${variantName} ${fontSize} Tf`);
209
+ this.raw(`0 Tr`);
210
+ this.showText(seg.text, segIsUnicode, segEncMap);
211
+ x += this.measureTextWidthWithFont(seg.text, variantName, fontSize);
212
+ }
213
+ else {
214
+ // Fallback simulation (no variant font provided)
215
+ // Always restore regular font in case a variant was active
216
+ this.raw(`/${regularFontName} ${fontSize} Tf`);
217
+ const shear = seg.italic ? PdfGraphics.ITALIC_SHEAR : 0;
218
+ this.raw(`1 0 ${shear} 1 ${x.toFixed(3)} ${startY.toFixed(3)} Tm`);
219
+ if (seg.bold) {
220
+ const sw = (fontSize * PdfGraphics.BOLD_STROKE_RATIO).toFixed(3);
221
+ this.raw(`${sw} w 2 Tr`);
222
+ }
223
+ else {
224
+ this.raw(`0 Tr`);
225
+ }
226
+ this.showText(seg.text, isUnicode, reverseEncodingMap);
227
+ x += this.measureTextWidth(seg.text, fontSize);
228
+ }
229
+ }
230
+ // Restore regular font and fill-only rendering mode
231
+ this.raw(`/${regularFontName} ${fontSize} Tf`);
232
+ this.raw(`0 Tr`);
233
+ return this;
234
+ }
235
+ /**
236
+ * Parses a markdown string and renders the styled segments into the current
237
+ * BT…ET block. Pass `multiline` to wrap across multiple lines.
238
+ */
239
+ showMarkdown(markdown, isUnicode, reverseEncodingMap, x, y, fontSize, multiline) {
240
+ const segments = parseMarkdownSegments(markdown);
241
+ if (multiline) {
242
+ const plainText = segments.map((s) => s.text).join('');
243
+ const lines = this.wrapTextToLines(plainText, multiline.availableWidth);
244
+ const styledLines = PdfGraphics.splitSegmentsToLines(segments, lines);
245
+ for (let i = 0; i < styledLines.length; i++) {
246
+ this.showSegments(styledLines[i], isUnicode, reverseEncodingMap, x, y - i * multiline.lineHeight, fontSize);
247
+ }
248
+ }
249
+ else {
250
+ this.showSegments(segments, isUnicode, reverseEncodingMap, x, y, fontSize);
251
+ }
252
+ return this;
253
+ }
97
254
  build() {
98
255
  return this.lines.join('\n') + '\n';
99
256
  }
@@ -142,16 +299,23 @@ export class PdfGraphics {
142
299
  }
143
300
  /**
144
301
  * Wrap text to fit within the specified width, breaking at word boundaries.
302
+ * When a bold font variant is configured, uses its metrics conservatively
303
+ * to prevent bold glyphs from overflowing field bounds.
145
304
  */
146
305
  wrapTextToLines(text, maxWidth, fontSize) {
147
306
  if (!this.currentFont) {
148
307
  throw new Error('No font set - call setDefaultAppearance() first');
149
308
  }
309
+ const boldFontName = this.fontVariantNames?.bold;
310
+ const size = fontSize ?? this.currentFont.size;
311
+ const measure = (t) => boldFontName
312
+ ? this.measureTextWidthWithFont(t, boldFontName, size)
313
+ : this.measureTextWidth(t, fontSize);
150
314
  // Handle explicit line breaks first
151
315
  const paragraphs = text.split('\n');
152
316
  const wrappedLines = [];
153
317
  for (const paragraph of paragraphs) {
154
- if (this.measureTextWidth(paragraph, fontSize) <= maxWidth) {
318
+ if (measure(paragraph) <= maxWidth) {
155
319
  wrappedLines.push(paragraph);
156
320
  continue;
157
321
  }
@@ -160,7 +324,7 @@ export class PdfGraphics {
160
324
  let currentLine = '';
161
325
  for (const word of words) {
162
326
  const testLine = currentLine ? `${currentLine} ${word}` : word;
163
- if (this.measureTextWidth(testLine, fontSize) <= maxWidth) {
327
+ if (measure(testLine) <= maxWidth) {
164
328
  currentLine = testLine;
165
329
  }
166
330
  else {
@@ -189,6 +353,7 @@ export class PdfGraphics {
189
353
  if (!this.currentFont) {
190
354
  throw new Error('No font set - call setDefaultAppearance() first');
191
355
  }
356
+ const boldFontName = this.fontVariantNames?.bold;
192
357
  const startSize = this.currentFont.size;
193
358
  const minSize = 0.5;
194
359
  const fits = (size) => {
@@ -199,7 +364,10 @@ export class PdfGraphics {
199
364
  return lines.length * size * lineHeight <= maxHeight;
200
365
  }
201
366
  // Single-line mode: text must fit within maxWidth.
202
- return this.measureTextWidth(text, size) <= maxWidth;
367
+ return boldFontName
368
+ ? this.measureTextWidthWithFont(text, boldFontName, size) <=
369
+ maxWidth
370
+ : this.measureTextWidth(text, size) <= maxWidth;
203
371
  };
204
372
  if (fits(startSize))
205
373
  return startSize;
@@ -218,11 +386,16 @@ export class PdfGraphics {
218
386
  return lo;
219
387
  }
220
388
  breakLongWord(word, maxWidth, fontSize) {
389
+ const boldFontName = this.fontVariantNames?.bold;
390
+ const size = fontSize ?? this.currentFont?.size ?? 12;
391
+ const measure = (t) => boldFontName
392
+ ? this.measureTextWidthWithFont(t, boldFontName, size)
393
+ : this.measureTextWidth(t, fontSize);
221
394
  const lines = [];
222
395
  let currentLine = '';
223
396
  for (const char of word) {
224
397
  const testLine = currentLine + char;
225
- if (this.measureTextWidth(testLine, fontSize) <= maxWidth) {
398
+ if (measure(testLine) <= maxWidth) {
226
399
  currentLine = testLine;
227
400
  }
228
401
  else {
@@ -2,6 +2,7 @@ import { PdfDefaultAppearance } from '../fields/pdf-default-appearance.js';
2
2
  import { PdfAppearanceStream } from './pdf-appearance-stream.js';
3
3
  import type { PdfDictionary } from '../../core/objects/pdf-dictionary.js';
4
4
  import type { PdfFont } from '../../fonts/pdf-font.js';
5
+ import { type FontVariantNames } from './pdf-graphics.js';
5
6
  /**
6
7
  * Appearance stream for text fields (single-line, multiline, comb).
7
8
  * Enhanced with word wrapping and automatic font scaling.
@@ -18,5 +19,7 @@ export declare class PdfTextAppearanceStream extends PdfAppearanceStream {
18
19
  resolvedFonts?: Map<string, PdfFont>;
19
20
  isUnicode?: boolean;
20
21
  reverseEncodingMap?: Map<string, number>;
22
+ markdown?: string;
23
+ fontVariantNames?: FontVariantNames;
21
24
  });
22
25
  }
@@ -21,6 +21,7 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
21
21
  // Create graphics with font context for text measurement
22
22
  const g = new PdfGraphics({
23
23
  resolvedFonts: ctx.resolvedFonts,
24
+ fontVariantNames: ctx.fontVariantNames,
24
25
  });
25
26
  g.beginMarkedContent();
26
27
  g.save();
@@ -43,8 +44,7 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
43
44
  // shrink further if text overflows the width.
44
45
  finalFontSize = Math.min(DEFAULT_FONT_SIZE, availableHeight);
45
46
  g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
46
- const textWidth = g.measureTextWidth(value);
47
- if (textWidth > availableWidth) {
47
+ if (g.measureTextWidth(value) > availableWidth) {
48
48
  finalFontSize = g.calculateFittingFontSize(value, availableWidth);
49
49
  }
50
50
  finalFontSize = Math.max(finalFontSize, 0.5);
@@ -101,27 +101,39 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
101
101
  const renderLineHeight = finalFontSize * 1.2;
102
102
  const startY = height - padding - finalFontSize;
103
103
  g.beginText();
104
- g.moveTo(padding, startY);
105
- for (let i = 0; i < lines.length; i++) {
106
- if (i > 0)
107
- g.moveTo(0, -renderLineHeight);
108
- g.showText(lines[i].replace(/\r/g, ''), isUnicode, reverseEncodingMap);
104
+ if (ctx.markdown) {
105
+ g.showMarkdown(ctx.markdown, isUnicode, reverseEncodingMap, padding, startY, finalFontSize, {
106
+ availableWidth,
107
+ lineHeight: renderLineHeight,
108
+ });
109
+ }
110
+ else {
111
+ g.moveTo(padding, startY);
112
+ for (let i = 0; i < lines.length; i++) {
113
+ if (i > 0)
114
+ g.moveTo(0, -renderLineHeight);
115
+ g.showText(lines[i].replace(/\r/g, ''), isUnicode, reverseEncodingMap);
116
+ }
109
117
  }
110
118
  g.endText();
111
119
  }
112
120
  else {
113
121
  // Single line — for non-auto-size, shrink if text overflows
114
122
  if (!autoSize) {
115
- const textWidth = g.measureTextWidth(value);
116
- if (textWidth > availableWidth) {
123
+ if (g.measureTextWidth(value) > availableWidth) {
117
124
  finalFontSize = g.calculateFittingFontSize(value, availableWidth);
118
125
  g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
119
126
  }
120
127
  }
121
128
  const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
122
129
  g.beginText();
123
- g.moveTo(padding, textY);
124
- g.showText(value, isUnicode, reverseEncodingMap);
130
+ if (ctx.markdown) {
131
+ g.showMarkdown(ctx.markdown, isUnicode, reverseEncodingMap, padding, textY, finalFontSize);
132
+ }
133
+ else {
134
+ g.moveTo(padding, textY);
135
+ g.showText(value, isUnicode, reverseEncodingMap);
136
+ }
125
137
  g.endText();
126
138
  }
127
139
  g.restore();
@@ -4,6 +4,7 @@ import { PdfString } from '../../core/objects/pdf-string.js';
4
4
  import { PdfObjectReference } from '../../core/objects/pdf-object-reference.js';
5
5
  import { PdfIndirectObject } from '../../core/objects/pdf-indirect-object.js';
6
6
  import { PdfFont } from '../../fonts/pdf-font.js';
7
+ import type { FontFamily } from '../../fonts/font-family.js';
7
8
  import { PdfStream } from '../../core/objects/pdf-stream.js';
8
9
  import { PdfWidgetAnnotation } from '../../annotations/pdf-widget-annotation.js';
9
10
  import type { PdfFieldType } from './types.js';
@@ -19,6 +20,9 @@ import { PdfJavaScriptAction } from '../js/pdf-javascript-action.js';
19
20
  */
20
21
  export declare abstract class PdfFormField extends PdfWidgetAnnotation {
21
22
  defaultGenerateAppearance: boolean;
23
+ /** Raw markdown string set by markdownValue; cleared by setRawValue. */
24
+ protected _markdownValue?: string;
25
+ private _fontFamily?;
22
26
  /** @internal */
23
27
  _form?: PdfAcroForm;
24
28
  constructor(other?: PdfIndirectObject | {
@@ -41,6 +45,12 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
41
45
  * Returns undefined if neither source provides the font.
42
46
  */
43
47
  buildFontResources(fontName: string): PdfDictionary | undefined;
48
+ /**
49
+ * Like buildFontResources but includes multiple font names (regular +
50
+ * variant fonts) in a single Resources/Font dictionary. Falls through to
51
+ * buildFontResources when only one name is provided.
52
+ */
53
+ buildAllFontResources(fontNames: string[]): PdfDictionary | undefined;
44
54
  get fieldType(): PdfFieldType | null;
45
55
  set fieldType(type: PdfFieldType | null);
46
56
  get name(): string;
@@ -53,11 +63,25 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
53
63
  get value(): string;
54
64
  protected setRawValue(val: string | PdfString): void;
55
65
  set value(val: string | PdfString);
66
+ set markdownValue(val: string);
56
67
  get fontSize(): number | null;
57
68
  set fontSize(size: number);
58
69
  get fontName(): string | null;
59
70
  set fontName(fontName: string);
60
71
  set font(font: PdfFont | null);
72
+ private _embedFontInDR;
73
+ get fontFamily(): FontFamily | null;
74
+ set fontFamily(family: FontFamily | null);
75
+ get fontVariantNames(): {
76
+ bold?: string;
77
+ italic?: string;
78
+ boldItalic?: string;
79
+ };
80
+ protected get resolvedVariantFonts(): {
81
+ bold?: PdfFont;
82
+ italic?: PdfFont;
83
+ boldItalic?: PdfFont;
84
+ };
61
85
  get flags(): PdfFormFieldFlags;
62
86
  set flags(v: PdfFormFieldFlags);
63
87
  get readOnly(): boolean;
@@ -14,6 +14,7 @@ import { PdfFieldType as PdfFieldTypeConst } from './types.js';
14
14
  import { PdfFormFieldFlags } from './pdf-form-field-flags.js';
15
15
  import { PdfFieldActions } from '../js/pdf-field-actions.js';
16
16
  import { PdfJavaScriptAction } from '../js/pdf-javascript-action.js';
17
+ import { parseMarkdownSegments } from '../../utils/parse-markdown-segments.js';
17
18
  /**
18
19
  * Abstract base form field class. Extends PdfWidgetAnnotation with form-specific properties:
19
20
  * FT, V, DA, Ff, T (name), field hierarchy (parent/children/siblings).
@@ -21,6 +22,9 @@ import { PdfJavaScriptAction } from '../js/pdf-javascript-action.js';
21
22
  */
22
23
  export class PdfFormField extends PdfWidgetAnnotation {
23
24
  defaultGenerateAppearance = true;
25
+ /** Raw markdown string set by markdownValue; cleared by setRawValue. */
26
+ _markdownValue;
27
+ _fontFamily;
24
28
  /** @internal */
25
29
  _form;
26
30
  constructor(other) {
@@ -180,6 +184,39 @@ export class PdfFormField extends PdfWidgetAnnotation {
180
184
  }
181
185
  return undefined;
182
186
  }
187
+ /**
188
+ * Like buildFontResources but includes multiple font names (regular +
189
+ * variant fonts) in a single Resources/Font dictionary. Falls through to
190
+ * buildFontResources when only one name is provided.
191
+ */
192
+ buildAllFontResources(fontNames) {
193
+ if (fontNames.length <= 1)
194
+ return this.buildFontResources(fontNames[0]);
195
+ const dr = this.defaultResources;
196
+ const fontRaw = dr?.get('Font');
197
+ let drFontDict;
198
+ if (fontRaw instanceof PdfObjectReference) {
199
+ const resolved = fontRaw.resolve()?.content;
200
+ if (resolved instanceof PdfDictionary)
201
+ drFontDict = resolved;
202
+ }
203
+ else if (fontRaw instanceof PdfDictionary) {
204
+ drFontDict = fontRaw;
205
+ }
206
+ const resFontDict = new PdfDictionary();
207
+ for (const name of fontNames) {
208
+ if (drFontDict?.get(name)) {
209
+ resFontDict.set(name, drFontDict.get(name));
210
+ }
211
+ }
212
+ if (resFontDict.keys().length > 0) {
213
+ const resources = new PdfDictionary();
214
+ resources.set('Font', resFontDict);
215
+ return resources;
216
+ }
217
+ // Fallback to single-font path for the primary font name
218
+ return this.buildFontResources(fontNames[0]);
219
+ }
183
220
  get fieldType() {
184
221
  const ft = PdfFormField.getFieldType(this);
185
222
  switch (ft) {
@@ -266,6 +303,7 @@ export class PdfFormField extends PdfWidgetAnnotation {
266
303
  return '';
267
304
  }
268
305
  setRawValue(val) {
306
+ this._markdownValue = undefined;
269
307
  const targets = [this];
270
308
  const parent = this.parent;
271
309
  if (parent?.fieldType) {
@@ -311,6 +349,21 @@ export class PdfFormField extends PdfWidgetAnnotation {
311
349
  set value(val) {
312
350
  this.setRawValue(val);
313
351
  }
352
+ set markdownValue(val) {
353
+ const plainText = parseMarkdownSegments(val)
354
+ .map((s) => s.text)
355
+ .join('');
356
+ // Store plain text as V without triggering appearance generation yet
357
+ const saved = this.defaultGenerateAppearance;
358
+ this.defaultGenerateAppearance = false;
359
+ this.setRawValue(plainText);
360
+ this.defaultGenerateAppearance = saved;
361
+ // setRawValue cleared _markdownValue; store the markdown string now
362
+ this._markdownValue = val;
363
+ if (this.defaultGenerateAppearance) {
364
+ this.generateAppearance();
365
+ }
366
+ }
314
367
  get fontSize() {
315
368
  const da = this.defaultAppearance || '';
316
369
  const parsed = PdfDefaultAppearance.parse(da);
@@ -352,6 +405,8 @@ export class PdfFormField extends PdfWidgetAnnotation {
352
405
  }
353
406
  const resourceName = font.resourceName;
354
407
  const currentSize = this.fontSize ?? 12;
408
+ this._embedFontInDR(font);
409
+ // Update the DA string to use the font
355
410
  const da = this.defaultAppearance || '';
356
411
  if (!da) {
357
412
  this.content.set('DA', new PdfDefaultAppearance(resourceName, currentSize, '0 g'));
@@ -363,6 +418,62 @@ export class PdfFormField extends PdfWidgetAnnotation {
363
418
  this.content.set('DA', parsed);
364
419
  }
365
420
  }
421
+ _embedFontInDR(font) {
422
+ const resourceName = font.resourceName;
423
+ // Add font to field's default resources
424
+ const dr = this.content.get('DR') || new PdfDictionary();
425
+ let fontDict = dr.get('Font');
426
+ if (!fontDict) {
427
+ fontDict = new PdfDictionary();
428
+ dr.set('Font', fontDict);
429
+ }
430
+ fontDict.set(resourceName, font.reference);
431
+ this.content.set('DR', dr);
432
+ // Also add to form's default resources if available
433
+ if (this._form) {
434
+ const formDr = this._form.defaultResources ||
435
+ new PdfDictionary();
436
+ let formFontDict = formDr.get('Font');
437
+ if (!formFontDict) {
438
+ formFontDict = new PdfDictionary();
439
+ formDr.set('Font', formFontDict);
440
+ }
441
+ formFontDict.set(resourceName, font.reference);
442
+ this._form.defaultResources = formDr;
443
+ }
444
+ }
445
+ get fontFamily() {
446
+ return this._fontFamily ?? null;
447
+ }
448
+ set fontFamily(family) {
449
+ if (family === null) {
450
+ this._fontFamily = undefined;
451
+ return;
452
+ }
453
+ this._fontFamily = family;
454
+ // Setting regular via the existing setter keeps DA string in sync
455
+ this.font = family.regular;
456
+ if (family.bold)
457
+ this._embedFontInDR(family.bold);
458
+ if (family.italic)
459
+ this._embedFontInDR(family.italic);
460
+ if (family.boldItalic)
461
+ this._embedFontInDR(family.boldItalic);
462
+ }
463
+ get fontVariantNames() {
464
+ return {
465
+ bold: this._fontFamily?.bold?.resourceName,
466
+ italic: this._fontFamily?.italic?.resourceName,
467
+ boldItalic: this._fontFamily?.boldItalic?.resourceName,
468
+ };
469
+ }
470
+ get resolvedVariantFonts() {
471
+ return {
472
+ bold: this._fontFamily?.bold,
473
+ italic: this._fontFamily?.italic,
474
+ boldItalic: this._fontFamily?.boldItalic,
475
+ };
476
+ }
366
477
  get flags() {
367
478
  const flags = new PdfFormFieldFlags(this.content.get('Ff') ?? this.parent?.content.get('Ff') ?? 0);
368
479
  flags.onChange(() => {
@@ -21,7 +21,24 @@ export class PdfTextFormField extends PdfFormField {
21
21
  if (!parsed)
22
22
  return false;
23
23
  const font = this.font;
24
- const fontResources = this.buildFontResources(parsed.fontName);
24
+ const variantNames = this.fontVariantNames;
25
+ const variantFonts = this.resolvedVariantFonts;
26
+ const allFontNames = [
27
+ parsed.fontName,
28
+ variantNames.bold,
29
+ variantNames.italic,
30
+ variantNames.boldItalic,
31
+ ].filter((n) => !!n);
32
+ const fontResources = this.buildAllFontResources(allFontNames);
33
+ const resolvedFonts = new Map();
34
+ if (font)
35
+ resolvedFonts.set(parsed.fontName, font);
36
+ if (variantFonts.bold)
37
+ resolvedFonts.set(variantNames.bold, variantFonts.bold);
38
+ if (variantFonts.italic)
39
+ resolvedFonts.set(variantNames.italic, variantFonts.italic);
40
+ if (variantFonts.boldItalic)
41
+ resolvedFonts.set(variantNames.boldItalic, variantFonts.boldItalic);
25
42
  const isUnicode = font?.isUnicode ?? false;
26
43
  const reverseEncodingMap = font?.reverseEncodingMap;
27
44
  this.appearanceStream = new PdfTextAppearanceStream({
@@ -32,8 +49,11 @@ export class PdfTextFormField extends PdfFormField {
32
49
  comb: this.comb,
33
50
  maxLen: this.maxLen,
34
51
  fontResources,
52
+ resolvedFonts,
35
53
  isUnicode,
36
54
  reverseEncodingMap,
55
+ markdown: this._markdownValue,
56
+ fontVariantNames: variantNames,
37
57
  });
38
58
  if (options?.makeReadOnly) {
39
59
  this.readOnly = true;
@@ -0,0 +1,12 @@
1
+ import type { PdfFont } from './pdf-font.js';
2
+ /**
3
+ * A group of font variants for a single typeface family.
4
+ * Set on a form field via `field.fontFamily` to enable true bold/italic
5
+ * rendering in markdown appearance streams rather than stroke simulation.
6
+ */
7
+ export interface FontFamily {
8
+ regular: PdfFont;
9
+ bold?: PdfFont;
10
+ italic?: PdfFont;
11
+ boldItalic?: PdfFont;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
1
  export * from './types.js';
2
2
  export * from './pdf-font.js';
3
+ export * from './font-family.js';
3
4
  export * from './parsers/index.js';
@@ -1,3 +1,4 @@
1
1
  export * from './types.js';
2
2
  export * from './pdf-font.js';
3
+ export * from './font-family.js';
3
4
  export * from './parsers/index.js';
@@ -41,10 +41,10 @@ export declare class OtfParser implements FontParser {
41
41
  private parseOS2;
42
42
  private parsePost;
43
43
  private parseName;
44
- private parseCmap;
44
+ parseCmap(): Map<number, number>;
45
45
  private parseCmapFormat4;
46
46
  private parseCmapFormat12;
47
- private parseHmtx;
47
+ parseHmtx(): Map<number, number>;
48
48
  private parseMaxp;
49
49
  private estimateStemV;
50
50
  }
@@ -33,10 +33,10 @@ export declare class TtfParser implements FontParser {
33
33
  private parseOS2;
34
34
  private parsePost;
35
35
  private parseName;
36
- private parseCmap;
36
+ parseCmap(): Map<number, number>;
37
37
  private parseCmapFormat4;
38
38
  private parseCmapFormat12;
39
- private parseHmtx;
39
+ parseHmtx(): Map<number, number>;
40
40
  private parseMaxp;
41
41
  private estimateStemV;
42
42
  }
@@ -24,6 +24,8 @@ export declare class WoffParser implements FontParser {
24
24
  * Gets character widths for a range of characters.
25
25
  */
26
26
  getCharWidths(firstChar: number, lastChar: number): number[];
27
+ parseCmap(): Map<number, number>;
28
+ parseHmtx(): Map<number, number>;
27
29
  private decompressWoff;
28
30
  }
29
31
  /**
@@ -35,6 +35,12 @@ export class WoffParser {
35
35
  getCharWidths(firstChar, lastChar) {
36
36
  return this.ttfParser.getCharWidths(firstChar, lastChar);
37
37
  }
38
+ parseCmap() {
39
+ return this.ttfParser.parseCmap();
40
+ }
41
+ parseHmtx() {
42
+ return this.ttfParser.parseHmtx();
43
+ }
38
44
  decompressWoff(woffData) {
39
45
  const data = new DataView(woffData.buffer, woffData.byteOffset, woffData.byteLength);
40
46
  // Verify WOFF signature
@@ -213,6 +213,27 @@ export class PdfFont extends PdfIndirectObject {
213
213
  * @returns The raw character width or null if not found
214
214
  */
215
215
  getRawCharacterWidth(charCode) {
216
+ // For Type0 fonts, check CID widths in descriptor
217
+ if (this.isUnicode && this._descriptor) {
218
+ const unicodeDesc = this._descriptor;
219
+ if (unicodeDesc.cidWidths) {
220
+ // Find width for this CID (which equals Unicode code point for Identity-H)
221
+ for (const entry of unicodeDesc.cidWidths) {
222
+ if ('width' in entry && entry.cid === charCode) {
223
+ return entry.width;
224
+ }
225
+ else if ('widths' in entry) {
226
+ const offset = charCode - entry.startCid;
227
+ if (offset >= 0 && offset < entry.widths.length) {
228
+ return entry.widths[offset];
229
+ }
230
+ }
231
+ }
232
+ // Not found, use default width
233
+ return unicodeDesc.defaultWidth ?? 1000;
234
+ }
235
+ }
236
+ // For Type1/TrueType fonts, use simple widths array
216
237
  if (this.widths === undefined || this.firstChar === undefined) {
217
238
  return null;
218
239
  }
@@ -285,8 +306,7 @@ export class PdfFont extends PdfIndirectObject {
285
306
  * @returns A PdfFont instance ready to be written to the PDF
286
307
  */
287
308
  static fromBytes(data) {
288
- const parser = parseFont(data);
289
- return PdfFont.fromParser(parser);
309
+ return PdfFont.fromFile(data);
290
310
  }
291
311
  /**
292
312
  * Creates a PdfFont from a FontParser instance.
@@ -582,8 +602,37 @@ export class PdfFont extends PdfIndirectObject {
582
602
  if (descriptor.cidWidths && descriptor.cidWidths.length > 0) {
583
603
  cidFontDict.set('W', PdfFont.buildCIDWidthArray(descriptor.cidWidths));
584
604
  }
585
- // CIDToGIDMap
586
- cidFontDict.set('CIDToGIDMap', new PdfName(descriptor.cidToGidMap ?? 'Identity'));
605
+ // CIDToGIDMap - Generate binary stream if unicodeMappings provided
606
+ if (unicodeMappings && unicodeMappings.size > 0) {
607
+ // Create a binary CIDToGIDMap stream
608
+ // For Identity-H encoding, CID = Unicode value
609
+ // We need to map each CID to its GID
610
+ const maxCid = Math.max(...Array.from(unicodeMappings.keys()));
611
+ const cidToGidData = new Uint8Array((maxCid + 1) * 2);
612
+ // Initialize all mappings to 0 (missing glyph)
613
+ for (let i = 0; i < cidToGidData.length; i++) {
614
+ cidToGidData[i] = 0;
615
+ }
616
+ // Fill in the mappings
617
+ for (const [unicode, glyphId] of unicodeMappings.entries()) {
618
+ const offset = unicode * 2;
619
+ // Big-endian 16-bit glyph ID
620
+ cidToGidData[offset] = (glyphId >> 8) & 0xff;
621
+ cidToGidData[offset + 1] = glyphId & 0xff;
622
+ }
623
+ const cidToGidStream = new PdfStream({
624
+ header: new PdfDictionary(),
625
+ original: cidToGidData,
626
+ });
627
+ cidToGidStream.addFilter('FlateDecode');
628
+ const cidToGidObject = new PdfIndirectObject({
629
+ content: cidToGidStream,
630
+ });
631
+ cidFontDict.set('CIDToGIDMap', cidToGidObject.reference);
632
+ }
633
+ else {
634
+ cidFontDict.set('CIDToGIDMap', new PdfName(descriptor.cidToGidMap ?? 'Identity'));
635
+ }
587
636
  const cidFontObject = new PdfIndirectObject({
588
637
  content: cidFontDict,
589
638
  });
@@ -611,21 +660,58 @@ export class PdfFont extends PdfIndirectObject {
611
660
  static fromFile(fontData, options) {
612
661
  // Parse the font to extract metadata
613
662
  const parser = parseFont(fontData);
663
+ // Check if CFF-based OTF (not supported)
664
+ if (parser instanceof OtfParser && parser.isCFFBased()) {
665
+ throw new Error('CFF-based OTF fonts are not supported yet');
666
+ }
614
667
  const info = parser.getFontInfo();
615
668
  // Auto-generate font name from metadata if not provided
616
669
  const fontName = options?.fontName ?? info.postScriptName ?? info.fullName;
617
670
  // Get the appropriate descriptor based on unicode option
618
671
  const descriptor = parser.getFontDescriptor(fontName);
672
+ // Auto-detect if Unicode font is needed if not explicitly specified
673
+ let useUnicode = options?.unicode;
674
+ if (useUnicode === undefined) {
675
+ // Check if font has glyphs beyond WinAnsiEncoding range (0-255)
676
+ const cmap = parser.parseCmap();
677
+ useUnicode = Array.from(cmap.keys()).some((unicode) => unicode > 0xff);
678
+ }
619
679
  // Embed using the appropriate method and return PdfFont
620
- if (options?.unicode) {
680
+ if (useUnicode) {
681
+ // Auto-extract cmap if not provided
682
+ const unicodeMappings = options?.unicodeMappings ?? parser.parseCmap();
683
+ // Extract glyph widths for Unicode characters
684
+ const hmtx = parser.parseHmtx();
685
+ const unitsPerEm = info.unitsPerEm;
686
+ const cidWidths = [];
687
+ // Build CID widths array from cmap + hmtx
688
+ for (const [unicode, glyphId] of unicodeMappings.entries()) {
689
+ const glyphWidth = hmtx.get(glyphId) ?? 0;
690
+ // Scale to 1000-unit em square
691
+ const scaledWidth = Math.round((glyphWidth * 1000) / unitsPerEm);
692
+ cidWidths.push({ cid: unicode, width: scaledWidth });
693
+ }
694
+ // Compute average width for default
695
+ const avgWidth = cidWidths.length > 0
696
+ ? Math.round(cidWidths.reduce((sum, entry) => {
697
+ if ('width' in entry) {
698
+ return sum + entry.width;
699
+ }
700
+ else {
701
+ // Average the widths in the range
702
+ const rangeSum = entry.widths.reduce((a, b) => a + b, 0);
703
+ return sum + rangeSum / entry.widths.length;
704
+ }
705
+ }, 0) / cidWidths.length)
706
+ : 1000;
621
707
  // For Unicode fonts, we need a UnicodeFontDescriptor
622
- // Create one by extending the base descriptor
623
708
  const unicodeDescriptor = {
624
709
  ...descriptor,
625
- defaultWidth: 1000,
710
+ defaultWidth: avgWidth,
711
+ cidWidths,
626
712
  cidToGidMap: 'Identity',
627
713
  };
628
- return PdfFont.fromType0Data(fontData, fontName, unicodeDescriptor, options.unicodeMappings);
714
+ return PdfFont.fromType0Data(fontData, fontName, unicodeDescriptor, unicodeMappings);
629
715
  }
630
716
  else {
631
717
  // Use standard TrueType embedding
@@ -8,6 +8,16 @@ export interface FontParser {
8
8
  getFontDescriptor(fontName?: string): FontDescriptor;
9
9
  getCharWidths(firstChar: number, lastChar: number): number[];
10
10
  getFontData(): ByteArray;
11
+ /**
12
+ * Parses the font's cmap table to extract Unicode to glyph ID mappings.
13
+ * @returns A map from Unicode code points to glyph IDs
14
+ */
15
+ parseCmap(): Map<number, number>;
16
+ /**
17
+ * Parses the font's hmtx table to extract glyph advance widths.
18
+ * @returns A map from glyph IDs to advance widths (in font units)
19
+ */
20
+ parseHmtx(): Map<number, number>;
11
21
  }
12
22
  /**
13
23
  * Parsed TrueType font information.
@@ -0,0 +1,12 @@
1
+ export type StyledSegment = {
2
+ text: string;
3
+ bold: boolean;
4
+ italic: boolean;
5
+ };
6
+ /**
7
+ * Parses a markdown string with **bold**, *italic*, and ***bold+italic***
8
+ * syntax into an array of styled text segments.
9
+ *
10
+ * Unrecognized or unmatched asterisks are emitted as plain text.
11
+ */
12
+ export declare function parseMarkdownSegments(text: string): StyledSegment[];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Parses a markdown string with **bold**, *italic*, and ***bold+italic***
3
+ * syntax into an array of styled text segments.
4
+ *
5
+ * Unrecognized or unmatched asterisks are emitted as plain text.
6
+ */
7
+ export function parseMarkdownSegments(text) {
8
+ const segments = [];
9
+ // Match *** before ** before * to handle longest-match first
10
+ const re = /\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*|([^*]+|\*)/gs;
11
+ let match;
12
+ while ((match = re.exec(text)) !== null) {
13
+ if (match[1] !== undefined) {
14
+ segments.push({ text: match[1], bold: true, italic: true });
15
+ }
16
+ else if (match[2] !== undefined) {
17
+ segments.push({ text: match[2], bold: true, italic: false });
18
+ }
19
+ else if (match[3] !== undefined) {
20
+ segments.push({ text: match[3], bold: false, italic: true });
21
+ }
22
+ else if (match[4] !== undefined && match[4].length > 0) {
23
+ segments.push({ text: match[4], bold: false, italic: false });
24
+ }
25
+ }
26
+ return segments;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-lite",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "exports": {