@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.cjs
CHANGED
|
@@ -81,6 +81,52 @@ function escapeHtml(value) {
|
|
|
81
81
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// src/visual-font.ts
|
|
85
|
+
function visualFontAliases(pageNumber, fonts) {
|
|
86
|
+
return new Map(
|
|
87
|
+
fonts.filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
function visualFontFace(font, aliases) {
|
|
91
|
+
if (font.format !== "truetype") return "";
|
|
92
|
+
const alias = aliases.get(font.id);
|
|
93
|
+
if (!alias) return "";
|
|
94
|
+
const styles2 = visualFontStyles(font.family, alias).filter(
|
|
95
|
+
(style) => !style.startsWith("font-family:")
|
|
96
|
+
);
|
|
97
|
+
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
98
|
+
}
|
|
99
|
+
function visualFontStyles(fontFamily, alias) {
|
|
100
|
+
const normalized = fontFamily?.toLowerCase() ?? "";
|
|
101
|
+
const styles2 = [];
|
|
102
|
+
let fallback;
|
|
103
|
+
if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
|
|
104
|
+
fallback = "Courier New,Courier,monospace";
|
|
105
|
+
} else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
|
|
106
|
+
fallback = "Times New Roman,Times,serif";
|
|
107
|
+
} else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte|^mstt/.test(normalized)) {
|
|
108
|
+
fallback = "Arial,Helvetica,sans-serif";
|
|
109
|
+
}
|
|
110
|
+
if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
|
|
111
|
+
if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
|
|
112
|
+
if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
|
|
113
|
+
return styles2;
|
|
114
|
+
}
|
|
115
|
+
function base64(bytes) {
|
|
116
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
117
|
+
let output = "";
|
|
118
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
119
|
+
const first = bytes[index] ?? 0;
|
|
120
|
+
const second = bytes[index + 1] ?? 0;
|
|
121
|
+
const third = bytes[index + 2] ?? 0;
|
|
122
|
+
output += alphabet[first >> 2];
|
|
123
|
+
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
124
|
+
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
125
|
+
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
126
|
+
}
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
|
|
84
130
|
// src/semantic-media.ts
|
|
85
131
|
function semanticMedia(page) {
|
|
86
132
|
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
@@ -106,12 +152,51 @@ function vectorMedia(page) {
|
|
|
106
152
|
...fills.map(fillBounds)
|
|
107
153
|
]);
|
|
108
154
|
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
|
|
109
|
-
const
|
|
155
|
+
const aliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
156
|
+
const visualCodeFonts = new Set(
|
|
157
|
+
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
158
|
+
);
|
|
159
|
+
const visualSpans = page.visualSpans ?? page.spans;
|
|
160
|
+
const overlay = visualSpans.filter(
|
|
161
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
|
|
162
|
+
);
|
|
163
|
+
const consumedSpans = page.spans.filter(
|
|
164
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
|
|
165
|
+
);
|
|
166
|
+
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
167
|
+
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
110
168
|
return {
|
|
111
169
|
bounds,
|
|
112
|
-
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"
|
|
170
|
+
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>`,
|
|
171
|
+
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
113
172
|
};
|
|
114
173
|
}
|
|
174
|
+
function withoutSemanticMediaSpans(page, media) {
|
|
175
|
+
const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
|
|
176
|
+
return consumed.size > 0 ? { ...page, spans: page.spans.filter((span) => !consumed.has(span)) } : page;
|
|
177
|
+
}
|
|
178
|
+
function vectorText(span, pageHeight, aliases) {
|
|
179
|
+
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
180
|
+
const styles2 = [
|
|
181
|
+
cssColor(span.color) ? `fill:${span.color}` : "",
|
|
182
|
+
unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
|
|
183
|
+
...visualFontStyles(
|
|
184
|
+
span.fontFamily,
|
|
185
|
+
span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
|
|
186
|
+
)
|
|
187
|
+
].filter(Boolean).join(";");
|
|
188
|
+
const anchorY = pageHeight - span.bounds.y;
|
|
189
|
+
const transform = span.transform;
|
|
190
|
+
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)}"`;
|
|
191
|
+
const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
192
|
+
const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
|
|
193
|
+
return `<text${position} font-size="${number(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
|
|
194
|
+
}
|
|
195
|
+
function centerInside(inner, outer) {
|
|
196
|
+
const x = inner.x + inner.width / 2;
|
|
197
|
+
const y = inner.y + inner.height / 2;
|
|
198
|
+
return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
|
|
199
|
+
}
|
|
115
200
|
function vectorFill(fill) {
|
|
116
201
|
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
117
202
|
const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
@@ -197,20 +282,6 @@ function rgbBmp(image) {
|
|
|
197
282
|
}
|
|
198
283
|
return output;
|
|
199
284
|
}
|
|
200
|
-
function base64(bytes) {
|
|
201
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
202
|
-
let output = "";
|
|
203
|
-
for (let index = 0; index < bytes.length; index += 3) {
|
|
204
|
-
const first = bytes[index] ?? 0;
|
|
205
|
-
const second = bytes[index + 1] ?? 0;
|
|
206
|
-
const third = bytes[index + 2] ?? 0;
|
|
207
|
-
output += alphabet[first >> 2];
|
|
208
|
-
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
209
|
-
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
210
|
-
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
211
|
-
}
|
|
212
|
-
return output;
|
|
213
|
-
}
|
|
214
285
|
function safePath(value) {
|
|
215
286
|
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
216
287
|
}
|
|
@@ -226,6 +297,9 @@ function unitInterval(value) {
|
|
|
226
297
|
function number(value) {
|
|
227
298
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
228
299
|
}
|
|
300
|
+
function escapeHtml2(value) {
|
|
301
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
302
|
+
}
|
|
229
303
|
|
|
230
304
|
// src/semantic-document.ts
|
|
231
305
|
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
@@ -352,13 +426,13 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
352
426
|
if (block.type === "paragraph") {
|
|
353
427
|
if (isTitledRecord(block)) {
|
|
354
428
|
const [institution, ...details] = block.lines;
|
|
355
|
-
if (institution) await write(`<h3>${
|
|
356
|
-
for (const detail of details) await write(`<p>${
|
|
429
|
+
if (institution) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
|
|
430
|
+
for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
|
|
357
431
|
continue;
|
|
358
432
|
}
|
|
359
433
|
if (isUnmarkedList(block)) {
|
|
360
434
|
await write(
|
|
361
|
-
`<ul>${block.lines.map((line) => `<li>${
|
|
435
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
|
|
362
436
|
);
|
|
363
437
|
continue;
|
|
364
438
|
}
|
|
@@ -367,7 +441,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
367
441
|
}
|
|
368
442
|
if (block.type === "employment") {
|
|
369
443
|
await write(
|
|
370
|
-
`<section><h3>${
|
|
444
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
|
|
371
445
|
);
|
|
372
446
|
employmentOpen = true;
|
|
373
447
|
continue;
|
|
@@ -384,8 +458,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
384
458
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
385
459
|
};
|
|
386
460
|
for await (const page of pages) {
|
|
387
|
-
const
|
|
388
|
-
|
|
461
|
+
const media = semanticMedia(page);
|
|
462
|
+
const structured = (0, import_structure.structurePage)(withoutSemanticMediaSpans(page, media));
|
|
463
|
+
buffer.push({ width: page.width, height: page.height, structured, media });
|
|
389
464
|
stats.pagesProcessed += 1;
|
|
390
465
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
391
466
|
stats.peakBufferedLines = Math.max(
|
|
@@ -498,7 +573,7 @@ function sameRow(left, right) {
|
|
|
498
573
|
}
|
|
499
574
|
function tableRow(row, header) {
|
|
500
575
|
const cell = header ? "th" : "td";
|
|
501
|
-
return `<tr>${row.map((value) => `<${cell}>${
|
|
576
|
+
return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
|
|
502
577
|
}
|
|
503
578
|
function isFinancialSummary(block) {
|
|
504
579
|
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
@@ -508,22 +583,22 @@ function isNumericValue(value) {
|
|
|
508
583
|
}
|
|
509
584
|
function financialSummaryRow(entry, columns) {
|
|
510
585
|
const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
|
|
511
|
-
return `<tr><th scope="row"${colspan}>${
|
|
586
|
+
return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
|
|
512
587
|
}
|
|
513
588
|
function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
514
589
|
if (block.type === "heading")
|
|
515
590
|
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
|
|
516
591
|
if (block.type === "paragraph")
|
|
517
592
|
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
518
|
-
if (block.type === "preformatted") return `<pre>${
|
|
593
|
+
if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
|
|
519
594
|
if (block.type === "definitionList") {
|
|
520
595
|
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()))) {
|
|
521
596
|
return block.entries.map(
|
|
522
|
-
(entry) => `<section><h2>${
|
|
597
|
+
(entry) => `<section><h2>${escapeHtml3(titleCase(entry.term))}</h2><p>${escapeHtml3(entry.description)}</p></section>`
|
|
523
598
|
).join("");
|
|
524
599
|
}
|
|
525
600
|
const list = `<dl>${block.entries.map(
|
|
526
|
-
(entry) => `<div><dt>${
|
|
601
|
+
(entry) => `<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
|
|
527
602
|
).join("")}</dl>`;
|
|
528
603
|
return isFinancialSummary(block) ? `<section>${list}</section>` : list;
|
|
529
604
|
}
|
|
@@ -534,10 +609,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
534
609
|
return block.items.map(labeledSectionHtml).join("");
|
|
535
610
|
}
|
|
536
611
|
if (block.type === "employment") {
|
|
537
|
-
return `<section><h3>${
|
|
612
|
+
return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
|
|
538
613
|
}
|
|
539
614
|
const tag = block.ordered ? "ol" : "ul";
|
|
540
|
-
return `<${tag}>${block.items.map((item) => `<li>${
|
|
615
|
+
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml3(item.text)}</li>`).join("")}</${tag}>`;
|
|
541
616
|
}
|
|
542
617
|
function semanticBlockY(block) {
|
|
543
618
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
@@ -547,28 +622,28 @@ function cardTableRow(item) {
|
|
|
547
622
|
const trailing = item.details.at(-1) ?? "";
|
|
548
623
|
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
549
624
|
const description = item.details.slice(0, -1).join(" ");
|
|
550
|
-
const detail = description ? `<br><span>${
|
|
625
|
+
const detail = description ? `<br><span>${escapeHtml3(description)}</span>` : "";
|
|
551
626
|
const quantity = match?.[1] ?? "";
|
|
552
627
|
const amount = match?.[2] ?? trailing;
|
|
553
|
-
return `<tr><th scope="row">${
|
|
628
|
+
return `<tr><th scope="row">${escapeHtml3(item.title)}${detail}</th><td>${escapeHtml3(quantity)}</td><td>${escapeHtml3(amount)}</td></tr>`;
|
|
554
629
|
}
|
|
555
630
|
function labeledSectionHtml(item) {
|
|
556
631
|
const heading = titleCase(item.label);
|
|
557
632
|
const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
|
|
558
633
|
if (postal) {
|
|
559
634
|
const [name, ...address] = item.content;
|
|
560
|
-
const content = [name ? `<strong>${
|
|
561
|
-
return `<section><h2>${
|
|
635
|
+
const content = [name ? `<strong>${escapeHtml3(name)}</strong>` : "", ...address.map(escapeHtml3)].filter(Boolean).join("<br>");
|
|
636
|
+
return `<section><h2>${escapeHtml3(heading)}</h2><address>${content}</address></section>`;
|
|
562
637
|
}
|
|
563
|
-
return `<section><h2>${
|
|
564
|
-
(content, index) => `<p>${index === 0 ? `<strong>${
|
|
638
|
+
return `<section><h2>${escapeHtml3(heading)}</h2>${item.content.map(
|
|
639
|
+
(content, index) => `<p>${index === 0 ? `<strong>${escapeHtml3(content)}</strong>` : escapeHtml3(content)}</p>`
|
|
565
640
|
).join("")}</section>`;
|
|
566
641
|
}
|
|
567
642
|
function titleCase(value) {
|
|
568
643
|
const normalized = value.trim().toLocaleLowerCase("en");
|
|
569
644
|
return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
|
|
570
645
|
}
|
|
571
|
-
function
|
|
646
|
+
function escapeHtml3(value) {
|
|
572
647
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
573
648
|
}
|
|
574
649
|
|
|
@@ -582,7 +657,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
582
657
|
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
583
658
|
);
|
|
584
659
|
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
585
|
-
await write(`<title>${
|
|
660
|
+
await write(`<title>${escapeHtml4(options.title ?? "PDF document")}</title>`);
|
|
586
661
|
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
587
662
|
await write("</head><body>");
|
|
588
663
|
}
|
|
@@ -628,14 +703,14 @@ async function writePositionedPage(page, write, options) {
|
|
|
628
703
|
await write(
|
|
629
704
|
`<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">`
|
|
630
705
|
);
|
|
631
|
-
const fontAliases =
|
|
632
|
-
(page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
|
|
633
|
-
);
|
|
706
|
+
const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
634
707
|
const type3Fonts = new Map(
|
|
635
708
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
636
709
|
);
|
|
637
710
|
if ((options.includeStyles ?? true) && page.fonts?.length) {
|
|
638
|
-
await write(
|
|
711
|
+
await write(
|
|
712
|
+
`<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
713
|
+
);
|
|
639
714
|
}
|
|
640
715
|
await write(
|
|
641
716
|
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
|
|
@@ -709,7 +784,7 @@ function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
|
709
784
|
const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
|
|
710
785
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
711
786
|
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
712
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${
|
|
787
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
713
788
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
714
789
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
715
790
|
}
|
|
@@ -786,17 +861,17 @@ function positionedSpan(span, fontAliases) {
|
|
|
786
861
|
`font-size:${number2(span.fontSize)}pt`,
|
|
787
862
|
...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
|
|
788
863
|
...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
|
|
789
|
-
...
|
|
864
|
+
...visualFontStyles(
|
|
790
865
|
span.fontFamily,
|
|
791
866
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
792
867
|
)
|
|
793
868
|
].join(";");
|
|
794
|
-
return `<span class="pdf-span"${direction} style="${style}">${
|
|
869
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
|
|
795
870
|
}
|
|
796
871
|
async function writeFlowPage(page, write) {
|
|
797
|
-
const structured = (0, import_structure2.structurePage)(page);
|
|
798
|
-
const defaultColor = dominantTextColor(structured.lines);
|
|
799
872
|
const media = semanticMedia(page);
|
|
873
|
+
const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
|
|
874
|
+
const defaultColor = dominantTextColor(structured.lines);
|
|
800
875
|
let mediaIndex = 0;
|
|
801
876
|
await write(
|
|
802
877
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
@@ -817,39 +892,39 @@ async function writeFlowPage(page, write) {
|
|
|
817
892
|
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
|
|
818
893
|
);
|
|
819
894
|
} else if (block.type === "preformatted") {
|
|
820
|
-
await write(`<pre>${
|
|
895
|
+
await write(`<pre>${escapeHtml4(block.text)}</pre>`);
|
|
821
896
|
} else if (block.type === "definitionList") {
|
|
822
897
|
await write("<dl>");
|
|
823
898
|
for (const entry of block.entries) {
|
|
824
899
|
await write(
|
|
825
|
-
`<div><dt>${
|
|
900
|
+
`<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`
|
|
826
901
|
);
|
|
827
902
|
}
|
|
828
903
|
await write("</dl>");
|
|
829
904
|
} else if (block.type === "cardList") {
|
|
830
905
|
await write('<div class="pdf-semantic-cards">');
|
|
831
906
|
for (const item of block.items) {
|
|
832
|
-
await write(`<article><h3>${
|
|
833
|
-
for (const detail of item.details) await write(`<p>${
|
|
907
|
+
await write(`<article><h3>${escapeHtml4(item.title)}</h3>`);
|
|
908
|
+
for (const detail of item.details) await write(`<p>${escapeHtml4(detail)}</p>`);
|
|
834
909
|
await write("</article>");
|
|
835
910
|
}
|
|
836
911
|
await write("</div>");
|
|
837
912
|
} else if (block.type === "sectionGroup") {
|
|
838
913
|
await write('<div class="pdf-semantic-sections">');
|
|
839
914
|
for (const item of block.items) {
|
|
840
|
-
await write(`<section><h3>${
|
|
841
|
-
for (const content of item.content) await write(`<p>${
|
|
915
|
+
await write(`<section><h3>${escapeHtml4(item.label)}</h3>`);
|
|
916
|
+
for (const content of item.content) await write(`<p>${escapeHtml4(content)}</p>`);
|
|
842
917
|
await write("</section>");
|
|
843
918
|
}
|
|
844
919
|
await write("</div>");
|
|
845
920
|
} else if (block.type === "employment") {
|
|
846
921
|
await write(
|
|
847
|
-
`<section><h3>${
|
|
922
|
+
`<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
|
|
848
923
|
);
|
|
849
924
|
} else {
|
|
850
925
|
const tag = block.ordered ? "ol" : "ul";
|
|
851
926
|
await write(`<${tag}>`);
|
|
852
|
-
for (const item of block.items) await write(`<li>${
|
|
927
|
+
for (const item of block.items) await write(`<li>${escapeHtml4(item.text)}</li>`);
|
|
853
928
|
await write(`</${tag}>`);
|
|
854
929
|
}
|
|
855
930
|
}
|
|
@@ -867,7 +942,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
867
942
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
868
943
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
869
944
|
const direction = directionAttribute([span]);
|
|
870
|
-
const font =
|
|
945
|
+
const font = visualFontStyles(
|
|
871
946
|
span.fontFamily,
|
|
872
947
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
873
948
|
).join(";");
|
|
@@ -901,7 +976,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
901
976
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
902
977
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
903
978
|
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
|
|
904
|
-
return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${
|
|
979
|
+
return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
905
980
|
}
|
|
906
981
|
function isAdobeCjkFont(fontFamily) {
|
|
907
982
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -956,24 +1031,6 @@ function isUnitInterval(value) {
|
|
|
956
1031
|
function isSvgPath(value) {
|
|
957
1032
|
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
958
1033
|
}
|
|
959
|
-
function fontStyles(fontFamily, alias) {
|
|
960
|
-
const normalized = fontFamily?.toLowerCase() ?? "";
|
|
961
|
-
const styles2 = [];
|
|
962
|
-
let fallback;
|
|
963
|
-
if (/courier|mono|nimbusmono|^cmtt/.test(normalized)) {
|
|
964
|
-
fallback = "Courier New,Courier,monospace";
|
|
965
|
-
} else if (/times|minion|serif|baskerville|georgia|nimbusrom|guardian.*egyp|^cm[rs]y?\d/.test(normalized)) {
|
|
966
|
-
fallback = "Times New Roman,Times,serif";
|
|
967
|
-
} else if (/helvetica|arial|sans|nimbussan|calibre|myriad|panton|^tte/.test(normalized)) {
|
|
968
|
-
fallback = "Arial,Helvetica,sans-serif";
|
|
969
|
-
} else if (/^mstt/.test(normalized)) {
|
|
970
|
-
fallback = "Arial,Helvetica,sans-serif";
|
|
971
|
-
}
|
|
972
|
-
if (alias || fallback) styles2.push(`font-family:${[alias, fallback].filter(Boolean).join(",")}`);
|
|
973
|
-
if (/bold|black|semibold|demi|medi|^tte/.test(normalized)) styles2.push("font-weight:700");
|
|
974
|
-
if (/italic|oblique|slant|ital(?:$|[_-])/.test(normalized)) styles2.push("font-style:italic");
|
|
975
|
-
return styles2;
|
|
976
|
-
}
|
|
977
1034
|
function isMonospace(fontFamily) {
|
|
978
1035
|
return /courier|mono/i.test(fontFamily ?? "");
|
|
979
1036
|
}
|
|
@@ -985,29 +1042,6 @@ function hasNonIdentityTransform(transform) {
|
|
|
985
1042
|
const identity = [1, 0, 0, 1];
|
|
986
1043
|
return transform.some((value, index) => Math.abs(value - (identity[index] ?? 0)) > 1e-6);
|
|
987
1044
|
}
|
|
988
|
-
function fontFace(font, aliases) {
|
|
989
|
-
if (font.format !== "truetype") return "";
|
|
990
|
-
const alias = aliases.get(font.id);
|
|
991
|
-
if (!alias) return "";
|
|
992
|
-
const styles2 = fontStyles(font.family, alias).filter(
|
|
993
|
-
(style) => !style.startsWith("font-family:")
|
|
994
|
-
);
|
|
995
|
-
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
996
|
-
}
|
|
997
|
-
function base642(bytes) {
|
|
998
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
999
|
-
let output = "";
|
|
1000
|
-
for (let index = 0; index < bytes.length; index += 3) {
|
|
1001
|
-
const first = bytes[index] ?? 0;
|
|
1002
|
-
const second = bytes[index + 1] ?? 0;
|
|
1003
|
-
const third = bytes[index + 2] ?? 0;
|
|
1004
|
-
output += alphabet[first >> 2];
|
|
1005
|
-
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
1006
|
-
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
1007
|
-
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
1008
|
-
}
|
|
1009
|
-
return output;
|
|
1010
|
-
}
|
|
1011
1045
|
function directionAttribute(spans) {
|
|
1012
1046
|
const rtl = spans.filter((span) => span.direction === "rtl").length;
|
|
1013
1047
|
const vertical = spans.filter((span) => span.direction === "ttb").length;
|
|
@@ -1018,9 +1052,9 @@ function number2(value) {
|
|
|
1018
1052
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
1019
1053
|
}
|
|
1020
1054
|
function escapeAttribute(value) {
|
|
1021
|
-
return
|
|
1055
|
+
return escapeHtml4(value).replaceAll("`", "`");
|
|
1022
1056
|
}
|
|
1023
|
-
function
|
|
1057
|
+
function escapeHtml4(value) {
|
|
1024
1058
|
return [...value].map((character) => {
|
|
1025
1059
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
1026
1060
|
if (codePoint === 13) return "\n";
|