@boxpdf/html-writer 0.1.18 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,39 +2,150 @@
2
2
  import { structurePage as structurePage2, tableToHtml } from "@boxpdf/reader/structure";
3
3
 
4
4
  // src/semantic-caption.ts
5
- function isClearMediaCaption(media, block, pageWidth, pageHeight, pageLines) {
6
- if (block.type !== "paragraph" || block.lines.length === 0) return false;
7
- const bounds2 = unionLines(block.lines);
8
- const lineHeight = median(block.lines.map((line) => line.bounds.height));
9
- if (media.bounds.width < pageWidth * 0.2 || media.bounds.height < lineHeight * 10) return false;
10
- if (media.bounds.x < -2 || media.bounds.y < -2 || media.bounds.x + media.bounds.width > pageWidth + 2 || media.bounds.y + media.bounds.height > pageHeight + 2)
11
- return false;
12
- const gap = media.bounds.y - (bounds2.y + bounds2.height);
13
- if (gap < -lineHeight * 0.15 || gap > lineHeight * 1.25) return false;
14
- const mediaCenter = media.bounds.x + media.bounds.width / 2;
15
- const captionCenter = bounds2.x + bounds2.width / 2;
16
- if (Math.abs(mediaCenter - captionCenter) > Math.max(3, media.bounds.width * 0.03)) return false;
17
- if (bounds2.width < media.bounds.width * 0.45 || bounds2.width > media.bounds.width * 1.06) {
18
- return false;
19
- }
20
- const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
21
- if (!first) return false;
22
- const otherLines = pageLines.filter((line) => !block.lines.includes(line));
23
- return fontSignature(first) !== dominantFontSignature(otherLines);
24
- }
5
+ var minimumCaptionScore = 0.72;
25
6
  function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
7
+ const candidates = blocks.flatMap((block) => {
8
+ const candidate = captionCandidate(block);
9
+ return candidate ? [candidate] : [];
10
+ });
11
+ const preliminary = media.flatMap(
12
+ (item) => candidates.flatMap((candidate) => {
13
+ const evidence = scoreCaption(item, candidate, pageWidth, pageHeight, pageLines, 0);
14
+ return evidence ? [{ media: item, candidate, evidence }] : [];
15
+ })
16
+ );
17
+ const patterns = repeatedPatterns(preliminary);
18
+ const edges = preliminary.map((edge) => {
19
+ const evidence = scoreCaption(
20
+ edge.media,
21
+ edge.candidate,
22
+ pageWidth,
23
+ pageHeight,
24
+ pageLines,
25
+ patterns.get(patternKey(edge)) ?? 0
26
+ );
27
+ return evidence ? { ...edge, evidence } : void 0;
28
+ }).filter((edge) => Boolean(edge)).filter((edge) => edge.evidence.score >= minimumCaptionScore);
29
+ const bestForMedia = bestEdges(edges, (edge) => edge.media);
30
+ const bestForCaption = bestEdges(edges, (edge) => edge.candidate.block);
26
31
  const associations = /* @__PURE__ */ new Map();
27
- for (const item of media) {
28
- const candidates = blocks.filter((block) => isClearMediaCaption(item, block, pageWidth, pageHeight, pageLines)).filter((block) => !associations.has(block)).sort((left, right) => captionGap(item, left) - captionGap(item, right));
29
- const caption = candidates[0];
30
- if (caption) associations.set(caption, item);
32
+ for (const edge of edges) {
33
+ if (bestForMedia.get(edge.media) === edge && bestForCaption.get(edge.candidate.block) === edge) {
34
+ associations.set(edge.candidate.block, edge.media);
35
+ }
31
36
  }
32
37
  return associations;
33
38
  }
34
- function captionGap(media, block) {
35
- if (block.type !== "paragraph") return Number.POSITIVE_INFINITY;
36
- const bounds2 = unionLines(block.lines);
37
- return Math.abs(media.bounds.y - bounds2.y - bounds2.height);
39
+ function scoreCaption(media, candidate, pageWidth, pageHeight, pageLines, repeatedAlignment) {
40
+ if (!insidePage(media.bounds, pageWidth, pageHeight)) return void 0;
41
+ if (media.bounds.width < pageWidth * 0.06 || media.bounds.height < candidate.lineHeight * 1.5)
42
+ return void 0;
43
+ const relation = verticalRelation(media.bounds, candidate.bounds);
44
+ if (!relation) return void 0;
45
+ const maximumGap = Math.max(candidate.lineHeight * 3, pageHeight * 0.035);
46
+ if (relation.gap > maximumGap) return void 0;
47
+ const overlapWidth = overlap(
48
+ media.bounds.x,
49
+ media.bounds.width,
50
+ candidate.bounds.x,
51
+ candidate.bounds.width
52
+ );
53
+ const horizontalOverlap = overlapWidth / Math.max(1, Math.min(media.bounds.width, candidate.bounds.width));
54
+ const centerDistance = Math.abs(center(media.bounds) - center(candidate.bounds));
55
+ const centerAlignment = clamp01(
56
+ 1 - centerDistance / Math.max(media.bounds.width, candidate.bounds.width)
57
+ );
58
+ if (horizontalOverlap < 0.45 && centerAlignment < 0.82) return void 0;
59
+ const relativeWidth = Math.min(media.bounds.width, candidate.bounds.width) / Math.max(media.bounds.width, candidate.bounds.width);
60
+ const gapRatio = clamp01(1 - relation.gap / maximumGap);
61
+ const interveningContent = interveningScore(media.bounds, candidate, relation.side, pageLines);
62
+ if (interveningContent === 0) return void 0;
63
+ if (relation.side === "above" && (gapRatio < 0.6 || interveningContent < 1)) return void 0;
64
+ const fontContrast = captionFontContrast(candidate, pageLines);
65
+ const surroundingWhitespace = whitespaceScore(candidate, relation.side, relation.gap, pageLines);
66
+ const score = gapRatio * 0.22 + horizontalOverlap * 0.17 + centerAlignment * 0.13 + relativeWidth * 0.09 + fontContrast * 0.13 + surroundingWhitespace * 0.09 + interveningContent * 0.09 + repeatedAlignment * 0.08;
67
+ return {
68
+ score,
69
+ side: relation.side,
70
+ gapRatio,
71
+ horizontalOverlap,
72
+ centerAlignment,
73
+ relativeWidth,
74
+ fontContrast,
75
+ surroundingWhitespace,
76
+ interveningContent,
77
+ repeatedAlignment
78
+ };
79
+ }
80
+ function captionCandidate(block) {
81
+ if (block.type !== "paragraph" || block.lines.length === 0) return void 0;
82
+ const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
83
+ if (!first) return void 0;
84
+ return {
85
+ block,
86
+ bounds: unionLines(block.lines),
87
+ lineHeight: median(block.lines.map((line) => line.bounds.height)),
88
+ font: fontSignature(first)
89
+ };
90
+ }
91
+ function verticalRelation(media, caption) {
92
+ const belowGap = media.y - (caption.y + caption.height);
93
+ if (belowGap >= -caption.height * 0.15) return { side: "below", gap: Math.max(0, belowGap) };
94
+ const aboveGap = caption.y - (media.y + media.height);
95
+ if (aboveGap >= -caption.height * 0.15) return { side: "above", gap: Math.max(0, aboveGap) };
96
+ return void 0;
97
+ }
98
+ function interveningScore(media, candidate, side, pageLines) {
99
+ const lower = side === "below" ? candidate.bounds.y + candidate.bounds.height : media.y + media.height;
100
+ const upper = side === "below" ? media.y : candidate.bounds.y;
101
+ const blockers = pageLines.filter(
102
+ (line) => !candidate.block.lines.includes(line) && line.bounds.y < upper && line.bounds.y + line.bounds.height > lower && overlap(line.bounds.x, line.bounds.width, media.x, media.width) / Math.max(1, Math.min(line.bounds.width, media.width)) >= 0.25
103
+ );
104
+ return blockers.length === 0 ? 1 : blockers.length === 1 ? 0.35 : 0;
105
+ }
106
+ function captionFontContrast(candidate, pageLines) {
107
+ const otherLines = pageLines.filter((line) => !candidate.block.lines.includes(line));
108
+ if (candidate.font !== dominantFontSignature(otherLines)) return 1;
109
+ const candidateSize = median(
110
+ candidate.block.lines.flatMap((line) => line.spans.map((span) => span.fontSize))
111
+ );
112
+ const bodySize = median(otherLines.flatMap((line) => line.spans.map((span) => span.fontSize)));
113
+ return Math.abs(candidateSize - bodySize) >= 0.75 ? 0.65 : 0.15;
114
+ }
115
+ function whitespaceScore(candidate, side, mediaGap, pageLines) {
116
+ const awayGaps = pageLines.filter((line) => !candidate.block.lines.includes(line)).filter(
117
+ (line) => overlap(line.bounds.x, line.bounds.width, candidate.bounds.x, candidate.bounds.width) > 0
118
+ ).flatMap((line) => {
119
+ if (side === "below" && line.bounds.y + line.bounds.height <= candidate.bounds.y)
120
+ return [candidate.bounds.y - line.bounds.y - line.bounds.height];
121
+ if (side === "above" && line.bounds.y >= candidate.bounds.y + candidate.bounds.height)
122
+ return [line.bounds.y - candidate.bounds.y - candidate.bounds.height];
123
+ return [];
124
+ });
125
+ const awayGap = Math.min(...awayGaps, Number.POSITIVE_INFINITY);
126
+ return Number.isFinite(awayGap) ? clamp01((awayGap + candidate.lineHeight * 0.25) / (mediaGap + candidate.lineHeight)) : 1;
127
+ }
128
+ function repeatedPatterns(edges) {
129
+ const counts = /* @__PURE__ */ new Map();
130
+ for (const edge of edges.filter((item) => item.evidence.score >= minimumCaptionScore - 0.08)) {
131
+ counts.set(patternKey(edge), (counts.get(patternKey(edge)) ?? 0) + 1);
132
+ }
133
+ return new Map([...counts].map(([key, count]) => [key, count >= 2 ? 1 : 0]));
134
+ }
135
+ function patternKey(edge) {
136
+ const widthRatio = edge.candidate.bounds.width / Math.max(1, edge.media.bounds.width);
137
+ return `${edge.candidate.font}|${edge.evidence.side}|${Math.round(widthRatio * 4) / 4}`;
138
+ }
139
+ function bestEdges(edges, key) {
140
+ const output = /* @__PURE__ */ new Map();
141
+ for (const edge of edges) {
142
+ const existing = output.get(key(edge));
143
+ if (!existing || edge.evidence.score > existing.evidence.score) output.set(key(edge), edge);
144
+ }
145
+ return output;
146
+ }
147
+ function insidePage(bounds2, pageWidth, pageHeight) {
148
+ return bounds2.x >= -2 && bounds2.y >= -2 && bounds2.x + bounds2.width <= pageWidth + 2 && bounds2.y + bounds2.height <= pageHeight + 2;
38
149
  }
39
150
  function unionLines(lines) {
40
151
  const x = Math.min(...lines.map((line) => line.bounds.x));
@@ -54,6 +165,15 @@ function dominantFontSignature(lines) {
54
165
  function fontSignature(span) {
55
166
  return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
56
167
  }
168
+ function center(bounds2) {
169
+ return bounds2.x + bounds2.width / 2;
170
+ }
171
+ function overlap(left, leftSize, right, rightSize) {
172
+ return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
173
+ }
174
+ function clamp01(value) {
175
+ return Math.max(0, Math.min(1, value));
176
+ }
57
177
  function median(values) {
58
178
  const ordered = [...values].sort((left, right) => left - right);
59
179
  return ordered[Math.floor(ordered.length / 2)] ?? 1;
@@ -75,6 +195,12 @@ function dominantTextColor(lines) {
75
195
  return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
76
196
  }
77
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) {
78
204
  const ranges = [];
79
205
  let cursor = 0;
80
206
  for (const span of lines.flatMap((line) => line.spans)) {
@@ -100,11 +226,11 @@ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
100
226
  let html = "";
101
227
  let offset = 0;
102
228
  for (const range of merged) {
103
- html += escapeHtml(text.slice(offset, range.start));
104
- 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);
105
231
  offset = range.end;
106
232
  }
107
- return html + escapeHtml(text.slice(offset));
233
+ return html + escapeText(text.slice(offset), format);
108
234
  }
109
235
  function mergeRanges(ranges, text) {
110
236
  const merged = [];
@@ -118,13 +244,19 @@ function mergeRanges(ranges, text) {
118
244
  }
119
245
  return merged;
120
246
  }
121
- function styledHtml(value, range) {
122
- let html = escapeHtml(value);
247
+ function styledText(value, range, format) {
248
+ let html = escapeText(value, format);
123
249
  if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
124
- if (range.italic) html = `<em>${html}</em>`;
125
- 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}**`;
126
252
  return html;
127
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
+ }
128
260
  function normalizedColor(value) {
129
261
  if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
130
262
  const color = value.toLowerCase();
@@ -260,22 +392,39 @@ function base64(bytes) {
260
392
  }
261
393
 
262
394
  // src/semantic-media.ts
263
- function semanticMedia(page) {
264
- const output = (page.images ?? []).map((image) => rasterMedia(image));
265
- output.push(...vectorMedia(page));
266
- return output.sort((left, right) => right.bounds.y - left.bounds.y);
395
+ function semanticMedia(page, imageOptions = "embedded") {
396
+ if (imageOptions === "excluded") return [];
397
+ const output = (page.images ?? []).map(
398
+ (image, index) => rasterMedia(image, page.number, index, imageOptions)
399
+ );
400
+ output.push(...vectorMedia(page, imageOptions));
401
+ return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
402
+ }
403
+ async function prepareSemanticMedia(page, imageOptions, onImage) {
404
+ const media = semanticMedia(page, imageOptions);
405
+ for (const item of media) {
406
+ for (const asset of item.assets ?? []) await onImage?.(asset);
407
+ delete item.assets;
408
+ }
409
+ return media;
267
410
  }
268
- function rasterMedia(image) {
411
+ function rasterMedia(image, pageNumber, index, imageOptions) {
269
412
  const bounds2 = transformedUnitBounds(image.transform);
270
413
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
271
414
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
415
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
416
+ const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
417
+ const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
272
418
  const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
273
419
  return {
274
420
  bounds: bounds2,
275
- 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}">`
421
+ kind: "raster",
422
+ html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
423
+ markdown: `![](${source})`,
424
+ ...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
276
425
  };
277
426
  }
278
- function vectorMedia(page) {
427
+ function vectorMedia(page, imageOptions) {
279
428
  const primitives = [
280
429
  ...(page.paths ?? []).flatMap((path, index) => {
281
430
  const bounds2 = vectorPathBounds(path);
@@ -293,7 +442,7 @@ function vectorMedia(page) {
293
442
  const visualCodeFonts = new Set(
294
443
  (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
295
444
  );
296
- return components.map((component) => {
445
+ return components.map((component, componentIndex) => {
297
446
  const bounds2 = component.bounds;
298
447
  const paths = component.primitives.flatMap(
299
448
  (primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
@@ -310,13 +459,82 @@ function vectorMedia(page) {
310
459
  );
311
460
  const fontIds = new Set(overlay.map((span) => span.fontAssetId));
312
461
  const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
462
+ const svg = `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number2(bounds2.x)} ${number2(page.height - bounds2.y - bounds2.height)} ${number2(bounds2.width)} ${number2(bounds2.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}${paths.length ? `<defs>${vectorPathClipDefinitions(paths, page.number)}</defs>` : ""}<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">${fills.map(vectorFillSvg).join("") + paths.map(({ path, index }) => vectorPathSvg(path, page.number, index)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`;
463
+ const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
313
464
  return {
314
465
  bounds: bounds2,
315
- 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>`,
466
+ kind: "vector",
467
+ html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
468
+ markdown: imageOptions === "references" ? `![](${name})` : svg,
469
+ ...imageOptions === "references" ? {
470
+ assets: [
471
+ { name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
472
+ ]
473
+ } : {},
316
474
  ...consumedSpans.length > 0 ? { consumedSpans } : {}
317
475
  };
318
476
  });
319
477
  }
478
+ function mediaComponents(media, page) {
479
+ const components = [];
480
+ for (const item of media) {
481
+ if (isPageBackdrop(item.bounds, page)) {
482
+ components.push([item]);
483
+ continue;
484
+ }
485
+ const matches = components.filter(
486
+ (component) => !component.some((member) => isPageBackdrop(member.bounds, page)) && component.some((member) => mediaPiecesTouch(member.bounds, item.bounds))
487
+ );
488
+ if (matches.length === 0) {
489
+ components.push([item]);
490
+ continue;
491
+ }
492
+ const target = matches[0];
493
+ target.push(item);
494
+ for (const component of matches.slice(1)) {
495
+ target.push(...component);
496
+ components.splice(components.indexOf(component), 1);
497
+ }
498
+ }
499
+ return components.map((component) => compositeMedia(component));
500
+ }
501
+ function compositeMedia(items) {
502
+ if (items.length === 1) return items[0];
503
+ const bounds2 = unionBounds(items.map((item) => item.bounds));
504
+ const layers = items.map((item) => {
505
+ const left = (item.bounds.x - bounds2.x) / bounds2.width * 100;
506
+ const top = (bounds2.y + bounds2.height - item.bounds.y - item.bounds.height) / bounds2.height * 100;
507
+ const width = item.bounds.width / bounds2.width * 100;
508
+ const height = item.bounds.height / bounds2.height * 100;
509
+ return `<div style="position:absolute;left:${number2(left)}%;top:${number2(top)}%;width:${number2(width)}%;height:${number2(height)}%;overflow:hidden">${item.html}</div>`;
510
+ }).join("");
511
+ return {
512
+ bounds: bounds2,
513
+ kind: "composite",
514
+ html: `<div class="pdf-semantic-media pdf-semantic-media-composite" style="position:relative;max-width:100%;width:${number2(bounds2.width)}px;aspect-ratio:${number2(bounds2.width)}/${number2(bounds2.height)}">${layers}</div>`,
515
+ markdown: items.map((item) => item.markdown).join("\n\n"),
516
+ consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
517
+ assets: items.flatMap((item) => item.assets ?? [])
518
+ };
519
+ }
520
+ function mediaPiecesTouch(left, right) {
521
+ const xOverlap = overlap2(left.x, left.width, right.x, right.width);
522
+ const yOverlap = overlap2(left.y, left.height, right.y, right.height);
523
+ if (xOverlap > 0 && yOverlap > 0) return true;
524
+ const horizontalGap = axisGap(left.x, left.width, right.x, right.width);
525
+ const verticalGap = axisGap(left.y, left.height, right.y, right.height);
526
+ if (horizontalGap <= 2 && yOverlap / Math.min(left.height, right.height) >= 0.65) return true;
527
+ return verticalGap <= 2 && xOverlap / Math.min(left.width, right.width) >= 0.65;
528
+ }
529
+ function isPageBackdrop(bounds2, page) {
530
+ return bounds2.width * bounds2.height >= page.width * page.height * 0.7;
531
+ }
532
+ function overlap2(left, leftSize, right, rightSize) {
533
+ return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
534
+ }
535
+ function axisGap(left, leftSize, right, rightSize) {
536
+ return Math.max(0, right - left - leftSize, left - right - rightSize);
537
+ }
320
538
  function vectorComponents(primitives, padding) {
321
539
  const components = [];
322
540
  for (const primitive of primitives) {
@@ -435,7 +653,7 @@ function escapeHtml2(value) {
435
653
  }
436
654
 
437
655
  // src/semantic-document.ts
438
- async function writeSemanticDocument(pages, write, lookaheadPages) {
656
+ async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
439
657
  const stats = {
440
658
  pagesProcessed: 0,
441
659
  peakBufferedPages: 0,
@@ -453,27 +671,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
453
671
  let contentStarted = false;
454
672
  let employmentOpen = false;
455
673
  let pendingParagraph;
456
- await write('<article class="pdf-semantic-document">');
674
+ const markdown = format === "markdown";
675
+ const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
676
+ const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
677
+ await output('<article class="pdf-semantic-document">');
457
678
  const closeTable = async () => {
458
679
  if (!activeTable) return;
459
- await write("</table>");
680
+ await output("</table>", "\n");
460
681
  activeTable = void 0;
461
682
  while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
462
683
  };
463
684
  const closeSections = async (minimumLevel = 0) => {
464
685
  while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
465
- await write("</section>");
686
+ await output("</section>");
466
687
  sectionLevels.pop();
467
688
  }
468
689
  };
469
690
  const flushPendingParagraph = async () => {
470
691
  if (!pendingParagraph) return;
471
- await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
692
+ await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
472
693
  pendingParagraph = void 0;
473
694
  };
474
695
  const closeEmployment = async () => {
475
696
  if (!employmentOpen) return;
476
- await write("</section>");
697
+ await output("</section>");
477
698
  employmentOpen = false;
478
699
  };
479
700
  const emitPage = async (page, future) => {
@@ -487,29 +708,53 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
487
708
  page.structured.lines
488
709
  );
489
710
  const captionedMedia = new Set(captions.values());
711
+ const emittedMedia = /* @__PURE__ */ new Set();
490
712
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
491
713
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
492
714
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
493
715
  const nextBlock = page.structured.blocks[blockIndex + 1];
494
716
  const blockY = semanticBlockY(block);
495
717
  let emittedAsCaption = false;
718
+ while (page.media[mediaIndex] && emittedMedia.has(page.media[mediaIndex])) {
719
+ mediaIndex += 1;
720
+ }
496
721
  while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
497
722
  await flushPendingParagraph();
498
723
  const item = page.media[mediaIndex];
499
724
  if (item && captions.get(block) === item && block.type === "paragraph") {
500
- const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
725
+ const html2 = markdown ? `${item.markdown}
726
+
727
+ *${inlineText(block.text, block.lines, defaultColor)}*
728
+
729
+ ` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
501
730
  if (activeTable) pendingMedia.push(html2);
502
731
  else await write(html2);
732
+ emittedMedia.add(item);
503
733
  mediaIndex += 1;
504
734
  emittedAsCaption = true;
505
735
  break;
506
736
  }
507
737
  if (item && captionedMedia.has(item)) break;
508
- const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
738
+ const html = markdown ? `${item?.markdown ?? ""}
739
+
740
+ ` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
509
741
  if (activeTable) pendingMedia.push(html);
510
742
  else await write(html);
511
743
  mediaIndex += 1;
512
744
  }
745
+ const associatedMedia = captions.get(block);
746
+ if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
747
+ await flushPendingParagraph();
748
+ const html = markdown ? `${associatedMedia.markdown}
749
+
750
+ *${inlineText(block.text, block.lines, defaultColor)}*
751
+
752
+ ` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
753
+ if (activeTable) pendingMedia.push(html);
754
+ else await write(html);
755
+ emittedMedia.add(associatedMedia);
756
+ emittedAsCaption = true;
757
+ }
513
758
  if (emittedAsCaption) continue;
514
759
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
515
760
  stats.suppressedFurniture += 1;
@@ -518,8 +763,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
518
763
  await flushPendingParagraph();
519
764
  if (employmentOpen && block.type !== "list") await closeEmployment();
520
765
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
521
- await write(
522
- `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
766
+ await output(
767
+ `<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
768
+ `# ${inlineText(block.text, block.lines, defaultColor, false)}
769
+
770
+ `
523
771
  );
524
772
  headerOpen = true;
525
773
  continue;
@@ -527,19 +775,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
527
775
  if (headerOpen) {
528
776
  if (block.type === "paragraph") {
529
777
  const tag = isContactBlock(block) ? "address" : "p";
530
- await write(
531
- `<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
778
+ await output(
779
+ `<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
780
+ `${inlineText(block.text, block.lines, defaultColor)}
781
+
782
+ `
532
783
  );
533
784
  headerHasParagraph = true;
534
785
  continue;
535
786
  }
536
787
  if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
537
- await write(
538
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
788
+ await output(
789
+ `<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
790
+ `${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
791
+
792
+ `
539
793
  );
540
794
  continue;
541
795
  }
542
- await write("</header>");
796
+ await output("</header>");
543
797
  headerOpen = false;
544
798
  contentStarted = true;
545
799
  }
@@ -547,23 +801,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
547
801
  const rows = tableToRows(block.table);
548
802
  if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
549
803
  const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
550
- for (const row of continuationRows) await write(tableRow(row, false));
804
+ for (const row of continuationRows)
805
+ await write(markdown ? markdownTableRow(row) : tableRow(row, false));
551
806
  activeTable.table = block.table;
552
807
  stats.mergedTables += 1;
553
808
  continue;
554
809
  }
555
810
  await closeTable();
556
811
  const header = tableHeader(rows);
557
- await write("<table>");
558
- for (const [index, row] of rows.entries())
559
- await write(tableRow(row, Boolean(header && index === 0)));
812
+ await output("<table>", markdownTableStart(rows, header));
813
+ const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
814
+ for (const [index, row] of markdownRows.entries())
815
+ await write(
816
+ markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
817
+ );
560
818
  activeTable = { table: block.table, header };
561
819
  continue;
562
820
  }
563
821
  if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
564
822
  const columns = activeTable.table.columns.length;
565
- await write(
566
- `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`
823
+ await output(
824
+ `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
825
+ block.entries.map(
826
+ (entry) => markdownTableRow([
827
+ entry.term,
828
+ ...Array(Math.max(0, columns - 2)).fill(""),
829
+ entry.description
830
+ ])
831
+ ).join("")
567
832
  );
568
833
  await closeTable();
569
834
  continue;
@@ -572,8 +837,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
572
837
  if (block.type === "heading") {
573
838
  const level = contentStarted && block.level === 1 ? 2 : block.level;
574
839
  await closeSections(level);
575
- await write(
576
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
840
+ await output(
841
+ `<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
842
+ `${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
843
+
844
+ `
577
845
  );
578
846
  sectionLevels.push(level);
579
847
  continue;
@@ -581,13 +849,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
581
849
  if (block.type === "paragraph") {
582
850
  if (isTitledRecord(block)) {
583
851
  const [institution, ...details] = block.lines;
584
- if (institution) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
585
- for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
852
+ if (institution)
853
+ await output(
854
+ `<h3>${escapeHtml3(institution.text)}</h3>`,
855
+ `### ${escapeMarkdown2(institution.text)}
856
+
857
+ `
858
+ );
859
+ for (const detail of details)
860
+ await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
861
+
862
+ `);
586
863
  continue;
587
864
  }
588
865
  if (isUnmarkedList(block)) {
589
- await write(
590
- `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
866
+ await output(
867
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
868
+ `${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
869
+
870
+ `
591
871
  );
592
872
  continue;
593
873
  }
@@ -595,27 +875,40 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
595
875
  continue;
596
876
  }
597
877
  if (block.type === "employment") {
598
- await write(
599
- `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
878
+ await output(
879
+ `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
880
+ `### ${escapeMarkdown2(block.role)}
881
+
882
+ ${escapeMarkdown2(block.organization)}
883
+
884
+ ${escapeMarkdown2(block.date)}
885
+
886
+ `
600
887
  );
601
888
  employmentOpen = true;
602
889
  continue;
603
890
  }
604
- await write(semanticBlockHtml(block, defaultColor));
891
+ await write(semanticBlockOutput(block, defaultColor, format));
605
892
  }
606
893
  while (mediaIndex < page.media.length) {
607
- await flushPendingParagraph();
608
- const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
609
- if (activeTable) pendingMedia.push(html);
610
- else await write(html);
894
+ const item = page.media[mediaIndex];
895
+ if (item && !emittedMedia.has(item)) {
896
+ await flushPendingParagraph();
897
+ const html = markdown ? `${item.markdown}
898
+
899
+ ` : `<div class="pdf-semantic-visual">${item.html}</div>`;
900
+ if (activeTable) pendingMedia.push(html);
901
+ else await write(html);
902
+ }
611
903
  mediaIndex += 1;
612
904
  }
613
905
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
614
906
  };
615
907
  for await (const page of pages) {
616
- const media = semanticMedia(page);
908
+ const media = await prepareSemanticMedia(page, imageOptions, onImage);
617
909
  const structured = structurePage(withoutSemanticMediaSpans(page, media));
618
910
  buffer.push({ width: page.width, height: page.height, structured, media });
911
+ restoreObservedHyphens(buffer);
619
912
  stats.pagesProcessed += 1;
620
913
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
621
914
  stats.peakBufferedLines = Math.max(
@@ -631,22 +924,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
631
924
  const ready = buffer.shift();
632
925
  if (ready) await emitPage(ready, buffer);
633
926
  }
634
- if (headerOpen) await write("</header>");
927
+ if (headerOpen) await output("</header>");
635
928
  await closeTable();
636
929
  await closeEmployment();
637
930
  if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
638
931
  await closeSections();
639
- await write(
640
- `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
932
+ await output(
933
+ `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
934
+ `---
935
+
936
+ ${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
641
937
  );
642
938
  pendingParagraph = void 0;
643
939
  } else {
644
940
  await flushPendingParagraph();
645
941
  await closeSections();
646
942
  }
647
- await write("</article>");
943
+ await output("</article>");
648
944
  return stats;
649
945
  }
946
+ function restoreObservedHyphens(buffer) {
947
+ const terms = new Set(
948
+ buffer.flatMap(
949
+ (page) => page.structured.lines.flatMap(
950
+ (line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
951
+ )
952
+ )
953
+ );
954
+ for (const page of buffer) {
955
+ for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
956
+ }
957
+ }
958
+ function restoreBlockHyphens(block, terms) {
959
+ const restore = (value) => restoreTextHyphens(value, terms);
960
+ if (block.type === "insetGroup") {
961
+ for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
962
+ } else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
963
+ block.text = restore(block.text);
964
+ } else if (block.type === "list") {
965
+ for (const item of block.items) item.text = restore(item.text);
966
+ } else if (block.type === "definitionList") {
967
+ for (const entry of block.entries) {
968
+ entry.term = restore(entry.term);
969
+ entry.description = restore(entry.description);
970
+ }
971
+ } else if (block.type === "cardList") {
972
+ for (const item of block.items) {
973
+ item.title = restore(item.title);
974
+ item.details = item.details.map(restore);
975
+ }
976
+ } else if (block.type === "sectionGroup") {
977
+ for (const item of block.items) {
978
+ item.label = restore(item.label);
979
+ item.content = item.content.map(restore);
980
+ }
981
+ } else if (block.type === "employment") {
982
+ block.role = restore(block.role);
983
+ block.organization = restore(block.organization);
984
+ block.date = restore(block.date);
985
+ }
986
+ }
987
+ function restoreTextHyphens(value, terms) {
988
+ let output = value;
989
+ for (const term of terms) {
990
+ const collapsed = term.replace(/[-‐‑]/gu, "");
991
+ if (collapsed === term || !output.includes(collapsed)) continue;
992
+ const pattern = new RegExp(
993
+ `(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
994
+ "gu"
995
+ );
996
+ output = output.replace(pattern, term);
997
+ }
998
+ return output;
999
+ }
1000
+ function escapeRegularExpression(value) {
1001
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1002
+ }
650
1003
  function isContactBlock(block) {
651
1004
  const text = block.text;
652
1005
  const signals = [
@@ -730,6 +1083,20 @@ function tableRow(row, header) {
730
1083
  const cell = header ? "th" : "td";
731
1084
  return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
732
1085
  }
1086
+ function markdownTableStart(rows, header) {
1087
+ const columns = rows[0]?.length ?? 0;
1088
+ if (columns === 0) return "";
1089
+ const heading = header ?? Array(columns).fill("");
1090
+ return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
1091
+ }
1092
+ function markdownTableRow(row, shouldEscape = true) {
1093
+ const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
1094
+ return `| ${cells.join(" | ")} |
1095
+ `;
1096
+ }
1097
+ function escapeMarkdownTableCell(value) {
1098
+ return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
1099
+ }
733
1100
  function isFinancialSummary(block) {
734
1101
  return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
735
1102
  }
@@ -777,6 +1144,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
777
1144
  const tag = block.ordered ? "ol" : "ul";
778
1145
  return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
779
1146
  }
1147
+ function semanticBlockOutput(block, defaultColor, format) {
1148
+ return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
1149
+ }
1150
+ function semanticBlockMarkdown(block, defaultColor = "#000000") {
1151
+ if (block.type === "insetGroup") {
1152
+ const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
1153
+ return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
1154
+
1155
+ `;
1156
+ }
1157
+ if (block.type === "table") {
1158
+ const rows = tableToRows(block.table);
1159
+ const header = tableHeader(rows);
1160
+ return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
1161
+ `;
1162
+ }
1163
+ if (block.type === "heading") {
1164
+ return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
1165
+
1166
+ `;
1167
+ }
1168
+ if (block.type === "paragraph") {
1169
+ return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
1170
+
1171
+ `;
1172
+ }
1173
+ if (block.type === "preformatted") {
1174
+ const fence = block.text.includes("```") ? "````" : "```";
1175
+ return `${fence}
1176
+ ${block.text}
1177
+ ${fence}
1178
+
1179
+ `;
1180
+ }
1181
+ if (block.type === "definitionList") {
1182
+ return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
1183
+
1184
+ `;
1185
+ }
1186
+ if (block.type === "cardList") {
1187
+ const rows = [
1188
+ ["Item", "Quantity", "Amount"],
1189
+ ...block.items.map((item) => {
1190
+ const trailing = item.details.at(-1) ?? "";
1191
+ const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
1192
+ const detail = item.details.slice(0, -1).join(" ");
1193
+ return [
1194
+ `${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
1195
+ match?.[1] ?? "",
1196
+ match?.[2] ?? trailing
1197
+ ];
1198
+ })
1199
+ ];
1200
+ return `## Items ordered
1201
+
1202
+ ${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
1203
+ `;
1204
+ }
1205
+ if (block.type === "sectionGroup") {
1206
+ return block.items.map(
1207
+ (item) => `## ${escapeMarkdown2(titleCase(item.label))}
1208
+
1209
+ ${item.content.map(
1210
+ (content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
1211
+ ).join("\n\n")}
1212
+
1213
+ `
1214
+ ).join("");
1215
+ }
1216
+ if (block.type === "employment") {
1217
+ return `### ${escapeMarkdown2(block.role)}
1218
+
1219
+ ${escapeMarkdown2(block.organization)}
1220
+
1221
+ ${escapeMarkdown2(block.date)}
1222
+
1223
+ `;
1224
+ }
1225
+ return `${block.items.map(
1226
+ (item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
1227
+ ).join("\n")}
1228
+
1229
+ `;
1230
+ }
780
1231
  function semanticBlockY(block) {
781
1232
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
782
1233
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -809,6 +1260,9 @@ function titleCase(value) {
809
1260
  function escapeHtml3(value) {
810
1261
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
811
1262
  }
1263
+ function escapeMarkdown2(value) {
1264
+ return value.replace(/([\\`*_[\]<>])/g, "\\$1");
1265
+ }
812
1266
 
813
1267
  // src/index.ts
814
1268
  var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-semantic-document h1,.pdf-page--semantic h1{font-size:1.7em}.pdf-semantic-document h2,.pdf-page--semantic h2{font-size:1.5em}.pdf-semantic-document h3,.pdf-page--semantic h3{font-size:1.35em}.pdf-semantic-document h4,.pdf-page--semantic h4{font-size:1.1em}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
@@ -825,9 +1279,18 @@ async function writeHtmlDocument(pages, write, options = {}) {
825
1279
  await write("</head><body>");
826
1280
  }
827
1281
  await write('<main class="pdf-document">');
828
- if (resolveProfile(options) === "semantic") {
1282
+ const profile = resolveProfile(options);
1283
+ const imageOptions = resolveImageOptions(profile, options);
1284
+ validateImageOptions(imageOptions, options);
1285
+ if (profile === "semantic") {
829
1286
  const lookahead = semanticLookahead(options.semanticLookaheadPages);
830
- const stats = await writeSemanticDocument(pages, write, lookahead);
1287
+ const stats = await writeSemanticDocument(
1288
+ pages,
1289
+ write,
1290
+ lookahead,
1291
+ imageOptions,
1292
+ options.onImage
1293
+ );
831
1294
  options.onSemanticStats?.(stats);
832
1295
  } else {
833
1296
  for await (const page of pages) await writePage(page, write, options);
@@ -835,6 +1298,20 @@ async function writeHtmlDocument(pages, write, options = {}) {
835
1298
  await write("</main>");
836
1299
  if (includeDocument) await write("</body></html>");
837
1300
  }
1301
+ async function writeMarkdownDocument(pages, write, options = {}) {
1302
+ const imageOptions = options.imageOptions ?? "excluded";
1303
+ validateImageOptions(imageOptions, options);
1304
+ const lookahead = semanticLookahead(options.semanticLookaheadPages);
1305
+ const stats = await writeSemanticDocument(
1306
+ pages,
1307
+ write,
1308
+ lookahead,
1309
+ imageOptions,
1310
+ options.onImage,
1311
+ "markdown"
1312
+ );
1313
+ options.onSemanticStats?.(stats);
1314
+ }
838
1315
  function semanticLookahead(value) {
839
1316
  const lookahead = value ?? 4;
840
1317
  if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
@@ -843,7 +1320,9 @@ function semanticLookahead(value) {
843
1320
  return lookahead;
844
1321
  }
845
1322
  async function writePage(page, write, options = {}) {
846
- if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
1323
+ const profile = resolveProfile(options);
1324
+ validateImageOptions(resolveImageOptions(profile, options), options);
1325
+ if (profile === "semantic") await writeFlowPage(page, write, options);
847
1326
  else await writePositionedPage(page, write, options);
848
1327
  }
849
1328
  async function pageToHtml(page, options = {}) {
@@ -858,7 +1337,9 @@ async function pageToHtml(page, options = {}) {
858
1337
  return output;
859
1338
  }
860
1339
  async function writePositionedPage(page, write, options) {
861
- const visualSpans = page.visualSpans ?? page.spans;
1340
+ const imageOptions = resolveImageOptions("visual", options);
1341
+ const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
1342
+ const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
862
1343
  const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
863
1344
  const quarterTurn = page.rotate === 90 || page.rotate === 270;
864
1345
  const displayWidth = quarterTurn ? page.height : page.width;
@@ -870,25 +1351,32 @@ async function writePositionedPage(page, write, options) {
870
1351
  const type3Fonts = new Map(
871
1352
  (page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
872
1353
  );
1354
+ const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
873
1355
  if ((options.includeStyles ?? true) && page.fonts?.length) {
874
1356
  await write(
875
1357
  `<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
876
1358
  );
877
1359
  }
1360
+ if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
878
1361
  await write(
879
1362
  `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
880
1363
  );
881
1364
  await write(
882
1365
  `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
883
1366
  );
884
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
1367
+ const clipDefinitions = imageClipDefinitions(
1368
+ imageOptions === "excluded" ? [] : page.images ?? [],
1369
+ page.number,
1370
+ page.height
1371
+ ) + vectorPathClipDefinitions(
885
1372
  (page.paths ?? []).map((path, index) => ({ path, index })),
886
1373
  page.number
887
1374
  );
888
1375
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
889
1376
  if (reflectedOverlay) {
890
1377
  for (const [index, image] of (page.images ?? []).entries()) {
891
- await write(visualImage(image, page.height, page.number, index));
1378
+ const source = visualImages[index];
1379
+ if (source) await write(visualImage(image, page.height, page.number, index, source));
892
1380
  }
893
1381
  }
894
1382
  if (page.fills?.length || page.paths?.length) {
@@ -901,14 +1389,29 @@ async function writePositionedPage(page, write, options) {
901
1389
  }
902
1390
  if (!reflectedOverlay) {
903
1391
  for (const [index, image] of (page.images ?? []).entries()) {
904
- await write(visualImage(image, page.height, page.number, index));
1392
+ const source = visualImages[index];
1393
+ if (source) await write(visualImage(image, page.height, page.number, index, source));
905
1394
  }
906
1395
  }
907
- for (const span of visualSpans) {
1396
+ for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
1397
+ const span = visualSpans[spanIndex];
1398
+ if (!span) continue;
908
1399
  if (!usesPositionedSpan(span)) {
909
1400
  const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
1401
+ const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
1402
+ if (line) {
1403
+ await write(line.html);
1404
+ spanIndex = line.endIndex;
1405
+ continue;
1406
+ }
910
1407
  await write(
911
- type3 ? visualType3Text(span, type3, page.height) : visualText(span, page.height, fontAliases, reflectedOverlay && page.rotate === 180)
1408
+ type3 ? visualType3Text(span, type3, page.height) : visualText(
1409
+ span,
1410
+ page.height,
1411
+ fontAliases,
1412
+ reflectedOverlay && page.rotate === 180,
1413
+ textClasses?.names
1414
+ )
912
1415
  );
913
1416
  }
914
1417
  }
@@ -918,23 +1421,131 @@ async function writePositionedPage(page, write, options) {
918
1421
  }
919
1422
  await write("</div></section>");
920
1423
  }
1424
+ function coalesceVisualSpans(spans) {
1425
+ const output = [];
1426
+ for (const span of spans) {
1427
+ const previous = output.at(-1);
1428
+ if (!previous || !canCoalesceVisualSpans(previous, span)) {
1429
+ output.push(span);
1430
+ continue;
1431
+ }
1432
+ output[output.length - 1] = {
1433
+ ...previous,
1434
+ text: previous.text + span.text,
1435
+ bounds: {
1436
+ ...previous.bounds,
1437
+ width: span.bounds.x + span.bounds.width - previous.bounds.x,
1438
+ height: Math.max(previous.bounds.height, span.bounds.height)
1439
+ }
1440
+ };
1441
+ }
1442
+ return output;
1443
+ }
1444
+ function canCoalesceVisualSpans(left, right) {
1445
+ if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
1446
+ if (left.direction !== "ltr" || right.direction !== "ltr") return false;
1447
+ if (/guardian/i.test(left.fontFamily ?? "")) return false;
1448
+ if (left.glyphCodes || right.glyphCodes) return false;
1449
+ if (!sameVisualTextState(left, right)) return false;
1450
+ const tolerance = Math.max(0.02, left.fontSize * 0.015);
1451
+ if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
1452
+ const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
1453
+ return !right.hasLeadingSpace && gap >= -tolerance && gap <= tolerance;
1454
+ }
1455
+ function sameVisualTextState(left, right) {
1456
+ return Math.abs(left.fontSize - right.fontSize) <= 1e-3 && left.fontName === right.fontName && left.fontFamily === right.fontFamily && left.fontAssetId === right.fontAssetId && left.color === right.color && left.fillOpacity === right.fillOpacity && left.strokeColor === right.strokeColor && left.strokeWidth === right.strokeWidth && left.strokeOpacity === right.strokeOpacity && left.renderingMode === right.renderingMode && sameTransform(left.transform, right.transform);
1457
+ }
1458
+ function sameTransform(left, right) {
1459
+ if (!left || !right) return left === right;
1460
+ return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
1461
+ }
1462
+ function visualTextClasses(pageNumber, spans, fontAliases) {
1463
+ const names = /* @__PURE__ */ new Map();
1464
+ let css = "";
1465
+ for (const span of spans) {
1466
+ if (usesPositionedSpan(span) || span.glyphCodes) {
1467
+ continue;
1468
+ }
1469
+ const style = visualTextClassStyle(span, fontAliases);
1470
+ if (!style || names.has(style)) continue;
1471
+ const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
1472
+ names.set(style, name);
1473
+ css += `.${name}{${style}}`;
1474
+ }
1475
+ return { css, names };
1476
+ }
1477
+ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
1478
+ const first = spans[startIndex];
1479
+ if (!first || !canGroupVisualTextLine(first)) return void 0;
1480
+ const style = visualTextClassStyle(first, fontAliases);
1481
+ const className = styleClasses.get(style);
1482
+ if (!className) return void 0;
1483
+ let endIndex = startIndex;
1484
+ while (endIndex + 1 < spans.length) {
1485
+ const next = spans[endIndex + 1];
1486
+ if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
1487
+ break;
1488
+ }
1489
+ endIndex += 1;
1490
+ }
1491
+ if (endIndex === startIndex) return void 0;
1492
+ const baseline = pageHeight - first.bounds.y;
1493
+ const lineSpans = spans.slice(startIndex, endIndex + 1);
1494
+ const content = lineSpans.map(
1495
+ (span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
1496
+ ).join("");
1497
+ return {
1498
+ html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
1499
+ endIndex
1500
+ };
1501
+ }
1502
+ function visualTextTspan(span, dx) {
1503
+ const extent = span.bounds.width;
1504
+ const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
1505
+ const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1506
+ return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
1507
+ }
1508
+ function textSpanGap(previous, current) {
1509
+ if (!previous) return 0;
1510
+ const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
1511
+ const adjustment = current.textAdjustmentBefore;
1512
+ return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
1513
+ }
1514
+ function canGroupVisualTextLine(span) {
1515
+ return !usesPositionedSpan(span) && !span.glyphCodes && span.direction === "ltr" && !isHebrewPaintOrder(span) && !hasNonIdentityTransform(span.transform) && span.renderingMode !== 3 && span.renderingMode !== 7 && (span.fontAssetId !== void 0 || !isAdobeCjkFont(span.fontFamily));
1516
+ }
921
1517
  function usesReflectedVisualOverlay(page, spans) {
922
1518
  return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
923
1519
  (span) => span.transform !== void 0 && Math.abs(span.transform[0] + 1) < 1e-6 && Math.abs(span.transform[1]) < 1e-6 && Math.abs(span.transform[2]) < 1e-6 && Math.abs(span.transform[3] - 1) < 1e-6
924
1520
  );
925
1521
  }
926
- function visualImage(image, pageHeight, pageNumber, imageIndex) {
1522
+ function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
927
1523
  const [a, b, c, d, e, f] = image.transform;
928
1524
  const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
929
1525
  const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
930
- const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
931
- const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
932
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
1526
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
933
1527
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
934
1528
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
935
1529
  }
936
1530
  return output;
937
1531
  }
1532
+ async function prepareVisualImages(page, imageOptions, onImage) {
1533
+ if (imageOptions === "excluded") return [];
1534
+ const sources = [];
1535
+ for (const [index, image] of (page.images ?? []).entries()) {
1536
+ const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
1537
+ const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
1538
+ if (imageOptions === "embedded") {
1539
+ sources.push(`data:${mimeType};base64,${base64(data)}`);
1540
+ continue;
1541
+ }
1542
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
1543
+ const name = `page-${page.number}-image-${index + 1}.${extension}`;
1544
+ await onImage?.({ name, mimeType, data });
1545
+ sources.push(name);
1546
+ }
1547
+ return sources;
1548
+ }
938
1549
  function imageClipDefinitions(images, pageNumber, pageHeight) {
939
1550
  return images.flatMap(
940
1551
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
@@ -1001,8 +1612,9 @@ function positionedSpan(span, fontAliases) {
1001
1612
  ].join(";");
1002
1613
  return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
1003
1614
  }
1004
- async function writeFlowPage(page, write) {
1005
- const media = semanticMedia(page);
1615
+ async function writeFlowPage(page, write, options) {
1616
+ const imageOptions = resolveImageOptions("semantic", options);
1617
+ const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
1006
1618
  const structured = structurePage2(withoutSemanticMediaSpans(page, media));
1007
1619
  const defaultColor = dominantTextColor(structured.lines);
1008
1620
  let mediaIndex = 0;
@@ -1014,18 +1626,22 @@ async function writeFlowPage(page, write) {
1014
1626
  structured.lines
1015
1627
  );
1016
1628
  const captionedMedia = new Set(captions.values());
1629
+ const emittedMedia = /* @__PURE__ */ new Set();
1017
1630
  await write(
1018
1631
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
1019
1632
  );
1020
1633
  for (const block of structured.blocks) {
1021
1634
  const blockY = semanticBlockY2(block);
1022
1635
  let emittedAsCaption = false;
1636
+ while (media[mediaIndex] && emittedMedia.has(media[mediaIndex]))
1637
+ mediaIndex += 1;
1023
1638
  while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
1024
1639
  const item = media[mediaIndex];
1025
1640
  if (item && captions.get(block) === item && block.type === "paragraph") {
1026
1641
  await write(
1027
1642
  `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
1028
1643
  );
1644
+ emittedMedia.add(item);
1029
1645
  mediaIndex += 1;
1030
1646
  emittedAsCaption = true;
1031
1647
  break;
@@ -1034,6 +1650,14 @@ async function writeFlowPage(page, write) {
1034
1650
  await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
1035
1651
  mediaIndex += 1;
1036
1652
  }
1653
+ const associatedMedia = captions.get(block);
1654
+ if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
1655
+ await write(
1656
+ `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
1657
+ );
1658
+ emittedMedia.add(associatedMedia);
1659
+ emittedAsCaption = true;
1660
+ }
1037
1661
  if (emittedAsCaption) continue;
1038
1662
  if (block.type === "table") await write(tableToHtml(block.table));
1039
1663
  else if (block.type === "heading") {
@@ -1088,7 +1712,10 @@ async function writeFlowPage(page, write) {
1088
1712
  }
1089
1713
  }
1090
1714
  while (mediaIndex < media.length) {
1091
- await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
1715
+ const item = media[mediaIndex];
1716
+ if (item && !emittedMedia.has(item)) {
1717
+ await write(`<div class="pdf-semantic-visual">${item.html}</div>`);
1718
+ }
1092
1719
  mediaIndex += 1;
1093
1720
  }
1094
1721
  await write("</section>");
@@ -1124,10 +1751,37 @@ function semanticBlockY2(block) {
1124
1751
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
1125
1752
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
1126
1753
  }
1127
- function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
1754
+ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
1128
1755
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
1129
1756
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
1130
1757
  const direction = directionAttribute([span]);
1758
+ const style = visualTextStyle(span, fontAliases);
1759
+ const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
1760
+ const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
1761
+ const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
1762
+ const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
1763
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1764
+ const transform = counterRotateReflectedText && span.transform ? [
1765
+ span.transform[0],
1766
+ span.transform[1],
1767
+ span.transform[2],
1768
+ -span.transform[3]
1769
+ ] : span.transform;
1770
+ const transformed = hasNonIdentityTransform(transform);
1771
+ const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
1772
+ const basisX = transform?.[0] ?? 1;
1773
+ const basisY = transform?.[1] ?? 0;
1774
+ const anchorX = span.bounds.x + basisX * rtlOffset;
1775
+ const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1776
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1777
+ return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
1778
+ }
1779
+ function visualTextClassStyle(span, fontAliases) {
1780
+ const style = visualTextStyle(span, fontAliases);
1781
+ const fontSize = `font-size:${number3(span.fontSize)}px`;
1782
+ return style ? `${style};${fontSize}` : fontSize;
1783
+ }
1784
+ function visualTextStyle(span, fontAliases) {
1131
1785
  const font = visualFontStyles(
1132
1786
  span.fontFamily,
1133
1787
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -1137,7 +1791,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1137
1791
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
1138
1792
  const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
1139
1793
  const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
1140
- const style = [
1794
+ return [
1141
1795
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
1142
1796
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
1143
1797
  strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
@@ -1147,22 +1801,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1147
1801
  strokeOpacity,
1148
1802
  font
1149
1803
  ].filter(Boolean).join(";");
1150
- const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
1151
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1152
- const transform = counterRotateReflectedText && span.transform ? [
1153
- span.transform[0],
1154
- span.transform[1],
1155
- span.transform[2],
1156
- -span.transform[3]
1157
- ] : span.transform;
1158
- const transformed = hasNonIdentityTransform(transform);
1159
- const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
1160
- const basisX = transform?.[0] ?? 1;
1161
- const basisY = transform?.[1] ?? 0;
1162
- const anchorX = span.bounds.x + basisX * rtlOffset;
1163
- const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1164
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1165
- return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
1166
1804
  }
1167
1805
  function isAdobeCjkFont(fontFamily) {
1168
1806
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -1257,9 +1895,18 @@ function resolveProfile(options) {
1257
1895
  }
1258
1896
  return options.profile ?? legacyProfile;
1259
1897
  }
1898
+ function resolveImageOptions(profile, options) {
1899
+ return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
1900
+ }
1901
+ function validateImageOptions(imageOptions, options) {
1902
+ if (imageOptions === "references" && !options.onImage) {
1903
+ throw new Error('imageOptions "references" requires an onImage callback');
1904
+ }
1905
+ }
1260
1906
  export {
1261
1907
  pageToHtml,
1262
1908
  writeHtmlDocument,
1909
+ writeMarkdownDocument,
1263
1910
  writePage
1264
1911
  };
1265
1912
  //# sourceMappingURL=index.js.map