@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/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 += escapeHtml(text.slice(offset, range.start));
224
- html += styledHtml(text.slice(range.start, range.end), range);
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 + escapeHtml(text.slice(offset));
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 styledHtml(value, range) {
242
- let html = escapeHtml(value);
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
- const output = (page.images ?? []).map((image) => rasterMedia(image));
385
- output.push(...vectorMedia(page));
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 rasterMedia(image) {
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="data:${mime};base64,${base64(data)}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`
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: `![](${source})`,
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: `<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>`,
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" ? `![](${name})` : 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
- consumedSpans: items.flatMap((item) => item.consumedSpans ?? [])
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
- await write('<article class="pdf-semantic-document">');
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 write("</table>");
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 write("</section>");
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(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
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 write("</section>");
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 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
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 = `<div class="pdf-semantic-visual">${item?.html}</div>`;
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 = `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
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 write(
716
- `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</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 write(
725
- `<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${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 write(
732
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</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 write("</header>");
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) await write(tableRow(row, false));
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 write("<table>");
752
- for (const [index, row] of rows.entries())
753
- await write(tableRow(row, Boolean(header && index === 0)));
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 write(
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 write(
770
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</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) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
779
- for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
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 write(
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 write(
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(semanticBlockHtml(block, defaultColor));
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 = `<div class="pdf-semantic-visual">${item.html}</div>`;
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 = semanticMedia(page);
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 write("</header>");
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 write(
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 write("</article>");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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
- if (resolveProfile(options) === "semantic") {
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(pages, write, lookahead);
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
- if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
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 visualSpans = page.visualSpans ?? page.spans;
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(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
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
- await write(visualImage(image, page.height, page.number, index));
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
- await write(visualImage(image, page.height, page.number, index));
1392
+ const source = visualImages[index];
1393
+ if (source) await write(visualImage(image, page.height, page.number, index, source));
1102
1394
  }
1103
1395
  }
1104
- for (const span of visualSpans) {
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(span, page.height, fontAliases, reflectedOverlay && page.rotate === 180)
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
- const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
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 media = semanticMedia(page);
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
- const style = [
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