@polotno/pdf-export 0.1.38 → 0.1.40
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/README.md +61 -8
- package/lib/index.d.ts +66 -8
- package/lib/index.js +25 -145
- package/package.json +17 -18
- package/lib/compare-render.d.ts +0 -1
- package/lib/compare-render.js +0 -185
- package/lib/figure.d.ts +0 -10
- package/lib/figure.js +0 -54
- package/lib/filters.d.ts +0 -2
- package/lib/filters.js +0 -163
- package/lib/ghostscript.d.ts +0 -21
- package/lib/ghostscript.js +0 -132
- package/lib/group.d.ts +0 -5
- package/lib/group.js +0 -5
- package/lib/image.d.ts +0 -38
- package/lib/image.js +0 -279
- package/lib/line.d.ts +0 -10
- package/lib/line.js +0 -66
- package/lib/pdf-import/coordinate-transform.d.ts +0 -51
- package/lib/pdf-import/coordinate-transform.js +0 -99
- package/lib/pdf-import/element-builder.d.ts +0 -21
- package/lib/pdf-import/element-builder.js +0 -163
- package/lib/pdf-import/font-mapper.d.ts +0 -17
- package/lib/pdf-import/font-mapper.js +0 -142
- package/lib/pdf-import/index.d.ts +0 -35
- package/lib/pdf-import/index.js +0 -105
- package/lib/pdf-import/parser.d.ts +0 -29
- package/lib/pdf-import/parser.js +0 -285
- package/lib/pdf-import/text-analysis.d.ts +0 -17
- package/lib/pdf-import/text-analysis.js +0 -186
- package/lib/pdf-import/types.d.ts +0 -101
- package/lib/pdf-import/types.js +0 -1
- package/lib/scripts/compare-json.d.ts +0 -1
- package/lib/scripts/compare-json.js +0 -141
- package/lib/spot-colors.d.ts +0 -38
- package/lib/spot-colors.js +0 -141
- package/lib/svg-render.d.ts +0 -9
- package/lib/svg-render.js +0 -63
- package/lib/svg.d.ts +0 -12
- package/lib/svg.js +0 -224
- package/lib/text/fonts.d.ts +0 -16
- package/lib/text/fonts.js +0 -113
- package/lib/text/index.d.ts +0 -8
- package/lib/text/index.js +0 -42
- package/lib/text/layout.d.ts +0 -22
- package/lib/text/layout.js +0 -522
- package/lib/text/parser.d.ts +0 -46
- package/lib/text/parser.js +0 -415
- package/lib/text/render.d.ts +0 -8
- package/lib/text/render.js +0 -237
- package/lib/text/types.d.ts +0 -91
- package/lib/text/types.js +0 -1
- package/lib/text.d.ts +0 -39
- package/lib/text.js +0 -576
- package/lib/utils.d.ts +0 -16
- package/lib/utils.js +0 -124
package/lib/text/parser.js
DELETED
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
import { decode as decodeEntities } from 'html-entities';
|
|
2
|
-
export function decodeHtmlEntities(text) {
|
|
3
|
-
if (!text) {
|
|
4
|
-
return text;
|
|
5
|
-
}
|
|
6
|
-
const decoded = decodeEntities(text);
|
|
7
|
-
// Don't replace tabs here - we'll handle them with expandTabsToTabStops
|
|
8
|
-
return decoded;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Normalize rich text HTML by converting block-level line breaks into newline characters
|
|
12
|
-
* while preserving inline formatting tags.
|
|
13
|
-
*/
|
|
14
|
-
export function normalizeRichText(text) {
|
|
15
|
-
if (!text) {
|
|
16
|
-
return text;
|
|
17
|
-
}
|
|
18
|
-
let normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
19
|
-
// Convert explicit HTML break tags into newline characters
|
|
20
|
-
normalized = normalized.replace(/<br\s*\/?>/gi, '\n');
|
|
21
|
-
// Treat paragraph boundaries as newlines and drop opening tags
|
|
22
|
-
normalized = normalized.replace(/<\/p\s*>/gi, '\n');
|
|
23
|
-
normalized = normalized.replace(/<p[^>]*>/gi, '');
|
|
24
|
-
// Collapse excessive consecutive newlines produced by HTML cleanup
|
|
25
|
-
normalized = normalized.replace(/\n{3,}/g, '\n\n');
|
|
26
|
-
// Trim stray leading/trailing newlines introduced by paragraph conversion
|
|
27
|
-
normalized = normalized.replace(/^\n+/, '').replace(/\n+$/, '');
|
|
28
|
-
// Decode common HTML non-breaking space entities into their unicode counterpart
|
|
29
|
-
normalized = normalized.replace(/&(nbsp|#160|#xA0);/gi, '\u00A0');
|
|
30
|
-
// Strip zero-width characters that can create missing-glyph boxes in PDF output
|
|
31
|
-
normalized = normalized.replace(/[\u200B\u200C\u200D\uFEFF\u2060]/g, '');
|
|
32
|
-
return normalized;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Parse HTML text into styled segments
|
|
36
|
-
*/
|
|
37
|
-
export function parseHTMLToSegments(html, baseElement) {
|
|
38
|
-
const segments = [];
|
|
39
|
-
const tagStack = [];
|
|
40
|
-
// Regex to match tags and text content
|
|
41
|
-
const regex = /<(\/?)(strong|b|em|i|u|span)([^>]*)>|([^<]+)/gi;
|
|
42
|
-
let match;
|
|
43
|
-
while ((match = regex.exec(html)) !== null) {
|
|
44
|
-
if (match[4]) {
|
|
45
|
-
// Text content
|
|
46
|
-
const text = decodeHtmlEntities(match[4]);
|
|
47
|
-
// Calculate current styles from tag stack
|
|
48
|
-
let bold = false;
|
|
49
|
-
let italic = false;
|
|
50
|
-
let underline = false;
|
|
51
|
-
let color = undefined;
|
|
52
|
-
for (const tag of tagStack) {
|
|
53
|
-
if (tag.tag === 'strong' || tag.tag === 'b')
|
|
54
|
-
bold = true;
|
|
55
|
-
if (tag.tag === 'em' || tag.tag === 'i')
|
|
56
|
-
italic = true;
|
|
57
|
-
if (tag.tag === 'u')
|
|
58
|
-
underline = true;
|
|
59
|
-
if (tag.color)
|
|
60
|
-
color = tag.color;
|
|
61
|
-
}
|
|
62
|
-
segments.push({
|
|
63
|
-
text,
|
|
64
|
-
bold,
|
|
65
|
-
italic,
|
|
66
|
-
underline,
|
|
67
|
-
color,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
// Tag
|
|
72
|
-
const isClosing = match[1] === '/';
|
|
73
|
-
const tagName = match[2].toLowerCase();
|
|
74
|
-
const attributes = match[3];
|
|
75
|
-
if (isClosing) {
|
|
76
|
-
// Remove from stack
|
|
77
|
-
const index = tagStack.findIndex((t) => t.tag === tagName);
|
|
78
|
-
if (index !== -1) {
|
|
79
|
-
tagStack.splice(index, 1);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
// Add to stack
|
|
84
|
-
const tagData = { tag: tagName };
|
|
85
|
-
// Parse color from span style attribute
|
|
86
|
-
if (attributes) {
|
|
87
|
-
const colorMatch = /style=["'](?:[^"']*)?color:\s*([^;"']+)/i.exec(attributes);
|
|
88
|
-
if (colorMatch) {
|
|
89
|
-
tagData.color = colorMatch[1].trim();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
tagStack.push(tagData);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return segments;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Parse HTML into tokens (text and tags)
|
|
100
|
-
*/
|
|
101
|
-
export function tokenizeHTML(html) {
|
|
102
|
-
const tokens = [];
|
|
103
|
-
const regex = /<(\/?)(strong|b|em|i|u|span)([^>]*)>|([^<]+)/gi;
|
|
104
|
-
let match;
|
|
105
|
-
while ((match = regex.exec(html)) !== null) {
|
|
106
|
-
if (match[4]) {
|
|
107
|
-
// Text content
|
|
108
|
-
const decodedContent = decodeHtmlEntities(match[4]);
|
|
109
|
-
tokens.push({
|
|
110
|
-
type: 'text',
|
|
111
|
-
content: match[4],
|
|
112
|
-
decodedContent,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
// Tag
|
|
117
|
-
const isClosing = match[1] === '/';
|
|
118
|
-
const tagName = match[2].toLowerCase();
|
|
119
|
-
tokens.push({
|
|
120
|
-
type: 'tag',
|
|
121
|
-
content: match[0],
|
|
122
|
-
tagName: tagName,
|
|
123
|
-
isClosing: isClosing,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return tokens;
|
|
128
|
-
}
|
|
129
|
-
const VOID_ELEMENTS = new Set(['br']);
|
|
130
|
-
function parseAttributes(raw) {
|
|
131
|
-
const attributes = {};
|
|
132
|
-
const attrRegex = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
133
|
-
let attrMatch;
|
|
134
|
-
while ((attrMatch = attrRegex.exec(raw)) !== null) {
|
|
135
|
-
const name = attrMatch[1].toLowerCase();
|
|
136
|
-
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '';
|
|
137
|
-
attributes[name] = value;
|
|
138
|
-
}
|
|
139
|
-
return attributes;
|
|
140
|
-
}
|
|
141
|
-
function parseSimpleHTML(html) {
|
|
142
|
-
const root = {
|
|
143
|
-
type: 'element',
|
|
144
|
-
tagName: 'root',
|
|
145
|
-
attributes: {},
|
|
146
|
-
children: [],
|
|
147
|
-
};
|
|
148
|
-
const stack = [root];
|
|
149
|
-
const tagRegex = /<\/?([a-zA-Z0-9:-]+)([^>]*)>/g;
|
|
150
|
-
let lastIndex = 0;
|
|
151
|
-
let match;
|
|
152
|
-
while ((match = tagRegex.exec(html)) !== null) {
|
|
153
|
-
if (match.index > lastIndex) {
|
|
154
|
-
const textContent = html.slice(lastIndex, match.index);
|
|
155
|
-
if (textContent) {
|
|
156
|
-
stack[stack.length - 1].children.push({
|
|
157
|
-
type: 'text',
|
|
158
|
-
content: textContent,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const fullMatch = match[0];
|
|
163
|
-
const isClosing = fullMatch.startsWith('</');
|
|
164
|
-
const tagName = match[1].toLowerCase();
|
|
165
|
-
const attrChunk = match[2] || '';
|
|
166
|
-
if (isClosing) {
|
|
167
|
-
for (let i = stack.length - 1; i >= 0; i--) {
|
|
168
|
-
if (stack[i].tagName === tagName) {
|
|
169
|
-
stack.length = i;
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
const attributes = parseAttributes(attrChunk);
|
|
176
|
-
const elementNode = {
|
|
177
|
-
type: 'element',
|
|
178
|
-
tagName,
|
|
179
|
-
attributes,
|
|
180
|
-
children: [],
|
|
181
|
-
};
|
|
182
|
-
const parent = stack[stack.length - 1];
|
|
183
|
-
parent.children.push(elementNode);
|
|
184
|
-
const selfClosing = fullMatch.endsWith('/>') || VOID_ELEMENTS.has(tagName);
|
|
185
|
-
if (!selfClosing) {
|
|
186
|
-
stack.push(elementNode);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
lastIndex = tagRegex.lastIndex;
|
|
190
|
-
}
|
|
191
|
-
if (lastIndex < html.length) {
|
|
192
|
-
const remaining = html.slice(lastIndex);
|
|
193
|
-
if (remaining) {
|
|
194
|
-
stack[stack.length - 1].children.push({
|
|
195
|
-
type: 'text',
|
|
196
|
-
content: remaining,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return root;
|
|
201
|
-
}
|
|
202
|
-
function serializeAttributes(attributes) {
|
|
203
|
-
const entries = Object.entries(attributes);
|
|
204
|
-
if (!entries.length) {
|
|
205
|
-
return '';
|
|
206
|
-
}
|
|
207
|
-
return (' ' +
|
|
208
|
-
entries
|
|
209
|
-
.map(([key, value]) => value === '' ? key : `${key}="${value.replace(/"/g, '"')}"`)
|
|
210
|
-
.join(' '));
|
|
211
|
-
}
|
|
212
|
-
function shouldSkipNode(node) {
|
|
213
|
-
if (node.type === 'element' && node.tagName === 'span') {
|
|
214
|
-
const classAttr = node.attributes['class'] || node.attributes['className'] || '';
|
|
215
|
-
if (/\bql-cursor\b/.test(classAttr)) {
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
function serializeNodes(nodes) {
|
|
222
|
-
return nodes
|
|
223
|
-
.map((node) => {
|
|
224
|
-
if (shouldSkipNode(node)) {
|
|
225
|
-
return '';
|
|
226
|
-
}
|
|
227
|
-
if (node.type === 'text') {
|
|
228
|
-
return node.content.replace(/\uFEFF/g, '');
|
|
229
|
-
}
|
|
230
|
-
const attrs = serializeAttributes(node.attributes);
|
|
231
|
-
if (VOID_ELEMENTS.has(node.tagName)) {
|
|
232
|
-
return `<${node.tagName}${attrs}>`;
|
|
233
|
-
}
|
|
234
|
-
return `<${node.tagName}${attrs}>${serializeNodes(node.children)}</${node.tagName}>`;
|
|
235
|
-
})
|
|
236
|
-
.join('');
|
|
237
|
-
}
|
|
238
|
-
function getIndentLevelFromAttributes(attributes) {
|
|
239
|
-
const classAttr = attributes['class'] || attributes['className'] || '';
|
|
240
|
-
const match = classAttr.match(/ql-indent-(\d+)/);
|
|
241
|
-
return match ? parseInt(match[1], 10) : 0;
|
|
242
|
-
}
|
|
243
|
-
function detectIndentLevel(node) {
|
|
244
|
-
const directIndent = getIndentLevelFromAttributes(node.attributes);
|
|
245
|
-
if (directIndent > 0) {
|
|
246
|
-
return directIndent;
|
|
247
|
-
}
|
|
248
|
-
for (const child of node.children) {
|
|
249
|
-
if (child.type === 'element' &&
|
|
250
|
-
child.tagName !== 'ul' &&
|
|
251
|
-
child.tagName !== 'ol') {
|
|
252
|
-
const childIndent = detectIndentLevel(child);
|
|
253
|
-
if (childIndent > 0) {
|
|
254
|
-
return childIndent;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return 0;
|
|
259
|
-
}
|
|
260
|
-
function nodesToParagraphs(nodes, baseIndentLevel = 0) {
|
|
261
|
-
const paragraphs = [];
|
|
262
|
-
let pendingInline = [];
|
|
263
|
-
const flushInline = () => {
|
|
264
|
-
if (pendingInline.length === 0) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const html = serializeNodes(pendingInline);
|
|
268
|
-
paragraphs.push({ html });
|
|
269
|
-
pendingInline = [];
|
|
270
|
-
};
|
|
271
|
-
for (const node of nodes) {
|
|
272
|
-
if (shouldSkipNode(node)) {
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (node.type === 'text') {
|
|
276
|
-
const newlineRegex = /\n+/g;
|
|
277
|
-
let lastIndex = 0;
|
|
278
|
-
let match;
|
|
279
|
-
while ((match = newlineRegex.exec(node.content)) !== null) {
|
|
280
|
-
const chunk = node.content.slice(lastIndex, match.index);
|
|
281
|
-
if (chunk.length > 0) {
|
|
282
|
-
pendingInline.push({
|
|
283
|
-
type: 'text',
|
|
284
|
-
content: chunk,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
flushInline();
|
|
288
|
-
const extraBreaks = match[0].length - 1;
|
|
289
|
-
for (let extra = 0; extra < extraBreaks; extra++) {
|
|
290
|
-
paragraphs.push({ html: '' });
|
|
291
|
-
}
|
|
292
|
-
lastIndex = newlineRegex.lastIndex;
|
|
293
|
-
}
|
|
294
|
-
const rest = node.content.slice(lastIndex);
|
|
295
|
-
if (rest.length > 0) {
|
|
296
|
-
pendingInline.push({
|
|
297
|
-
type: 'text',
|
|
298
|
-
content: rest,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (node.tagName === 'p') {
|
|
304
|
-
flushInline();
|
|
305
|
-
const html = serializeNodes(node.children);
|
|
306
|
-
paragraphs.push({
|
|
307
|
-
html,
|
|
308
|
-
});
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
if (node.tagName === 'br') {
|
|
312
|
-
flushInline();
|
|
313
|
-
paragraphs.push({ html: '' });
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (node.tagName === 'ul' || node.tagName === 'ol') {
|
|
317
|
-
flushInline();
|
|
318
|
-
paragraphs.push(...collectListParagraphs(node, baseIndentLevel));
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
pendingInline.push(node);
|
|
322
|
-
}
|
|
323
|
-
flushInline();
|
|
324
|
-
return paragraphs;
|
|
325
|
-
}
|
|
326
|
-
function collectListParagraphs(listNode, baseIndentLevel) {
|
|
327
|
-
const paragraphs = [];
|
|
328
|
-
const listIndent = baseIndentLevel + getIndentLevelFromAttributes(listNode.attributes);
|
|
329
|
-
let counter = 1;
|
|
330
|
-
for (const child of listNode.children) {
|
|
331
|
-
if (child.type !== 'element' || child.tagName !== 'li') {
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const liIndentFromAttribute = getIndentLevelFromAttributes(child.attributes);
|
|
335
|
-
const detectedIndent = detectIndentLevel(child);
|
|
336
|
-
const indentLevel = liIndentFromAttribute > 0
|
|
337
|
-
? listIndent + liIndentFromAttribute
|
|
338
|
-
: listIndent + detectedIndent;
|
|
339
|
-
const itemParagraphs = nodesToParagraphs(child.children, indentLevel);
|
|
340
|
-
let markerAssigned = false;
|
|
341
|
-
for (const paragraph of itemParagraphs) {
|
|
342
|
-
if (!paragraph.listMeta) {
|
|
343
|
-
paragraph.listMeta = {
|
|
344
|
-
type: listNode.tagName,
|
|
345
|
-
index: counter,
|
|
346
|
-
indentLevel,
|
|
347
|
-
displayMarker: !markerAssigned,
|
|
348
|
-
};
|
|
349
|
-
markerAssigned = true;
|
|
350
|
-
}
|
|
351
|
-
paragraphs.push(paragraph);
|
|
352
|
-
}
|
|
353
|
-
if (!markerAssigned) {
|
|
354
|
-
paragraphs.push({
|
|
355
|
-
html: '',
|
|
356
|
-
listMeta: {
|
|
357
|
-
type: listNode.tagName,
|
|
358
|
-
index: counter,
|
|
359
|
-
indentLevel,
|
|
360
|
-
displayMarker: true,
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
if (listNode.tagName === 'ol') {
|
|
365
|
-
counter += 1;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return paragraphs;
|
|
369
|
-
}
|
|
370
|
-
export function parseHtmlToParagraphs(html) {
|
|
371
|
-
const root = parseSimpleHTML(html);
|
|
372
|
-
return nodesToParagraphs(root.children);
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Reconstruct HTML from tokens while maintaining proper tag nesting across line breaks
|
|
376
|
-
* @param tokens - Array of parsed HTML tokens
|
|
377
|
-
* @param openTags - Tags that were opened in previous lines and should be carried forward
|
|
378
|
-
* @returns Reconstructed HTML string and the updated list of open tags
|
|
379
|
-
*/
|
|
380
|
-
export function tokensToHTML(tokens, openTags) {
|
|
381
|
-
let html = '';
|
|
382
|
-
const tagStack = [...openTags]; // Clone the open tags
|
|
383
|
-
// Prepend any open tags
|
|
384
|
-
for (const tag of openTags) {
|
|
385
|
-
html += tag.fullTag;
|
|
386
|
-
}
|
|
387
|
-
// Process tokens
|
|
388
|
-
for (const token of tokens) {
|
|
389
|
-
if (token.type === 'text') {
|
|
390
|
-
html += token.content;
|
|
391
|
-
}
|
|
392
|
-
else if (token.type === 'tag') {
|
|
393
|
-
html += token.content;
|
|
394
|
-
if (token.isClosing) {
|
|
395
|
-
// Remove from stack
|
|
396
|
-
const idx = tagStack.findIndex((t) => t.name === token.tagName);
|
|
397
|
-
if (idx !== -1) {
|
|
398
|
-
tagStack.splice(idx, 1);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
// Add to stack
|
|
403
|
-
tagStack.push({
|
|
404
|
-
name: token.tagName,
|
|
405
|
-
fullTag: token.content,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
// Close any remaining open tags for this line
|
|
411
|
-
for (let i = tagStack.length - 1; i >= 0; i--) {
|
|
412
|
-
html += `</${tagStack[i].name}>`;
|
|
413
|
-
}
|
|
414
|
-
return { html, openTags: tagStack };
|
|
415
|
-
}
|
package/lib/text/render.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { TextElement, TextLine, LineRenderContext, SegmentRenderOptions, RenderSegment } from './types.js';
|
|
2
|
-
export declare function drawListMarker(doc: PDFKit.PDFDocument, element: TextElement, line: TextLine, lineXOffset: number, lineYOffset: number, fonts: Record<string, boolean>, color: string, opacity: number, mode: 'fill' | 'stroke', textOptions: PDFKit.Mixins.TextOptions): Promise<void>;
|
|
3
|
-
export declare function prepareLineForRendering(doc: PDFKit.PDFDocument, element: TextElement, line: TextLine, lineIndex: number, yOffset: number, lineHeightPx: number, cumulativeYOffset: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>, markerColor: string, markerOpacity: number, markerMode: 'fill' | 'stroke'): Promise<LineRenderContext>;
|
|
4
|
-
export declare function renderSegmentsForLine(doc: PDFKit.PDFDocument, element: TextElement, line: TextLine, renderSegments: RenderSegment[], context: LineRenderContext, textOptions: PDFKit.Mixins.TextOptions, options: SegmentRenderOptions): Promise<void>;
|
|
5
|
-
export declare function renderPDFX1aStroke(doc: PDFKit.PDFDocument, element: TextElement, textLines: TextLine[], yOffset: number, lineHeightPx: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>): Promise<void>;
|
|
6
|
-
export declare function renderStandardStroke(doc: PDFKit.PDFDocument, element: TextElement, textLines: TextLine[], yOffset: number, lineHeightPx: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>): Promise<void>;
|
|
7
|
-
export declare function renderTextFill(doc: PDFKit.PDFDocument, element: TextElement, textLines: TextLine[], yOffset: number, lineHeightPx: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>): Promise<void>;
|
|
8
|
-
export declare function renderTextBackground(doc: PDFKit.PDFDocument, element: TextElement, verticalAlignmentOffset: number, textOptions: PDFKit.Mixins.TextOptions): void;
|
package/lib/text/render.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { stripHtml } from 'string-strip-html';
|
|
2
|
-
import { parseColor } from '../utils.js';
|
|
3
|
-
import { calculateLineXOffset, calculateTextContentXOffset, calculateEffectiveWidth, buildRenderSegmentsForLine, } from './layout.js';
|
|
4
|
-
import { loadFontForSegment } from './fonts.js';
|
|
5
|
-
export async function drawListMarker(doc, element, line, lineXOffset, lineYOffset, fonts, color, opacity, mode, textOptions) {
|
|
6
|
-
if (!line.listMeta || !line.listMeta.showMarker) {
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
const markerSegment = {
|
|
10
|
-
text: line.listMeta.markerText,
|
|
11
|
-
};
|
|
12
|
-
const fontKey = await loadFontForSegment(doc, markerSegment, element, fonts);
|
|
13
|
-
const previousFontSize = doc._fontSize !== undefined
|
|
14
|
-
? doc._fontSize
|
|
15
|
-
: element.fontSize;
|
|
16
|
-
doc.font(fontKey);
|
|
17
|
-
doc.fontSize(line.listMeta.markerFontSize);
|
|
18
|
-
if (mode === 'fill') {
|
|
19
|
-
doc.fillColor(color, opacity);
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
doc.strokeColor(color, opacity);
|
|
23
|
-
}
|
|
24
|
-
doc.text(line.listMeta.markerText, lineXOffset + line.listMeta.indentPx, lineYOffset, {
|
|
25
|
-
...textOptions,
|
|
26
|
-
width: line.listMeta.markerBoxWidth,
|
|
27
|
-
align: line.listMeta.markerAlignment,
|
|
28
|
-
continued: false,
|
|
29
|
-
lineBreak: false,
|
|
30
|
-
underline: false,
|
|
31
|
-
characterSpacing: 0,
|
|
32
|
-
stroke: mode === 'stroke',
|
|
33
|
-
fill: mode === 'fill',
|
|
34
|
-
});
|
|
35
|
-
doc.fontSize(previousFontSize);
|
|
36
|
-
}
|
|
37
|
-
export async function prepareLineForRendering(doc, element, line, lineIndex, yOffset, lineHeightPx, cumulativeYOffset, textOptions, fonts, markerColor, markerOpacity, markerMode) {
|
|
38
|
-
const markerXOffset = calculateLineXOffset(element, line);
|
|
39
|
-
const lineYOffset = yOffset + cumulativeYOffset;
|
|
40
|
-
const strippedLineText = stripHtml(line.text).result;
|
|
41
|
-
const heightOfLine = line.text === ''
|
|
42
|
-
? lineHeightPx
|
|
43
|
-
: doc.heightOfString(strippedLineText, textOptions);
|
|
44
|
-
const contentStartX = calculateTextContentXOffset(element, line);
|
|
45
|
-
const isJustify = element.align === 'justify';
|
|
46
|
-
// Disable justification if line contains tabs - tabs position content precisely,
|
|
47
|
-
// and justification would interfere by spreading spaces
|
|
48
|
-
const hasTabs = line.text.includes('\t');
|
|
49
|
-
const widthOption = isJustify && !hasTabs
|
|
50
|
-
? Math.max(element.width - (line.listMeta ? line.listMeta.textStartPx : 0), 1)
|
|
51
|
-
: undefined;
|
|
52
|
-
// Handle list markers if needed
|
|
53
|
-
if (line.listMeta?.showMarker) {
|
|
54
|
-
await drawListMarker(doc, element, line, markerXOffset, lineYOffset, fonts, markerColor, markerOpacity, markerMode, textOptions);
|
|
55
|
-
// Restore color after drawing marker (marker may have changed the color state)
|
|
56
|
-
if (markerMode === 'fill') {
|
|
57
|
-
doc.fillColor(markerColor, markerOpacity);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
doc.strokeColor(markerColor, markerOpacity);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
markerXOffset,
|
|
65
|
-
lineYOffset,
|
|
66
|
-
contentStartX,
|
|
67
|
-
widthOption,
|
|
68
|
-
heightOfLine,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
export async function renderSegmentsForLine(doc, element, line, renderSegments, context, textOptions, options) {
|
|
72
|
-
const { mode, color, opacity = element.opacity, heightOfLine, offsetX = 0, offsetY = 0, applySegmentColor, } = options;
|
|
73
|
-
const totalTextSegments = renderSegments.filter((seg) => seg.type === 'text').length;
|
|
74
|
-
let processedTextSegments = 0;
|
|
75
|
-
// Position cursor at line start (with optional offset)
|
|
76
|
-
doc.x = context.contentStartX + offsetX;
|
|
77
|
-
doc.y = context.lineYOffset + offsetY;
|
|
78
|
-
for (const renderSegment of renderSegments) {
|
|
79
|
-
if (renderSegment.type === 'tab') {
|
|
80
|
-
const advanceWidth = renderSegment.advanceWidth ?? 0;
|
|
81
|
-
doc.x = context.contentStartX + advanceWidth + offsetX;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const { segment, text = '', wordSpacing = 0, fontKey } = renderSegment;
|
|
85
|
-
processedTextSegments += 1;
|
|
86
|
-
const isLastSegment = processedTextSegments === totalTextSegments;
|
|
87
|
-
doc.font(fontKey);
|
|
88
|
-
doc.fontSize(element.fontSize);
|
|
89
|
-
// Apply color (either segment-specific or global)
|
|
90
|
-
if (applySegmentColor) {
|
|
91
|
-
applySegmentColor(segment);
|
|
92
|
-
}
|
|
93
|
-
else if (color) {
|
|
94
|
-
if (mode === 'fill') {
|
|
95
|
-
doc.fillColor(color, opacity);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
doc.strokeColor(color, opacity);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const hasUnderline = segment.underline || textOptions.underline || false;
|
|
102
|
-
const effectiveWidth = calculateEffectiveWidth(element, line, context.widthOption, hasUnderline);
|
|
103
|
-
doc.text(text, {
|
|
104
|
-
...textOptions,
|
|
105
|
-
width: effectiveWidth,
|
|
106
|
-
height: heightOfLine,
|
|
107
|
-
continued: !isLastSegment,
|
|
108
|
-
underline: hasUnderline,
|
|
109
|
-
lineBreak: hasUnderline,
|
|
110
|
-
stroke: mode === 'stroke',
|
|
111
|
-
fill: mode === 'fill',
|
|
112
|
-
wordSpacing,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
export async function renderPDFX1aStroke(doc, element, textLines, yOffset, lineHeightPx, textOptions, fonts) {
|
|
117
|
-
const strokeColor = parseColor(element.stroke).hex;
|
|
118
|
-
const strokeWidth = element.strokeWidth;
|
|
119
|
-
// Generate stroke offsets in a circle pattern (8 directions)
|
|
120
|
-
const offsets = [];
|
|
121
|
-
for (let angle = 0; angle < 360; angle += 45) {
|
|
122
|
-
const radian = (angle * Math.PI) / 180;
|
|
123
|
-
offsets.push({
|
|
124
|
-
x: Math.cos(radian) * strokeWidth,
|
|
125
|
-
y: Math.sin(radian) * strokeWidth,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
// Render stroke layer by drawing text multiple times with offsets
|
|
129
|
-
doc.save();
|
|
130
|
-
doc.fillColor(strokeColor, element.opacity);
|
|
131
|
-
for (let i = 0; i < textLines.length; i++) {
|
|
132
|
-
const line = textLines[i];
|
|
133
|
-
const context = await prepareLineForRendering(doc, element, line, i, yOffset, lineHeightPx, i * lineHeightPx, textOptions, fonts, strokeColor, element.opacity, 'fill');
|
|
134
|
-
const renderSegments = await buildRenderSegmentsForLine(doc, element, line.text, textOptions, fonts);
|
|
135
|
-
// Render with each offset to create stroke effect
|
|
136
|
-
for (const offset of offsets) {
|
|
137
|
-
doc.text('', context.contentStartX + offset.x, context.lineYOffset + offset.y, {
|
|
138
|
-
height: 0,
|
|
139
|
-
width: 0,
|
|
140
|
-
});
|
|
141
|
-
await renderSegmentsForLine(doc, element, line, renderSegments, context, textOptions, {
|
|
142
|
-
mode: 'fill',
|
|
143
|
-
color: strokeColor,
|
|
144
|
-
opacity: element.opacity,
|
|
145
|
-
offsetX: offset.x,
|
|
146
|
-
offsetY: offset.y,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
doc.restore();
|
|
151
|
-
await renderTextFill(doc, element, textLines, yOffset, lineHeightPx, textOptions, fonts);
|
|
152
|
-
}
|
|
153
|
-
export async function renderStandardStroke(doc, element, textLines, yOffset, lineHeightPx, textOptions, fonts) {
|
|
154
|
-
const strokeParsedColor = parseColor(element.stroke);
|
|
155
|
-
doc.save();
|
|
156
|
-
doc.lineWidth(element.strokeWidth);
|
|
157
|
-
doc.lineCap('round').lineJoin('round');
|
|
158
|
-
doc.strokeColor(strokeParsedColor.hex, element.opacity);
|
|
159
|
-
let cumulativeYOffset = 0;
|
|
160
|
-
for (let i = 0; i < textLines.length; i++) {
|
|
161
|
-
const line = textLines[i];
|
|
162
|
-
const context = await prepareLineForRendering(doc, element, line, i, yOffset, lineHeightPx, cumulativeYOffset, textOptions, fonts, strokeParsedColor.hex, element.opacity, 'stroke');
|
|
163
|
-
cumulativeYOffset += context.heightOfLine;
|
|
164
|
-
const renderSegments = await buildRenderSegmentsForLine(doc, element, line.text, textOptions, fonts);
|
|
165
|
-
await renderSegmentsForLine(doc, element, line, renderSegments, context, textOptions, {
|
|
166
|
-
mode: 'stroke',
|
|
167
|
-
color: strokeParsedColor.hex,
|
|
168
|
-
opacity: element.opacity,
|
|
169
|
-
heightOfLine: context.heightOfLine,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
doc.restore();
|
|
173
|
-
}
|
|
174
|
-
export async function renderTextFill(doc, element, textLines, yOffset, lineHeightPx, textOptions, fonts) {
|
|
175
|
-
if (!element.fill) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const baseParsedColor = parseColor(element.fill);
|
|
179
|
-
const baseOpacity = Math.min(baseParsedColor.rgba[3] ?? 1, element.opacity, 1);
|
|
180
|
-
doc.fillColor(baseParsedColor.hex, baseOpacity);
|
|
181
|
-
let cumulativeYOffset = 0;
|
|
182
|
-
for (let i = 0; i < textLines.length; i++) {
|
|
183
|
-
const line = textLines[i];
|
|
184
|
-
const context = await prepareLineForRendering(doc, element, line, i, yOffset, lineHeightPx, cumulativeYOffset, textOptions, fonts, baseParsedColor.hex, baseOpacity, 'fill');
|
|
185
|
-
cumulativeYOffset += context.heightOfLine;
|
|
186
|
-
const renderSegments = await buildRenderSegmentsForLine(doc, element, line.text, textOptions, fonts);
|
|
187
|
-
// Apply segment-specific colors for rich text
|
|
188
|
-
const applySegmentColor = (segment) => {
|
|
189
|
-
const segmentParsedColor = segment.color
|
|
190
|
-
? parseColor(segment.color)
|
|
191
|
-
: parseColor(element.fill);
|
|
192
|
-
// Fallback to element fill if segment color parsing fails
|
|
193
|
-
const segmentColor = segmentParsedColor?.hex || parseColor(element.fill).hex || '#000000';
|
|
194
|
-
// Segment alpha can be NaN (e.g. rgba(..., var(--x, 1))) -> fallback to 1
|
|
195
|
-
const a = segmentParsedColor?.rgba?.[3];
|
|
196
|
-
const segmentOpacity = Math.min(typeof a === 'number' && a >= 0 && a <= 1 ? a : 1, element.opacity, 1);
|
|
197
|
-
doc.fillColor(segmentColor, segmentOpacity);
|
|
198
|
-
};
|
|
199
|
-
await renderSegmentsForLine(doc, element, line, renderSegments, context, textOptions, {
|
|
200
|
-
mode: 'fill',
|
|
201
|
-
heightOfLine: context.heightOfLine,
|
|
202
|
-
applySegmentColor,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
export function renderTextBackground(doc, element, verticalAlignmentOffset, textOptions) {
|
|
207
|
-
if (!element.backgroundEnabled) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const strippedContent = stripHtml(element.text).result;
|
|
211
|
-
const padding = element.backgroundPadding * (element.fontSize * element.lineHeight);
|
|
212
|
-
const cornerRadius = element.backgroundCornerRadius *
|
|
213
|
-
(element.fontSize * element.lineHeight * 0.5);
|
|
214
|
-
const textWidth = doc.widthOfString(strippedContent, {
|
|
215
|
-
...textOptions,
|
|
216
|
-
width: element.width,
|
|
217
|
-
});
|
|
218
|
-
const textHeight = doc.heightOfString(strippedContent, {
|
|
219
|
-
...textOptions,
|
|
220
|
-
width: element.width,
|
|
221
|
-
});
|
|
222
|
-
let bgX = -padding / 2;
|
|
223
|
-
let bgY = verticalAlignmentOffset - padding / 2;
|
|
224
|
-
const bgWidth = textWidth + padding;
|
|
225
|
-
const bgHeight = textHeight + padding;
|
|
226
|
-
// Adjust horizontal position based on text alignment
|
|
227
|
-
if (element.align === 'center') {
|
|
228
|
-
bgX = (element.width - textWidth) / 2 - padding / 2;
|
|
229
|
-
}
|
|
230
|
-
else if (element.align === 'right') {
|
|
231
|
-
bgX = element.width - textWidth - padding / 2;
|
|
232
|
-
}
|
|
233
|
-
doc.roundedRect(bgX, bgY, bgWidth, bgHeight, cornerRadius);
|
|
234
|
-
doc.fillColor(parseColor(element.backgroundColor).hex);
|
|
235
|
-
doc.fill();
|
|
236
|
-
doc.fillColor(parseColor(element.fill).hex, element.opacity);
|
|
237
|
-
}
|