@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.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
pageToHtml: () => pageToHtml,
|
|
24
24
|
writeHtmlDocument: () => writeHtmlDocument,
|
|
25
|
+
writeMarkdownDocument: () => writeMarkdownDocument,
|
|
25
26
|
writePage: () => writePage
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -218,6 +219,12 @@ function dominantTextColor(lines) {
|
|
|
218
219
|
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
219
220
|
}
|
|
220
221
|
function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
222
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "html");
|
|
223
|
+
}
|
|
224
|
+
function semanticTextMarkdown(text, lines, defaultColor, preserveWeight = true) {
|
|
225
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "markdown");
|
|
226
|
+
}
|
|
227
|
+
function semanticText(text, lines, defaultColor, preserveWeight, format) {
|
|
221
228
|
const ranges = [];
|
|
222
229
|
let cursor = 0;
|
|
223
230
|
for (const span of lines.flatMap((line) => line.spans)) {
|
|
@@ -243,11 +250,11 @@ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
|
243
250
|
let html = "";
|
|
244
251
|
let offset = 0;
|
|
245
252
|
for (const range of merged) {
|
|
246
|
-
html +=
|
|
247
|
-
html +=
|
|
253
|
+
html += escapeText(text.slice(offset, range.start), format);
|
|
254
|
+
html += styledText(text.slice(range.start, range.end), range, format);
|
|
248
255
|
offset = range.end;
|
|
249
256
|
}
|
|
250
|
-
return html +
|
|
257
|
+
return html + escapeText(text.slice(offset), format);
|
|
251
258
|
}
|
|
252
259
|
function mergeRanges(ranges, text) {
|
|
253
260
|
const merged = [];
|
|
@@ -261,13 +268,19 @@ function mergeRanges(ranges, text) {
|
|
|
261
268
|
}
|
|
262
269
|
return merged;
|
|
263
270
|
}
|
|
264
|
-
function
|
|
265
|
-
let html =
|
|
271
|
+
function styledText(value, range, format) {
|
|
272
|
+
let html = escapeText(value, format);
|
|
266
273
|
if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
|
|
267
|
-
if (range.italic) html = `<em>${html}</em
|
|
268
|
-
if (range.bold) html = `<strong>${html}</strong
|
|
274
|
+
if (range.italic) html = format === "html" ? `<em>${html}</em>` : `_${html}_`;
|
|
275
|
+
if (range.bold) html = format === "html" ? `<strong>${html}</strong>` : `**${html}**`;
|
|
269
276
|
return html;
|
|
270
277
|
}
|
|
278
|
+
function escapeText(value, format) {
|
|
279
|
+
return format === "html" ? escapeHtml(value) : escapeMarkdown(value);
|
|
280
|
+
}
|
|
281
|
+
function escapeMarkdown(value) {
|
|
282
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
283
|
+
}
|
|
271
284
|
function normalizedColor(value) {
|
|
272
285
|
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
273
286
|
const color = value.toLowerCase();
|
|
@@ -403,23 +416,39 @@ function base64(bytes) {
|
|
|
403
416
|
}
|
|
404
417
|
|
|
405
418
|
// src/semantic-media.ts
|
|
406
|
-
function semanticMedia(page) {
|
|
407
|
-
|
|
408
|
-
output
|
|
419
|
+
function semanticMedia(page, imageOptions = "embedded") {
|
|
420
|
+
if (imageOptions === "excluded") return [];
|
|
421
|
+
const output = (page.images ?? []).map(
|
|
422
|
+
(image, index) => rasterMedia(image, page.number, index, imageOptions)
|
|
423
|
+
);
|
|
424
|
+
output.push(...vectorMedia(page, imageOptions));
|
|
409
425
|
return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
|
|
410
426
|
}
|
|
411
|
-
function
|
|
427
|
+
async function prepareSemanticMedia(page, imageOptions, onImage) {
|
|
428
|
+
const media = semanticMedia(page, imageOptions);
|
|
429
|
+
for (const item of media) {
|
|
430
|
+
for (const asset of item.assets ?? []) await onImage?.(asset);
|
|
431
|
+
delete item.assets;
|
|
432
|
+
}
|
|
433
|
+
return media;
|
|
434
|
+
}
|
|
435
|
+
function rasterMedia(image, pageNumber, index, imageOptions) {
|
|
412
436
|
const bounds2 = transformedUnitBounds(image.transform);
|
|
413
437
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
414
438
|
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
439
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
440
|
+
const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
|
|
441
|
+
const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
|
|
415
442
|
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
416
443
|
return {
|
|
417
444
|
bounds: bounds2,
|
|
418
445
|
kind: "raster",
|
|
419
|
-
html: `<img class="pdf-semantic-media" src="
|
|
446
|
+
html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
|
|
447
|
+
markdown: ``,
|
|
448
|
+
...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
|
|
420
449
|
};
|
|
421
450
|
}
|
|
422
|
-
function vectorMedia(page) {
|
|
451
|
+
function vectorMedia(page, imageOptions) {
|
|
423
452
|
const primitives = [
|
|
424
453
|
...(page.paths ?? []).flatMap((path, index) => {
|
|
425
454
|
const bounds2 = vectorPathBounds(path);
|
|
@@ -437,7 +466,7 @@ function vectorMedia(page) {
|
|
|
437
466
|
const visualCodeFonts = new Set(
|
|
438
467
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
439
468
|
);
|
|
440
|
-
return components.map((component) => {
|
|
469
|
+
return components.map((component, componentIndex) => {
|
|
441
470
|
const bounds2 = component.bounds;
|
|
442
471
|
const paths = component.primitives.flatMap(
|
|
443
472
|
(primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
|
|
@@ -454,10 +483,18 @@ function vectorMedia(page) {
|
|
|
454
483
|
);
|
|
455
484
|
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
456
485
|
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
486
|
+
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>`;
|
|
487
|
+
const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
|
|
457
488
|
return {
|
|
458
489
|
bounds: bounds2,
|
|
459
490
|
kind: "vector",
|
|
460
|
-
html: `<
|
|
491
|
+
html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
|
|
492
|
+
markdown: imageOptions === "references" ? `` : svg,
|
|
493
|
+
...imageOptions === "references" ? {
|
|
494
|
+
assets: [
|
|
495
|
+
{ name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
|
|
496
|
+
]
|
|
497
|
+
} : {},
|
|
461
498
|
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
462
499
|
};
|
|
463
500
|
});
|
|
@@ -499,7 +536,9 @@ function compositeMedia(items) {
|
|
|
499
536
|
bounds: bounds2,
|
|
500
537
|
kind: "composite",
|
|
501
538
|
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>`,
|
|
502
|
-
|
|
539
|
+
markdown: items.map((item) => item.markdown).join("\n\n"),
|
|
540
|
+
consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
|
|
541
|
+
assets: items.flatMap((item) => item.assets ?? [])
|
|
503
542
|
};
|
|
504
543
|
}
|
|
505
544
|
function mediaPiecesTouch(left, right) {
|
|
@@ -638,7 +677,7 @@ function escapeHtml2(value) {
|
|
|
638
677
|
}
|
|
639
678
|
|
|
640
679
|
// src/semantic-document.ts
|
|
641
|
-
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
680
|
+
async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
|
|
642
681
|
const stats = {
|
|
643
682
|
pagesProcessed: 0,
|
|
644
683
|
peakBufferedPages: 0,
|
|
@@ -656,27 +695,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
656
695
|
let contentStarted = false;
|
|
657
696
|
let employmentOpen = false;
|
|
658
697
|
let pendingParagraph;
|
|
659
|
-
|
|
698
|
+
const markdown = format === "markdown";
|
|
699
|
+
const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
|
|
700
|
+
const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
|
|
701
|
+
await output('<article class="pdf-semantic-document">');
|
|
660
702
|
const closeTable = async () => {
|
|
661
703
|
if (!activeTable) return;
|
|
662
|
-
await
|
|
704
|
+
await output("</table>", "\n");
|
|
663
705
|
activeTable = void 0;
|
|
664
706
|
while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
|
|
665
707
|
};
|
|
666
708
|
const closeSections = async (minimumLevel = 0) => {
|
|
667
709
|
while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
|
|
668
|
-
await
|
|
710
|
+
await output("</section>");
|
|
669
711
|
sectionLevels.pop();
|
|
670
712
|
}
|
|
671
713
|
};
|
|
672
714
|
const flushPendingParagraph = async () => {
|
|
673
715
|
if (!pendingParagraph) return;
|
|
674
|
-
await write(
|
|
716
|
+
await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
|
|
675
717
|
pendingParagraph = void 0;
|
|
676
718
|
};
|
|
677
719
|
const closeEmployment = async () => {
|
|
678
720
|
if (!employmentOpen) return;
|
|
679
|
-
await
|
|
721
|
+
await output("</section>");
|
|
680
722
|
employmentOpen = false;
|
|
681
723
|
};
|
|
682
724
|
const emitPage = async (page, future) => {
|
|
@@ -704,7 +746,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
704
746
|
await flushPendingParagraph();
|
|
705
747
|
const item = page.media[mediaIndex];
|
|
706
748
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
707
|
-
const html2 =
|
|
749
|
+
const html2 = markdown ? `${item.markdown}
|
|
750
|
+
|
|
751
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
752
|
+
|
|
753
|
+
` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
708
754
|
if (activeTable) pendingMedia.push(html2);
|
|
709
755
|
else await write(html2);
|
|
710
756
|
emittedMedia.add(item);
|
|
@@ -713,7 +759,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
713
759
|
break;
|
|
714
760
|
}
|
|
715
761
|
if (item && captionedMedia.has(item)) break;
|
|
716
|
-
const html =
|
|
762
|
+
const html = markdown ? `${item?.markdown ?? ""}
|
|
763
|
+
|
|
764
|
+
` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
|
|
717
765
|
if (activeTable) pendingMedia.push(html);
|
|
718
766
|
else await write(html);
|
|
719
767
|
mediaIndex += 1;
|
|
@@ -721,7 +769,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
721
769
|
const associatedMedia = captions.get(block);
|
|
722
770
|
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
723
771
|
await flushPendingParagraph();
|
|
724
|
-
const html =
|
|
772
|
+
const html = markdown ? `${associatedMedia.markdown}
|
|
773
|
+
|
|
774
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
775
|
+
|
|
776
|
+
` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
725
777
|
if (activeTable) pendingMedia.push(html);
|
|
726
778
|
else await write(html);
|
|
727
779
|
emittedMedia.add(associatedMedia);
|
|
@@ -735,8 +787,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
735
787
|
await flushPendingParagraph();
|
|
736
788
|
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
737
789
|
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
738
|
-
await
|
|
739
|
-
`<header><h1>${
|
|
790
|
+
await output(
|
|
791
|
+
`<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
|
|
792
|
+
`# ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
793
|
+
|
|
794
|
+
`
|
|
740
795
|
);
|
|
741
796
|
headerOpen = true;
|
|
742
797
|
continue;
|
|
@@ -744,19 +799,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
744
799
|
if (headerOpen) {
|
|
745
800
|
if (block.type === "paragraph") {
|
|
746
801
|
const tag = isContactBlock(block) ? "address" : "p";
|
|
747
|
-
await
|
|
748
|
-
`<${tag}>${
|
|
802
|
+
await output(
|
|
803
|
+
`<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
|
|
804
|
+
`${inlineText(block.text, block.lines, defaultColor)}
|
|
805
|
+
|
|
806
|
+
`
|
|
749
807
|
);
|
|
750
808
|
headerHasParagraph = true;
|
|
751
809
|
continue;
|
|
752
810
|
}
|
|
753
811
|
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
754
|
-
await
|
|
755
|
-
`<h${block.level}>${
|
|
812
|
+
await output(
|
|
813
|
+
`<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
|
|
814
|
+
`${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
815
|
+
|
|
816
|
+
`
|
|
756
817
|
);
|
|
757
818
|
continue;
|
|
758
819
|
}
|
|
759
|
-
await
|
|
820
|
+
await output("</header>");
|
|
760
821
|
headerOpen = false;
|
|
761
822
|
contentStarted = true;
|
|
762
823
|
}
|
|
@@ -764,23 +825,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
764
825
|
const rows = (0, import_structure.tableToRows)(block.table);
|
|
765
826
|
if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
|
|
766
827
|
const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
|
|
767
|
-
for (const row of continuationRows)
|
|
828
|
+
for (const row of continuationRows)
|
|
829
|
+
await write(markdown ? markdownTableRow(row) : tableRow(row, false));
|
|
768
830
|
activeTable.table = block.table;
|
|
769
831
|
stats.mergedTables += 1;
|
|
770
832
|
continue;
|
|
771
833
|
}
|
|
772
834
|
await closeTable();
|
|
773
835
|
const header = tableHeader(rows);
|
|
774
|
-
await
|
|
775
|
-
|
|
776
|
-
|
|
836
|
+
await output("<table>", markdownTableStart(rows, header));
|
|
837
|
+
const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
|
|
838
|
+
for (const [index, row] of markdownRows.entries())
|
|
839
|
+
await write(
|
|
840
|
+
markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
|
|
841
|
+
);
|
|
777
842
|
activeTable = { table: block.table, header };
|
|
778
843
|
continue;
|
|
779
844
|
}
|
|
780
845
|
if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
|
|
781
846
|
const columns = activeTable.table.columns.length;
|
|
782
|
-
await
|
|
783
|
-
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot
|
|
847
|
+
await output(
|
|
848
|
+
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
|
|
849
|
+
block.entries.map(
|
|
850
|
+
(entry) => markdownTableRow([
|
|
851
|
+
entry.term,
|
|
852
|
+
...Array(Math.max(0, columns - 2)).fill(""),
|
|
853
|
+
entry.description
|
|
854
|
+
])
|
|
855
|
+
).join("")
|
|
784
856
|
);
|
|
785
857
|
await closeTable();
|
|
786
858
|
continue;
|
|
@@ -789,8 +861,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
789
861
|
if (block.type === "heading") {
|
|
790
862
|
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
791
863
|
await closeSections(level);
|
|
792
|
-
await
|
|
793
|
-
`<section data-level="${level}"><h${level}>${
|
|
864
|
+
await output(
|
|
865
|
+
`<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
|
|
866
|
+
`${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
867
|
+
|
|
868
|
+
`
|
|
794
869
|
);
|
|
795
870
|
sectionLevels.push(level);
|
|
796
871
|
continue;
|
|
@@ -798,13 +873,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
798
873
|
if (block.type === "paragraph") {
|
|
799
874
|
if (isTitledRecord(block)) {
|
|
800
875
|
const [institution, ...details] = block.lines;
|
|
801
|
-
if (institution)
|
|
802
|
-
|
|
876
|
+
if (institution)
|
|
877
|
+
await output(
|
|
878
|
+
`<h3>${escapeHtml3(institution.text)}</h3>`,
|
|
879
|
+
`### ${escapeMarkdown2(institution.text)}
|
|
880
|
+
|
|
881
|
+
`
|
|
882
|
+
);
|
|
883
|
+
for (const detail of details)
|
|
884
|
+
await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
|
|
885
|
+
|
|
886
|
+
`);
|
|
803
887
|
continue;
|
|
804
888
|
}
|
|
805
889
|
if (isUnmarkedList(block)) {
|
|
806
|
-
await
|
|
807
|
-
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul
|
|
890
|
+
await output(
|
|
891
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
|
|
892
|
+
`${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
|
|
893
|
+
|
|
894
|
+
`
|
|
808
895
|
);
|
|
809
896
|
continue;
|
|
810
897
|
}
|
|
@@ -812,19 +899,28 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
812
899
|
continue;
|
|
813
900
|
}
|
|
814
901
|
if (block.type === "employment") {
|
|
815
|
-
await
|
|
816
|
-
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p
|
|
902
|
+
await output(
|
|
903
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
|
|
904
|
+
`### ${escapeMarkdown2(block.role)}
|
|
905
|
+
|
|
906
|
+
${escapeMarkdown2(block.organization)}
|
|
907
|
+
|
|
908
|
+
${escapeMarkdown2(block.date)}
|
|
909
|
+
|
|
910
|
+
`
|
|
817
911
|
);
|
|
818
912
|
employmentOpen = true;
|
|
819
913
|
continue;
|
|
820
914
|
}
|
|
821
|
-
await write(
|
|
915
|
+
await write(semanticBlockOutput(block, defaultColor, format));
|
|
822
916
|
}
|
|
823
917
|
while (mediaIndex < page.media.length) {
|
|
824
918
|
const item = page.media[mediaIndex];
|
|
825
919
|
if (item && !emittedMedia.has(item)) {
|
|
826
920
|
await flushPendingParagraph();
|
|
827
|
-
const html =
|
|
921
|
+
const html = markdown ? `${item.markdown}
|
|
922
|
+
|
|
923
|
+
` : `<div class="pdf-semantic-visual">${item.html}</div>`;
|
|
828
924
|
if (activeTable) pendingMedia.push(html);
|
|
829
925
|
else await write(html);
|
|
830
926
|
}
|
|
@@ -833,9 +929,10 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
833
929
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
834
930
|
};
|
|
835
931
|
for await (const page of pages) {
|
|
836
|
-
const media =
|
|
932
|
+
const media = await prepareSemanticMedia(page, imageOptions, onImage);
|
|
837
933
|
const structured = (0, import_structure.structurePage)(withoutSemanticMediaSpans(page, media));
|
|
838
934
|
buffer.push({ width: page.width, height: page.height, structured, media });
|
|
935
|
+
restoreObservedHyphens(buffer);
|
|
839
936
|
stats.pagesProcessed += 1;
|
|
840
937
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
841
938
|
stats.peakBufferedLines = Math.max(
|
|
@@ -851,22 +948,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
851
948
|
const ready = buffer.shift();
|
|
852
949
|
if (ready) await emitPage(ready, buffer);
|
|
853
950
|
}
|
|
854
|
-
if (headerOpen) await
|
|
951
|
+
if (headerOpen) await output("</header>");
|
|
855
952
|
await closeTable();
|
|
856
953
|
await closeEmployment();
|
|
857
954
|
if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
|
|
858
955
|
await closeSections();
|
|
859
|
-
await
|
|
860
|
-
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer
|
|
956
|
+
await output(
|
|
957
|
+
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
|
|
958
|
+
`---
|
|
959
|
+
|
|
960
|
+
${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
|
|
861
961
|
);
|
|
862
962
|
pendingParagraph = void 0;
|
|
863
963
|
} else {
|
|
864
964
|
await flushPendingParagraph();
|
|
865
965
|
await closeSections();
|
|
866
966
|
}
|
|
867
|
-
await
|
|
967
|
+
await output("</article>");
|
|
868
968
|
return stats;
|
|
869
969
|
}
|
|
970
|
+
function restoreObservedHyphens(buffer) {
|
|
971
|
+
const terms = new Set(
|
|
972
|
+
buffer.flatMap(
|
|
973
|
+
(page) => page.structured.lines.flatMap(
|
|
974
|
+
(line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
for (const page of buffer) {
|
|
979
|
+
for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function restoreBlockHyphens(block, terms) {
|
|
983
|
+
const restore = (value) => restoreTextHyphens(value, terms);
|
|
984
|
+
if (block.type === "insetGroup") {
|
|
985
|
+
for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
|
|
986
|
+
} else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
|
|
987
|
+
block.text = restore(block.text);
|
|
988
|
+
} else if (block.type === "list") {
|
|
989
|
+
for (const item of block.items) item.text = restore(item.text);
|
|
990
|
+
} else if (block.type === "definitionList") {
|
|
991
|
+
for (const entry of block.entries) {
|
|
992
|
+
entry.term = restore(entry.term);
|
|
993
|
+
entry.description = restore(entry.description);
|
|
994
|
+
}
|
|
995
|
+
} else if (block.type === "cardList") {
|
|
996
|
+
for (const item of block.items) {
|
|
997
|
+
item.title = restore(item.title);
|
|
998
|
+
item.details = item.details.map(restore);
|
|
999
|
+
}
|
|
1000
|
+
} else if (block.type === "sectionGroup") {
|
|
1001
|
+
for (const item of block.items) {
|
|
1002
|
+
item.label = restore(item.label);
|
|
1003
|
+
item.content = item.content.map(restore);
|
|
1004
|
+
}
|
|
1005
|
+
} else if (block.type === "employment") {
|
|
1006
|
+
block.role = restore(block.role);
|
|
1007
|
+
block.organization = restore(block.organization);
|
|
1008
|
+
block.date = restore(block.date);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function restoreTextHyphens(value, terms) {
|
|
1012
|
+
let output = value;
|
|
1013
|
+
for (const term of terms) {
|
|
1014
|
+
const collapsed = term.replace(/[-‐‑]/gu, "");
|
|
1015
|
+
if (collapsed === term || !output.includes(collapsed)) continue;
|
|
1016
|
+
const pattern = new RegExp(
|
|
1017
|
+
`(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
|
|
1018
|
+
"gu"
|
|
1019
|
+
);
|
|
1020
|
+
output = output.replace(pattern, term);
|
|
1021
|
+
}
|
|
1022
|
+
return output;
|
|
1023
|
+
}
|
|
1024
|
+
function escapeRegularExpression(value) {
|
|
1025
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1026
|
+
}
|
|
870
1027
|
function isContactBlock(block) {
|
|
871
1028
|
const text = block.text;
|
|
872
1029
|
const signals = [
|
|
@@ -950,6 +1107,20 @@ function tableRow(row, header) {
|
|
|
950
1107
|
const cell = header ? "th" : "td";
|
|
951
1108
|
return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
|
|
952
1109
|
}
|
|
1110
|
+
function markdownTableStart(rows, header) {
|
|
1111
|
+
const columns = rows[0]?.length ?? 0;
|
|
1112
|
+
if (columns === 0) return "";
|
|
1113
|
+
const heading = header ?? Array(columns).fill("");
|
|
1114
|
+
return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
|
|
1115
|
+
}
|
|
1116
|
+
function markdownTableRow(row, shouldEscape = true) {
|
|
1117
|
+
const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
|
|
1118
|
+
return `| ${cells.join(" | ")} |
|
|
1119
|
+
`;
|
|
1120
|
+
}
|
|
1121
|
+
function escapeMarkdownTableCell(value) {
|
|
1122
|
+
return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
|
|
1123
|
+
}
|
|
953
1124
|
function isFinancialSummary(block) {
|
|
954
1125
|
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
955
1126
|
}
|
|
@@ -997,6 +1168,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
997
1168
|
const tag = block.ordered ? "ol" : "ul";
|
|
998
1169
|
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
999
1170
|
}
|
|
1171
|
+
function semanticBlockOutput(block, defaultColor, format) {
|
|
1172
|
+
return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
|
|
1173
|
+
}
|
|
1174
|
+
function semanticBlockMarkdown(block, defaultColor = "#000000") {
|
|
1175
|
+
if (block.type === "insetGroup") {
|
|
1176
|
+
const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
|
|
1177
|
+
return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
|
|
1178
|
+
|
|
1179
|
+
`;
|
|
1180
|
+
}
|
|
1181
|
+
if (block.type === "table") {
|
|
1182
|
+
const rows = (0, import_structure.tableToRows)(block.table);
|
|
1183
|
+
const header = tableHeader(rows);
|
|
1184
|
+
return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
|
|
1185
|
+
`;
|
|
1186
|
+
}
|
|
1187
|
+
if (block.type === "heading") {
|
|
1188
|
+
return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
|
|
1189
|
+
|
|
1190
|
+
`;
|
|
1191
|
+
}
|
|
1192
|
+
if (block.type === "paragraph") {
|
|
1193
|
+
return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
|
|
1194
|
+
|
|
1195
|
+
`;
|
|
1196
|
+
}
|
|
1197
|
+
if (block.type === "preformatted") {
|
|
1198
|
+
const fence = block.text.includes("```") ? "````" : "```";
|
|
1199
|
+
return `${fence}
|
|
1200
|
+
${block.text}
|
|
1201
|
+
${fence}
|
|
1202
|
+
|
|
1203
|
+
`;
|
|
1204
|
+
}
|
|
1205
|
+
if (block.type === "definitionList") {
|
|
1206
|
+
return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
|
|
1207
|
+
|
|
1208
|
+
`;
|
|
1209
|
+
}
|
|
1210
|
+
if (block.type === "cardList") {
|
|
1211
|
+
const rows = [
|
|
1212
|
+
["Item", "Quantity", "Amount"],
|
|
1213
|
+
...block.items.map((item) => {
|
|
1214
|
+
const trailing = item.details.at(-1) ?? "";
|
|
1215
|
+
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
1216
|
+
const detail = item.details.slice(0, -1).join(" ");
|
|
1217
|
+
return [
|
|
1218
|
+
`${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
1219
|
+
match?.[1] ?? "",
|
|
1220
|
+
match?.[2] ?? trailing
|
|
1221
|
+
];
|
|
1222
|
+
})
|
|
1223
|
+
];
|
|
1224
|
+
return `## Items ordered
|
|
1225
|
+
|
|
1226
|
+
${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
if (block.type === "sectionGroup") {
|
|
1230
|
+
return block.items.map(
|
|
1231
|
+
(item) => `## ${escapeMarkdown2(titleCase(item.label))}
|
|
1232
|
+
|
|
1233
|
+
${item.content.map(
|
|
1234
|
+
(content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
|
|
1235
|
+
).join("\n\n")}
|
|
1236
|
+
|
|
1237
|
+
`
|
|
1238
|
+
).join("");
|
|
1239
|
+
}
|
|
1240
|
+
if (block.type === "employment") {
|
|
1241
|
+
return `### ${escapeMarkdown2(block.role)}
|
|
1242
|
+
|
|
1243
|
+
${escapeMarkdown2(block.organization)}
|
|
1244
|
+
|
|
1245
|
+
${escapeMarkdown2(block.date)}
|
|
1246
|
+
|
|
1247
|
+
`;
|
|
1248
|
+
}
|
|
1249
|
+
return `${block.items.map(
|
|
1250
|
+
(item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
|
|
1251
|
+
).join("\n")}
|
|
1252
|
+
|
|
1253
|
+
`;
|
|
1254
|
+
}
|
|
1000
1255
|
function semanticBlockY(block) {
|
|
1001
1256
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
1002
1257
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
@@ -1029,6 +1284,9 @@ function titleCase(value) {
|
|
|
1029
1284
|
function escapeHtml3(value) {
|
|
1030
1285
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1031
1286
|
}
|
|
1287
|
+
function escapeMarkdown2(value) {
|
|
1288
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
1289
|
+
}
|
|
1032
1290
|
|
|
1033
1291
|
// src/index.ts
|
|
1034
1292
|
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}`;
|
|
@@ -1045,9 +1303,18 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
1045
1303
|
await write("</head><body>");
|
|
1046
1304
|
}
|
|
1047
1305
|
await write('<main class="pdf-document">');
|
|
1048
|
-
|
|
1306
|
+
const profile = resolveProfile(options);
|
|
1307
|
+
const imageOptions = resolveImageOptions(profile, options);
|
|
1308
|
+
validateImageOptions(imageOptions, options);
|
|
1309
|
+
if (profile === "semantic") {
|
|
1049
1310
|
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
1050
|
-
const stats = await writeSemanticDocument(
|
|
1311
|
+
const stats = await writeSemanticDocument(
|
|
1312
|
+
pages,
|
|
1313
|
+
write,
|
|
1314
|
+
lookahead,
|
|
1315
|
+
imageOptions,
|
|
1316
|
+
options.onImage
|
|
1317
|
+
);
|
|
1051
1318
|
options.onSemanticStats?.(stats);
|
|
1052
1319
|
} else {
|
|
1053
1320
|
for await (const page of pages) await writePage(page, write, options);
|
|
@@ -1055,6 +1322,20 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
1055
1322
|
await write("</main>");
|
|
1056
1323
|
if (includeDocument) await write("</body></html>");
|
|
1057
1324
|
}
|
|
1325
|
+
async function writeMarkdownDocument(pages, write, options = {}) {
|
|
1326
|
+
const imageOptions = options.imageOptions ?? "excluded";
|
|
1327
|
+
validateImageOptions(imageOptions, options);
|
|
1328
|
+
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
1329
|
+
const stats = await writeSemanticDocument(
|
|
1330
|
+
pages,
|
|
1331
|
+
write,
|
|
1332
|
+
lookahead,
|
|
1333
|
+
imageOptions,
|
|
1334
|
+
options.onImage,
|
|
1335
|
+
"markdown"
|
|
1336
|
+
);
|
|
1337
|
+
options.onSemanticStats?.(stats);
|
|
1338
|
+
}
|
|
1058
1339
|
function semanticLookahead(value) {
|
|
1059
1340
|
const lookahead = value ?? 4;
|
|
1060
1341
|
if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
|
|
@@ -1063,7 +1344,9 @@ function semanticLookahead(value) {
|
|
|
1063
1344
|
return lookahead;
|
|
1064
1345
|
}
|
|
1065
1346
|
async function writePage(page, write, options = {}) {
|
|
1066
|
-
|
|
1347
|
+
const profile = resolveProfile(options);
|
|
1348
|
+
validateImageOptions(resolveImageOptions(profile, options), options);
|
|
1349
|
+
if (profile === "semantic") await writeFlowPage(page, write, options);
|
|
1067
1350
|
else await writePositionedPage(page, write, options);
|
|
1068
1351
|
}
|
|
1069
1352
|
async function pageToHtml(page, options = {}) {
|
|
@@ -1078,7 +1361,9 @@ async function pageToHtml(page, options = {}) {
|
|
|
1078
1361
|
return output;
|
|
1079
1362
|
}
|
|
1080
1363
|
async function writePositionedPage(page, write, options) {
|
|
1081
|
-
const
|
|
1364
|
+
const imageOptions = resolveImageOptions("visual", options);
|
|
1365
|
+
const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
|
|
1366
|
+
const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
|
|
1082
1367
|
const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
|
|
1083
1368
|
const quarterTurn = page.rotate === 90 || page.rotate === 270;
|
|
1084
1369
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
@@ -1090,25 +1375,32 @@ async function writePositionedPage(page, write, options) {
|
|
|
1090
1375
|
const type3Fonts = new Map(
|
|
1091
1376
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
1092
1377
|
);
|
|
1378
|
+
const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
|
|
1093
1379
|
if ((options.includeStyles ?? true) && page.fonts?.length) {
|
|
1094
1380
|
await write(
|
|
1095
1381
|
`<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
1096
1382
|
);
|
|
1097
1383
|
}
|
|
1384
|
+
if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
|
|
1098
1385
|
await write(
|
|
1099
1386
|
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
|
|
1100
1387
|
);
|
|
1101
1388
|
await write(
|
|
1102
1389
|
`<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)}">`
|
|
1103
1390
|
);
|
|
1104
|
-
const clipDefinitions = imageClipDefinitions(
|
|
1391
|
+
const clipDefinitions = imageClipDefinitions(
|
|
1392
|
+
imageOptions === "excluded" ? [] : page.images ?? [],
|
|
1393
|
+
page.number,
|
|
1394
|
+
page.height
|
|
1395
|
+
) + vectorPathClipDefinitions(
|
|
1105
1396
|
(page.paths ?? []).map((path, index) => ({ path, index })),
|
|
1106
1397
|
page.number
|
|
1107
1398
|
);
|
|
1108
1399
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
1109
1400
|
if (reflectedOverlay) {
|
|
1110
1401
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1111
|
-
|
|
1402
|
+
const source = visualImages[index];
|
|
1403
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
1112
1404
|
}
|
|
1113
1405
|
}
|
|
1114
1406
|
if (page.fills?.length || page.paths?.length) {
|
|
@@ -1121,14 +1413,29 @@ async function writePositionedPage(page, write, options) {
|
|
|
1121
1413
|
}
|
|
1122
1414
|
if (!reflectedOverlay) {
|
|
1123
1415
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1124
|
-
|
|
1416
|
+
const source = visualImages[index];
|
|
1417
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
1125
1418
|
}
|
|
1126
1419
|
}
|
|
1127
|
-
for (
|
|
1420
|
+
for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
|
|
1421
|
+
const span = visualSpans[spanIndex];
|
|
1422
|
+
if (!span) continue;
|
|
1128
1423
|
if (!usesPositionedSpan(span)) {
|
|
1129
1424
|
const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
|
|
1425
|
+
const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
|
|
1426
|
+
if (line) {
|
|
1427
|
+
await write(line.html);
|
|
1428
|
+
spanIndex = line.endIndex;
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1130
1431
|
await write(
|
|
1131
|
-
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1432
|
+
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1433
|
+
span,
|
|
1434
|
+
page.height,
|
|
1435
|
+
fontAliases,
|
|
1436
|
+
reflectedOverlay && page.rotate === 180,
|
|
1437
|
+
textClasses?.names
|
|
1438
|
+
)
|
|
1132
1439
|
);
|
|
1133
1440
|
}
|
|
1134
1441
|
}
|
|
@@ -1138,23 +1445,131 @@ async function writePositionedPage(page, write, options) {
|
|
|
1138
1445
|
}
|
|
1139
1446
|
await write("</div></section>");
|
|
1140
1447
|
}
|
|
1448
|
+
function coalesceVisualSpans(spans) {
|
|
1449
|
+
const output = [];
|
|
1450
|
+
for (const span of spans) {
|
|
1451
|
+
const previous = output.at(-1);
|
|
1452
|
+
if (!previous || !canCoalesceVisualSpans(previous, span)) {
|
|
1453
|
+
output.push(span);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
output[output.length - 1] = {
|
|
1457
|
+
...previous,
|
|
1458
|
+
text: previous.text + span.text,
|
|
1459
|
+
bounds: {
|
|
1460
|
+
...previous.bounds,
|
|
1461
|
+
width: span.bounds.x + span.bounds.width - previous.bounds.x,
|
|
1462
|
+
height: Math.max(previous.bounds.height, span.bounds.height)
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
return output;
|
|
1467
|
+
}
|
|
1468
|
+
function canCoalesceVisualSpans(left, right) {
|
|
1469
|
+
if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
|
|
1470
|
+
if (left.direction !== "ltr" || right.direction !== "ltr") return false;
|
|
1471
|
+
if (/guardian/i.test(left.fontFamily ?? "")) return false;
|
|
1472
|
+
if (left.glyphCodes || right.glyphCodes) return false;
|
|
1473
|
+
if (!sameVisualTextState(left, right)) return false;
|
|
1474
|
+
const tolerance = Math.max(0.02, left.fontSize * 0.015);
|
|
1475
|
+
if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
|
|
1476
|
+
const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
|
|
1477
|
+
return !right.hasLeadingSpace && gap >= -tolerance && gap <= tolerance;
|
|
1478
|
+
}
|
|
1479
|
+
function sameVisualTextState(left, right) {
|
|
1480
|
+
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);
|
|
1481
|
+
}
|
|
1482
|
+
function sameTransform(left, right) {
|
|
1483
|
+
if (!left || !right) return left === right;
|
|
1484
|
+
return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
|
|
1485
|
+
}
|
|
1486
|
+
function visualTextClasses(pageNumber, spans, fontAliases) {
|
|
1487
|
+
const names = /* @__PURE__ */ new Map();
|
|
1488
|
+
let css = "";
|
|
1489
|
+
for (const span of spans) {
|
|
1490
|
+
if (usesPositionedSpan(span) || span.glyphCodes) {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const style = visualTextClassStyle(span, fontAliases);
|
|
1494
|
+
if (!style || names.has(style)) continue;
|
|
1495
|
+
const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
|
|
1496
|
+
names.set(style, name);
|
|
1497
|
+
css += `.${name}{${style}}`;
|
|
1498
|
+
}
|
|
1499
|
+
return { css, names };
|
|
1500
|
+
}
|
|
1501
|
+
function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
|
|
1502
|
+
const first = spans[startIndex];
|
|
1503
|
+
if (!first || !canGroupVisualTextLine(first)) return void 0;
|
|
1504
|
+
const style = visualTextClassStyle(first, fontAliases);
|
|
1505
|
+
const className = styleClasses.get(style);
|
|
1506
|
+
if (!className) return void 0;
|
|
1507
|
+
let endIndex = startIndex;
|
|
1508
|
+
while (endIndex + 1 < spans.length) {
|
|
1509
|
+
const next = spans[endIndex + 1];
|
|
1510
|
+
if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
endIndex += 1;
|
|
1514
|
+
}
|
|
1515
|
+
if (endIndex === startIndex) return void 0;
|
|
1516
|
+
const baseline = pageHeight - first.bounds.y;
|
|
1517
|
+
const lineSpans = spans.slice(startIndex, endIndex + 1);
|
|
1518
|
+
const content = lineSpans.map(
|
|
1519
|
+
(span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
|
|
1520
|
+
).join("");
|
|
1521
|
+
return {
|
|
1522
|
+
html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
|
|
1523
|
+
endIndex
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function visualTextTspan(span, dx) {
|
|
1527
|
+
const extent = span.bounds.width;
|
|
1528
|
+
const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
|
|
1529
|
+
const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1530
|
+
return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
|
|
1531
|
+
}
|
|
1532
|
+
function textSpanGap(previous, current) {
|
|
1533
|
+
if (!previous) return 0;
|
|
1534
|
+
const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
|
|
1535
|
+
const adjustment = current.textAdjustmentBefore;
|
|
1536
|
+
return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
|
|
1537
|
+
}
|
|
1538
|
+
function canGroupVisualTextLine(span) {
|
|
1539
|
+
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));
|
|
1540
|
+
}
|
|
1141
1541
|
function usesReflectedVisualOverlay(page, spans) {
|
|
1142
1542
|
return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
|
|
1143
1543
|
(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
|
|
1144
1544
|
);
|
|
1145
1545
|
}
|
|
1146
|
-
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
1546
|
+
function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
|
|
1147
1547
|
const [a, b, c, d, e, f] = image.transform;
|
|
1148
1548
|
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
|
|
1149
1549
|
const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
|
|
1150
|
-
|
|
1151
|
-
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
1152
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
1550
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
|
|
1153
1551
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
1154
1552
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
1155
1553
|
}
|
|
1156
1554
|
return output;
|
|
1157
1555
|
}
|
|
1556
|
+
async function prepareVisualImages(page, imageOptions, onImage) {
|
|
1557
|
+
if (imageOptions === "excluded") return [];
|
|
1558
|
+
const sources = [];
|
|
1559
|
+
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1560
|
+
const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
1561
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
1562
|
+
if (imageOptions === "embedded") {
|
|
1563
|
+
sources.push(`data:${mimeType};base64,${base64(data)}`);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
1567
|
+
const name = `page-${page.number}-image-${index + 1}.${extension}`;
|
|
1568
|
+
await onImage?.({ name, mimeType, data });
|
|
1569
|
+
sources.push(name);
|
|
1570
|
+
}
|
|
1571
|
+
return sources;
|
|
1572
|
+
}
|
|
1158
1573
|
function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
1159
1574
|
return images.flatMap(
|
|
1160
1575
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
@@ -1221,8 +1636,9 @@ function positionedSpan(span, fontAliases) {
|
|
|
1221
1636
|
].join(";");
|
|
1222
1637
|
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
|
|
1223
1638
|
}
|
|
1224
|
-
async function writeFlowPage(page, write) {
|
|
1225
|
-
const
|
|
1639
|
+
async function writeFlowPage(page, write, options) {
|
|
1640
|
+
const imageOptions = resolveImageOptions("semantic", options);
|
|
1641
|
+
const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
|
|
1226
1642
|
const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
|
|
1227
1643
|
const defaultColor = dominantTextColor(structured.lines);
|
|
1228
1644
|
let mediaIndex = 0;
|
|
@@ -1359,10 +1775,37 @@ function semanticBlockY2(block) {
|
|
|
1359
1775
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
1360
1776
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
1361
1777
|
}
|
|
1362
|
-
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
|
|
1778
|
+
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
|
|
1363
1779
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
1364
1780
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
1365
1781
|
const direction = directionAttribute([span]);
|
|
1782
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1783
|
+
const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
|
|
1784
|
+
const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
|
|
1785
|
+
const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
|
|
1786
|
+
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1787
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1788
|
+
const transform = counterRotateReflectedText && span.transform ? [
|
|
1789
|
+
span.transform[0],
|
|
1790
|
+
span.transform[1],
|
|
1791
|
+
span.transform[2],
|
|
1792
|
+
-span.transform[3]
|
|
1793
|
+
] : span.transform;
|
|
1794
|
+
const transformed = hasNonIdentityTransform(transform);
|
|
1795
|
+
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1796
|
+
const basisX = transform?.[0] ?? 1;
|
|
1797
|
+
const basisY = transform?.[1] ?? 0;
|
|
1798
|
+
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1799
|
+
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1800
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1801
|
+
return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
|
|
1802
|
+
}
|
|
1803
|
+
function visualTextClassStyle(span, fontAliases) {
|
|
1804
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1805
|
+
const fontSize = `font-size:${number3(span.fontSize)}px`;
|
|
1806
|
+
return style ? `${style};${fontSize}` : fontSize;
|
|
1807
|
+
}
|
|
1808
|
+
function visualTextStyle(span, fontAliases) {
|
|
1366
1809
|
const font = visualFontStyles(
|
|
1367
1810
|
span.fontFamily,
|
|
1368
1811
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -1372,7 +1815,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1372
1815
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
1373
1816
|
const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
|
|
1374
1817
|
const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
|
|
1375
|
-
|
|
1818
|
+
return [
|
|
1376
1819
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
1377
1820
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
1378
1821
|
strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
|
|
@@ -1382,22 +1825,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1382
1825
|
strokeOpacity,
|
|
1383
1826
|
font
|
|
1384
1827
|
].filter(Boolean).join(";");
|
|
1385
|
-
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1386
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1387
|
-
const transform = counterRotateReflectedText && span.transform ? [
|
|
1388
|
-
span.transform[0],
|
|
1389
|
-
span.transform[1],
|
|
1390
|
-
span.transform[2],
|
|
1391
|
-
-span.transform[3]
|
|
1392
|
-
] : span.transform;
|
|
1393
|
-
const transformed = hasNonIdentityTransform(transform);
|
|
1394
|
-
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1395
|
-
const basisX = transform?.[0] ?? 1;
|
|
1396
|
-
const basisY = transform?.[1] ?? 0;
|
|
1397
|
-
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1398
|
-
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1399
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1400
|
-
return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
1401
1828
|
}
|
|
1402
1829
|
function isAdobeCjkFont(fontFamily) {
|
|
1403
1830
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -1492,10 +1919,19 @@ function resolveProfile(options) {
|
|
|
1492
1919
|
}
|
|
1493
1920
|
return options.profile ?? legacyProfile;
|
|
1494
1921
|
}
|
|
1922
|
+
function resolveImageOptions(profile, options) {
|
|
1923
|
+
return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
|
|
1924
|
+
}
|
|
1925
|
+
function validateImageOptions(imageOptions, options) {
|
|
1926
|
+
if (imageOptions === "references" && !options.onImage) {
|
|
1927
|
+
throw new Error('imageOptions "references" requires an onImage callback');
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1495
1930
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1496
1931
|
0 && (module.exports = {
|
|
1497
1932
|
pageToHtml,
|
|
1498
1933
|
writeHtmlDocument,
|
|
1934
|
+
writeMarkdownDocument,
|
|
1499
1935
|
writePage
|
|
1500
1936
|
});
|
|
1501
1937
|
//# sourceMappingURL=index.cjs.map
|