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