@boxpdf/html-writer 0.1.19 → 0.1.21
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 +45 -8
- package/dist/index.cjs +522 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +521 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -195,6 +195,12 @@ function dominantTextColor(lines) {
|
|
|
195
195
|
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
196
196
|
}
|
|
197
197
|
function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
198
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "html");
|
|
199
|
+
}
|
|
200
|
+
function semanticTextMarkdown(text, lines, defaultColor, preserveWeight = true) {
|
|
201
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "markdown");
|
|
202
|
+
}
|
|
203
|
+
function semanticText(text, lines, defaultColor, preserveWeight, format) {
|
|
198
204
|
const ranges = [];
|
|
199
205
|
let cursor = 0;
|
|
200
206
|
for (const span of lines.flatMap((line) => line.spans)) {
|
|
@@ -220,11 +226,11 @@ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
|
220
226
|
let html = "";
|
|
221
227
|
let offset = 0;
|
|
222
228
|
for (const range of merged) {
|
|
223
|
-
html +=
|
|
224
|
-
html +=
|
|
229
|
+
html += escapeText(text.slice(offset, range.start), format);
|
|
230
|
+
html += styledText(text.slice(range.start, range.end), range, format);
|
|
225
231
|
offset = range.end;
|
|
226
232
|
}
|
|
227
|
-
return html +
|
|
233
|
+
return html + escapeText(text.slice(offset), format);
|
|
228
234
|
}
|
|
229
235
|
function mergeRanges(ranges, text) {
|
|
230
236
|
const merged = [];
|
|
@@ -238,13 +244,19 @@ function mergeRanges(ranges, text) {
|
|
|
238
244
|
}
|
|
239
245
|
return merged;
|
|
240
246
|
}
|
|
241
|
-
function
|
|
242
|
-
let html =
|
|
247
|
+
function styledText(value, range, format) {
|
|
248
|
+
let html = escapeText(value, format);
|
|
243
249
|
if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
|
|
244
|
-
if (range.italic) html = `<em>${html}</em
|
|
245
|
-
if (range.bold) html = `<strong>${html}</strong
|
|
250
|
+
if (range.italic) html = format === "html" ? `<em>${html}</em>` : `_${html}_`;
|
|
251
|
+
if (range.bold) html = format === "html" ? `<strong>${html}</strong>` : `**${html}**`;
|
|
246
252
|
return html;
|
|
247
253
|
}
|
|
254
|
+
function escapeText(value, format) {
|
|
255
|
+
return format === "html" ? escapeHtml(value) : escapeMarkdown(value);
|
|
256
|
+
}
|
|
257
|
+
function escapeMarkdown(value) {
|
|
258
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
259
|
+
}
|
|
248
260
|
function normalizedColor(value) {
|
|
249
261
|
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
250
262
|
const color = value.toLowerCase();
|
|
@@ -380,23 +392,39 @@ function base64(bytes) {
|
|
|
380
392
|
}
|
|
381
393
|
|
|
382
394
|
// src/semantic-media.ts
|
|
383
|
-
function semanticMedia(page) {
|
|
384
|
-
|
|
385
|
-
output
|
|
395
|
+
function semanticMedia(page, imageOptions = "embedded") {
|
|
396
|
+
if (imageOptions === "excluded") return [];
|
|
397
|
+
const output = (page.images ?? []).map(
|
|
398
|
+
(image, index) => rasterMedia(image, page.number, index, imageOptions)
|
|
399
|
+
);
|
|
400
|
+
output.push(...vectorMedia(page, imageOptions));
|
|
386
401
|
return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
|
|
387
402
|
}
|
|
388
|
-
function
|
|
403
|
+
async function prepareSemanticMedia(page, imageOptions, onImage) {
|
|
404
|
+
const media = semanticMedia(page, imageOptions);
|
|
405
|
+
for (const item of media) {
|
|
406
|
+
for (const asset of item.assets ?? []) await onImage?.(asset);
|
|
407
|
+
delete item.assets;
|
|
408
|
+
}
|
|
409
|
+
return media;
|
|
410
|
+
}
|
|
411
|
+
function rasterMedia(image, pageNumber, index, imageOptions) {
|
|
389
412
|
const bounds2 = transformedUnitBounds(image.transform);
|
|
390
413
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
391
414
|
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
415
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
416
|
+
const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
|
|
417
|
+
const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
|
|
392
418
|
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
393
419
|
return {
|
|
394
420
|
bounds: bounds2,
|
|
395
421
|
kind: "raster",
|
|
396
|
-
html: `<img class="pdf-semantic-media" src="
|
|
422
|
+
html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
|
|
423
|
+
markdown: ``,
|
|
424
|
+
...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
|
|
397
425
|
};
|
|
398
426
|
}
|
|
399
|
-
function vectorMedia(page) {
|
|
427
|
+
function vectorMedia(page, imageOptions) {
|
|
400
428
|
const primitives = [
|
|
401
429
|
...(page.paths ?? []).flatMap((path, index) => {
|
|
402
430
|
const bounds2 = vectorPathBounds(path);
|
|
@@ -414,7 +442,7 @@ function vectorMedia(page) {
|
|
|
414
442
|
const visualCodeFonts = new Set(
|
|
415
443
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
416
444
|
);
|
|
417
|
-
return components.map((component) => {
|
|
445
|
+
return components.map((component, componentIndex) => {
|
|
418
446
|
const bounds2 = component.bounds;
|
|
419
447
|
const paths = component.primitives.flatMap(
|
|
420
448
|
(primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
|
|
@@ -431,10 +459,18 @@ function vectorMedia(page) {
|
|
|
431
459
|
);
|
|
432
460
|
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
433
461
|
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
462
|
+
const svg = `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number2(bounds2.x)} ${number2(page.height - bounds2.y - bounds2.height)} ${number2(bounds2.width)} ${number2(bounds2.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}${paths.length ? `<defs>${vectorPathClipDefinitions(paths, page.number)}</defs>` : ""}<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">${fills.map(vectorFillSvg).join("") + paths.map(({ path, index }) => vectorPathSvg(path, page.number, index)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`;
|
|
463
|
+
const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
|
|
434
464
|
return {
|
|
435
465
|
bounds: bounds2,
|
|
436
466
|
kind: "vector",
|
|
437
|
-
html: `<
|
|
467
|
+
html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
|
|
468
|
+
markdown: imageOptions === "references" ? `` : svg,
|
|
469
|
+
...imageOptions === "references" ? {
|
|
470
|
+
assets: [
|
|
471
|
+
{ name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
|
|
472
|
+
]
|
|
473
|
+
} : {},
|
|
438
474
|
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
439
475
|
};
|
|
440
476
|
});
|
|
@@ -476,7 +512,9 @@ function compositeMedia(items) {
|
|
|
476
512
|
bounds: bounds2,
|
|
477
513
|
kind: "composite",
|
|
478
514
|
html: `<div class="pdf-semantic-media pdf-semantic-media-composite" style="position:relative;max-width:100%;width:${number2(bounds2.width)}px;aspect-ratio:${number2(bounds2.width)}/${number2(bounds2.height)}">${layers}</div>`,
|
|
479
|
-
|
|
515
|
+
markdown: items.map((item) => item.markdown).join("\n\n"),
|
|
516
|
+
consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
|
|
517
|
+
assets: items.flatMap((item) => item.assets ?? [])
|
|
480
518
|
};
|
|
481
519
|
}
|
|
482
520
|
function mediaPiecesTouch(left, right) {
|
|
@@ -615,7 +653,7 @@ function escapeHtml2(value) {
|
|
|
615
653
|
}
|
|
616
654
|
|
|
617
655
|
// src/semantic-document.ts
|
|
618
|
-
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
656
|
+
async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
|
|
619
657
|
const stats = {
|
|
620
658
|
pagesProcessed: 0,
|
|
621
659
|
peakBufferedPages: 0,
|
|
@@ -633,27 +671,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
633
671
|
let contentStarted = false;
|
|
634
672
|
let employmentOpen = false;
|
|
635
673
|
let pendingParagraph;
|
|
636
|
-
|
|
674
|
+
const markdown = format === "markdown";
|
|
675
|
+
const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
|
|
676
|
+
const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
|
|
677
|
+
await output('<article class="pdf-semantic-document">');
|
|
637
678
|
const closeTable = async () => {
|
|
638
679
|
if (!activeTable) return;
|
|
639
|
-
await
|
|
680
|
+
await output("</table>", "\n");
|
|
640
681
|
activeTable = void 0;
|
|
641
682
|
while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
|
|
642
683
|
};
|
|
643
684
|
const closeSections = async (minimumLevel = 0) => {
|
|
644
685
|
while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
|
|
645
|
-
await
|
|
686
|
+
await output("</section>");
|
|
646
687
|
sectionLevels.pop();
|
|
647
688
|
}
|
|
648
689
|
};
|
|
649
690
|
const flushPendingParagraph = async () => {
|
|
650
691
|
if (!pendingParagraph) return;
|
|
651
|
-
await write(
|
|
692
|
+
await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
|
|
652
693
|
pendingParagraph = void 0;
|
|
653
694
|
};
|
|
654
695
|
const closeEmployment = async () => {
|
|
655
696
|
if (!employmentOpen) return;
|
|
656
|
-
await
|
|
697
|
+
await output("</section>");
|
|
657
698
|
employmentOpen = false;
|
|
658
699
|
};
|
|
659
700
|
const emitPage = async (page, future) => {
|
|
@@ -681,7 +722,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
681
722
|
await flushPendingParagraph();
|
|
682
723
|
const item = page.media[mediaIndex];
|
|
683
724
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
684
|
-
const html2 =
|
|
725
|
+
const html2 = markdown ? `${item.markdown}
|
|
726
|
+
|
|
727
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
728
|
+
|
|
729
|
+
` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
685
730
|
if (activeTable) pendingMedia.push(html2);
|
|
686
731
|
else await write(html2);
|
|
687
732
|
emittedMedia.add(item);
|
|
@@ -690,7 +735,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
690
735
|
break;
|
|
691
736
|
}
|
|
692
737
|
if (item && captionedMedia.has(item)) break;
|
|
693
|
-
const html =
|
|
738
|
+
const html = markdown ? `${item?.markdown ?? ""}
|
|
739
|
+
|
|
740
|
+
` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
|
|
694
741
|
if (activeTable) pendingMedia.push(html);
|
|
695
742
|
else await write(html);
|
|
696
743
|
mediaIndex += 1;
|
|
@@ -698,7 +745,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
698
745
|
const associatedMedia = captions.get(block);
|
|
699
746
|
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
700
747
|
await flushPendingParagraph();
|
|
701
|
-
const html =
|
|
748
|
+
const html = markdown ? `${associatedMedia.markdown}
|
|
749
|
+
|
|
750
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
751
|
+
|
|
752
|
+
` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
702
753
|
if (activeTable) pendingMedia.push(html);
|
|
703
754
|
else await write(html);
|
|
704
755
|
emittedMedia.add(associatedMedia);
|
|
@@ -712,8 +763,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
712
763
|
await flushPendingParagraph();
|
|
713
764
|
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
714
765
|
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
715
|
-
await
|
|
716
|
-
`<header><h1>${
|
|
766
|
+
await output(
|
|
767
|
+
`<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
|
|
768
|
+
`# ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
769
|
+
|
|
770
|
+
`
|
|
717
771
|
);
|
|
718
772
|
headerOpen = true;
|
|
719
773
|
continue;
|
|
@@ -721,19 +775,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
721
775
|
if (headerOpen) {
|
|
722
776
|
if (block.type === "paragraph") {
|
|
723
777
|
const tag = isContactBlock(block) ? "address" : "p";
|
|
724
|
-
await
|
|
725
|
-
`<${tag}>${
|
|
778
|
+
await output(
|
|
779
|
+
`<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
|
|
780
|
+
`${inlineText(block.text, block.lines, defaultColor)}
|
|
781
|
+
|
|
782
|
+
`
|
|
726
783
|
);
|
|
727
784
|
headerHasParagraph = true;
|
|
728
785
|
continue;
|
|
729
786
|
}
|
|
730
787
|
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
731
|
-
await
|
|
732
|
-
`<h${block.level}>${
|
|
788
|
+
await output(
|
|
789
|
+
`<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
|
|
790
|
+
`${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
791
|
+
|
|
792
|
+
`
|
|
733
793
|
);
|
|
734
794
|
continue;
|
|
735
795
|
}
|
|
736
|
-
await
|
|
796
|
+
await output("</header>");
|
|
737
797
|
headerOpen = false;
|
|
738
798
|
contentStarted = true;
|
|
739
799
|
}
|
|
@@ -741,23 +801,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
741
801
|
const rows = tableToRows(block.table);
|
|
742
802
|
if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
|
|
743
803
|
const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
|
|
744
|
-
for (const row of continuationRows)
|
|
804
|
+
for (const row of continuationRows)
|
|
805
|
+
await write(markdown ? markdownTableRow(row) : tableRow(row, false));
|
|
745
806
|
activeTable.table = block.table;
|
|
746
807
|
stats.mergedTables += 1;
|
|
747
808
|
continue;
|
|
748
809
|
}
|
|
749
810
|
await closeTable();
|
|
750
811
|
const header = tableHeader(rows);
|
|
751
|
-
await
|
|
752
|
-
|
|
753
|
-
|
|
812
|
+
await output("<table>", markdownTableStart(rows, header));
|
|
813
|
+
const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
|
|
814
|
+
for (const [index, row] of markdownRows.entries())
|
|
815
|
+
await write(
|
|
816
|
+
markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
|
|
817
|
+
);
|
|
754
818
|
activeTable = { table: block.table, header };
|
|
755
819
|
continue;
|
|
756
820
|
}
|
|
757
821
|
if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
|
|
758
822
|
const columns = activeTable.table.columns.length;
|
|
759
|
-
await
|
|
760
|
-
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot
|
|
823
|
+
await output(
|
|
824
|
+
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
|
|
825
|
+
block.entries.map(
|
|
826
|
+
(entry) => markdownTableRow([
|
|
827
|
+
entry.term,
|
|
828
|
+
...Array(Math.max(0, columns - 2)).fill(""),
|
|
829
|
+
entry.description
|
|
830
|
+
])
|
|
831
|
+
).join("")
|
|
761
832
|
);
|
|
762
833
|
await closeTable();
|
|
763
834
|
continue;
|
|
@@ -766,8 +837,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
766
837
|
if (block.type === "heading") {
|
|
767
838
|
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
768
839
|
await closeSections(level);
|
|
769
|
-
await
|
|
770
|
-
`<section data-level="${level}"><h${level}>${
|
|
840
|
+
await output(
|
|
841
|
+
`<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
|
|
842
|
+
`${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
843
|
+
|
|
844
|
+
`
|
|
771
845
|
);
|
|
772
846
|
sectionLevels.push(level);
|
|
773
847
|
continue;
|
|
@@ -775,13 +849,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
775
849
|
if (block.type === "paragraph") {
|
|
776
850
|
if (isTitledRecord(block)) {
|
|
777
851
|
const [institution, ...details] = block.lines;
|
|
778
|
-
if (institution)
|
|
779
|
-
|
|
852
|
+
if (institution)
|
|
853
|
+
await output(
|
|
854
|
+
`<h3>${escapeHtml3(institution.text)}</h3>`,
|
|
855
|
+
`### ${escapeMarkdown2(institution.text)}
|
|
856
|
+
|
|
857
|
+
`
|
|
858
|
+
);
|
|
859
|
+
for (const detail of details)
|
|
860
|
+
await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
|
|
861
|
+
|
|
862
|
+
`);
|
|
780
863
|
continue;
|
|
781
864
|
}
|
|
782
865
|
if (isUnmarkedList(block)) {
|
|
783
|
-
await
|
|
784
|
-
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul
|
|
866
|
+
await output(
|
|
867
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
|
|
868
|
+
`${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
|
|
869
|
+
|
|
870
|
+
`
|
|
785
871
|
);
|
|
786
872
|
continue;
|
|
787
873
|
}
|
|
@@ -789,19 +875,28 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
789
875
|
continue;
|
|
790
876
|
}
|
|
791
877
|
if (block.type === "employment") {
|
|
792
|
-
await
|
|
793
|
-
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p
|
|
878
|
+
await output(
|
|
879
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
|
|
880
|
+
`### ${escapeMarkdown2(block.role)}
|
|
881
|
+
|
|
882
|
+
${escapeMarkdown2(block.organization)}
|
|
883
|
+
|
|
884
|
+
${escapeMarkdown2(block.date)}
|
|
885
|
+
|
|
886
|
+
`
|
|
794
887
|
);
|
|
795
888
|
employmentOpen = true;
|
|
796
889
|
continue;
|
|
797
890
|
}
|
|
798
|
-
await write(
|
|
891
|
+
await write(semanticBlockOutput(block, defaultColor, format));
|
|
799
892
|
}
|
|
800
893
|
while (mediaIndex < page.media.length) {
|
|
801
894
|
const item = page.media[mediaIndex];
|
|
802
895
|
if (item && !emittedMedia.has(item)) {
|
|
803
896
|
await flushPendingParagraph();
|
|
804
|
-
const html =
|
|
897
|
+
const html = markdown ? `${item.markdown}
|
|
898
|
+
|
|
899
|
+
` : `<div class="pdf-semantic-visual">${item.html}</div>`;
|
|
805
900
|
if (activeTable) pendingMedia.push(html);
|
|
806
901
|
else await write(html);
|
|
807
902
|
}
|
|
@@ -810,9 +905,10 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
810
905
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
811
906
|
};
|
|
812
907
|
for await (const page of pages) {
|
|
813
|
-
const media =
|
|
908
|
+
const media = await prepareSemanticMedia(page, imageOptions, onImage);
|
|
814
909
|
const structured = structurePage(withoutSemanticMediaSpans(page, media));
|
|
815
910
|
buffer.push({ width: page.width, height: page.height, structured, media });
|
|
911
|
+
restoreObservedHyphens(buffer);
|
|
816
912
|
stats.pagesProcessed += 1;
|
|
817
913
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
818
914
|
stats.peakBufferedLines = Math.max(
|
|
@@ -828,22 +924,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
828
924
|
const ready = buffer.shift();
|
|
829
925
|
if (ready) await emitPage(ready, buffer);
|
|
830
926
|
}
|
|
831
|
-
if (headerOpen) await
|
|
927
|
+
if (headerOpen) await output("</header>");
|
|
832
928
|
await closeTable();
|
|
833
929
|
await closeEmployment();
|
|
834
930
|
if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
|
|
835
931
|
await closeSections();
|
|
836
|
-
await
|
|
837
|
-
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer
|
|
932
|
+
await output(
|
|
933
|
+
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
|
|
934
|
+
`---
|
|
935
|
+
|
|
936
|
+
${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
|
|
838
937
|
);
|
|
839
938
|
pendingParagraph = void 0;
|
|
840
939
|
} else {
|
|
841
940
|
await flushPendingParagraph();
|
|
842
941
|
await closeSections();
|
|
843
942
|
}
|
|
844
|
-
await
|
|
943
|
+
await output("</article>");
|
|
845
944
|
return stats;
|
|
846
945
|
}
|
|
946
|
+
function restoreObservedHyphens(buffer) {
|
|
947
|
+
const terms = new Set(
|
|
948
|
+
buffer.flatMap(
|
|
949
|
+
(page) => page.structured.lines.flatMap(
|
|
950
|
+
(line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
);
|
|
954
|
+
for (const page of buffer) {
|
|
955
|
+
for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function restoreBlockHyphens(block, terms) {
|
|
959
|
+
const restore = (value) => restoreTextHyphens(value, terms);
|
|
960
|
+
if (block.type === "insetGroup") {
|
|
961
|
+
for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
|
|
962
|
+
} else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
|
|
963
|
+
block.text = restore(block.text);
|
|
964
|
+
} else if (block.type === "list") {
|
|
965
|
+
for (const item of block.items) item.text = restore(item.text);
|
|
966
|
+
} else if (block.type === "definitionList") {
|
|
967
|
+
for (const entry of block.entries) {
|
|
968
|
+
entry.term = restore(entry.term);
|
|
969
|
+
entry.description = restore(entry.description);
|
|
970
|
+
}
|
|
971
|
+
} else if (block.type === "cardList") {
|
|
972
|
+
for (const item of block.items) {
|
|
973
|
+
item.title = restore(item.title);
|
|
974
|
+
item.details = item.details.map(restore);
|
|
975
|
+
}
|
|
976
|
+
} else if (block.type === "sectionGroup") {
|
|
977
|
+
for (const item of block.items) {
|
|
978
|
+
item.label = restore(item.label);
|
|
979
|
+
item.content = item.content.map(restore);
|
|
980
|
+
}
|
|
981
|
+
} else if (block.type === "employment") {
|
|
982
|
+
block.role = restore(block.role);
|
|
983
|
+
block.organization = restore(block.organization);
|
|
984
|
+
block.date = restore(block.date);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function restoreTextHyphens(value, terms) {
|
|
988
|
+
let output = value;
|
|
989
|
+
for (const term of terms) {
|
|
990
|
+
const collapsed = term.replace(/[-‐‑]/gu, "");
|
|
991
|
+
if (collapsed === term || !output.includes(collapsed)) continue;
|
|
992
|
+
const pattern = new RegExp(
|
|
993
|
+
`(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
|
|
994
|
+
"gu"
|
|
995
|
+
);
|
|
996
|
+
output = output.replace(pattern, term);
|
|
997
|
+
}
|
|
998
|
+
return output;
|
|
999
|
+
}
|
|
1000
|
+
function escapeRegularExpression(value) {
|
|
1001
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1002
|
+
}
|
|
847
1003
|
function isContactBlock(block) {
|
|
848
1004
|
const text = block.text;
|
|
849
1005
|
const signals = [
|
|
@@ -927,6 +1083,20 @@ function tableRow(row, header) {
|
|
|
927
1083
|
const cell = header ? "th" : "td";
|
|
928
1084
|
return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
|
|
929
1085
|
}
|
|
1086
|
+
function markdownTableStart(rows, header) {
|
|
1087
|
+
const columns = rows[0]?.length ?? 0;
|
|
1088
|
+
if (columns === 0) return "";
|
|
1089
|
+
const heading = header ?? Array(columns).fill("");
|
|
1090
|
+
return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
|
|
1091
|
+
}
|
|
1092
|
+
function markdownTableRow(row, shouldEscape = true) {
|
|
1093
|
+
const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
|
|
1094
|
+
return `| ${cells.join(" | ")} |
|
|
1095
|
+
`;
|
|
1096
|
+
}
|
|
1097
|
+
function escapeMarkdownTableCell(value) {
|
|
1098
|
+
return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
|
|
1099
|
+
}
|
|
930
1100
|
function isFinancialSummary(block) {
|
|
931
1101
|
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
932
1102
|
}
|
|
@@ -974,6 +1144,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
974
1144
|
const tag = block.ordered ? "ol" : "ul";
|
|
975
1145
|
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
976
1146
|
}
|
|
1147
|
+
function semanticBlockOutput(block, defaultColor, format) {
|
|
1148
|
+
return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
|
|
1149
|
+
}
|
|
1150
|
+
function semanticBlockMarkdown(block, defaultColor = "#000000") {
|
|
1151
|
+
if (block.type === "insetGroup") {
|
|
1152
|
+
const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
|
|
1153
|
+
return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
|
|
1154
|
+
|
|
1155
|
+
`;
|
|
1156
|
+
}
|
|
1157
|
+
if (block.type === "table") {
|
|
1158
|
+
const rows = tableToRows(block.table);
|
|
1159
|
+
const header = tableHeader(rows);
|
|
1160
|
+
return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
|
|
1161
|
+
`;
|
|
1162
|
+
}
|
|
1163
|
+
if (block.type === "heading") {
|
|
1164
|
+
return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
|
|
1165
|
+
|
|
1166
|
+
`;
|
|
1167
|
+
}
|
|
1168
|
+
if (block.type === "paragraph") {
|
|
1169
|
+
return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
|
|
1170
|
+
|
|
1171
|
+
`;
|
|
1172
|
+
}
|
|
1173
|
+
if (block.type === "preformatted") {
|
|
1174
|
+
const fence = block.text.includes("```") ? "````" : "```";
|
|
1175
|
+
return `${fence}
|
|
1176
|
+
${block.text}
|
|
1177
|
+
${fence}
|
|
1178
|
+
|
|
1179
|
+
`;
|
|
1180
|
+
}
|
|
1181
|
+
if (block.type === "definitionList") {
|
|
1182
|
+
return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
|
|
1183
|
+
|
|
1184
|
+
`;
|
|
1185
|
+
}
|
|
1186
|
+
if (block.type === "cardList") {
|
|
1187
|
+
const rows = [
|
|
1188
|
+
["Item", "Quantity", "Amount"],
|
|
1189
|
+
...block.items.map((item) => {
|
|
1190
|
+
const trailing = item.details.at(-1) ?? "";
|
|
1191
|
+
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
1192
|
+
const detail = item.details.slice(0, -1).join(" ");
|
|
1193
|
+
return [
|
|
1194
|
+
`${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
1195
|
+
match?.[1] ?? "",
|
|
1196
|
+
match?.[2] ?? trailing
|
|
1197
|
+
];
|
|
1198
|
+
})
|
|
1199
|
+
];
|
|
1200
|
+
return `## Items ordered
|
|
1201
|
+
|
|
1202
|
+
${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
|
|
1203
|
+
`;
|
|
1204
|
+
}
|
|
1205
|
+
if (block.type === "sectionGroup") {
|
|
1206
|
+
return block.items.map(
|
|
1207
|
+
(item) => `## ${escapeMarkdown2(titleCase(item.label))}
|
|
1208
|
+
|
|
1209
|
+
${item.content.map(
|
|
1210
|
+
(content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
|
|
1211
|
+
).join("\n\n")}
|
|
1212
|
+
|
|
1213
|
+
`
|
|
1214
|
+
).join("");
|
|
1215
|
+
}
|
|
1216
|
+
if (block.type === "employment") {
|
|
1217
|
+
return `### ${escapeMarkdown2(block.role)}
|
|
1218
|
+
|
|
1219
|
+
${escapeMarkdown2(block.organization)}
|
|
1220
|
+
|
|
1221
|
+
${escapeMarkdown2(block.date)}
|
|
1222
|
+
|
|
1223
|
+
`;
|
|
1224
|
+
}
|
|
1225
|
+
return `${block.items.map(
|
|
1226
|
+
(item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
|
|
1227
|
+
).join("\n")}
|
|
1228
|
+
|
|
1229
|
+
`;
|
|
1230
|
+
}
|
|
977
1231
|
function semanticBlockY(block) {
|
|
978
1232
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
979
1233
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
@@ -1006,6 +1260,9 @@ function titleCase(value) {
|
|
|
1006
1260
|
function escapeHtml3(value) {
|
|
1007
1261
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1008
1262
|
}
|
|
1263
|
+
function escapeMarkdown2(value) {
|
|
1264
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
1265
|
+
}
|
|
1009
1266
|
|
|
1010
1267
|
// src/index.ts
|
|
1011
1268
|
var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-semantic-document h1,.pdf-page--semantic h1{font-size:1.7em}.pdf-semantic-document h2,.pdf-page--semantic h2{font-size:1.5em}.pdf-semantic-document h3,.pdf-page--semantic h3{font-size:1.35em}.pdf-semantic-document h4,.pdf-page--semantic h4{font-size:1.1em}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
|
|
@@ -1022,9 +1279,18 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
1022
1279
|
await write("</head><body>");
|
|
1023
1280
|
}
|
|
1024
1281
|
await write('<main class="pdf-document">');
|
|
1025
|
-
|
|
1282
|
+
const profile = resolveProfile(options);
|
|
1283
|
+
const imageOptions = resolveImageOptions(profile, options);
|
|
1284
|
+
validateImageOptions(imageOptions, options);
|
|
1285
|
+
if (profile === "semantic") {
|
|
1026
1286
|
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
1027
|
-
const stats = await writeSemanticDocument(
|
|
1287
|
+
const stats = await writeSemanticDocument(
|
|
1288
|
+
pages,
|
|
1289
|
+
write,
|
|
1290
|
+
lookahead,
|
|
1291
|
+
imageOptions,
|
|
1292
|
+
options.onImage
|
|
1293
|
+
);
|
|
1028
1294
|
options.onSemanticStats?.(stats);
|
|
1029
1295
|
} else {
|
|
1030
1296
|
for await (const page of pages) await writePage(page, write, options);
|
|
@@ -1032,6 +1298,20 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
1032
1298
|
await write("</main>");
|
|
1033
1299
|
if (includeDocument) await write("</body></html>");
|
|
1034
1300
|
}
|
|
1301
|
+
async function writeMarkdownDocument(pages, write, options = {}) {
|
|
1302
|
+
const imageOptions = options.imageOptions ?? "excluded";
|
|
1303
|
+
validateImageOptions(imageOptions, options);
|
|
1304
|
+
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
1305
|
+
const stats = await writeSemanticDocument(
|
|
1306
|
+
pages,
|
|
1307
|
+
write,
|
|
1308
|
+
lookahead,
|
|
1309
|
+
imageOptions,
|
|
1310
|
+
options.onImage,
|
|
1311
|
+
"markdown"
|
|
1312
|
+
);
|
|
1313
|
+
options.onSemanticStats?.(stats);
|
|
1314
|
+
}
|
|
1035
1315
|
function semanticLookahead(value) {
|
|
1036
1316
|
const lookahead = value ?? 4;
|
|
1037
1317
|
if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
|
|
@@ -1040,7 +1320,9 @@ function semanticLookahead(value) {
|
|
|
1040
1320
|
return lookahead;
|
|
1041
1321
|
}
|
|
1042
1322
|
async function writePage(page, write, options = {}) {
|
|
1043
|
-
|
|
1323
|
+
const profile = resolveProfile(options);
|
|
1324
|
+
validateImageOptions(resolveImageOptions(profile, options), options);
|
|
1325
|
+
if (profile === "semantic") await writeFlowPage(page, write, options);
|
|
1044
1326
|
else await writePositionedPage(page, write, options);
|
|
1045
1327
|
}
|
|
1046
1328
|
async function pageToHtml(page, options = {}) {
|
|
@@ -1055,7 +1337,9 @@ async function pageToHtml(page, options = {}) {
|
|
|
1055
1337
|
return output;
|
|
1056
1338
|
}
|
|
1057
1339
|
async function writePositionedPage(page, write, options) {
|
|
1058
|
-
const
|
|
1340
|
+
const imageOptions = resolveImageOptions("visual", options);
|
|
1341
|
+
const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
|
|
1342
|
+
const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
|
|
1059
1343
|
const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
|
|
1060
1344
|
const quarterTurn = page.rotate === 90 || page.rotate === 270;
|
|
1061
1345
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
@@ -1067,25 +1351,32 @@ async function writePositionedPage(page, write, options) {
|
|
|
1067
1351
|
const type3Fonts = new Map(
|
|
1068
1352
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
1069
1353
|
);
|
|
1354
|
+
const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
|
|
1070
1355
|
if ((options.includeStyles ?? true) && page.fonts?.length) {
|
|
1071
1356
|
await write(
|
|
1072
1357
|
`<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
1073
1358
|
);
|
|
1074
1359
|
}
|
|
1360
|
+
if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
|
|
1075
1361
|
await write(
|
|
1076
1362
|
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
|
|
1077
1363
|
);
|
|
1078
1364
|
await write(
|
|
1079
1365
|
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
|
|
1080
1366
|
);
|
|
1081
|
-
const clipDefinitions = imageClipDefinitions(
|
|
1367
|
+
const clipDefinitions = imageClipDefinitions(
|
|
1368
|
+
imageOptions === "excluded" ? [] : page.images ?? [],
|
|
1369
|
+
page.number,
|
|
1370
|
+
page.height
|
|
1371
|
+
) + vectorPathClipDefinitions(
|
|
1082
1372
|
(page.paths ?? []).map((path, index) => ({ path, index })),
|
|
1083
1373
|
page.number
|
|
1084
1374
|
);
|
|
1085
1375
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
1086
1376
|
if (reflectedOverlay) {
|
|
1087
1377
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1088
|
-
|
|
1378
|
+
const source = visualImages[index];
|
|
1379
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
1089
1380
|
}
|
|
1090
1381
|
}
|
|
1091
1382
|
if (page.fills?.length || page.paths?.length) {
|
|
@@ -1098,14 +1389,29 @@ async function writePositionedPage(page, write, options) {
|
|
|
1098
1389
|
}
|
|
1099
1390
|
if (!reflectedOverlay) {
|
|
1100
1391
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1101
|
-
|
|
1392
|
+
const source = visualImages[index];
|
|
1393
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
1102
1394
|
}
|
|
1103
1395
|
}
|
|
1104
|
-
for (
|
|
1396
|
+
for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
|
|
1397
|
+
const span = visualSpans[spanIndex];
|
|
1398
|
+
if (!span) continue;
|
|
1105
1399
|
if (!usesPositionedSpan(span)) {
|
|
1106
1400
|
const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
|
|
1401
|
+
const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
|
|
1402
|
+
if (line) {
|
|
1403
|
+
await write(line.html);
|
|
1404
|
+
spanIndex = line.endIndex;
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1107
1407
|
await write(
|
|
1108
|
-
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1408
|
+
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1409
|
+
span,
|
|
1410
|
+
page.height,
|
|
1411
|
+
fontAliases,
|
|
1412
|
+
reflectedOverlay && page.rotate === 180,
|
|
1413
|
+
textClasses?.names
|
|
1414
|
+
)
|
|
1109
1415
|
);
|
|
1110
1416
|
}
|
|
1111
1417
|
}
|
|
@@ -1115,23 +1421,131 @@ async function writePositionedPage(page, write, options) {
|
|
|
1115
1421
|
}
|
|
1116
1422
|
await write("</div></section>");
|
|
1117
1423
|
}
|
|
1424
|
+
function coalesceVisualSpans(spans) {
|
|
1425
|
+
const output = [];
|
|
1426
|
+
for (const span of spans) {
|
|
1427
|
+
const previous = output.at(-1);
|
|
1428
|
+
if (!previous || !canCoalesceVisualSpans(previous, span)) {
|
|
1429
|
+
output.push(span);
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
output[output.length - 1] = {
|
|
1433
|
+
...previous,
|
|
1434
|
+
text: previous.text + span.text,
|
|
1435
|
+
bounds: {
|
|
1436
|
+
...previous.bounds,
|
|
1437
|
+
width: span.bounds.x + span.bounds.width - previous.bounds.x,
|
|
1438
|
+
height: Math.max(previous.bounds.height, span.bounds.height)
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
return output;
|
|
1443
|
+
}
|
|
1444
|
+
function canCoalesceVisualSpans(left, right) {
|
|
1445
|
+
if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
|
|
1446
|
+
if (left.direction !== "ltr" || right.direction !== "ltr") return false;
|
|
1447
|
+
if (/guardian/i.test(left.fontFamily ?? "")) return false;
|
|
1448
|
+
if (left.glyphCodes || right.glyphCodes) return false;
|
|
1449
|
+
if (!sameVisualTextState(left, right)) return false;
|
|
1450
|
+
const tolerance = Math.max(0.02, left.fontSize * 0.015);
|
|
1451
|
+
if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
|
|
1452
|
+
const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
|
|
1453
|
+
return !right.hasLeadingSpace && gap >= -tolerance && gap <= tolerance;
|
|
1454
|
+
}
|
|
1455
|
+
function sameVisualTextState(left, right) {
|
|
1456
|
+
return Math.abs(left.fontSize - right.fontSize) <= 1e-3 && left.fontName === right.fontName && left.fontFamily === right.fontFamily && left.fontAssetId === right.fontAssetId && left.color === right.color && left.fillOpacity === right.fillOpacity && left.strokeColor === right.strokeColor && left.strokeWidth === right.strokeWidth && left.strokeOpacity === right.strokeOpacity && left.renderingMode === right.renderingMode && sameTransform(left.transform, right.transform);
|
|
1457
|
+
}
|
|
1458
|
+
function sameTransform(left, right) {
|
|
1459
|
+
if (!left || !right) return left === right;
|
|
1460
|
+
return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
|
|
1461
|
+
}
|
|
1462
|
+
function visualTextClasses(pageNumber, spans, fontAliases) {
|
|
1463
|
+
const names = /* @__PURE__ */ new Map();
|
|
1464
|
+
let css = "";
|
|
1465
|
+
for (const span of spans) {
|
|
1466
|
+
if (usesPositionedSpan(span) || span.glyphCodes) {
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
const style = visualTextClassStyle(span, fontAliases);
|
|
1470
|
+
if (!style || names.has(style)) continue;
|
|
1471
|
+
const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
|
|
1472
|
+
names.set(style, name);
|
|
1473
|
+
css += `.${name}{${style}}`;
|
|
1474
|
+
}
|
|
1475
|
+
return { css, names };
|
|
1476
|
+
}
|
|
1477
|
+
function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
|
|
1478
|
+
const first = spans[startIndex];
|
|
1479
|
+
if (!first || !canGroupVisualTextLine(first)) return void 0;
|
|
1480
|
+
const style = visualTextClassStyle(first, fontAliases);
|
|
1481
|
+
const className = styleClasses.get(style);
|
|
1482
|
+
if (!className) return void 0;
|
|
1483
|
+
let endIndex = startIndex;
|
|
1484
|
+
while (endIndex + 1 < spans.length) {
|
|
1485
|
+
const next = spans[endIndex + 1];
|
|
1486
|
+
if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
endIndex += 1;
|
|
1490
|
+
}
|
|
1491
|
+
if (endIndex === startIndex) return void 0;
|
|
1492
|
+
const baseline = pageHeight - first.bounds.y;
|
|
1493
|
+
const lineSpans = spans.slice(startIndex, endIndex + 1);
|
|
1494
|
+
const content = lineSpans.map(
|
|
1495
|
+
(span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
|
|
1496
|
+
).join("");
|
|
1497
|
+
return {
|
|
1498
|
+
html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
|
|
1499
|
+
endIndex
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
function visualTextTspan(span, dx) {
|
|
1503
|
+
const extent = span.bounds.width;
|
|
1504
|
+
const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
|
|
1505
|
+
const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1506
|
+
return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
|
|
1507
|
+
}
|
|
1508
|
+
function textSpanGap(previous, current) {
|
|
1509
|
+
if (!previous) return 0;
|
|
1510
|
+
const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
|
|
1511
|
+
const adjustment = current.textAdjustmentBefore;
|
|
1512
|
+
return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
|
|
1513
|
+
}
|
|
1514
|
+
function canGroupVisualTextLine(span) {
|
|
1515
|
+
return !usesPositionedSpan(span) && !span.glyphCodes && span.direction === "ltr" && !isHebrewPaintOrder(span) && !hasNonIdentityTransform(span.transform) && span.renderingMode !== 3 && span.renderingMode !== 7 && (span.fontAssetId !== void 0 || !isAdobeCjkFont(span.fontFamily));
|
|
1516
|
+
}
|
|
1118
1517
|
function usesReflectedVisualOverlay(page, spans) {
|
|
1119
1518
|
return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
|
|
1120
1519
|
(span) => span.transform !== void 0 && Math.abs(span.transform[0] + 1) < 1e-6 && Math.abs(span.transform[1]) < 1e-6 && Math.abs(span.transform[2]) < 1e-6 && Math.abs(span.transform[3] - 1) < 1e-6
|
|
1121
1520
|
);
|
|
1122
1521
|
}
|
|
1123
|
-
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
1522
|
+
function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
|
|
1124
1523
|
const [a, b, c, d, e, f] = image.transform;
|
|
1125
1524
|
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
|
|
1126
1525
|
const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
|
|
1127
|
-
|
|
1128
|
-
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
1129
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
1526
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
|
|
1130
1527
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
1131
1528
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
1132
1529
|
}
|
|
1133
1530
|
return output;
|
|
1134
1531
|
}
|
|
1532
|
+
async function prepareVisualImages(page, imageOptions, onImage) {
|
|
1533
|
+
if (imageOptions === "excluded") return [];
|
|
1534
|
+
const sources = [];
|
|
1535
|
+
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1536
|
+
const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
1537
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
1538
|
+
if (imageOptions === "embedded") {
|
|
1539
|
+
sources.push(`data:${mimeType};base64,${base64(data)}`);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
1543
|
+
const name = `page-${page.number}-image-${index + 1}.${extension}`;
|
|
1544
|
+
await onImage?.({ name, mimeType, data });
|
|
1545
|
+
sources.push(name);
|
|
1546
|
+
}
|
|
1547
|
+
return sources;
|
|
1548
|
+
}
|
|
1135
1549
|
function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
1136
1550
|
return images.flatMap(
|
|
1137
1551
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
@@ -1198,8 +1612,9 @@ function positionedSpan(span, fontAliases) {
|
|
|
1198
1612
|
].join(";");
|
|
1199
1613
|
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
|
|
1200
1614
|
}
|
|
1201
|
-
async function writeFlowPage(page, write) {
|
|
1202
|
-
const
|
|
1615
|
+
async function writeFlowPage(page, write, options) {
|
|
1616
|
+
const imageOptions = resolveImageOptions("semantic", options);
|
|
1617
|
+
const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
|
|
1203
1618
|
const structured = structurePage2(withoutSemanticMediaSpans(page, media));
|
|
1204
1619
|
const defaultColor = dominantTextColor(structured.lines);
|
|
1205
1620
|
let mediaIndex = 0;
|
|
@@ -1336,10 +1751,37 @@ function semanticBlockY2(block) {
|
|
|
1336
1751
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
1337
1752
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
1338
1753
|
}
|
|
1339
|
-
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
|
|
1754
|
+
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
|
|
1340
1755
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
1341
1756
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
1342
1757
|
const direction = directionAttribute([span]);
|
|
1758
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1759
|
+
const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
|
|
1760
|
+
const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
|
|
1761
|
+
const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
|
|
1762
|
+
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1763
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1764
|
+
const transform = counterRotateReflectedText && span.transform ? [
|
|
1765
|
+
span.transform[0],
|
|
1766
|
+
span.transform[1],
|
|
1767
|
+
span.transform[2],
|
|
1768
|
+
-span.transform[3]
|
|
1769
|
+
] : span.transform;
|
|
1770
|
+
const transformed = hasNonIdentityTransform(transform);
|
|
1771
|
+
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1772
|
+
const basisX = transform?.[0] ?? 1;
|
|
1773
|
+
const basisY = transform?.[1] ?? 0;
|
|
1774
|
+
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1775
|
+
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1776
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1777
|
+
return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
|
|
1778
|
+
}
|
|
1779
|
+
function visualTextClassStyle(span, fontAliases) {
|
|
1780
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1781
|
+
const fontSize = `font-size:${number3(span.fontSize)}px`;
|
|
1782
|
+
return style ? `${style};${fontSize}` : fontSize;
|
|
1783
|
+
}
|
|
1784
|
+
function visualTextStyle(span, fontAliases) {
|
|
1343
1785
|
const font = visualFontStyles(
|
|
1344
1786
|
span.fontFamily,
|
|
1345
1787
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -1349,7 +1791,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1349
1791
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
1350
1792
|
const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
|
|
1351
1793
|
const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
|
|
1352
|
-
|
|
1794
|
+
return [
|
|
1353
1795
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
1354
1796
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
1355
1797
|
strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
|
|
@@ -1359,22 +1801,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1359
1801
|
strokeOpacity,
|
|
1360
1802
|
font
|
|
1361
1803
|
].filter(Boolean).join(";");
|
|
1362
|
-
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1363
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1364
|
-
const transform = counterRotateReflectedText && span.transform ? [
|
|
1365
|
-
span.transform[0],
|
|
1366
|
-
span.transform[1],
|
|
1367
|
-
span.transform[2],
|
|
1368
|
-
-span.transform[3]
|
|
1369
|
-
] : span.transform;
|
|
1370
|
-
const transformed = hasNonIdentityTransform(transform);
|
|
1371
|
-
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1372
|
-
const basisX = transform?.[0] ?? 1;
|
|
1373
|
-
const basisY = transform?.[1] ?? 0;
|
|
1374
|
-
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1375
|
-
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1376
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1377
|
-
return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
1378
1804
|
}
|
|
1379
1805
|
function isAdobeCjkFont(fontFamily) {
|
|
1380
1806
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -1469,9 +1895,18 @@ function resolveProfile(options) {
|
|
|
1469
1895
|
}
|
|
1470
1896
|
return options.profile ?? legacyProfile;
|
|
1471
1897
|
}
|
|
1898
|
+
function resolveImageOptions(profile, options) {
|
|
1899
|
+
return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
|
|
1900
|
+
}
|
|
1901
|
+
function validateImageOptions(imageOptions, options) {
|
|
1902
|
+
if (imageOptions === "references" && !options.onImage) {
|
|
1903
|
+
throw new Error('imageOptions "references" requires an onImage callback');
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1472
1906
|
export {
|
|
1473
1907
|
pageToHtml,
|
|
1474
1908
|
writeHtmlDocument,
|
|
1909
|
+
writeMarkdownDocument,
|
|
1475
1910
|
writePage
|
|
1476
1911
|
};
|
|
1477
1912
|
//# sourceMappingURL=index.js.map
|