pdf-lite 1.6.4 → 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.
- package/dist/acroform/appearance/pdf-button-appearance-stream.js +1 -1
- package/dist/acroform/appearance/pdf-graphics.d.ts +48 -0
- package/dist/acroform/appearance/pdf-graphics.js +177 -4
- package/dist/acroform/appearance/pdf-text-appearance-stream.d.ts +3 -0
- package/dist/acroform/appearance/pdf-text-appearance-stream.js +58 -29
- package/dist/acroform/fields/pdf-button-form-field.d.ts +11 -2
- package/dist/acroform/fields/pdf-button-form-field.js +76 -37
- package/dist/acroform/fields/pdf-choice-form-field.js +2 -2
- package/dist/acroform/fields/pdf-form-field.d.ts +39 -9
- package/dist/acroform/fields/pdf-form-field.js +234 -46
- package/dist/acroform/fields/pdf-text-form-field.js +23 -3
- package/dist/acroform/pdf-acro-form.d.ts +1 -0
- package/dist/acroform/pdf-acro-form.js +15 -4
- package/dist/acroform/xfa/pdf-xfa-data.d.ts +2 -1
- package/dist/acroform/xfa/pdf-xfa-data.js +36 -28
- package/dist/annotations/pdf-annotation.d.ts +1 -1
- package/dist/annotations/pdf-annotation.js +16 -2
- package/dist/core/objects/pdf-array.d.ts +6 -1
- package/dist/core/objects/pdf-array.js +3 -0
- package/dist/core/objects/pdf-boolean.d.ts +6 -2
- package/dist/core/objects/pdf-boolean.js +3 -0
- package/dist/core/objects/pdf-comment.d.ts +6 -2
- package/dist/core/objects/pdf-comment.js +3 -0
- package/dist/core/objects/pdf-date.d.ts +4 -0
- package/dist/core/objects/pdf-date.js +3 -0
- package/dist/core/objects/pdf-dictionary.d.ts +5 -0
- package/dist/core/objects/pdf-dictionary.js +16 -0
- package/dist/core/objects/pdf-hexadecimal.d.ts +6 -2
- package/dist/core/objects/pdf-hexadecimal.js +3 -0
- package/dist/core/objects/pdf-indirect-object.d.ts +8 -1
- package/dist/core/objects/pdf-indirect-object.js +14 -0
- package/dist/core/objects/pdf-name.d.ts +6 -2
- package/dist/core/objects/pdf-name.js +3 -0
- package/dist/core/objects/pdf-null.d.ts +5 -2
- package/dist/core/objects/pdf-null.js +3 -0
- package/dist/core/objects/pdf-number.d.ts +6 -1
- package/dist/core/objects/pdf-number.js +3 -0
- package/dist/core/objects/pdf-object-reference.d.ts +5 -0
- package/dist/core/objects/pdf-object-reference.js +7 -0
- package/dist/core/objects/pdf-object.d.ts +1 -0
- package/dist/core/objects/pdf-start-xref.d.ts +6 -1
- package/dist/core/objects/pdf-start-xref.js +3 -0
- package/dist/core/objects/pdf-stream.d.ts +8 -0
- package/dist/core/objects/pdf-stream.js +7 -0
- package/dist/core/objects/pdf-string.d.ts +8 -2
- package/dist/core/objects/pdf-string.js +9 -0
- package/dist/core/objects/pdf-trailer.d.ts +7 -0
- package/dist/core/objects/pdf-trailer.js +3 -0
- package/dist/core/objects/pdf-xref-table.d.ts +31 -5
- package/dist/core/objects/pdf-xref-table.js +23 -0
- package/dist/fonts/font-family.d.ts +12 -0
- package/dist/fonts/font-family.js +1 -0
- package/dist/fonts/index.d.ts +1 -0
- package/dist/fonts/index.js +1 -0
- package/dist/fonts/parsers/otf-parser.d.ts +2 -2
- package/dist/fonts/parsers/ttf-parser.d.ts +2 -2
- package/dist/fonts/parsers/woff-parser.d.ts +2 -0
- package/dist/fonts/parsers/woff-parser.js +6 -0
- package/dist/fonts/pdf-font.js +94 -8
- package/dist/fonts/types.d.ts +10 -0
- package/dist/pdf/pdf-document.d.ts +7 -0
- package/dist/pdf/pdf-document.js +6 -0
- package/dist/pdf/pdf-revision.d.ts +4 -0
- package/dist/pdf/pdf-revision.js +6 -0
- package/dist/utils/iterable-readable-stream.d.ts +2 -0
- package/dist/utils/iterable-readable-stream.js +8 -1
- package/dist/utils/parse-markdown-segments.d.ts +12 -0
- package/dist/utils/parse-markdown-segments.js +27 -0
- package/dist/utils/xml.d.ts +9 -0
- package/dist/utils/xml.js +59 -0
- package/package.json +2 -2
|
@@ -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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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();
|
|
@@ -28,8 +29,8 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
|
|
|
28
29
|
g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, DEFAULT_FONT_SIZE, ctx.da.colorOp));
|
|
29
30
|
// ── Determine font size ──────────────────────────────────────
|
|
30
31
|
let finalFontSize;
|
|
31
|
-
if (autoSize) {
|
|
32
|
-
//
|
|
32
|
+
if (autoSize && ctx.multiline) {
|
|
33
|
+
// Multiline auto-size: default to 12pt with wrapping, then
|
|
33
34
|
// shrink only if the wrapped text still doesn't fit.
|
|
34
35
|
finalFontSize = DEFAULT_FONT_SIZE;
|
|
35
36
|
const testLines = g.wrapTextToLines(value, availableWidth);
|
|
@@ -38,6 +39,16 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
|
|
|
38
39
|
}
|
|
39
40
|
finalFontSize = Math.max(finalFontSize, 0.5);
|
|
40
41
|
}
|
|
42
|
+
else if (autoSize) {
|
|
43
|
+
// Single-line auto-size: fit to available height, then
|
|
44
|
+
// shrink further if text overflows the width.
|
|
45
|
+
finalFontSize = Math.min(DEFAULT_FONT_SIZE, availableHeight);
|
|
46
|
+
g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
|
|
47
|
+
if (g.measureTextWidth(value) > availableWidth) {
|
|
48
|
+
finalFontSize = g.calculateFittingFontSize(value, availableWidth);
|
|
49
|
+
}
|
|
50
|
+
finalFontSize = Math.max(finalFontSize, 0.5);
|
|
51
|
+
}
|
|
41
52
|
else {
|
|
42
53
|
finalFontSize = ctx.da.fontSize;
|
|
43
54
|
}
|
|
@@ -45,30 +56,14 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
|
|
|
45
56
|
const finalDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
|
|
46
57
|
g.setDefaultAppearance(finalDA);
|
|
47
58
|
let lines = [];
|
|
48
|
-
if (ctx.
|
|
49
|
-
if (!autoSize) {
|
|
50
|
-
const testLines = g.wrapTextToLines(value, availableWidth);
|
|
51
|
-
const lineHeight = finalFontSize * 1.2;
|
|
52
|
-
if (testLines.length * lineHeight > availableHeight) {
|
|
53
|
-
finalFontSize = g.calculateFittingFontSize(value, availableWidth, availableHeight, 1.2);
|
|
54
|
-
g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
lines = g.wrapTextToLines(value, availableWidth);
|
|
58
|
-
const renderLineHeight = finalFontSize * 1.2;
|
|
59
|
-
const startY = height - padding - finalFontSize;
|
|
60
|
-
g.beginText();
|
|
61
|
-
g.moveTo(padding, startY);
|
|
62
|
-
for (let i = 0; i < lines.length; i++) {
|
|
63
|
-
if (i > 0)
|
|
64
|
-
g.moveTo(0, -renderLineHeight);
|
|
65
|
-
g.showText(lines[i].replace(/\r/g, ''), isUnicode, reverseEncodingMap);
|
|
66
|
-
}
|
|
67
|
-
g.endText();
|
|
68
|
-
}
|
|
69
|
-
else if (ctx.comb && ctx.maxLen) {
|
|
59
|
+
if (ctx.comb && ctx.maxLen) {
|
|
70
60
|
const cellWidth = width / ctx.maxLen;
|
|
71
61
|
const chars = [...value];
|
|
62
|
+
// For auto-size comb, fit font to cell height
|
|
63
|
+
if (autoSize) {
|
|
64
|
+
finalFontSize = Math.min(height - 2 * padding, cellWidth);
|
|
65
|
+
g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
|
|
66
|
+
}
|
|
72
67
|
let maxCharWidth = 0;
|
|
73
68
|
let widestChar = chars[0] ?? '';
|
|
74
69
|
for (const char of chars) {
|
|
@@ -85,26 +80,60 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
|
|
|
85
80
|
const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
|
|
86
81
|
g.beginText();
|
|
87
82
|
for (let i = 0; i < chars.length && i < ctx.maxLen; i++) {
|
|
88
|
-
const
|
|
83
|
+
const charWidth = g.measureTextWidth(chars[i]);
|
|
84
|
+
const cellX = cellWidth * i + (cellWidth - charWidth) / 2;
|
|
89
85
|
g.moveTo(cellX, textY);
|
|
90
86
|
g.showText(chars[i], isUnicode, reverseEncodingMap);
|
|
91
87
|
g.moveTo(-cellX, -textY);
|
|
92
88
|
}
|
|
93
89
|
g.endText();
|
|
94
90
|
}
|
|
91
|
+
else if (ctx.multiline) {
|
|
92
|
+
if (!autoSize) {
|
|
93
|
+
const testLines = g.wrapTextToLines(value, availableWidth);
|
|
94
|
+
const lineHeight = finalFontSize * 1.2;
|
|
95
|
+
if (testLines.length * lineHeight > availableHeight) {
|
|
96
|
+
finalFontSize = g.calculateFittingFontSize(value, availableWidth, availableHeight, 1.2);
|
|
97
|
+
g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines = g.wrapTextToLines(value, availableWidth);
|
|
101
|
+
const renderLineHeight = finalFontSize * 1.2;
|
|
102
|
+
const startY = height - padding - finalFontSize;
|
|
103
|
+
g.beginText();
|
|
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
|
+
}
|
|
117
|
+
}
|
|
118
|
+
g.endText();
|
|
119
|
+
}
|
|
95
120
|
else {
|
|
96
121
|
// Single line — for non-auto-size, shrink if text overflows
|
|
97
122
|
if (!autoSize) {
|
|
98
|
-
|
|
99
|
-
if (textWidth > availableWidth) {
|
|
123
|
+
if (g.measureTextWidth(value) > availableWidth) {
|
|
100
124
|
finalFontSize = g.calculateFittingFontSize(value, availableWidth);
|
|
101
125
|
g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
|
|
102
126
|
}
|
|
103
127
|
}
|
|
104
128
|
const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
|
|
105
129
|
g.beginText();
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
137
|
g.endText();
|
|
109
138
|
}
|
|
110
139
|
g.restore();
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { PdfFormField } from './pdf-form-field.js';
|
|
2
2
|
import { PdfString } from '../../core/objects/pdf-string.js';
|
|
3
|
+
import type { PdfAcroForm } from '../pdf-acro-form.js';
|
|
4
|
+
import type { PdfIndirectObject } from '../../core/objects/pdf-indirect-object.js';
|
|
3
5
|
/**
|
|
4
6
|
* Button form field subtype (checkboxes, radio buttons, push buttons).
|
|
5
7
|
*/
|
|
6
8
|
export declare class PdfButtonFormField extends PdfFormField {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
constructor(other?: PdfIndirectObject | {
|
|
10
|
+
form?: PdfAcroForm;
|
|
11
|
+
});
|
|
12
|
+
private initWidget;
|
|
13
|
+
set isWidget(val: boolean);
|
|
14
|
+
get isGroup(): boolean;
|
|
15
|
+
get value(): string;
|
|
16
|
+
set value(val: string | PdfString);
|
|
9
17
|
get checked(): boolean;
|
|
10
18
|
set checked(isChecked: boolean);
|
|
11
19
|
generateAppearance(options?: {
|
|
12
20
|
makeReadOnly?: boolean;
|
|
21
|
+
onStateName?: string;
|
|
13
22
|
}): boolean;
|
|
14
23
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PdfFormField } from './pdf-form-field.js';
|
|
2
2
|
import { PdfButtonAppearanceStream } from '../appearance/pdf-button-appearance-stream.js';
|
|
3
|
-
import { PdfName } from '../../core/objects/pdf-name.js';
|
|
4
3
|
import { PdfString } from '../../core/objects/pdf-string.js';
|
|
5
4
|
/**
|
|
6
5
|
* Button form field subtype (checkboxes, radio buttons, push buttons).
|
|
@@ -9,50 +8,80 @@ export class PdfButtonFormField extends PdfFormField {
|
|
|
9
8
|
static {
|
|
10
9
|
PdfFormField.registerFieldType('Btn', PdfButtonFormField);
|
|
11
10
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const strVal = val instanceof PdfString ? val.value : val;
|
|
17
|
-
if (strVal.trim() === '') {
|
|
18
|
-
this.content.delete('V');
|
|
19
|
-
fieldParent?.content.delete('V');
|
|
20
|
-
this.content.delete('AS');
|
|
21
|
-
return false;
|
|
11
|
+
constructor(other) {
|
|
12
|
+
super(other);
|
|
13
|
+
if (this.isWidget && this.appearanceStates.length === 0) {
|
|
14
|
+
this.initWidget();
|
|
22
15
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
}
|
|
17
|
+
initWidget() {
|
|
18
|
+
this.rect ||= [0, 0, 50, 50];
|
|
19
|
+
this.generateAppearance({
|
|
20
|
+
onStateName: this.onState ?? 'Yes',
|
|
21
|
+
});
|
|
22
|
+
this.appearanceState ??= 'Off';
|
|
23
|
+
}
|
|
24
|
+
set isWidget(val) {
|
|
25
|
+
super.isWidget = val;
|
|
26
|
+
// Only initialize the widget if it has no existing appearances.
|
|
27
|
+
// `appearanceStream` is write-only on PdfFormField, so instead check
|
|
28
|
+
// the underlying appearance data structures.
|
|
29
|
+
if (val &&
|
|
30
|
+
!this.appearanceStreamDict &&
|
|
31
|
+
this.appearanceStates.length === 0) {
|
|
32
|
+
this.initWidget();
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
}
|
|
35
|
+
get isGroup() {
|
|
36
|
+
return this.children.length > 0;
|
|
37
|
+
}
|
|
38
|
+
get value() {
|
|
39
|
+
return super.value;
|
|
40
|
+
}
|
|
41
|
+
set value(val) {
|
|
42
|
+
const strVal = val instanceof PdfString ? val.value : val;
|
|
43
|
+
this.setRawValue(new PdfString(strVal));
|
|
44
|
+
if (this.isGroup) {
|
|
45
|
+
const children = this.children;
|
|
46
|
+
// 'Off' means explicitly uncheck all children
|
|
47
|
+
if (strVal === 'Off') {
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
child.appearanceState = 'Off';
|
|
50
|
+
if (this._form)
|
|
51
|
+
child.form = this._form;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
let wasSet = false;
|
|
56
|
+
for (const child of children) {
|
|
57
|
+
const foundState = child.onStates.includes(strVal);
|
|
58
|
+
if (!wasSet && foundState) {
|
|
59
|
+
wasSet = true;
|
|
60
|
+
}
|
|
61
|
+
child.appearanceState = foundState ? strVal : 'Off';
|
|
62
|
+
if (this._form)
|
|
63
|
+
child.form = this._form;
|
|
64
|
+
}
|
|
65
|
+
if (!wasSet && children.length > 0) {
|
|
66
|
+
// If value doesn't match any on-state, check first child by default
|
|
67
|
+
children[0].appearanceState =
|
|
68
|
+
children[0].onStates[0] ?? 'Off';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
33
71
|
}
|
|
34
72
|
else {
|
|
35
|
-
|
|
73
|
+
this.appearanceState = strVal;
|
|
36
74
|
}
|
|
37
|
-
this.content.set('V', new PdfName(resolved));
|
|
38
|
-
fieldParent?.content.set('V', new PdfName(resolved));
|
|
39
|
-
this.content.set('AS', new PdfName(resolved));
|
|
40
|
-
return true;
|
|
41
75
|
}
|
|
42
76
|
get checked() {
|
|
43
|
-
|
|
44
|
-
return v instanceof PdfName && v.value !== 'Off';
|
|
77
|
+
return !(this.appearanceState === 'Off' || this.value === 'Off');
|
|
45
78
|
}
|
|
46
79
|
set checked(isChecked) {
|
|
47
|
-
const target = this.parent ?? this;
|
|
48
80
|
if (isChecked) {
|
|
49
|
-
|
|
50
|
-
target.content.set('V', new PdfName(onState));
|
|
51
|
-
this.content.set('AS', new PdfName(onState));
|
|
81
|
+
this.value = this.onState ?? 'Yes';
|
|
52
82
|
}
|
|
53
83
|
else {
|
|
54
|
-
|
|
55
|
-
this.content.set('AS', new PdfName('Off'));
|
|
84
|
+
this.value = 'Off';
|
|
56
85
|
}
|
|
57
86
|
}
|
|
58
87
|
generateAppearance(options) {
|
|
@@ -71,11 +100,21 @@ export class PdfButtonFormField extends PdfFormField {
|
|
|
71
100
|
height,
|
|
72
101
|
contentStream: '',
|
|
73
102
|
});
|
|
74
|
-
const
|
|
75
|
-
this.
|
|
76
|
-
|
|
103
|
+
const existingOnState = this.onState;
|
|
104
|
+
const as = this.appearanceState;
|
|
105
|
+
const onKey = options?.onStateName ??
|
|
106
|
+
(as && as !== 'Off' ? as : (existingOnState ?? 'Yes'));
|
|
107
|
+
if (onKey === 'Off') {
|
|
108
|
+
throw new Error("Invalid on-state name 'Off' for button field appearance stream");
|
|
109
|
+
}
|
|
110
|
+
this.appearanceStream = {
|
|
111
|
+
[onKey]: yesAppearance,
|
|
77
112
|
Off: noAppearance,
|
|
78
|
-
}
|
|
113
|
+
};
|
|
114
|
+
this.downAppearanceStream = {
|
|
115
|
+
[onKey]: yesAppearance,
|
|
116
|
+
Off: noAppearance,
|
|
117
|
+
};
|
|
79
118
|
if (options?.makeReadOnly) {
|
|
80
119
|
this.readOnly = true;
|
|
81
120
|
this.print = true;
|