@boxpdf/html-writer 0.1.13 → 0.1.15
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/index.cjs +133 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +133 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -58,6 +58,52 @@ function escapeHtml(value) {
|
|
|
58
58
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// src/visual-font.ts
|
|
62
|
+
function visualFontAliases(pageNumber, fonts) {
|
|
63
|
+
return new Map(
|
|
64
|
+
fonts.filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function visualFontFace(font, aliases) {
|
|
68
|
+
if (font.format !== "truetype") return "";
|
|
69
|
+
const alias = aliases.get(font.id);
|
|
70
|
+
if (!alias) return "";
|
|
71
|
+
const styles2 = visualFontStyles(font.family, alias).filter(
|
|
72
|
+
(style) => !style.startsWith("font-family:")
|
|
73
|
+
);
|
|
74
|
+
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
75
|
+
}
|
|
76
|
+
function visualFontStyles(fontFamily, alias) {
|
|
77
|
+
const normalized = fontFamily?.toLowerCase() ?? "";
|
|
78
|
+
const styles2 = [];
|
|
79
|
+
let fallback;
|
|
80
|
+
if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
|
|
81
|
+
fallback = "Courier New,Courier,monospace";
|
|
82
|
+
} else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
|
|
83
|
+
fallback = "Times New Roman,Times,serif";
|
|
84
|
+
} else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte|^mstt/.test(normalized)) {
|
|
85
|
+
fallback = "Arial,Helvetica,sans-serif";
|
|
86
|
+
}
|
|
87
|
+
if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
|
|
88
|
+
if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
|
|
89
|
+
if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
|
|
90
|
+
return styles2;
|
|
91
|
+
}
|
|
92
|
+
function base64(bytes) {
|
|
93
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
94
|
+
let output = "";
|
|
95
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
96
|
+
const first = bytes[index] ?? 0;
|
|
97
|
+
const second = bytes[index + 1] ?? 0;
|
|
98
|
+
const third = bytes[index + 2] ?? 0;
|
|
99
|
+
output += alphabet[first >> 2];
|
|
100
|
+
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
101
|
+
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
102
|
+
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
103
|
+
}
|
|
104
|
+
return output;
|
|
105
|
+
}
|
|
106
|
+
|
|
61
107
|
// src/semantic-media.ts
|
|
62
108
|
function semanticMedia(page) {
|
|
63
109
|
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
@@ -83,12 +129,51 @@ function vectorMedia(page) {
|
|
|
83
129
|
...fills.map(fillBounds)
|
|
84
130
|
]);
|
|
85
131
|
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
|
|
86
|
-
const
|
|
132
|
+
const aliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
133
|
+
const visualCodeFonts = new Set(
|
|
134
|
+
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
135
|
+
);
|
|
136
|
+
const visualSpans = page.visualSpans ?? page.spans;
|
|
137
|
+
const overlay = visualSpans.filter(
|
|
138
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
|
|
139
|
+
);
|
|
140
|
+
const consumedSpans = page.spans.filter(
|
|
141
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
|
|
142
|
+
);
|
|
143
|
+
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
144
|
+
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
87
145
|
return {
|
|
88
146
|
bounds,
|
|
89
|
-
html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number(bounds.x)} ${number(page.height - bounds.y - bounds.height)} ${number(bounds.width)} ${number(bounds.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true"
|
|
147
|
+
html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number(bounds.x)} ${number(page.height - bounds.y - bounds.height)} ${number(bounds.width)} ${number(bounds.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}<g transform="translate(0 ${number(page.height)}) scale(1 -1)">${fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`,
|
|
148
|
+
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
90
149
|
};
|
|
91
150
|
}
|
|
151
|
+
function withoutSemanticMediaSpans(page, media) {
|
|
152
|
+
const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
|
|
153
|
+
return consumed.size > 0 ? { ...page, spans: page.spans.filter((span) => !consumed.has(span)) } : page;
|
|
154
|
+
}
|
|
155
|
+
function vectorText(span, pageHeight, aliases) {
|
|
156
|
+
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
157
|
+
const styles2 = [
|
|
158
|
+
cssColor(span.color) ? `fill:${span.color}` : "",
|
|
159
|
+
unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
|
|
160
|
+
...visualFontStyles(
|
|
161
|
+
span.fontFamily,
|
|
162
|
+
span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
|
|
163
|
+
)
|
|
164
|
+
].filter(Boolean).join(";");
|
|
165
|
+
const anchorY = pageHeight - span.bounds.y;
|
|
166
|
+
const transform = span.transform;
|
|
167
|
+
const position = transform ? ` x="0" y="0" transform="matrix(${transform.map(number).join(" ")} ${number(span.bounds.x)} ${number(anchorY)})"` : ` x="${number(span.bounds.x)}" y="${number(anchorY)}"`;
|
|
168
|
+
const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
169
|
+
const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
|
|
170
|
+
return `<text${position} font-size="${number(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
|
|
171
|
+
}
|
|
172
|
+
function centerInside(inner, outer) {
|
|
173
|
+
const x = inner.x + inner.width / 2;
|
|
174
|
+
const y = inner.y + inner.height / 2;
|
|
175
|
+
return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
|
|
176
|
+
}
|
|
92
177
|
function vectorFill(fill) {
|
|
93
178
|
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
94
179
|
const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
@@ -174,20 +259,6 @@ function rgbBmp(image) {
|
|
|
174
259
|
}
|
|
175
260
|
return output;
|
|
176
261
|
}
|
|
177
|
-
function base64(bytes) {
|
|
178
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
179
|
-
let output = "";
|
|
180
|
-
for (let index = 0; index < bytes.length; index += 3) {
|
|
181
|
-
const first = bytes[index] ?? 0;
|
|
182
|
-
const second = bytes[index + 1] ?? 0;
|
|
183
|
-
const third = bytes[index + 2] ?? 0;
|
|
184
|
-
output += alphabet[first >> 2];
|
|
185
|
-
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
186
|
-
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
187
|
-
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
188
|
-
}
|
|
189
|
-
return output;
|
|
190
|
-
}
|
|
191
262
|
function safePath(value) {
|
|
192
263
|
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
193
264
|
}
|
|
@@ -203,6 +274,9 @@ function unitInterval(value) {
|
|
|
203
274
|
function number(value) {
|
|
204
275
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
205
276
|
}
|
|
277
|
+
function escapeHtml2(value) {
|
|
278
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
279
|
+
}
|
|
206
280
|
|
|
207
281
|
// src/semantic-document.ts
|
|
208
282
|
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
@@ -329,13 +403,13 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
329
403
|
if (block.type === "paragraph") {
|
|
330
404
|
if (isTitledRecord(block)) {
|
|
331
405
|
const [institution, ...details] = block.lines;
|
|
332
|
-
if (institution) await write(`<h3>${
|
|
333
|
-
for (const detail of details) await write(`<p>${
|
|
406
|
+
if (institution) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
|
|
407
|
+
for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
|
|
334
408
|
continue;
|
|
335
409
|
}
|
|
336
410
|
if (isUnmarkedList(block)) {
|
|
337
411
|
await write(
|
|
338
|
-
`<ul>${block.lines.map((line) => `<li>${
|
|
412
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
|
|
339
413
|
);
|
|
340
414
|
continue;
|
|
341
415
|
}
|
|
@@ -344,7 +418,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
344
418
|
}
|
|
345
419
|
if (block.type === "employment") {
|
|
346
420
|
await write(
|
|
347
|
-
`<section><h3>${
|
|
421
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
|
|
348
422
|
);
|
|
349
423
|
employmentOpen = true;
|
|
350
424
|
continue;
|
|
@@ -361,8 +435,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
361
435
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
362
436
|
};
|
|
363
437
|
for await (const page of pages) {
|
|
364
|
-
const
|
|
365
|
-
|
|
438
|
+
const media = semanticMedia(page);
|
|
439
|
+
const structured = structurePage(withoutSemanticMediaSpans(page, media));
|
|
440
|
+
buffer.push({ width: page.width, height: page.height, structured, media });
|
|
366
441
|
stats.pagesProcessed += 1;
|
|
367
442
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
368
443
|
stats.peakBufferedLines = Math.max(
|
|
@@ -475,7 +550,7 @@ function sameRow(left, right) {
|
|
|
475
550
|
}
|
|
476
551
|
function tableRow(row, header) {
|
|
477
552
|
const cell = header ? "th" : "td";
|
|
478
|
-
return `<tr>${row.map((value) => `<${cell}>${
|
|
553
|
+
return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
|
|
479
554
|
}
|
|
480
555
|
function isFinancialSummary(block) {
|
|
481
556
|
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
@@ -485,22 +560,22 @@ function isNumericValue(value) {
|
|
|
485
560
|
}
|
|
486
561
|
function financialSummaryRow(entry, columns) {
|
|
487
562
|
const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
|
|
488
|
-
return `<tr><th scope="row"${colspan}>${
|
|
563
|
+
return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
|
|
489
564
|
}
|
|
490
565
|
function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
491
566
|
if (block.type === "heading")
|
|
492
567
|
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
|
|
493
568
|
if (block.type === "paragraph")
|
|
494
569
|
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
495
|
-
if (block.type === "preformatted") return `<pre>${
|
|
570
|
+
if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
|
|
496
571
|
if (block.type === "definitionList") {
|
|
497
572
|
if (block.entries.length <= 3 && block.entries.some((entry) => entry.description.trim().split(/\s+/).length >= 5) && block.entries.every((entry) => /^[A-Z][A-Z\s/-]*$/.test(entry.term.trim()))) {
|
|
498
573
|
return block.entries.map(
|
|
499
|
-
(entry) => `<section><h2>${
|
|
574
|
+
(entry) => `<section><h2>${escapeHtml3(titleCase(entry.term))}</h2><p>${escapeHtml3(entry.description)}</p></section>`
|
|
500
575
|
).join("");
|
|
501
576
|
}
|
|
502
577
|
const list = `<dl>${block.entries.map(
|
|
503
|
-
(entry) => `<div><dt>${
|
|
578
|
+
(entry) => `<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
|
|
504
579
|
).join("")}</dl>`;
|
|
505
580
|
return isFinancialSummary(block) ? `<section>${list}</section>` : list;
|
|
506
581
|
}
|
|
@@ -511,10 +586,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
511
586
|
return block.items.map(labeledSectionHtml).join("");
|
|
512
587
|
}
|
|
513
588
|
if (block.type === "employment") {
|
|
514
|
-
return `<section><h3>${
|
|
589
|
+
return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
|
|
515
590
|
}
|
|
516
591
|
const tag = block.ordered ? "ol" : "ul";
|
|
517
|
-
return `<${tag}>${block.items.map((item) => `<li>${
|
|
592
|
+
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml3(item.text)}</li>`).join("")}</${tag}>`;
|
|
518
593
|
}
|
|
519
594
|
function semanticBlockY(block) {
|
|
520
595
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
@@ -524,28 +599,28 @@ function cardTableRow(item) {
|
|
|
524
599
|
const trailing = item.details.at(-1) ?? "";
|
|
525
600
|
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
526
601
|
const description = item.details.slice(0, -1).join(" ");
|
|
527
|
-
const detail = description ? `<br><span>${
|
|
602
|
+
const detail = description ? `<br><span>${escapeHtml3(description)}</span>` : "";
|
|
528
603
|
const quantity = match?.[1] ?? "";
|
|
529
604
|
const amount = match?.[2] ?? trailing;
|
|
530
|
-
return `<tr><th scope="row">${
|
|
605
|
+
return `<tr><th scope="row">${escapeHtml3(item.title)}${detail}</th><td>${escapeHtml3(quantity)}</td><td>${escapeHtml3(amount)}</td></tr>`;
|
|
531
606
|
}
|
|
532
607
|
function labeledSectionHtml(item) {
|
|
533
608
|
const heading = titleCase(item.label);
|
|
534
609
|
const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
|
|
535
610
|
if (postal) {
|
|
536
611
|
const [name, ...address] = item.content;
|
|
537
|
-
const content = [name ? `<strong>${
|
|
538
|
-
return `<section><h2>${
|
|
612
|
+
const content = [name ? `<strong>${escapeHtml3(name)}</strong>` : "", ...address.map(escapeHtml3)].filter(Boolean).join("<br>");
|
|
613
|
+
return `<section><h2>${escapeHtml3(heading)}</h2><address>${content}</address></section>`;
|
|
539
614
|
}
|
|
540
|
-
return `<section><h2>${
|
|
541
|
-
(content, index) => `<p>${index === 0 ? `<strong>${
|
|
615
|
+
return `<section><h2>${escapeHtml3(heading)}</h2>${item.content.map(
|
|
616
|
+
(content, index) => `<p>${index === 0 ? `<strong>${escapeHtml3(content)}</strong>` : escapeHtml3(content)}</p>`
|
|
542
617
|
).join("")}</section>`;
|
|
543
618
|
}
|
|
544
619
|
function titleCase(value) {
|
|
545
620
|
const normalized = value.trim().toLocaleLowerCase("en");
|
|
546
621
|
return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
|
|
547
622
|
}
|
|
548
|
-
function
|
|
623
|
+
function escapeHtml3(value) {
|
|
549
624
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
550
625
|
}
|
|
551
626
|
|
|
@@ -559,7 +634,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
559
634
|
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
560
635
|
);
|
|
561
636
|
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
562
|
-
await write(`<title>${
|
|
637
|
+
await write(`<title>${escapeHtml4(options.title ?? "PDF document")}</title>`);
|
|
563
638
|
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
564
639
|
await write("</head><body>");
|
|
565
640
|
}
|
|
@@ -605,14 +680,14 @@ async function writePositionedPage(page, write, options) {
|
|
|
605
680
|
await write(
|
|
606
681
|
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number2(displayWidth)}pt;height:${number2(displayHeight)}pt">`
|
|
607
682
|
);
|
|
608
|
-
const fontAliases =
|
|
609
|
-
(page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
|
|
610
|
-
);
|
|
683
|
+
const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
611
684
|
const type3Fonts = new Map(
|
|
612
685
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
613
686
|
);
|
|
614
687
|
if ((options.includeStyles ?? true) && page.fonts?.length) {
|
|
615
|
-
await write(
|
|
688
|
+
await write(
|
|
689
|
+
`<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
690
|
+
);
|
|
616
691
|
}
|
|
617
692
|
await write(
|
|
618
693
|
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
|
|
@@ -686,7 +761,7 @@ function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
|
686
761
|
const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
|
|
687
762
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
688
763
|
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
689
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${
|
|
764
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
690
765
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
691
766
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
692
767
|
}
|
|
@@ -763,17 +838,17 @@ function positionedSpan(span, fontAliases) {
|
|
|
763
838
|
`font-size:${number2(span.fontSize)}pt`,
|
|
764
839
|
...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
|
|
765
840
|
...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
|
|
766
|
-
...
|
|
841
|
+
...visualFontStyles(
|
|
767
842
|
span.fontFamily,
|
|
768
843
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
769
844
|
)
|
|
770
845
|
].join(";");
|
|
771
|
-
return `<span class="pdf-span"${direction} style="${style}">${
|
|
846
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
|
|
772
847
|
}
|
|
773
848
|
async function writeFlowPage(page, write) {
|
|
774
|
-
const structured = structurePage2(page);
|
|
775
|
-
const defaultColor = dominantTextColor(structured.lines);
|
|
776
849
|
const media = semanticMedia(page);
|
|
850
|
+
const structured = structurePage2(withoutSemanticMediaSpans(page, media));
|
|
851
|
+
const defaultColor = dominantTextColor(structured.lines);
|
|
777
852
|
let mediaIndex = 0;
|
|
778
853
|
await write(
|
|
779
854
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
@@ -794,39 +869,39 @@ async function writeFlowPage(page, write) {
|
|
|
794
869
|
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
|
|
795
870
|
);
|
|
796
871
|
} else if (block.type === "preformatted") {
|
|
797
|
-
await write(`<pre>${
|
|
872
|
+
await write(`<pre>${escapeHtml4(block.text)}</pre>`);
|
|
798
873
|
} else if (block.type === "definitionList") {
|
|
799
874
|
await write("<dl>");
|
|
800
875
|
for (const entry of block.entries) {
|
|
801
876
|
await write(
|
|
802
|
-
`<div><dt>${
|
|
877
|
+
`<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`
|
|
803
878
|
);
|
|
804
879
|
}
|
|
805
880
|
await write("</dl>");
|
|
806
881
|
} else if (block.type === "cardList") {
|
|
807
882
|
await write('<div class="pdf-semantic-cards">');
|
|
808
883
|
for (const item of block.items) {
|
|
809
|
-
await write(`<article><h3>${
|
|
810
|
-
for (const detail of item.details) await write(`<p>${
|
|
884
|
+
await write(`<article><h3>${escapeHtml4(item.title)}</h3>`);
|
|
885
|
+
for (const detail of item.details) await write(`<p>${escapeHtml4(detail)}</p>`);
|
|
811
886
|
await write("</article>");
|
|
812
887
|
}
|
|
813
888
|
await write("</div>");
|
|
814
889
|
} else if (block.type === "sectionGroup") {
|
|
815
890
|
await write('<div class="pdf-semantic-sections">');
|
|
816
891
|
for (const item of block.items) {
|
|
817
|
-
await write(`<section><h3>${
|
|
818
|
-
for (const content of item.content) await write(`<p>${
|
|
892
|
+
await write(`<section><h3>${escapeHtml4(item.label)}</h3>`);
|
|
893
|
+
for (const content of item.content) await write(`<p>${escapeHtml4(content)}</p>`);
|
|
819
894
|
await write("</section>");
|
|
820
895
|
}
|
|
821
896
|
await write("</div>");
|
|
822
897
|
} else if (block.type === "employment") {
|
|
823
898
|
await write(
|
|
824
|
-
`<section><h3>${
|
|
899
|
+
`<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
|
|
825
900
|
);
|
|
826
901
|
} else {
|
|
827
902
|
const tag = block.ordered ? "ol" : "ul";
|
|
828
903
|
await write(`<${tag}>`);
|
|
829
|
-
for (const item of block.items) await write(`<li>${
|
|
904
|
+
for (const item of block.items) await write(`<li>${escapeHtml4(item.text)}</li>`);
|
|
830
905
|
await write(`</${tag}>`);
|
|
831
906
|
}
|
|
832
907
|
}
|
|
@@ -844,7 +919,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
844
919
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
845
920
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
846
921
|
const direction = directionAttribute([span]);
|
|
847
|
-
const font =
|
|
922
|
+
const font = visualFontStyles(
|
|
848
923
|
span.fontFamily,
|
|
849
924
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
850
925
|
).join(";");
|
|
@@ -878,7 +953,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
878
953
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
879
954
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
880
955
|
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
|
|
881
|
-
return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${
|
|
956
|
+
return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
882
957
|
}
|
|
883
958
|
function isAdobeCjkFont(fontFamily) {
|
|
884
959
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -933,24 +1008,6 @@ function isUnitInterval(value) {
|
|
|
933
1008
|
function isSvgPath(value) {
|
|
934
1009
|
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
935
1010
|
}
|
|
936
|
-
function fontStyles(fontFamily, alias) {
|
|
937
|
-
const normalized = fontFamily?.toLowerCase() ?? "";
|
|
938
|
-
const styles2 = [];
|
|
939
|
-
let fallback;
|
|
940
|
-
if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
|
|
941
|
-
fallback = "Courier New,Courier,monospace";
|
|
942
|
-
} else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
|
|
943
|
-
fallback = "Times New Roman,Times,serif";
|
|
944
|
-
} else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte/.test(normalized)) {
|
|
945
|
-
fallback = "Arial,Helvetica,sans-serif";
|
|
946
|
-
} else if (/^mstt/.test(normalized)) {
|
|
947
|
-
fallback = "Arial,Helvetica,sans-serif";
|
|
948
|
-
}
|
|
949
|
-
if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
|
|
950
|
-
if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
|
|
951
|
-
if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
|
|
952
|
-
return styles2;
|
|
953
|
-
}
|
|
954
1011
|
function isMonospace(fontFamily) {
|
|
955
1012
|
return /courier|mono/i.test(fontFamily ?? "");
|
|
956
1013
|
}
|
|
@@ -962,29 +1019,6 @@ function hasNonIdentityTransform(transform) {
|
|
|
962
1019
|
const identity = [1, 0, 0, 1];
|
|
963
1020
|
return transform.some((value, index) => Math.abs(value - (identity[index] ?? 0)) > 1e-6);
|
|
964
1021
|
}
|
|
965
|
-
function fontFace(font, aliases) {
|
|
966
|
-
if (font.format !== "truetype") return "";
|
|
967
|
-
const alias = aliases.get(font.id);
|
|
968
|
-
if (!alias) return "";
|
|
969
|
-
const styles2 = fontStyles(font.family, alias).filter(
|
|
970
|
-
(style) => !style.startsWith("font-family:")
|
|
971
|
-
);
|
|
972
|
-
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
973
|
-
}
|
|
974
|
-
function base642(bytes) {
|
|
975
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
976
|
-
let output = "";
|
|
977
|
-
for (let index = 0; index < bytes.length; index += 3) {
|
|
978
|
-
const first = bytes[index] ?? 0;
|
|
979
|
-
const second = bytes[index + 1] ?? 0;
|
|
980
|
-
const third = bytes[index + 2] ?? 0;
|
|
981
|
-
output += alphabet[first >> 2];
|
|
982
|
-
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
983
|
-
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
984
|
-
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
985
|
-
}
|
|
986
|
-
return output;
|
|
987
|
-
}
|
|
988
1022
|
function directionAttribute(spans) {
|
|
989
1023
|
const rtl = spans.filter((span) => span.direction === "rtl").length;
|
|
990
1024
|
const vertical = spans.filter((span) => span.direction === "ttb").length;
|
|
@@ -995,9 +1029,9 @@ function number2(value) {
|
|
|
995
1029
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
996
1030
|
}
|
|
997
1031
|
function escapeAttribute(value) {
|
|
998
|
-
return
|
|
1032
|
+
return escapeHtml4(value).replaceAll("`", "`");
|
|
999
1033
|
}
|
|
1000
|
-
function
|
|
1034
|
+
function escapeHtml4(value) {
|
|
1001
1035
|
return [...value].map((character) => {
|
|
1002
1036
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
1003
1037
|
if (codePoint === 13) return "\n";
|