@boxpdf/html-writer 0.1.19 → 0.1.22

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();
@@ -359,17 +372,20 @@ function number(value) {
359
372
  // src/visual-font.ts
360
373
  function visualFontAliases(pageNumber, fonts) {
361
374
  return new Map(
362
- fonts.filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
375
+ fonts.filter(
376
+ (font) => (font.format === "truetype" || font.format === "opentype") && !/(?:courier|^TTE)/i.test(font.family ?? "")
377
+ ).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
363
378
  );
364
379
  }
365
380
  function visualFontFace(font, aliases) {
366
- if (font.format !== "truetype") return "";
381
+ if (font.format !== "truetype" && font.format !== "opentype") return "";
367
382
  const alias = aliases.get(font.id);
368
383
  if (!alias) return "";
369
384
  const styles2 = visualFontStyles(font.family, alias).filter(
370
385
  (style) => !style.startsWith("font-family:")
371
386
  );
372
- return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
387
+ const mime = font.format === "opentype" ? "font/otf" : "font/ttf";
388
+ return `@font-face{font-family:${alias};src:url(data:${mime};base64,${base64(font.data)}) format("${font.format}");${styles2.join(";")}}`;
373
389
  }
374
390
  function visualFontStyles(fontFamily, alias) {
375
391
  const normalized = fontFamily?.toLowerCase() ?? "";
@@ -403,23 +419,39 @@ function base64(bytes) {
403
419
  }
404
420
 
405
421
  // src/semantic-media.ts
406
- function semanticMedia(page) {
407
- const output = (page.images ?? []).map((image) => rasterMedia(image));
408
- output.push(...vectorMedia(page));
422
+ function semanticMedia(page, imageOptions = "embedded") {
423
+ if (imageOptions === "excluded") return [];
424
+ const output = (page.images ?? []).map(
425
+ (image, index) => rasterMedia(image, page.number, index, imageOptions)
426
+ );
427
+ output.push(...vectorMedia(page, imageOptions));
409
428
  return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
410
429
  }
411
- function rasterMedia(image) {
430
+ async function prepareSemanticMedia(page, imageOptions, onImage) {
431
+ const media = semanticMedia(page, imageOptions);
432
+ for (const item of media) {
433
+ for (const asset of item.assets ?? []) await onImage?.(asset);
434
+ delete item.assets;
435
+ }
436
+ return media;
437
+ }
438
+ function rasterMedia(image, pageNumber, index, imageOptions) {
412
439
  const bounds2 = transformedUnitBounds(image.transform);
413
440
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
414
441
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
442
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
443
+ const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
444
+ const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
415
445
  const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
416
446
  return {
417
447
  bounds: bounds2,
418
448
  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}">`
449
+ html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
450
+ markdown: `![](${source})`,
451
+ ...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
420
452
  };
421
453
  }
422
- function vectorMedia(page) {
454
+ function vectorMedia(page, imageOptions) {
423
455
  const primitives = [
424
456
  ...(page.paths ?? []).flatMap((path, index) => {
425
457
  const bounds2 = vectorPathBounds(path);
@@ -435,9 +467,11 @@ function vectorMedia(page) {
435
467
  );
436
468
  const aliases = visualFontAliases(page.number, page.fonts ?? []);
437
469
  const visualCodeFonts = new Set(
438
- (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
470
+ (page.fonts ?? []).filter(
471
+ (font) => (font.format === "truetype" || font.format === "opentype") && font.visualCodeMapping
472
+ ).map((font) => font.id)
439
473
  );
440
- return components.map((component) => {
474
+ return components.map((component, componentIndex) => {
441
475
  const bounds2 = component.bounds;
442
476
  const paths = component.primitives.flatMap(
443
477
  (primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
@@ -454,10 +488,18 @@ function vectorMedia(page) {
454
488
  );
455
489
  const fontIds = new Set(overlay.map((span) => span.fontAssetId));
456
490
  const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
491
+ 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>`;
492
+ const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
457
493
  return {
458
494
  bounds: bounds2,
459
495
  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>`,
496
+ html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
497
+ markdown: imageOptions === "references" ? `![](${name})` : svg,
498
+ ...imageOptions === "references" ? {
499
+ assets: [
500
+ { name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
501
+ ]
502
+ } : {},
461
503
  ...consumedSpans.length > 0 ? { consumedSpans } : {}
462
504
  };
463
505
  });
@@ -499,7 +541,9 @@ function compositeMedia(items) {
499
541
  bounds: bounds2,
500
542
  kind: "composite",
501
543
  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 ?? [])
544
+ markdown: items.map((item) => item.markdown).join("\n\n"),
545
+ consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
546
+ assets: items.flatMap((item) => item.assets ?? [])
503
547
  };
504
548
  }
505
549
  function mediaPiecesTouch(left, right) {
@@ -638,7 +682,7 @@ function escapeHtml2(value) {
638
682
  }
639
683
 
640
684
  // src/semantic-document.ts
641
- async function writeSemanticDocument(pages, write, lookaheadPages) {
685
+ async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
642
686
  const stats = {
643
687
  pagesProcessed: 0,
644
688
  peakBufferedPages: 0,
@@ -656,27 +700,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
656
700
  let contentStarted = false;
657
701
  let employmentOpen = false;
658
702
  let pendingParagraph;
659
- await write('<article class="pdf-semantic-document">');
703
+ const markdown = format === "markdown";
704
+ const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
705
+ const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
706
+ await output('<article class="pdf-semantic-document">');
660
707
  const closeTable = async () => {
661
708
  if (!activeTable) return;
662
- await write("</table>");
709
+ await output("</table>", "\n");
663
710
  activeTable = void 0;
664
711
  while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
665
712
  };
666
713
  const closeSections = async (minimumLevel = 0) => {
667
714
  while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
668
- await write("</section>");
715
+ await output("</section>");
669
716
  sectionLevels.pop();
670
717
  }
671
718
  };
672
719
  const flushPendingParagraph = async () => {
673
720
  if (!pendingParagraph) return;
674
- await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
721
+ await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
675
722
  pendingParagraph = void 0;
676
723
  };
677
724
  const closeEmployment = async () => {
678
725
  if (!employmentOpen) return;
679
- await write("</section>");
726
+ await output("</section>");
680
727
  employmentOpen = false;
681
728
  };
682
729
  const emitPage = async (page, future) => {
@@ -704,7 +751,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
704
751
  await flushPendingParagraph();
705
752
  const item = page.media[mediaIndex];
706
753
  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>`;
754
+ const html2 = markdown ? `${item.markdown}
755
+
756
+ *${inlineText(block.text, block.lines, defaultColor)}*
757
+
758
+ ` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
708
759
  if (activeTable) pendingMedia.push(html2);
709
760
  else await write(html2);
710
761
  emittedMedia.add(item);
@@ -713,7 +764,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
713
764
  break;
714
765
  }
715
766
  if (item && captionedMedia.has(item)) break;
716
- const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
767
+ const html = markdown ? `${item?.markdown ?? ""}
768
+
769
+ ` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
717
770
  if (activeTable) pendingMedia.push(html);
718
771
  else await write(html);
719
772
  mediaIndex += 1;
@@ -721,7 +774,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
721
774
  const associatedMedia = captions.get(block);
722
775
  if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
723
776
  await flushPendingParagraph();
724
- const html = `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
777
+ const html = markdown ? `${associatedMedia.markdown}
778
+
779
+ *${inlineText(block.text, block.lines, defaultColor)}*
780
+
781
+ ` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
725
782
  if (activeTable) pendingMedia.push(html);
726
783
  else await write(html);
727
784
  emittedMedia.add(associatedMedia);
@@ -735,8 +792,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
735
792
  await flushPendingParagraph();
736
793
  if (employmentOpen && block.type !== "list") await closeEmployment();
737
794
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
738
- await write(
739
- `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
795
+ await output(
796
+ `<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
797
+ `# ${inlineText(block.text, block.lines, defaultColor, false)}
798
+
799
+ `
740
800
  );
741
801
  headerOpen = true;
742
802
  continue;
@@ -744,19 +804,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
744
804
  if (headerOpen) {
745
805
  if (block.type === "paragraph") {
746
806
  const tag = isContactBlock(block) ? "address" : "p";
747
- await write(
748
- `<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
807
+ await output(
808
+ `<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
809
+ `${inlineText(block.text, block.lines, defaultColor)}
810
+
811
+ `
749
812
  );
750
813
  headerHasParagraph = true;
751
814
  continue;
752
815
  }
753
816
  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}>`
817
+ await output(
818
+ `<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
819
+ `${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
820
+
821
+ `
756
822
  );
757
823
  continue;
758
824
  }
759
- await write("</header>");
825
+ await output("</header>");
760
826
  headerOpen = false;
761
827
  contentStarted = true;
762
828
  }
@@ -764,23 +830,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
764
830
  const rows = (0, import_structure.tableToRows)(block.table);
765
831
  if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
766
832
  const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
767
- for (const row of continuationRows) await write(tableRow(row, false));
833
+ for (const row of continuationRows)
834
+ await write(markdown ? markdownTableRow(row) : tableRow(row, false));
768
835
  activeTable.table = block.table;
769
836
  stats.mergedTables += 1;
770
837
  continue;
771
838
  }
772
839
  await closeTable();
773
840
  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)));
841
+ await output("<table>", markdownTableStart(rows, header));
842
+ const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
843
+ for (const [index, row] of markdownRows.entries())
844
+ await write(
845
+ markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
846
+ );
777
847
  activeTable = { table: block.table, header };
778
848
  continue;
779
849
  }
780
850
  if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
781
851
  const columns = activeTable.table.columns.length;
782
- await write(
783
- `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`
852
+ await output(
853
+ `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
854
+ block.entries.map(
855
+ (entry) => markdownTableRow([
856
+ entry.term,
857
+ ...Array(Math.max(0, columns - 2)).fill(""),
858
+ entry.description
859
+ ])
860
+ ).join("")
784
861
  );
785
862
  await closeTable();
786
863
  continue;
@@ -789,8 +866,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
789
866
  if (block.type === "heading") {
790
867
  const level = contentStarted && block.level === 1 ? 2 : block.level;
791
868
  await closeSections(level);
792
- await write(
793
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
869
+ await output(
870
+ `<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
871
+ `${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
872
+
873
+ `
794
874
  );
795
875
  sectionLevels.push(level);
796
876
  continue;
@@ -798,13 +878,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
798
878
  if (block.type === "paragraph") {
799
879
  if (isTitledRecord(block)) {
800
880
  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>`);
881
+ if (institution)
882
+ await output(
883
+ `<h3>${escapeHtml3(institution.text)}</h3>`,
884
+ `### ${escapeMarkdown2(institution.text)}
885
+
886
+ `
887
+ );
888
+ for (const detail of details)
889
+ await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
890
+
891
+ `);
803
892
  continue;
804
893
  }
805
894
  if (isUnmarkedList(block)) {
806
- await write(
807
- `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
895
+ await output(
896
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
897
+ `${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
898
+
899
+ `
808
900
  );
809
901
  continue;
810
902
  }
@@ -812,19 +904,28 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
812
904
  continue;
813
905
  }
814
906
  if (block.type === "employment") {
815
- await write(
816
- `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
907
+ await output(
908
+ `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
909
+ `### ${escapeMarkdown2(block.role)}
910
+
911
+ ${escapeMarkdown2(block.organization)}
912
+
913
+ ${escapeMarkdown2(block.date)}
914
+
915
+ `
817
916
  );
818
917
  employmentOpen = true;
819
918
  continue;
820
919
  }
821
- await write(semanticBlockHtml(block, defaultColor));
920
+ await write(semanticBlockOutput(block, defaultColor, format));
822
921
  }
823
922
  while (mediaIndex < page.media.length) {
824
923
  const item = page.media[mediaIndex];
825
924
  if (item && !emittedMedia.has(item)) {
826
925
  await flushPendingParagraph();
827
- const html = `<div class="pdf-semantic-visual">${item.html}</div>`;
926
+ const html = markdown ? `${item.markdown}
927
+
928
+ ` : `<div class="pdf-semantic-visual">${item.html}</div>`;
828
929
  if (activeTable) pendingMedia.push(html);
829
930
  else await write(html);
830
931
  }
@@ -833,9 +934,10 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
833
934
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
834
935
  };
835
936
  for await (const page of pages) {
836
- const media = semanticMedia(page);
937
+ const media = await prepareSemanticMedia(page, imageOptions, onImage);
837
938
  const structured = (0, import_structure.structurePage)(withoutSemanticMediaSpans(page, media));
838
939
  buffer.push({ width: page.width, height: page.height, structured, media });
940
+ restoreObservedHyphens(buffer);
839
941
  stats.pagesProcessed += 1;
840
942
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
841
943
  stats.peakBufferedLines = Math.max(
@@ -851,22 +953,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
851
953
  const ready = buffer.shift();
852
954
  if (ready) await emitPage(ready, buffer);
853
955
  }
854
- if (headerOpen) await write("</header>");
956
+ if (headerOpen) await output("</header>");
855
957
  await closeTable();
856
958
  await closeEmployment();
857
959
  if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
858
960
  await closeSections();
859
- await write(
860
- `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
961
+ await output(
962
+ `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
963
+ `---
964
+
965
+ ${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
861
966
  );
862
967
  pendingParagraph = void 0;
863
968
  } else {
864
969
  await flushPendingParagraph();
865
970
  await closeSections();
866
971
  }
867
- await write("</article>");
972
+ await output("</article>");
868
973
  return stats;
869
974
  }
975
+ function restoreObservedHyphens(buffer) {
976
+ const terms = new Set(
977
+ buffer.flatMap(
978
+ (page) => page.structured.lines.flatMap(
979
+ (line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
980
+ )
981
+ )
982
+ );
983
+ for (const page of buffer) {
984
+ for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
985
+ }
986
+ }
987
+ function restoreBlockHyphens(block, terms) {
988
+ const restore = (value) => restoreTextHyphens(value, terms);
989
+ if (block.type === "insetGroup") {
990
+ for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
991
+ } else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
992
+ block.text = restore(block.text);
993
+ } else if (block.type === "list") {
994
+ for (const item of block.items) item.text = restore(item.text);
995
+ } else if (block.type === "definitionList") {
996
+ for (const entry of block.entries) {
997
+ entry.term = restore(entry.term);
998
+ entry.description = restore(entry.description);
999
+ }
1000
+ } else if (block.type === "cardList") {
1001
+ for (const item of block.items) {
1002
+ item.title = restore(item.title);
1003
+ item.details = item.details.map(restore);
1004
+ }
1005
+ } else if (block.type === "sectionGroup") {
1006
+ for (const item of block.items) {
1007
+ item.label = restore(item.label);
1008
+ item.content = item.content.map(restore);
1009
+ }
1010
+ } else if (block.type === "employment") {
1011
+ block.role = restore(block.role);
1012
+ block.organization = restore(block.organization);
1013
+ block.date = restore(block.date);
1014
+ }
1015
+ }
1016
+ function restoreTextHyphens(value, terms) {
1017
+ let output = value;
1018
+ for (const term of terms) {
1019
+ const collapsed = term.replace(/[-‐‑]/gu, "");
1020
+ if (collapsed === term || !output.includes(collapsed)) continue;
1021
+ const pattern = new RegExp(
1022
+ `(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
1023
+ "gu"
1024
+ );
1025
+ output = output.replace(pattern, term);
1026
+ }
1027
+ return output;
1028
+ }
1029
+ function escapeRegularExpression(value) {
1030
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1031
+ }
870
1032
  function isContactBlock(block) {
871
1033
  const text = block.text;
872
1034
  const signals = [
@@ -950,6 +1112,20 @@ function tableRow(row, header) {
950
1112
  const cell = header ? "th" : "td";
951
1113
  return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
952
1114
  }
1115
+ function markdownTableStart(rows, header) {
1116
+ const columns = rows[0]?.length ?? 0;
1117
+ if (columns === 0) return "";
1118
+ const heading = header ?? Array(columns).fill("");
1119
+ return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
1120
+ }
1121
+ function markdownTableRow(row, shouldEscape = true) {
1122
+ const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
1123
+ return `| ${cells.join(" | ")} |
1124
+ `;
1125
+ }
1126
+ function escapeMarkdownTableCell(value) {
1127
+ return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
1128
+ }
953
1129
  function isFinancialSummary(block) {
954
1130
  return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
955
1131
  }
@@ -997,6 +1173,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
997
1173
  const tag = block.ordered ? "ol" : "ul";
998
1174
  return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
999
1175
  }
1176
+ function semanticBlockOutput(block, defaultColor, format) {
1177
+ return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
1178
+ }
1179
+ function semanticBlockMarkdown(block, defaultColor = "#000000") {
1180
+ if (block.type === "insetGroup") {
1181
+ const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
1182
+ return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
1183
+
1184
+ `;
1185
+ }
1186
+ if (block.type === "table") {
1187
+ const rows = (0, import_structure.tableToRows)(block.table);
1188
+ const header = tableHeader(rows);
1189
+ return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
1190
+ `;
1191
+ }
1192
+ if (block.type === "heading") {
1193
+ return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
1194
+
1195
+ `;
1196
+ }
1197
+ if (block.type === "paragraph") {
1198
+ return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
1199
+
1200
+ `;
1201
+ }
1202
+ if (block.type === "preformatted") {
1203
+ const fence = block.text.includes("```") ? "````" : "```";
1204
+ return `${fence}
1205
+ ${block.text}
1206
+ ${fence}
1207
+
1208
+ `;
1209
+ }
1210
+ if (block.type === "definitionList") {
1211
+ return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
1212
+
1213
+ `;
1214
+ }
1215
+ if (block.type === "cardList") {
1216
+ const rows = [
1217
+ ["Item", "Quantity", "Amount"],
1218
+ ...block.items.map((item) => {
1219
+ const trailing = item.details.at(-1) ?? "";
1220
+ const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
1221
+ const detail = item.details.slice(0, -1).join(" ");
1222
+ return [
1223
+ `${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
1224
+ match?.[1] ?? "",
1225
+ match?.[2] ?? trailing
1226
+ ];
1227
+ })
1228
+ ];
1229
+ return `## Items ordered
1230
+
1231
+ ${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
1232
+ `;
1233
+ }
1234
+ if (block.type === "sectionGroup") {
1235
+ return block.items.map(
1236
+ (item) => `## ${escapeMarkdown2(titleCase(item.label))}
1237
+
1238
+ ${item.content.map(
1239
+ (content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
1240
+ ).join("\n\n")}
1241
+
1242
+ `
1243
+ ).join("");
1244
+ }
1245
+ if (block.type === "employment") {
1246
+ return `### ${escapeMarkdown2(block.role)}
1247
+
1248
+ ${escapeMarkdown2(block.organization)}
1249
+
1250
+ ${escapeMarkdown2(block.date)}
1251
+
1252
+ `;
1253
+ }
1254
+ return `${block.items.map(
1255
+ (item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
1256
+ ).join("\n")}
1257
+
1258
+ `;
1259
+ }
1000
1260
  function semanticBlockY(block) {
1001
1261
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
1002
1262
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -1029,6 +1289,9 @@ function titleCase(value) {
1029
1289
  function escapeHtml3(value) {
1030
1290
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1031
1291
  }
1292
+ function escapeMarkdown2(value) {
1293
+ return value.replace(/([\\`*_[\]<>])/g, "\\$1");
1294
+ }
1032
1295
 
1033
1296
  // src/index.ts
1034
1297
  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,16 +1308,40 @@ async function writeHtmlDocument(pages, write, options = {}) {
1045
1308
  await write("</head><body>");
1046
1309
  }
1047
1310
  await write('<main class="pdf-document">');
1048
- if (resolveProfile(options) === "semantic") {
1311
+ const profile = resolveProfile(options);
1312
+ const imageOptions = resolveImageOptions(profile, options);
1313
+ validateImageOptions(imageOptions, options);
1314
+ if (profile === "semantic") {
1049
1315
  const lookahead = semanticLookahead(options.semanticLookaheadPages);
1050
- const stats = await writeSemanticDocument(pages, write, lookahead);
1316
+ const stats = await writeSemanticDocument(
1317
+ pages,
1318
+ write,
1319
+ lookahead,
1320
+ imageOptions,
1321
+ options.onImage
1322
+ );
1051
1323
  options.onSemanticStats?.(stats);
1052
1324
  } else {
1053
- for await (const page of pages) await writePage(page, write, options);
1325
+ const documentFonts = { entries: [] };
1326
+ for await (const page of pages) await writePositionedPage(page, write, options, documentFonts);
1054
1327
  }
1055
1328
  await write("</main>");
1056
1329
  if (includeDocument) await write("</body></html>");
1057
1330
  }
1331
+ async function writeMarkdownDocument(pages, write, options = {}) {
1332
+ const imageOptions = options.imageOptions ?? "excluded";
1333
+ validateImageOptions(imageOptions, options);
1334
+ const lookahead = semanticLookahead(options.semanticLookaheadPages);
1335
+ const stats = await writeSemanticDocument(
1336
+ pages,
1337
+ write,
1338
+ lookahead,
1339
+ imageOptions,
1340
+ options.onImage,
1341
+ "markdown"
1342
+ );
1343
+ options.onSemanticStats?.(stats);
1344
+ }
1058
1345
  function semanticLookahead(value) {
1059
1346
  const lookahead = value ?? 4;
1060
1347
  if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
@@ -1063,7 +1350,9 @@ function semanticLookahead(value) {
1063
1350
  return lookahead;
1064
1351
  }
1065
1352
  async function writePage(page, write, options = {}) {
1066
- if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
1353
+ const profile = resolveProfile(options);
1354
+ validateImageOptions(resolveImageOptions(profile, options), options);
1355
+ if (profile === "semantic") await writeFlowPage(page, write, options);
1067
1356
  else await writePositionedPage(page, write, options);
1068
1357
  }
1069
1358
  async function pageToHtml(page, options = {}) {
@@ -1077,8 +1366,10 @@ async function pageToHtml(page, options = {}) {
1077
1366
  );
1078
1367
  return output;
1079
1368
  }
1080
- async function writePositionedPage(page, write, options) {
1081
- const visualSpans = page.visualSpans ?? page.spans;
1369
+ async function writePositionedPage(page, write, options, documentFonts) {
1370
+ const imageOptions = resolveImageOptions("visual", options);
1371
+ const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
1372
+ const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
1082
1373
  const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
1083
1374
  const quarterTurn = page.rotate === 90 || page.rotate === 270;
1084
1375
  const displayWidth = quarterTurn ? page.height : page.width;
@@ -1086,29 +1377,40 @@ async function writePositionedPage(page, write, options) {
1086
1377
  await write(
1087
1378
  `<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number3(displayWidth)}pt;height:${number3(displayHeight)}pt">`
1088
1379
  );
1089
- const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
1380
+ const { aliases: fontAliases, fontsToEmit } = visualPageFonts(
1381
+ page.number,
1382
+ page.fonts ?? [],
1383
+ documentFonts
1384
+ );
1090
1385
  const type3Fonts = new Map(
1091
1386
  (page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
1092
1387
  );
1093
- if ((options.includeStyles ?? true) && page.fonts?.length) {
1388
+ const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
1389
+ if ((options.includeStyles ?? true) && fontsToEmit.length) {
1094
1390
  await write(
1095
- `<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
1391
+ `<style>${fontsToEmit.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
1096
1392
  );
1097
1393
  }
1394
+ if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
1098
1395
  await write(
1099
1396
  `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
1100
1397
  );
1101
1398
  await write(
1102
1399
  `<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
1400
  );
1104
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
1401
+ const clipDefinitions = imageClipDefinitions(
1402
+ imageOptions === "excluded" ? [] : page.images ?? [],
1403
+ page.number,
1404
+ page.height
1405
+ ) + vectorPathClipDefinitions(
1105
1406
  (page.paths ?? []).map((path, index) => ({ path, index })),
1106
1407
  page.number
1107
1408
  );
1108
1409
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
1109
1410
  if (reflectedOverlay) {
1110
1411
  for (const [index, image] of (page.images ?? []).entries()) {
1111
- await write(visualImage(image, page.height, page.number, index));
1412
+ const source = visualImages[index];
1413
+ if (source) await write(visualImage(image, page.height, page.number, index, source));
1112
1414
  }
1113
1415
  }
1114
1416
  if (page.fills?.length || page.paths?.length) {
@@ -1121,14 +1423,29 @@ async function writePositionedPage(page, write, options) {
1121
1423
  }
1122
1424
  if (!reflectedOverlay) {
1123
1425
  for (const [index, image] of (page.images ?? []).entries()) {
1124
- await write(visualImage(image, page.height, page.number, index));
1426
+ const source = visualImages[index];
1427
+ if (source) await write(visualImage(image, page.height, page.number, index, source));
1125
1428
  }
1126
1429
  }
1127
- for (const span of visualSpans) {
1430
+ for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
1431
+ const span = visualSpans[spanIndex];
1432
+ if (!span) continue;
1128
1433
  if (!usesPositionedSpan(span)) {
1129
1434
  const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
1435
+ const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
1436
+ if (line) {
1437
+ await write(line.html);
1438
+ spanIndex = line.endIndex;
1439
+ continue;
1440
+ }
1130
1441
  await write(
1131
- type3 ? visualType3Text(span, type3, page.height) : visualText(span, page.height, fontAliases, reflectedOverlay && page.rotate === 180)
1442
+ type3 ? visualType3Text(span, type3, page.height) : visualText(
1443
+ span,
1444
+ page.height,
1445
+ fontAliases,
1446
+ reflectedOverlay && page.rotate === 180,
1447
+ textClasses?.names
1448
+ )
1132
1449
  );
1133
1450
  }
1134
1451
  }
@@ -1138,23 +1455,191 @@ async function writePositionedPage(page, write, options) {
1138
1455
  }
1139
1456
  await write("</div></section>");
1140
1457
  }
1458
+ function visualPageFonts(pageNumber, fonts, documentFonts) {
1459
+ const aliases = visualFontAliases(pageNumber, fonts);
1460
+ if (!documentFonts) return { aliases, fontsToEmit: fonts };
1461
+ const fontsToEmit = [];
1462
+ for (const font of fonts) {
1463
+ if (!aliases.has(font.id) || font.format === "type3") continue;
1464
+ const existing = documentFonts.entries.find(
1465
+ (entry) => entry.format === font.format && equalBytes(entry.data, font.data)
1466
+ );
1467
+ if (existing) {
1468
+ aliases.set(font.id, existing.alias);
1469
+ continue;
1470
+ }
1471
+ const alias = `boxpdf-document-font-${documentFonts.entries.length + 1}`;
1472
+ aliases.set(font.id, alias);
1473
+ documentFonts.entries.push({ alias, data: font.data, format: font.format });
1474
+ fontsToEmit.push(font);
1475
+ }
1476
+ return { aliases, fontsToEmit };
1477
+ }
1478
+ function equalBytes(left, right) {
1479
+ if (left.length !== right.length) return false;
1480
+ return left.every((value, index) => value === right[index]);
1481
+ }
1482
+ function coalesceVisualSpans(spans) {
1483
+ const output = [];
1484
+ for (const span of spans) {
1485
+ const previous = output.at(-1);
1486
+ if (!previous || !canCoalesceVisualSpans(previous, span)) {
1487
+ output.push(span);
1488
+ continue;
1489
+ }
1490
+ output[output.length - 1] = {
1491
+ ...previous,
1492
+ text: previous.text + span.text,
1493
+ bounds: {
1494
+ ...previous.bounds,
1495
+ width: span.bounds.x + span.bounds.width - previous.bounds.x,
1496
+ height: Math.max(previous.bounds.height, span.bounds.height)
1497
+ }
1498
+ };
1499
+ }
1500
+ return output;
1501
+ }
1502
+ function canCoalesceVisualSpans(left, right) {
1503
+ if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
1504
+ if (left.direction !== "ltr" || right.direction !== "ltr") return false;
1505
+ if (/guardian/i.test(left.fontFamily ?? "")) return false;
1506
+ if (left.glyphCodes || right.glyphCodes) return false;
1507
+ if (left.fontAssetId || right.fontAssetId) return false;
1508
+ if (!sameVisualTextState(left, right)) return false;
1509
+ const tolerance = Math.max(0.02, left.fontSize * 0.015);
1510
+ if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
1511
+ const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
1512
+ if (right.hasLeadingSpace) return false;
1513
+ if (gap >= -tolerance && gap <= tolerance) return true;
1514
+ return right.textAdjustmentBefore !== void 0 && right.textAdjustmentBefore < 0 && Math.abs(right.textAdjustmentBefore - gap) <= 1e-3;
1515
+ }
1516
+ function sameVisualTextState(left, right) {
1517
+ 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);
1518
+ }
1519
+ function sameTransform(left, right) {
1520
+ if (!left || !right) return left === right;
1521
+ return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
1522
+ }
1523
+ function visualTextClasses(pageNumber, spans, fontAliases) {
1524
+ const names = /* @__PURE__ */ new Map();
1525
+ let css = "";
1526
+ for (const span of spans) {
1527
+ if (usesPositionedSpan(span) || span.glyphCodes) {
1528
+ continue;
1529
+ }
1530
+ const style = visualTextClassStyle(span, fontAliases);
1531
+ if (!style || names.has(style)) continue;
1532
+ const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
1533
+ names.set(style, name);
1534
+ css += `.${name}{${style}}`;
1535
+ }
1536
+ return { css, names };
1537
+ }
1538
+ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
1539
+ const first = spans[startIndex];
1540
+ if (!first || !canGroupVisualTextLine(first)) return void 0;
1541
+ const style = visualTextClassStyle(first, fontAliases);
1542
+ const className = styleClasses.get(style);
1543
+ if (!className) return void 0;
1544
+ let endIndex = startIndex;
1545
+ while (endIndex + 1 < spans.length) {
1546
+ const next = spans[endIndex + 1];
1547
+ if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
1548
+ break;
1549
+ }
1550
+ endIndex += 1;
1551
+ }
1552
+ if (endIndex === startIndex) return void 0;
1553
+ const baseline = pageHeight - first.bounds.y;
1554
+ const lineSpans = spans.slice(startIndex, endIndex + 1);
1555
+ const dxRun = visualDxTextRun(lineSpans, fontAliases);
1556
+ if (dxRun) {
1557
+ return {
1558
+ html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}" dx="${dxRun.offsets.map(number3).join(" ")}">${escapeHtml4(dxRun.text)}</text>`,
1559
+ endIndex
1560
+ };
1561
+ }
1562
+ const content = lineSpans.map(
1563
+ (span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
1564
+ ).join("");
1565
+ return {
1566
+ html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
1567
+ endIndex
1568
+ };
1569
+ }
1570
+ function visualDxTextRun(spans, fontAliases) {
1571
+ const first = spans[0];
1572
+ if (!first?.fontAssetId || !fontAliases.has(first.fontAssetId) || spans.length < 2)
1573
+ return void 0;
1574
+ let text = first.text;
1575
+ const offsets = characterOffsets(first, 0);
1576
+ for (let index = 1; index < spans.length; index += 1) {
1577
+ const previous = spans[index - 1];
1578
+ const current = spans[index];
1579
+ if (!previous || !current || current.fontAssetId !== first.fontAssetId) return void 0;
1580
+ const characters = [...current.text];
1581
+ if (characters.length === 0 || previous.naturalWidth === void 0) return void 0;
1582
+ const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
1583
+ const trailingSpacing = (previous.characterSpacing ?? 0) + (previous.text.endsWith(" ") ? previous.wordSpacing ?? 0 : 0);
1584
+ offsets.push(...characterOffsets(current, gap + trailingSpacing));
1585
+ text += current.text;
1586
+ }
1587
+ while (offsets.at(-1) === 0) offsets.pop();
1588
+ return offsets.length > 0 ? { text, offsets } : void 0;
1589
+ }
1590
+ function characterOffsets(span, first) {
1591
+ const characters = [...span.text];
1592
+ return characters.map(
1593
+ (_, index) => index === 0 ? first : (span.characterSpacing ?? 0) + (characters[index - 1] === " " ? span.wordSpacing ?? 0 : 0)
1594
+ );
1595
+ }
1596
+ function visualTextTspan(span, dx) {
1597
+ const extent = span.bounds.width;
1598
+ const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
1599
+ const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1600
+ return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
1601
+ }
1602
+ function textSpanGap(previous, current) {
1603
+ if (!previous) return 0;
1604
+ const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
1605
+ const adjustment = current.textAdjustmentBefore;
1606
+ return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
1607
+ }
1608
+ function canGroupVisualTextLine(span) {
1609
+ 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));
1610
+ }
1141
1611
  function usesReflectedVisualOverlay(page, spans) {
1142
1612
  return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
1143
1613
  (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
1614
  );
1145
1615
  }
1146
- function visualImage(image, pageHeight, pageNumber, imageIndex) {
1616
+ function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
1147
1617
  const [a, b, c, d, e, f] = image.transform;
1148
1618
  const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
1149
1619
  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}/>`;
1620
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
1153
1621
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
1154
1622
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
1155
1623
  }
1156
1624
  return output;
1157
1625
  }
1626
+ async function prepareVisualImages(page, imageOptions, onImage) {
1627
+ if (imageOptions === "excluded") return [];
1628
+ const sources = [];
1629
+ for (const [index, image] of (page.images ?? []).entries()) {
1630
+ const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
1631
+ const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
1632
+ if (imageOptions === "embedded") {
1633
+ sources.push(`data:${mimeType};base64,${base64(data)}`);
1634
+ continue;
1635
+ }
1636
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
1637
+ const name = `page-${page.number}-image-${index + 1}.${extension}`;
1638
+ await onImage?.({ name, mimeType, data });
1639
+ sources.push(name);
1640
+ }
1641
+ return sources;
1642
+ }
1158
1643
  function imageClipDefinitions(images, pageNumber, pageHeight) {
1159
1644
  return images.flatMap(
1160
1645
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
@@ -1221,8 +1706,9 @@ function positionedSpan(span, fontAliases) {
1221
1706
  ].join(";");
1222
1707
  return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
1223
1708
  }
1224
- async function writeFlowPage(page, write) {
1225
- const media = semanticMedia(page);
1709
+ async function writeFlowPage(page, write, options) {
1710
+ const imageOptions = resolveImageOptions("semantic", options);
1711
+ const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
1226
1712
  const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
1227
1713
  const defaultColor = dominantTextColor(structured.lines);
1228
1714
  let mediaIndex = 0;
@@ -1359,10 +1845,37 @@ function semanticBlockY2(block) {
1359
1845
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
1360
1846
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
1361
1847
  }
1362
- function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
1848
+ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
1363
1849
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
1364
1850
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
1365
1851
  const direction = directionAttribute([span]);
1852
+ const style = visualTextStyle(span, fontAliases);
1853
+ const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
1854
+ const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
1855
+ const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
1856
+ const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
1857
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1858
+ const transform = counterRotateReflectedText && span.transform ? [
1859
+ span.transform[0],
1860
+ span.transform[1],
1861
+ span.transform[2],
1862
+ -span.transform[3]
1863
+ ] : span.transform;
1864
+ const transformed = hasNonIdentityTransform(transform);
1865
+ const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
1866
+ const basisX = transform?.[0] ?? 1;
1867
+ const basisY = transform?.[1] ?? 0;
1868
+ const anchorX = span.bounds.x + basisX * rtlOffset;
1869
+ const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1870
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1871
+ return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
1872
+ }
1873
+ function visualTextClassStyle(span, fontAliases) {
1874
+ const style = visualTextStyle(span, fontAliases);
1875
+ const fontSize = `font-size:${number3(span.fontSize)}px`;
1876
+ return style ? `${style};${fontSize}` : fontSize;
1877
+ }
1878
+ function visualTextStyle(span, fontAliases) {
1366
1879
  const font = visualFontStyles(
1367
1880
  span.fontFamily,
1368
1881
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -1372,7 +1885,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1372
1885
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
1373
1886
  const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
1374
1887
  const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
1375
- const style = [
1888
+ return [
1376
1889
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
1377
1890
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
1378
1891
  strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
@@ -1382,22 +1895,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1382
1895
  strokeOpacity,
1383
1896
  font
1384
1897
  ].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
1898
  }
1402
1899
  function isAdobeCjkFont(fontFamily) {
1403
1900
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -1492,10 +1989,19 @@ function resolveProfile(options) {
1492
1989
  }
1493
1990
  return options.profile ?? legacyProfile;
1494
1991
  }
1992
+ function resolveImageOptions(profile, options) {
1993
+ return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
1994
+ }
1995
+ function validateImageOptions(imageOptions, options) {
1996
+ if (imageOptions === "references" && !options.onImage) {
1997
+ throw new Error('imageOptions "references" requires an onImage callback');
1998
+ }
1999
+ }
1495
2000
  // Annotate the CommonJS export names for ESM import in node:
1496
2001
  0 && (module.exports = {
1497
2002
  pageToHtml,
1498
2003
  writeHtmlDocument,
2004
+ writeMarkdownDocument,
1499
2005
  writePage
1500
2006
  });
1501
2007
  //# sourceMappingURL=index.cjs.map