@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.js CHANGED
@@ -195,6 +195,12 @@ function dominantTextColor(lines) {
195
195
  return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
196
196
  }
197
197
  function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
198
+ return semanticText(text, lines, defaultColor, preserveWeight, "html");
199
+ }
200
+ function semanticTextMarkdown(text, lines, defaultColor, preserveWeight = true) {
201
+ return semanticText(text, lines, defaultColor, preserveWeight, "markdown");
202
+ }
203
+ function semanticText(text, lines, defaultColor, preserveWeight, format) {
198
204
  const ranges = [];
199
205
  let cursor = 0;
200
206
  for (const span of lines.flatMap((line) => line.spans)) {
@@ -220,11 +226,11 @@ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
220
226
  let html = "";
221
227
  let offset = 0;
222
228
  for (const range of merged) {
223
- html += escapeHtml(text.slice(offset, range.start));
224
- html += styledHtml(text.slice(range.start, range.end), range);
229
+ html += escapeText(text.slice(offset, range.start), format);
230
+ html += styledText(text.slice(range.start, range.end), range, format);
225
231
  offset = range.end;
226
232
  }
227
- return html + escapeHtml(text.slice(offset));
233
+ return html + escapeText(text.slice(offset), format);
228
234
  }
229
235
  function mergeRanges(ranges, text) {
230
236
  const merged = [];
@@ -238,13 +244,19 @@ function mergeRanges(ranges, text) {
238
244
  }
239
245
  return merged;
240
246
  }
241
- function styledHtml(value, range) {
242
- let html = escapeHtml(value);
247
+ function styledText(value, range, format) {
248
+ let html = escapeText(value, format);
243
249
  if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
244
- if (range.italic) html = `<em>${html}</em>`;
245
- if (range.bold) html = `<strong>${html}</strong>`;
250
+ if (range.italic) html = format === "html" ? `<em>${html}</em>` : `_${html}_`;
251
+ if (range.bold) html = format === "html" ? `<strong>${html}</strong>` : `**${html}**`;
246
252
  return html;
247
253
  }
254
+ function escapeText(value, format) {
255
+ return format === "html" ? escapeHtml(value) : escapeMarkdown(value);
256
+ }
257
+ function escapeMarkdown(value) {
258
+ return value.replace(/([\\`*_[\]<>])/g, "\\$1");
259
+ }
248
260
  function normalizedColor(value) {
249
261
  if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
250
262
  const color = value.toLowerCase();
@@ -336,17 +348,20 @@ function number(value) {
336
348
  // src/visual-font.ts
337
349
  function visualFontAliases(pageNumber, fonts) {
338
350
  return new Map(
339
- fonts.filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
351
+ fonts.filter(
352
+ (font) => (font.format === "truetype" || font.format === "opentype") && !/(?:courier|^TTE)/i.test(font.family ?? "")
353
+ ).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
340
354
  );
341
355
  }
342
356
  function visualFontFace(font, aliases) {
343
- if (font.format !== "truetype") return "";
357
+ if (font.format !== "truetype" && font.format !== "opentype") return "";
344
358
  const alias = aliases.get(font.id);
345
359
  if (!alias) return "";
346
360
  const styles2 = visualFontStyles(font.family, alias).filter(
347
361
  (style) => !style.startsWith("font-family:")
348
362
  );
349
- return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
363
+ const mime = font.format === "opentype" ? "font/otf" : "font/ttf";
364
+ return `@font-face{font-family:${alias};src:url(data:${mime};base64,${base64(font.data)}) format("${font.format}");${styles2.join(";")}}`;
350
365
  }
351
366
  function visualFontStyles(fontFamily, alias) {
352
367
  const normalized = fontFamily?.toLowerCase() ?? "";
@@ -380,23 +395,39 @@ function base64(bytes) {
380
395
  }
381
396
 
382
397
  // src/semantic-media.ts
383
- function semanticMedia(page) {
384
- const output = (page.images ?? []).map((image) => rasterMedia(image));
385
- output.push(...vectorMedia(page));
398
+ function semanticMedia(page, imageOptions = "embedded") {
399
+ if (imageOptions === "excluded") return [];
400
+ const output = (page.images ?? []).map(
401
+ (image, index) => rasterMedia(image, page.number, index, imageOptions)
402
+ );
403
+ output.push(...vectorMedia(page, imageOptions));
386
404
  return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
387
405
  }
388
- function rasterMedia(image) {
406
+ async function prepareSemanticMedia(page, imageOptions, onImage) {
407
+ const media = semanticMedia(page, imageOptions);
408
+ for (const item of media) {
409
+ for (const asset of item.assets ?? []) await onImage?.(asset);
410
+ delete item.assets;
411
+ }
412
+ return media;
413
+ }
414
+ function rasterMedia(image, pageNumber, index, imageOptions) {
389
415
  const bounds2 = transformedUnitBounds(image.transform);
390
416
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
391
417
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
418
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
419
+ const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
420
+ const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
392
421
  const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
393
422
  return {
394
423
  bounds: bounds2,
395
424
  kind: "raster",
396
- html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`
425
+ html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
426
+ markdown: `![](${source})`,
427
+ ...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
397
428
  };
398
429
  }
399
- function vectorMedia(page) {
430
+ function vectorMedia(page, imageOptions) {
400
431
  const primitives = [
401
432
  ...(page.paths ?? []).flatMap((path, index) => {
402
433
  const bounds2 = vectorPathBounds(path);
@@ -412,9 +443,11 @@ function vectorMedia(page) {
412
443
  );
413
444
  const aliases = visualFontAliases(page.number, page.fonts ?? []);
414
445
  const visualCodeFonts = new Set(
415
- (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
446
+ (page.fonts ?? []).filter(
447
+ (font) => (font.format === "truetype" || font.format === "opentype") && font.visualCodeMapping
448
+ ).map((font) => font.id)
416
449
  );
417
- return components.map((component) => {
450
+ return components.map((component, componentIndex) => {
418
451
  const bounds2 = component.bounds;
419
452
  const paths = component.primitives.flatMap(
420
453
  (primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
@@ -431,10 +464,18 @@ function vectorMedia(page) {
431
464
  );
432
465
  const fontIds = new Set(overlay.map((span) => span.fontAssetId));
433
466
  const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
467
+ 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>`;
468
+ const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
434
469
  return {
435
470
  bounds: bounds2,
436
471
  kind: "vector",
437
- html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number2(bounds2.x)} ${number2(page.height - bounds2.y - bounds2.height)} ${number2(bounds2.width)} ${number2(bounds2.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}${paths.length ? `<defs>${vectorPathClipDefinitions(paths, page.number)}</defs>` : ""}<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">${fills.map(vectorFillSvg).join("") + paths.map(({ path, index }) => vectorPathSvg(path, page.number, index)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`,
472
+ html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
473
+ markdown: imageOptions === "references" ? `![](${name})` : svg,
474
+ ...imageOptions === "references" ? {
475
+ assets: [
476
+ { name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
477
+ ]
478
+ } : {},
438
479
  ...consumedSpans.length > 0 ? { consumedSpans } : {}
439
480
  };
440
481
  });
@@ -476,7 +517,9 @@ function compositeMedia(items) {
476
517
  bounds: bounds2,
477
518
  kind: "composite",
478
519
  html: `<div class="pdf-semantic-media pdf-semantic-media-composite" style="position:relative;max-width:100%;width:${number2(bounds2.width)}px;aspect-ratio:${number2(bounds2.width)}/${number2(bounds2.height)}">${layers}</div>`,
479
- consumedSpans: items.flatMap((item) => item.consumedSpans ?? [])
520
+ markdown: items.map((item) => item.markdown).join("\n\n"),
521
+ consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
522
+ assets: items.flatMap((item) => item.assets ?? [])
480
523
  };
481
524
  }
482
525
  function mediaPiecesTouch(left, right) {
@@ -615,7 +658,7 @@ function escapeHtml2(value) {
615
658
  }
616
659
 
617
660
  // src/semantic-document.ts
618
- async function writeSemanticDocument(pages, write, lookaheadPages) {
661
+ async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
619
662
  const stats = {
620
663
  pagesProcessed: 0,
621
664
  peakBufferedPages: 0,
@@ -633,27 +676,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
633
676
  let contentStarted = false;
634
677
  let employmentOpen = false;
635
678
  let pendingParagraph;
636
- await write('<article class="pdf-semantic-document">');
679
+ const markdown = format === "markdown";
680
+ const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
681
+ const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
682
+ await output('<article class="pdf-semantic-document">');
637
683
  const closeTable = async () => {
638
684
  if (!activeTable) return;
639
- await write("</table>");
685
+ await output("</table>", "\n");
640
686
  activeTable = void 0;
641
687
  while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
642
688
  };
643
689
  const closeSections = async (minimumLevel = 0) => {
644
690
  while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
645
- await write("</section>");
691
+ await output("</section>");
646
692
  sectionLevels.pop();
647
693
  }
648
694
  };
649
695
  const flushPendingParagraph = async () => {
650
696
  if (!pendingParagraph) return;
651
- await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
697
+ await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
652
698
  pendingParagraph = void 0;
653
699
  };
654
700
  const closeEmployment = async () => {
655
701
  if (!employmentOpen) return;
656
- await write("</section>");
702
+ await output("</section>");
657
703
  employmentOpen = false;
658
704
  };
659
705
  const emitPage = async (page, future) => {
@@ -681,7 +727,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
681
727
  await flushPendingParagraph();
682
728
  const item = page.media[mediaIndex];
683
729
  if (item && captions.get(block) === item && block.type === "paragraph") {
684
- const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
730
+ const html2 = markdown ? `${item.markdown}
731
+
732
+ *${inlineText(block.text, block.lines, defaultColor)}*
733
+
734
+ ` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
685
735
  if (activeTable) pendingMedia.push(html2);
686
736
  else await write(html2);
687
737
  emittedMedia.add(item);
@@ -690,7 +740,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
690
740
  break;
691
741
  }
692
742
  if (item && captionedMedia.has(item)) break;
693
- const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
743
+ const html = markdown ? `${item?.markdown ?? ""}
744
+
745
+ ` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
694
746
  if (activeTable) pendingMedia.push(html);
695
747
  else await write(html);
696
748
  mediaIndex += 1;
@@ -698,7 +750,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
698
750
  const associatedMedia = captions.get(block);
699
751
  if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
700
752
  await flushPendingParagraph();
701
- const html = `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
753
+ const html = markdown ? `${associatedMedia.markdown}
754
+
755
+ *${inlineText(block.text, block.lines, defaultColor)}*
756
+
757
+ ` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
702
758
  if (activeTable) pendingMedia.push(html);
703
759
  else await write(html);
704
760
  emittedMedia.add(associatedMedia);
@@ -712,8 +768,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
712
768
  await flushPendingParagraph();
713
769
  if (employmentOpen && block.type !== "list") await closeEmployment();
714
770
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
715
- await write(
716
- `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
771
+ await output(
772
+ `<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
773
+ `# ${inlineText(block.text, block.lines, defaultColor, false)}
774
+
775
+ `
717
776
  );
718
777
  headerOpen = true;
719
778
  continue;
@@ -721,19 +780,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
721
780
  if (headerOpen) {
722
781
  if (block.type === "paragraph") {
723
782
  const tag = isContactBlock(block) ? "address" : "p";
724
- await write(
725
- `<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
783
+ await output(
784
+ `<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
785
+ `${inlineText(block.text, block.lines, defaultColor)}
786
+
787
+ `
726
788
  );
727
789
  headerHasParagraph = true;
728
790
  continue;
729
791
  }
730
792
  if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
731
- await write(
732
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
793
+ await output(
794
+ `<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
795
+ `${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
796
+
797
+ `
733
798
  );
734
799
  continue;
735
800
  }
736
- await write("</header>");
801
+ await output("</header>");
737
802
  headerOpen = false;
738
803
  contentStarted = true;
739
804
  }
@@ -741,23 +806,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
741
806
  const rows = tableToRows(block.table);
742
807
  if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
743
808
  const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
744
- for (const row of continuationRows) await write(tableRow(row, false));
809
+ for (const row of continuationRows)
810
+ await write(markdown ? markdownTableRow(row) : tableRow(row, false));
745
811
  activeTable.table = block.table;
746
812
  stats.mergedTables += 1;
747
813
  continue;
748
814
  }
749
815
  await closeTable();
750
816
  const header = tableHeader(rows);
751
- await write("<table>");
752
- for (const [index, row] of rows.entries())
753
- await write(tableRow(row, Boolean(header && index === 0)));
817
+ await output("<table>", markdownTableStart(rows, header));
818
+ const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
819
+ for (const [index, row] of markdownRows.entries())
820
+ await write(
821
+ markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
822
+ );
754
823
  activeTable = { table: block.table, header };
755
824
  continue;
756
825
  }
757
826
  if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
758
827
  const columns = activeTable.table.columns.length;
759
- await write(
760
- `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`
828
+ await output(
829
+ `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
830
+ block.entries.map(
831
+ (entry) => markdownTableRow([
832
+ entry.term,
833
+ ...Array(Math.max(0, columns - 2)).fill(""),
834
+ entry.description
835
+ ])
836
+ ).join("")
761
837
  );
762
838
  await closeTable();
763
839
  continue;
@@ -766,8 +842,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
766
842
  if (block.type === "heading") {
767
843
  const level = contentStarted && block.level === 1 ? 2 : block.level;
768
844
  await closeSections(level);
769
- await write(
770
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
845
+ await output(
846
+ `<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
847
+ `${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
848
+
849
+ `
771
850
  );
772
851
  sectionLevels.push(level);
773
852
  continue;
@@ -775,13 +854,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
775
854
  if (block.type === "paragraph") {
776
855
  if (isTitledRecord(block)) {
777
856
  const [institution, ...details] = block.lines;
778
- if (institution) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
779
- for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
857
+ if (institution)
858
+ await output(
859
+ `<h3>${escapeHtml3(institution.text)}</h3>`,
860
+ `### ${escapeMarkdown2(institution.text)}
861
+
862
+ `
863
+ );
864
+ for (const detail of details)
865
+ await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
866
+
867
+ `);
780
868
  continue;
781
869
  }
782
870
  if (isUnmarkedList(block)) {
783
- await write(
784
- `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
871
+ await output(
872
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
873
+ `${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
874
+
875
+ `
785
876
  );
786
877
  continue;
787
878
  }
@@ -789,19 +880,28 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
789
880
  continue;
790
881
  }
791
882
  if (block.type === "employment") {
792
- await write(
793
- `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
883
+ await output(
884
+ `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
885
+ `### ${escapeMarkdown2(block.role)}
886
+
887
+ ${escapeMarkdown2(block.organization)}
888
+
889
+ ${escapeMarkdown2(block.date)}
890
+
891
+ `
794
892
  );
795
893
  employmentOpen = true;
796
894
  continue;
797
895
  }
798
- await write(semanticBlockHtml(block, defaultColor));
896
+ await write(semanticBlockOutput(block, defaultColor, format));
799
897
  }
800
898
  while (mediaIndex < page.media.length) {
801
899
  const item = page.media[mediaIndex];
802
900
  if (item && !emittedMedia.has(item)) {
803
901
  await flushPendingParagraph();
804
- const html = `<div class="pdf-semantic-visual">${item.html}</div>`;
902
+ const html = markdown ? `${item.markdown}
903
+
904
+ ` : `<div class="pdf-semantic-visual">${item.html}</div>`;
805
905
  if (activeTable) pendingMedia.push(html);
806
906
  else await write(html);
807
907
  }
@@ -810,9 +910,10 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
810
910
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
811
911
  };
812
912
  for await (const page of pages) {
813
- const media = semanticMedia(page);
913
+ const media = await prepareSemanticMedia(page, imageOptions, onImage);
814
914
  const structured = structurePage(withoutSemanticMediaSpans(page, media));
815
915
  buffer.push({ width: page.width, height: page.height, structured, media });
916
+ restoreObservedHyphens(buffer);
816
917
  stats.pagesProcessed += 1;
817
918
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
818
919
  stats.peakBufferedLines = Math.max(
@@ -828,22 +929,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
828
929
  const ready = buffer.shift();
829
930
  if (ready) await emitPage(ready, buffer);
830
931
  }
831
- if (headerOpen) await write("</header>");
932
+ if (headerOpen) await output("</header>");
832
933
  await closeTable();
833
934
  await closeEmployment();
834
935
  if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
835
936
  await closeSections();
836
- await write(
837
- `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
937
+ await output(
938
+ `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
939
+ `---
940
+
941
+ ${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
838
942
  );
839
943
  pendingParagraph = void 0;
840
944
  } else {
841
945
  await flushPendingParagraph();
842
946
  await closeSections();
843
947
  }
844
- await write("</article>");
948
+ await output("</article>");
845
949
  return stats;
846
950
  }
951
+ function restoreObservedHyphens(buffer) {
952
+ const terms = new Set(
953
+ buffer.flatMap(
954
+ (page) => page.structured.lines.flatMap(
955
+ (line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
956
+ )
957
+ )
958
+ );
959
+ for (const page of buffer) {
960
+ for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
961
+ }
962
+ }
963
+ function restoreBlockHyphens(block, terms) {
964
+ const restore = (value) => restoreTextHyphens(value, terms);
965
+ if (block.type === "insetGroup") {
966
+ for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
967
+ } else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
968
+ block.text = restore(block.text);
969
+ } else if (block.type === "list") {
970
+ for (const item of block.items) item.text = restore(item.text);
971
+ } else if (block.type === "definitionList") {
972
+ for (const entry of block.entries) {
973
+ entry.term = restore(entry.term);
974
+ entry.description = restore(entry.description);
975
+ }
976
+ } else if (block.type === "cardList") {
977
+ for (const item of block.items) {
978
+ item.title = restore(item.title);
979
+ item.details = item.details.map(restore);
980
+ }
981
+ } else if (block.type === "sectionGroup") {
982
+ for (const item of block.items) {
983
+ item.label = restore(item.label);
984
+ item.content = item.content.map(restore);
985
+ }
986
+ } else if (block.type === "employment") {
987
+ block.role = restore(block.role);
988
+ block.organization = restore(block.organization);
989
+ block.date = restore(block.date);
990
+ }
991
+ }
992
+ function restoreTextHyphens(value, terms) {
993
+ let output = value;
994
+ for (const term of terms) {
995
+ const collapsed = term.replace(/[-‐‑]/gu, "");
996
+ if (collapsed === term || !output.includes(collapsed)) continue;
997
+ const pattern = new RegExp(
998
+ `(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
999
+ "gu"
1000
+ );
1001
+ output = output.replace(pattern, term);
1002
+ }
1003
+ return output;
1004
+ }
1005
+ function escapeRegularExpression(value) {
1006
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1007
+ }
847
1008
  function isContactBlock(block) {
848
1009
  const text = block.text;
849
1010
  const signals = [
@@ -927,6 +1088,20 @@ function tableRow(row, header) {
927
1088
  const cell = header ? "th" : "td";
928
1089
  return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
929
1090
  }
1091
+ function markdownTableStart(rows, header) {
1092
+ const columns = rows[0]?.length ?? 0;
1093
+ if (columns === 0) return "";
1094
+ const heading = header ?? Array(columns).fill("");
1095
+ return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
1096
+ }
1097
+ function markdownTableRow(row, shouldEscape = true) {
1098
+ const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
1099
+ return `| ${cells.join(" | ")} |
1100
+ `;
1101
+ }
1102
+ function escapeMarkdownTableCell(value) {
1103
+ return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
1104
+ }
930
1105
  function isFinancialSummary(block) {
931
1106
  return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
932
1107
  }
@@ -974,6 +1149,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
974
1149
  const tag = block.ordered ? "ol" : "ul";
975
1150
  return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
976
1151
  }
1152
+ function semanticBlockOutput(block, defaultColor, format) {
1153
+ return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
1154
+ }
1155
+ function semanticBlockMarkdown(block, defaultColor = "#000000") {
1156
+ if (block.type === "insetGroup") {
1157
+ const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
1158
+ return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
1159
+
1160
+ `;
1161
+ }
1162
+ if (block.type === "table") {
1163
+ const rows = tableToRows(block.table);
1164
+ const header = tableHeader(rows);
1165
+ return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
1166
+ `;
1167
+ }
1168
+ if (block.type === "heading") {
1169
+ return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
1170
+
1171
+ `;
1172
+ }
1173
+ if (block.type === "paragraph") {
1174
+ return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
1175
+
1176
+ `;
1177
+ }
1178
+ if (block.type === "preformatted") {
1179
+ const fence = block.text.includes("```") ? "````" : "```";
1180
+ return `${fence}
1181
+ ${block.text}
1182
+ ${fence}
1183
+
1184
+ `;
1185
+ }
1186
+ if (block.type === "definitionList") {
1187
+ return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
1188
+
1189
+ `;
1190
+ }
1191
+ if (block.type === "cardList") {
1192
+ const rows = [
1193
+ ["Item", "Quantity", "Amount"],
1194
+ ...block.items.map((item) => {
1195
+ const trailing = item.details.at(-1) ?? "";
1196
+ const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
1197
+ const detail = item.details.slice(0, -1).join(" ");
1198
+ return [
1199
+ `${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
1200
+ match?.[1] ?? "",
1201
+ match?.[2] ?? trailing
1202
+ ];
1203
+ })
1204
+ ];
1205
+ return `## Items ordered
1206
+
1207
+ ${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
1208
+ `;
1209
+ }
1210
+ if (block.type === "sectionGroup") {
1211
+ return block.items.map(
1212
+ (item) => `## ${escapeMarkdown2(titleCase(item.label))}
1213
+
1214
+ ${item.content.map(
1215
+ (content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
1216
+ ).join("\n\n")}
1217
+
1218
+ `
1219
+ ).join("");
1220
+ }
1221
+ if (block.type === "employment") {
1222
+ return `### ${escapeMarkdown2(block.role)}
1223
+
1224
+ ${escapeMarkdown2(block.organization)}
1225
+
1226
+ ${escapeMarkdown2(block.date)}
1227
+
1228
+ `;
1229
+ }
1230
+ return `${block.items.map(
1231
+ (item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
1232
+ ).join("\n")}
1233
+
1234
+ `;
1235
+ }
977
1236
  function semanticBlockY(block) {
978
1237
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
979
1238
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -1006,6 +1265,9 @@ function titleCase(value) {
1006
1265
  function escapeHtml3(value) {
1007
1266
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1008
1267
  }
1268
+ function escapeMarkdown2(value) {
1269
+ return value.replace(/([\\`*_[\]<>])/g, "\\$1");
1270
+ }
1009
1271
 
1010
1272
  // src/index.ts
1011
1273
  var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-semantic-document h1,.pdf-page--semantic h1{font-size:1.7em}.pdf-semantic-document h2,.pdf-page--semantic h2{font-size:1.5em}.pdf-semantic-document h3,.pdf-page--semantic h3{font-size:1.35em}.pdf-semantic-document h4,.pdf-page--semantic h4{font-size:1.1em}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
@@ -1022,16 +1284,40 @@ async function writeHtmlDocument(pages, write, options = {}) {
1022
1284
  await write("</head><body>");
1023
1285
  }
1024
1286
  await write('<main class="pdf-document">');
1025
- if (resolveProfile(options) === "semantic") {
1287
+ const profile = resolveProfile(options);
1288
+ const imageOptions = resolveImageOptions(profile, options);
1289
+ validateImageOptions(imageOptions, options);
1290
+ if (profile === "semantic") {
1026
1291
  const lookahead = semanticLookahead(options.semanticLookaheadPages);
1027
- const stats = await writeSemanticDocument(pages, write, lookahead);
1292
+ const stats = await writeSemanticDocument(
1293
+ pages,
1294
+ write,
1295
+ lookahead,
1296
+ imageOptions,
1297
+ options.onImage
1298
+ );
1028
1299
  options.onSemanticStats?.(stats);
1029
1300
  } else {
1030
- for await (const page of pages) await writePage(page, write, options);
1301
+ const documentFonts = { entries: [] };
1302
+ for await (const page of pages) await writePositionedPage(page, write, options, documentFonts);
1031
1303
  }
1032
1304
  await write("</main>");
1033
1305
  if (includeDocument) await write("</body></html>");
1034
1306
  }
1307
+ async function writeMarkdownDocument(pages, write, options = {}) {
1308
+ const imageOptions = options.imageOptions ?? "excluded";
1309
+ validateImageOptions(imageOptions, options);
1310
+ const lookahead = semanticLookahead(options.semanticLookaheadPages);
1311
+ const stats = await writeSemanticDocument(
1312
+ pages,
1313
+ write,
1314
+ lookahead,
1315
+ imageOptions,
1316
+ options.onImage,
1317
+ "markdown"
1318
+ );
1319
+ options.onSemanticStats?.(stats);
1320
+ }
1035
1321
  function semanticLookahead(value) {
1036
1322
  const lookahead = value ?? 4;
1037
1323
  if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
@@ -1040,7 +1326,9 @@ function semanticLookahead(value) {
1040
1326
  return lookahead;
1041
1327
  }
1042
1328
  async function writePage(page, write, options = {}) {
1043
- if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
1329
+ const profile = resolveProfile(options);
1330
+ validateImageOptions(resolveImageOptions(profile, options), options);
1331
+ if (profile === "semantic") await writeFlowPage(page, write, options);
1044
1332
  else await writePositionedPage(page, write, options);
1045
1333
  }
1046
1334
  async function pageToHtml(page, options = {}) {
@@ -1054,8 +1342,10 @@ async function pageToHtml(page, options = {}) {
1054
1342
  );
1055
1343
  return output;
1056
1344
  }
1057
- async function writePositionedPage(page, write, options) {
1058
- const visualSpans = page.visualSpans ?? page.spans;
1345
+ async function writePositionedPage(page, write, options, documentFonts) {
1346
+ const imageOptions = resolveImageOptions("visual", options);
1347
+ const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
1348
+ const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
1059
1349
  const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
1060
1350
  const quarterTurn = page.rotate === 90 || page.rotate === 270;
1061
1351
  const displayWidth = quarterTurn ? page.height : page.width;
@@ -1063,29 +1353,40 @@ async function writePositionedPage(page, write, options) {
1063
1353
  await write(
1064
1354
  `<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">`
1065
1355
  );
1066
- const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
1356
+ const { aliases: fontAliases, fontsToEmit } = visualPageFonts(
1357
+ page.number,
1358
+ page.fonts ?? [],
1359
+ documentFonts
1360
+ );
1067
1361
  const type3Fonts = new Map(
1068
1362
  (page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
1069
1363
  );
1070
- if ((options.includeStyles ?? true) && page.fonts?.length) {
1364
+ const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
1365
+ if ((options.includeStyles ?? true) && fontsToEmit.length) {
1071
1366
  await write(
1072
- `<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
1367
+ `<style>${fontsToEmit.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
1073
1368
  );
1074
1369
  }
1370
+ if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
1075
1371
  await write(
1076
1372
  `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
1077
1373
  );
1078
1374
  await write(
1079
1375
  `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
1080
1376
  );
1081
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
1377
+ const clipDefinitions = imageClipDefinitions(
1378
+ imageOptions === "excluded" ? [] : page.images ?? [],
1379
+ page.number,
1380
+ page.height
1381
+ ) + vectorPathClipDefinitions(
1082
1382
  (page.paths ?? []).map((path, index) => ({ path, index })),
1083
1383
  page.number
1084
1384
  );
1085
1385
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
1086
1386
  if (reflectedOverlay) {
1087
1387
  for (const [index, image] of (page.images ?? []).entries()) {
1088
- await write(visualImage(image, page.height, page.number, index));
1388
+ const source = visualImages[index];
1389
+ if (source) await write(visualImage(image, page.height, page.number, index, source));
1089
1390
  }
1090
1391
  }
1091
1392
  if (page.fills?.length || page.paths?.length) {
@@ -1098,14 +1399,29 @@ async function writePositionedPage(page, write, options) {
1098
1399
  }
1099
1400
  if (!reflectedOverlay) {
1100
1401
  for (const [index, image] of (page.images ?? []).entries()) {
1101
- 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));
1102
1404
  }
1103
1405
  }
1104
- for (const span of visualSpans) {
1406
+ for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
1407
+ const span = visualSpans[spanIndex];
1408
+ if (!span) continue;
1105
1409
  if (!usesPositionedSpan(span)) {
1106
1410
  const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
1411
+ const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
1412
+ if (line) {
1413
+ await write(line.html);
1414
+ spanIndex = line.endIndex;
1415
+ continue;
1416
+ }
1107
1417
  await write(
1108
- type3 ? visualType3Text(span, type3, page.height) : visualText(span, page.height, fontAliases, reflectedOverlay && page.rotate === 180)
1418
+ type3 ? visualType3Text(span, type3, page.height) : visualText(
1419
+ span,
1420
+ page.height,
1421
+ fontAliases,
1422
+ reflectedOverlay && page.rotate === 180,
1423
+ textClasses?.names
1424
+ )
1109
1425
  );
1110
1426
  }
1111
1427
  }
@@ -1115,23 +1431,191 @@ async function writePositionedPage(page, write, options) {
1115
1431
  }
1116
1432
  await write("</div></section>");
1117
1433
  }
1434
+ function visualPageFonts(pageNumber, fonts, documentFonts) {
1435
+ const aliases = visualFontAliases(pageNumber, fonts);
1436
+ if (!documentFonts) return { aliases, fontsToEmit: fonts };
1437
+ const fontsToEmit = [];
1438
+ for (const font of fonts) {
1439
+ if (!aliases.has(font.id) || font.format === "type3") continue;
1440
+ const existing = documentFonts.entries.find(
1441
+ (entry) => entry.format === font.format && equalBytes(entry.data, font.data)
1442
+ );
1443
+ if (existing) {
1444
+ aliases.set(font.id, existing.alias);
1445
+ continue;
1446
+ }
1447
+ const alias = `boxpdf-document-font-${documentFonts.entries.length + 1}`;
1448
+ aliases.set(font.id, alias);
1449
+ documentFonts.entries.push({ alias, data: font.data, format: font.format });
1450
+ fontsToEmit.push(font);
1451
+ }
1452
+ return { aliases, fontsToEmit };
1453
+ }
1454
+ function equalBytes(left, right) {
1455
+ if (left.length !== right.length) return false;
1456
+ return left.every((value, index) => value === right[index]);
1457
+ }
1458
+ function coalesceVisualSpans(spans) {
1459
+ const output = [];
1460
+ for (const span of spans) {
1461
+ const previous = output.at(-1);
1462
+ if (!previous || !canCoalesceVisualSpans(previous, span)) {
1463
+ output.push(span);
1464
+ continue;
1465
+ }
1466
+ output[output.length - 1] = {
1467
+ ...previous,
1468
+ text: previous.text + span.text,
1469
+ bounds: {
1470
+ ...previous.bounds,
1471
+ width: span.bounds.x + span.bounds.width - previous.bounds.x,
1472
+ height: Math.max(previous.bounds.height, span.bounds.height)
1473
+ }
1474
+ };
1475
+ }
1476
+ return output;
1477
+ }
1478
+ function canCoalesceVisualSpans(left, right) {
1479
+ if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
1480
+ if (left.direction !== "ltr" || right.direction !== "ltr") return false;
1481
+ if (/guardian/i.test(left.fontFamily ?? "")) return false;
1482
+ if (left.glyphCodes || right.glyphCodes) return false;
1483
+ if (left.fontAssetId || right.fontAssetId) return false;
1484
+ if (!sameVisualTextState(left, right)) return false;
1485
+ const tolerance = Math.max(0.02, left.fontSize * 0.015);
1486
+ if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
1487
+ const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
1488
+ if (right.hasLeadingSpace) return false;
1489
+ if (gap >= -tolerance && gap <= tolerance) return true;
1490
+ return right.textAdjustmentBefore !== void 0 && right.textAdjustmentBefore < 0 && Math.abs(right.textAdjustmentBefore - gap) <= 1e-3;
1491
+ }
1492
+ function sameVisualTextState(left, right) {
1493
+ 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);
1494
+ }
1495
+ function sameTransform(left, right) {
1496
+ if (!left || !right) return left === right;
1497
+ return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
1498
+ }
1499
+ function visualTextClasses(pageNumber, spans, fontAliases) {
1500
+ const names = /* @__PURE__ */ new Map();
1501
+ let css = "";
1502
+ for (const span of spans) {
1503
+ if (usesPositionedSpan(span) || span.glyphCodes) {
1504
+ continue;
1505
+ }
1506
+ const style = visualTextClassStyle(span, fontAliases);
1507
+ if (!style || names.has(style)) continue;
1508
+ const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
1509
+ names.set(style, name);
1510
+ css += `.${name}{${style}}`;
1511
+ }
1512
+ return { css, names };
1513
+ }
1514
+ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
1515
+ const first = spans[startIndex];
1516
+ if (!first || !canGroupVisualTextLine(first)) return void 0;
1517
+ const style = visualTextClassStyle(first, fontAliases);
1518
+ const className = styleClasses.get(style);
1519
+ if (!className) return void 0;
1520
+ let endIndex = startIndex;
1521
+ while (endIndex + 1 < spans.length) {
1522
+ const next = spans[endIndex + 1];
1523
+ if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
1524
+ break;
1525
+ }
1526
+ endIndex += 1;
1527
+ }
1528
+ if (endIndex === startIndex) return void 0;
1529
+ const baseline = pageHeight - first.bounds.y;
1530
+ const lineSpans = spans.slice(startIndex, endIndex + 1);
1531
+ const dxRun = visualDxTextRun(lineSpans, fontAliases);
1532
+ if (dxRun) {
1533
+ return {
1534
+ html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}" dx="${dxRun.offsets.map(number3).join(" ")}">${escapeHtml4(dxRun.text)}</text>`,
1535
+ endIndex
1536
+ };
1537
+ }
1538
+ const content = lineSpans.map(
1539
+ (span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
1540
+ ).join("");
1541
+ return {
1542
+ html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
1543
+ endIndex
1544
+ };
1545
+ }
1546
+ function visualDxTextRun(spans, fontAliases) {
1547
+ const first = spans[0];
1548
+ if (!first?.fontAssetId || !fontAliases.has(first.fontAssetId) || spans.length < 2)
1549
+ return void 0;
1550
+ let text = first.text;
1551
+ const offsets = characterOffsets(first, 0);
1552
+ for (let index = 1; index < spans.length; index += 1) {
1553
+ const previous = spans[index - 1];
1554
+ const current = spans[index];
1555
+ if (!previous || !current || current.fontAssetId !== first.fontAssetId) return void 0;
1556
+ const characters = [...current.text];
1557
+ if (characters.length === 0 || previous.naturalWidth === void 0) return void 0;
1558
+ const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
1559
+ const trailingSpacing = (previous.characterSpacing ?? 0) + (previous.text.endsWith(" ") ? previous.wordSpacing ?? 0 : 0);
1560
+ offsets.push(...characterOffsets(current, gap + trailingSpacing));
1561
+ text += current.text;
1562
+ }
1563
+ while (offsets.at(-1) === 0) offsets.pop();
1564
+ return offsets.length > 0 ? { text, offsets } : void 0;
1565
+ }
1566
+ function characterOffsets(span, first) {
1567
+ const characters = [...span.text];
1568
+ return characters.map(
1569
+ (_, index) => index === 0 ? first : (span.characterSpacing ?? 0) + (characters[index - 1] === " " ? span.wordSpacing ?? 0 : 0)
1570
+ );
1571
+ }
1572
+ function visualTextTspan(span, dx) {
1573
+ const extent = span.bounds.width;
1574
+ const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
1575
+ const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1576
+ return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
1577
+ }
1578
+ function textSpanGap(previous, current) {
1579
+ if (!previous) return 0;
1580
+ const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
1581
+ const adjustment = current.textAdjustmentBefore;
1582
+ return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
1583
+ }
1584
+ function canGroupVisualTextLine(span) {
1585
+ 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));
1586
+ }
1118
1587
  function usesReflectedVisualOverlay(page, spans) {
1119
1588
  return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
1120
1589
  (span) => span.transform !== void 0 && Math.abs(span.transform[0] + 1) < 1e-6 && Math.abs(span.transform[1]) < 1e-6 && Math.abs(span.transform[2]) < 1e-6 && Math.abs(span.transform[3] - 1) < 1e-6
1121
1590
  );
1122
1591
  }
1123
- function visualImage(image, pageHeight, pageNumber, imageIndex) {
1592
+ function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
1124
1593
  const [a, b, c, d, e, f] = image.transform;
1125
1594
  const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
1126
1595
  const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
1127
- const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
1128
- const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
1129
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
1596
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
1130
1597
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
1131
1598
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
1132
1599
  }
1133
1600
  return output;
1134
1601
  }
1602
+ async function prepareVisualImages(page, imageOptions, onImage) {
1603
+ if (imageOptions === "excluded") return [];
1604
+ const sources = [];
1605
+ for (const [index, image] of (page.images ?? []).entries()) {
1606
+ const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
1607
+ const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
1608
+ if (imageOptions === "embedded") {
1609
+ sources.push(`data:${mimeType};base64,${base64(data)}`);
1610
+ continue;
1611
+ }
1612
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
1613
+ const name = `page-${page.number}-image-${index + 1}.${extension}`;
1614
+ await onImage?.({ name, mimeType, data });
1615
+ sources.push(name);
1616
+ }
1617
+ return sources;
1618
+ }
1135
1619
  function imageClipDefinitions(images, pageNumber, pageHeight) {
1136
1620
  return images.flatMap(
1137
1621
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
@@ -1198,8 +1682,9 @@ function positionedSpan(span, fontAliases) {
1198
1682
  ].join(";");
1199
1683
  return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
1200
1684
  }
1201
- async function writeFlowPage(page, write) {
1202
- const media = semanticMedia(page);
1685
+ async function writeFlowPage(page, write, options) {
1686
+ const imageOptions = resolveImageOptions("semantic", options);
1687
+ const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
1203
1688
  const structured = structurePage2(withoutSemanticMediaSpans(page, media));
1204
1689
  const defaultColor = dominantTextColor(structured.lines);
1205
1690
  let mediaIndex = 0;
@@ -1336,10 +1821,37 @@ function semanticBlockY2(block) {
1336
1821
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
1337
1822
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
1338
1823
  }
1339
- function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
1824
+ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
1340
1825
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
1341
1826
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
1342
1827
  const direction = directionAttribute([span]);
1828
+ const style = visualTextStyle(span, fontAliases);
1829
+ const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
1830
+ const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
1831
+ const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
1832
+ const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
1833
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1834
+ const transform = counterRotateReflectedText && span.transform ? [
1835
+ span.transform[0],
1836
+ span.transform[1],
1837
+ span.transform[2],
1838
+ -span.transform[3]
1839
+ ] : span.transform;
1840
+ const transformed = hasNonIdentityTransform(transform);
1841
+ const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
1842
+ const basisX = transform?.[0] ?? 1;
1843
+ const basisY = transform?.[1] ?? 0;
1844
+ const anchorX = span.bounds.x + basisX * rtlOffset;
1845
+ const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1846
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1847
+ return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
1848
+ }
1849
+ function visualTextClassStyle(span, fontAliases) {
1850
+ const style = visualTextStyle(span, fontAliases);
1851
+ const fontSize = `font-size:${number3(span.fontSize)}px`;
1852
+ return style ? `${style};${fontSize}` : fontSize;
1853
+ }
1854
+ function visualTextStyle(span, fontAliases) {
1343
1855
  const font = visualFontStyles(
1344
1856
  span.fontFamily,
1345
1857
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -1349,7 +1861,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1349
1861
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
1350
1862
  const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
1351
1863
  const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
1352
- const style = [
1864
+ return [
1353
1865
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
1354
1866
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
1355
1867
  strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
@@ -1359,22 +1871,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1359
1871
  strokeOpacity,
1360
1872
  font
1361
1873
  ].filter(Boolean).join(";");
1362
- const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
1363
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1364
- const transform = counterRotateReflectedText && span.transform ? [
1365
- span.transform[0],
1366
- span.transform[1],
1367
- span.transform[2],
1368
- -span.transform[3]
1369
- ] : span.transform;
1370
- const transformed = hasNonIdentityTransform(transform);
1371
- const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
1372
- const basisX = transform?.[0] ?? 1;
1373
- const basisY = transform?.[1] ?? 0;
1374
- const anchorX = span.bounds.x + basisX * rtlOffset;
1375
- const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1376
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1377
- return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
1378
1874
  }
1379
1875
  function isAdobeCjkFont(fontFamily) {
1380
1876
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -1469,9 +1965,18 @@ function resolveProfile(options) {
1469
1965
  }
1470
1966
  return options.profile ?? legacyProfile;
1471
1967
  }
1968
+ function resolveImageOptions(profile, options) {
1969
+ return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
1970
+ }
1971
+ function validateImageOptions(imageOptions, options) {
1972
+ if (imageOptions === "references" && !options.onImage) {
1973
+ throw new Error('imageOptions "references" requires an onImage callback');
1974
+ }
1975
+ }
1472
1976
  export {
1473
1977
  pageToHtml,
1474
1978
  writeHtmlDocument,
1979
+ writeMarkdownDocument,
1475
1980
  writePage
1476
1981
  };
1477
1982
  //# sourceMappingURL=index.js.map