@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.cjs CHANGED
@@ -22,45 +22,157 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  pageToHtml: () => pageToHtml,
24
24
  writeHtmlDocument: () => writeHtmlDocument,
25
+ writeMarkdownDocument: () => writeMarkdownDocument,
25
26
  writePage: () => writePage
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
  var import_structure2 = require("@boxpdf/reader/structure");
29
30
 
30
31
  // src/semantic-caption.ts
31
- function isClearMediaCaption(media, block, pageWidth, pageHeight, pageLines) {
32
- if (block.type !== "paragraph" || block.lines.length === 0) return false;
33
- const bounds2 = unionLines(block.lines);
34
- const lineHeight = median(block.lines.map((line) => line.bounds.height));
35
- if (media.bounds.width < pageWidth * 0.2 || media.bounds.height < lineHeight * 10) return false;
36
- 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)
37
- return false;
38
- const gap = media.bounds.y - (bounds2.y + bounds2.height);
39
- if (gap < -lineHeight * 0.15 || gap > lineHeight * 1.25) return false;
40
- const mediaCenter = media.bounds.x + media.bounds.width / 2;
41
- const captionCenter = bounds2.x + bounds2.width / 2;
42
- if (Math.abs(mediaCenter - captionCenter) > Math.max(3, media.bounds.width * 0.03)) return false;
43
- if (bounds2.width < media.bounds.width * 0.45 || bounds2.width > media.bounds.width * 1.06) {
44
- return false;
45
- }
46
- const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
47
- if (!first) return false;
48
- const otherLines = pageLines.filter((line) => !block.lines.includes(line));
49
- return fontSignature(first) !== dominantFontSignature(otherLines);
50
- }
32
+ var minimumCaptionScore = 0.72;
51
33
  function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
34
+ const candidates = blocks.flatMap((block) => {
35
+ const candidate = captionCandidate(block);
36
+ return candidate ? [candidate] : [];
37
+ });
38
+ const preliminary = media.flatMap(
39
+ (item) => candidates.flatMap((candidate) => {
40
+ const evidence = scoreCaption(item, candidate, pageWidth, pageHeight, pageLines, 0);
41
+ return evidence ? [{ media: item, candidate, evidence }] : [];
42
+ })
43
+ );
44
+ const patterns = repeatedPatterns(preliminary);
45
+ const edges = preliminary.map((edge) => {
46
+ const evidence = scoreCaption(
47
+ edge.media,
48
+ edge.candidate,
49
+ pageWidth,
50
+ pageHeight,
51
+ pageLines,
52
+ patterns.get(patternKey(edge)) ?? 0
53
+ );
54
+ return evidence ? { ...edge, evidence } : void 0;
55
+ }).filter((edge) => Boolean(edge)).filter((edge) => edge.evidence.score >= minimumCaptionScore);
56
+ const bestForMedia = bestEdges(edges, (edge) => edge.media);
57
+ const bestForCaption = bestEdges(edges, (edge) => edge.candidate.block);
52
58
  const associations = /* @__PURE__ */ new Map();
53
- for (const item of media) {
54
- 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));
55
- const caption = candidates[0];
56
- if (caption) associations.set(caption, item);
59
+ for (const edge of edges) {
60
+ if (bestForMedia.get(edge.media) === edge && bestForCaption.get(edge.candidate.block) === edge) {
61
+ associations.set(edge.candidate.block, edge.media);
62
+ }
57
63
  }
58
64
  return associations;
59
65
  }
60
- function captionGap(media, block) {
61
- if (block.type !== "paragraph") return Number.POSITIVE_INFINITY;
62
- const bounds2 = unionLines(block.lines);
63
- return Math.abs(media.bounds.y - bounds2.y - bounds2.height);
66
+ function scoreCaption(media, candidate, pageWidth, pageHeight, pageLines, repeatedAlignment) {
67
+ if (!insidePage(media.bounds, pageWidth, pageHeight)) return void 0;
68
+ if (media.bounds.width < pageWidth * 0.06 || media.bounds.height < candidate.lineHeight * 1.5)
69
+ return void 0;
70
+ const relation = verticalRelation(media.bounds, candidate.bounds);
71
+ if (!relation) return void 0;
72
+ const maximumGap = Math.max(candidate.lineHeight * 3, pageHeight * 0.035);
73
+ if (relation.gap > maximumGap) return void 0;
74
+ const overlapWidth = overlap(
75
+ media.bounds.x,
76
+ media.bounds.width,
77
+ candidate.bounds.x,
78
+ candidate.bounds.width
79
+ );
80
+ const horizontalOverlap = overlapWidth / Math.max(1, Math.min(media.bounds.width, candidate.bounds.width));
81
+ const centerDistance = Math.abs(center(media.bounds) - center(candidate.bounds));
82
+ const centerAlignment = clamp01(
83
+ 1 - centerDistance / Math.max(media.bounds.width, candidate.bounds.width)
84
+ );
85
+ if (horizontalOverlap < 0.45 && centerAlignment < 0.82) return void 0;
86
+ const relativeWidth = Math.min(media.bounds.width, candidate.bounds.width) / Math.max(media.bounds.width, candidate.bounds.width);
87
+ const gapRatio = clamp01(1 - relation.gap / maximumGap);
88
+ const interveningContent = interveningScore(media.bounds, candidate, relation.side, pageLines);
89
+ if (interveningContent === 0) return void 0;
90
+ if (relation.side === "above" && (gapRatio < 0.6 || interveningContent < 1)) return void 0;
91
+ const fontContrast = captionFontContrast(candidate, pageLines);
92
+ const surroundingWhitespace = whitespaceScore(candidate, relation.side, relation.gap, pageLines);
93
+ 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;
94
+ return {
95
+ score,
96
+ side: relation.side,
97
+ gapRatio,
98
+ horizontalOverlap,
99
+ centerAlignment,
100
+ relativeWidth,
101
+ fontContrast,
102
+ surroundingWhitespace,
103
+ interveningContent,
104
+ repeatedAlignment
105
+ };
106
+ }
107
+ function captionCandidate(block) {
108
+ if (block.type !== "paragraph" || block.lines.length === 0) return void 0;
109
+ const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
110
+ if (!first) return void 0;
111
+ return {
112
+ block,
113
+ bounds: unionLines(block.lines),
114
+ lineHeight: median(block.lines.map((line) => line.bounds.height)),
115
+ font: fontSignature(first)
116
+ };
117
+ }
118
+ function verticalRelation(media, caption) {
119
+ const belowGap = media.y - (caption.y + caption.height);
120
+ if (belowGap >= -caption.height * 0.15) return { side: "below", gap: Math.max(0, belowGap) };
121
+ const aboveGap = caption.y - (media.y + media.height);
122
+ if (aboveGap >= -caption.height * 0.15) return { side: "above", gap: Math.max(0, aboveGap) };
123
+ return void 0;
124
+ }
125
+ function interveningScore(media, candidate, side, pageLines) {
126
+ const lower = side === "below" ? candidate.bounds.y + candidate.bounds.height : media.y + media.height;
127
+ const upper = side === "below" ? media.y : candidate.bounds.y;
128
+ const blockers = pageLines.filter(
129
+ (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
130
+ );
131
+ return blockers.length === 0 ? 1 : blockers.length === 1 ? 0.35 : 0;
132
+ }
133
+ function captionFontContrast(candidate, pageLines) {
134
+ const otherLines = pageLines.filter((line) => !candidate.block.lines.includes(line));
135
+ if (candidate.font !== dominantFontSignature(otherLines)) return 1;
136
+ const candidateSize = median(
137
+ candidate.block.lines.flatMap((line) => line.spans.map((span) => span.fontSize))
138
+ );
139
+ const bodySize = median(otherLines.flatMap((line) => line.spans.map((span) => span.fontSize)));
140
+ return Math.abs(candidateSize - bodySize) >= 0.75 ? 0.65 : 0.15;
141
+ }
142
+ function whitespaceScore(candidate, side, mediaGap, pageLines) {
143
+ const awayGaps = pageLines.filter((line) => !candidate.block.lines.includes(line)).filter(
144
+ (line) => overlap(line.bounds.x, line.bounds.width, candidate.bounds.x, candidate.bounds.width) > 0
145
+ ).flatMap((line) => {
146
+ if (side === "below" && line.bounds.y + line.bounds.height <= candidate.bounds.y)
147
+ return [candidate.bounds.y - line.bounds.y - line.bounds.height];
148
+ if (side === "above" && line.bounds.y >= candidate.bounds.y + candidate.bounds.height)
149
+ return [line.bounds.y - candidate.bounds.y - candidate.bounds.height];
150
+ return [];
151
+ });
152
+ const awayGap = Math.min(...awayGaps, Number.POSITIVE_INFINITY);
153
+ return Number.isFinite(awayGap) ? clamp01((awayGap + candidate.lineHeight * 0.25) / (mediaGap + candidate.lineHeight)) : 1;
154
+ }
155
+ function repeatedPatterns(edges) {
156
+ const counts = /* @__PURE__ */ new Map();
157
+ for (const edge of edges.filter((item) => item.evidence.score >= minimumCaptionScore - 0.08)) {
158
+ counts.set(patternKey(edge), (counts.get(patternKey(edge)) ?? 0) + 1);
159
+ }
160
+ return new Map([...counts].map(([key, count]) => [key, count >= 2 ? 1 : 0]));
161
+ }
162
+ function patternKey(edge) {
163
+ const widthRatio = edge.candidate.bounds.width / Math.max(1, edge.media.bounds.width);
164
+ return `${edge.candidate.font}|${edge.evidence.side}|${Math.round(widthRatio * 4) / 4}`;
165
+ }
166
+ function bestEdges(edges, key) {
167
+ const output = /* @__PURE__ */ new Map();
168
+ for (const edge of edges) {
169
+ const existing = output.get(key(edge));
170
+ if (!existing || edge.evidence.score > existing.evidence.score) output.set(key(edge), edge);
171
+ }
172
+ return output;
173
+ }
174
+ function insidePage(bounds2, pageWidth, pageHeight) {
175
+ return bounds2.x >= -2 && bounds2.y >= -2 && bounds2.x + bounds2.width <= pageWidth + 2 && bounds2.y + bounds2.height <= pageHeight + 2;
64
176
  }
65
177
  function unionLines(lines) {
66
178
  const x = Math.min(...lines.map((line) => line.bounds.x));
@@ -80,6 +192,15 @@ function dominantFontSignature(lines) {
80
192
  function fontSignature(span) {
81
193
  return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
82
194
  }
195
+ function center(bounds2) {
196
+ return bounds2.x + bounds2.width / 2;
197
+ }
198
+ function overlap(left, leftSize, right, rightSize) {
199
+ return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
200
+ }
201
+ function clamp01(value) {
202
+ return Math.max(0, Math.min(1, value));
203
+ }
83
204
  function median(values) {
84
205
  const ordered = [...values].sort((left, right) => left - right);
85
206
  return ordered[Math.floor(ordered.length / 2)] ?? 1;
@@ -98,6 +219,12 @@ function dominantTextColor(lines) {
98
219
  return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
99
220
  }
100
221
  function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
222
+ return semanticText(text, lines, defaultColor, preserveWeight, "html");
223
+ }
224
+ function semanticTextMarkdown(text, lines, defaultColor, preserveWeight = true) {
225
+ return semanticText(text, lines, defaultColor, preserveWeight, "markdown");
226
+ }
227
+ function semanticText(text, lines, defaultColor, preserveWeight, format) {
101
228
  const ranges = [];
102
229
  let cursor = 0;
103
230
  for (const span of lines.flatMap((line) => line.spans)) {
@@ -123,11 +250,11 @@ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
123
250
  let html = "";
124
251
  let offset = 0;
125
252
  for (const range of merged) {
126
- html += escapeHtml(text.slice(offset, range.start));
127
- html += styledHtml(text.slice(range.start, range.end), range);
253
+ html += escapeText(text.slice(offset, range.start), format);
254
+ html += styledText(text.slice(range.start, range.end), range, format);
128
255
  offset = range.end;
129
256
  }
130
- return html + escapeHtml(text.slice(offset));
257
+ return html + escapeText(text.slice(offset), format);
131
258
  }
132
259
  function mergeRanges(ranges, text) {
133
260
  const merged = [];
@@ -141,13 +268,19 @@ function mergeRanges(ranges, text) {
141
268
  }
142
269
  return merged;
143
270
  }
144
- function styledHtml(value, range) {
145
- let html = escapeHtml(value);
271
+ function styledText(value, range, format) {
272
+ let html = escapeText(value, format);
146
273
  if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
147
- if (range.italic) html = `<em>${html}</em>`;
148
- if (range.bold) html = `<strong>${html}</strong>`;
274
+ if (range.italic) html = format === "html" ? `<em>${html}</em>` : `_${html}_`;
275
+ if (range.bold) html = format === "html" ? `<strong>${html}</strong>` : `**${html}**`;
149
276
  return html;
150
277
  }
278
+ function escapeText(value, format) {
279
+ return format === "html" ? escapeHtml(value) : escapeMarkdown(value);
280
+ }
281
+ function escapeMarkdown(value) {
282
+ return value.replace(/([\\`*_[\]<>])/g, "\\$1");
283
+ }
151
284
  function normalizedColor(value) {
152
285
  if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
153
286
  const color = value.toLowerCase();
@@ -283,22 +416,39 @@ function base64(bytes) {
283
416
  }
284
417
 
285
418
  // src/semantic-media.ts
286
- function semanticMedia(page) {
287
- const output = (page.images ?? []).map((image) => rasterMedia(image));
288
- output.push(...vectorMedia(page));
289
- return output.sort((left, right) => right.bounds.y - left.bounds.y);
419
+ function semanticMedia(page, imageOptions = "embedded") {
420
+ if (imageOptions === "excluded") return [];
421
+ const output = (page.images ?? []).map(
422
+ (image, index) => rasterMedia(image, page.number, index, imageOptions)
423
+ );
424
+ output.push(...vectorMedia(page, imageOptions));
425
+ return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
426
+ }
427
+ async function prepareSemanticMedia(page, imageOptions, onImage) {
428
+ const media = semanticMedia(page, imageOptions);
429
+ for (const item of media) {
430
+ for (const asset of item.assets ?? []) await onImage?.(asset);
431
+ delete item.assets;
432
+ }
433
+ return media;
290
434
  }
291
- function rasterMedia(image) {
435
+ function rasterMedia(image, pageNumber, index, imageOptions) {
292
436
  const bounds2 = transformedUnitBounds(image.transform);
293
437
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
294
438
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
439
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
440
+ const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
441
+ const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
295
442
  const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
296
443
  return {
297
444
  bounds: bounds2,
298
- 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}">`
445
+ kind: "raster",
446
+ html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
447
+ markdown: `![](${source})`,
448
+ ...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
299
449
  };
300
450
  }
301
- function vectorMedia(page) {
451
+ function vectorMedia(page, imageOptions) {
302
452
  const primitives = [
303
453
  ...(page.paths ?? []).flatMap((path, index) => {
304
454
  const bounds2 = vectorPathBounds(path);
@@ -316,7 +466,7 @@ function vectorMedia(page) {
316
466
  const visualCodeFonts = new Set(
317
467
  (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
318
468
  );
319
- return components.map((component) => {
469
+ return components.map((component, componentIndex) => {
320
470
  const bounds2 = component.bounds;
321
471
  const paths = component.primitives.flatMap(
322
472
  (primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
@@ -333,13 +483,82 @@ function vectorMedia(page) {
333
483
  );
334
484
  const fontIds = new Set(overlay.map((span) => span.fontAssetId));
335
485
  const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
486
+ const svg = `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number2(bounds2.x)} ${number2(page.height - bounds2.y - bounds2.height)} ${number2(bounds2.width)} ${number2(bounds2.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}${paths.length ? `<defs>${vectorPathClipDefinitions(paths, page.number)}</defs>` : ""}<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">${fills.map(vectorFillSvg).join("") + paths.map(({ path, index }) => vectorPathSvg(path, page.number, index)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`;
487
+ const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
336
488
  return {
337
489
  bounds: bounds2,
338
- 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>`,
490
+ kind: "vector",
491
+ html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
492
+ markdown: imageOptions === "references" ? `![](${name})` : svg,
493
+ ...imageOptions === "references" ? {
494
+ assets: [
495
+ { name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
496
+ ]
497
+ } : {},
339
498
  ...consumedSpans.length > 0 ? { consumedSpans } : {}
340
499
  };
341
500
  });
342
501
  }
502
+ function mediaComponents(media, page) {
503
+ const components = [];
504
+ for (const item of media) {
505
+ if (isPageBackdrop(item.bounds, page)) {
506
+ components.push([item]);
507
+ continue;
508
+ }
509
+ const matches = components.filter(
510
+ (component) => !component.some((member) => isPageBackdrop(member.bounds, page)) && component.some((member) => mediaPiecesTouch(member.bounds, item.bounds))
511
+ );
512
+ if (matches.length === 0) {
513
+ components.push([item]);
514
+ continue;
515
+ }
516
+ const target = matches[0];
517
+ target.push(item);
518
+ for (const component of matches.slice(1)) {
519
+ target.push(...component);
520
+ components.splice(components.indexOf(component), 1);
521
+ }
522
+ }
523
+ return components.map((component) => compositeMedia(component));
524
+ }
525
+ function compositeMedia(items) {
526
+ if (items.length === 1) return items[0];
527
+ const bounds2 = unionBounds(items.map((item) => item.bounds));
528
+ const layers = items.map((item) => {
529
+ const left = (item.bounds.x - bounds2.x) / bounds2.width * 100;
530
+ const top = (bounds2.y + bounds2.height - item.bounds.y - item.bounds.height) / bounds2.height * 100;
531
+ const width = item.bounds.width / bounds2.width * 100;
532
+ const height = item.bounds.height / bounds2.height * 100;
533
+ return `<div style="position:absolute;left:${number2(left)}%;top:${number2(top)}%;width:${number2(width)}%;height:${number2(height)}%;overflow:hidden">${item.html}</div>`;
534
+ }).join("");
535
+ return {
536
+ bounds: bounds2,
537
+ kind: "composite",
538
+ html: `<div class="pdf-semantic-media pdf-semantic-media-composite" style="position:relative;max-width:100%;width:${number2(bounds2.width)}px;aspect-ratio:${number2(bounds2.width)}/${number2(bounds2.height)}">${layers}</div>`,
539
+ markdown: items.map((item) => item.markdown).join("\n\n"),
540
+ consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
541
+ assets: items.flatMap((item) => item.assets ?? [])
542
+ };
543
+ }
544
+ function mediaPiecesTouch(left, right) {
545
+ const xOverlap = overlap2(left.x, left.width, right.x, right.width);
546
+ const yOverlap = overlap2(left.y, left.height, right.y, right.height);
547
+ if (xOverlap > 0 && yOverlap > 0) return true;
548
+ const horizontalGap = axisGap(left.x, left.width, right.x, right.width);
549
+ const verticalGap = axisGap(left.y, left.height, right.y, right.height);
550
+ if (horizontalGap <= 2 && yOverlap / Math.min(left.height, right.height) >= 0.65) return true;
551
+ return verticalGap <= 2 && xOverlap / Math.min(left.width, right.width) >= 0.65;
552
+ }
553
+ function isPageBackdrop(bounds2, page) {
554
+ return bounds2.width * bounds2.height >= page.width * page.height * 0.7;
555
+ }
556
+ function overlap2(left, leftSize, right, rightSize) {
557
+ return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
558
+ }
559
+ function axisGap(left, leftSize, right, rightSize) {
560
+ return Math.max(0, right - left - leftSize, left - right - rightSize);
561
+ }
343
562
  function vectorComponents(primitives, padding) {
344
563
  const components = [];
345
564
  for (const primitive of primitives) {
@@ -458,7 +677,7 @@ function escapeHtml2(value) {
458
677
  }
459
678
 
460
679
  // src/semantic-document.ts
461
- async function writeSemanticDocument(pages, write, lookaheadPages) {
680
+ async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
462
681
  const stats = {
463
682
  pagesProcessed: 0,
464
683
  peakBufferedPages: 0,
@@ -476,27 +695,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
476
695
  let contentStarted = false;
477
696
  let employmentOpen = false;
478
697
  let pendingParagraph;
479
- await write('<article class="pdf-semantic-document">');
698
+ const markdown = format === "markdown";
699
+ const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
700
+ const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
701
+ await output('<article class="pdf-semantic-document">');
480
702
  const closeTable = async () => {
481
703
  if (!activeTable) return;
482
- await write("</table>");
704
+ await output("</table>", "\n");
483
705
  activeTable = void 0;
484
706
  while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
485
707
  };
486
708
  const closeSections = async (minimumLevel = 0) => {
487
709
  while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
488
- await write("</section>");
710
+ await output("</section>");
489
711
  sectionLevels.pop();
490
712
  }
491
713
  };
492
714
  const flushPendingParagraph = async () => {
493
715
  if (!pendingParagraph) return;
494
- await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
716
+ await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
495
717
  pendingParagraph = void 0;
496
718
  };
497
719
  const closeEmployment = async () => {
498
720
  if (!employmentOpen) return;
499
- await write("</section>");
721
+ await output("</section>");
500
722
  employmentOpen = false;
501
723
  };
502
724
  const emitPage = async (page, future) => {
@@ -510,29 +732,53 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
510
732
  page.structured.lines
511
733
  );
512
734
  const captionedMedia = new Set(captions.values());
735
+ const emittedMedia = /* @__PURE__ */ new Set();
513
736
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
514
737
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
515
738
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
516
739
  const nextBlock = page.structured.blocks[blockIndex + 1];
517
740
  const blockY = semanticBlockY(block);
518
741
  let emittedAsCaption = false;
742
+ while (page.media[mediaIndex] && emittedMedia.has(page.media[mediaIndex])) {
743
+ mediaIndex += 1;
744
+ }
519
745
  while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
520
746
  await flushPendingParagraph();
521
747
  const item = page.media[mediaIndex];
522
748
  if (item && captions.get(block) === item && block.type === "paragraph") {
523
- const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
749
+ const html2 = markdown ? `${item.markdown}
750
+
751
+ *${inlineText(block.text, block.lines, defaultColor)}*
752
+
753
+ ` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
524
754
  if (activeTable) pendingMedia.push(html2);
525
755
  else await write(html2);
756
+ emittedMedia.add(item);
526
757
  mediaIndex += 1;
527
758
  emittedAsCaption = true;
528
759
  break;
529
760
  }
530
761
  if (item && captionedMedia.has(item)) break;
531
- const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
762
+ const html = markdown ? `${item?.markdown ?? ""}
763
+
764
+ ` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
532
765
  if (activeTable) pendingMedia.push(html);
533
766
  else await write(html);
534
767
  mediaIndex += 1;
535
768
  }
769
+ const associatedMedia = captions.get(block);
770
+ if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
771
+ await flushPendingParagraph();
772
+ const html = markdown ? `${associatedMedia.markdown}
773
+
774
+ *${inlineText(block.text, block.lines, defaultColor)}*
775
+
776
+ ` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
777
+ if (activeTable) pendingMedia.push(html);
778
+ else await write(html);
779
+ emittedMedia.add(associatedMedia);
780
+ emittedAsCaption = true;
781
+ }
536
782
  if (emittedAsCaption) continue;
537
783
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
538
784
  stats.suppressedFurniture += 1;
@@ -541,8 +787,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
541
787
  await flushPendingParagraph();
542
788
  if (employmentOpen && block.type !== "list") await closeEmployment();
543
789
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
544
- await write(
545
- `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
790
+ await output(
791
+ `<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
792
+ `# ${inlineText(block.text, block.lines, defaultColor, false)}
793
+
794
+ `
546
795
  );
547
796
  headerOpen = true;
548
797
  continue;
@@ -550,19 +799,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
550
799
  if (headerOpen) {
551
800
  if (block.type === "paragraph") {
552
801
  const tag = isContactBlock(block) ? "address" : "p";
553
- await write(
554
- `<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
802
+ await output(
803
+ `<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
804
+ `${inlineText(block.text, block.lines, defaultColor)}
805
+
806
+ `
555
807
  );
556
808
  headerHasParagraph = true;
557
809
  continue;
558
810
  }
559
811
  if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
560
- await write(
561
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
812
+ await output(
813
+ `<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
814
+ `${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
815
+
816
+ `
562
817
  );
563
818
  continue;
564
819
  }
565
- await write("</header>");
820
+ await output("</header>");
566
821
  headerOpen = false;
567
822
  contentStarted = true;
568
823
  }
@@ -570,23 +825,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
570
825
  const rows = (0, import_structure.tableToRows)(block.table);
571
826
  if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
572
827
  const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
573
- for (const row of continuationRows) await write(tableRow(row, false));
828
+ for (const row of continuationRows)
829
+ await write(markdown ? markdownTableRow(row) : tableRow(row, false));
574
830
  activeTable.table = block.table;
575
831
  stats.mergedTables += 1;
576
832
  continue;
577
833
  }
578
834
  await closeTable();
579
835
  const header = tableHeader(rows);
580
- await write("<table>");
581
- for (const [index, row] of rows.entries())
582
- await write(tableRow(row, Boolean(header && index === 0)));
836
+ await output("<table>", markdownTableStart(rows, header));
837
+ const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
838
+ for (const [index, row] of markdownRows.entries())
839
+ await write(
840
+ markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
841
+ );
583
842
  activeTable = { table: block.table, header };
584
843
  continue;
585
844
  }
586
845
  if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
587
846
  const columns = activeTable.table.columns.length;
588
- await write(
589
- `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`
847
+ await output(
848
+ `<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
849
+ block.entries.map(
850
+ (entry) => markdownTableRow([
851
+ entry.term,
852
+ ...Array(Math.max(0, columns - 2)).fill(""),
853
+ entry.description
854
+ ])
855
+ ).join("")
590
856
  );
591
857
  await closeTable();
592
858
  continue;
@@ -595,8 +861,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
595
861
  if (block.type === "heading") {
596
862
  const level = contentStarted && block.level === 1 ? 2 : block.level;
597
863
  await closeSections(level);
598
- await write(
599
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
864
+ await output(
865
+ `<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
866
+ `${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
867
+
868
+ `
600
869
  );
601
870
  sectionLevels.push(level);
602
871
  continue;
@@ -604,13 +873,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
604
873
  if (block.type === "paragraph") {
605
874
  if (isTitledRecord(block)) {
606
875
  const [institution, ...details] = block.lines;
607
- if (institution) await write(`<h3>${escapeHtml3(institution.text)}</h3>`);
608
- for (const detail of details) await write(`<p>${escapeHtml3(detail.text)}</p>`);
876
+ if (institution)
877
+ await output(
878
+ `<h3>${escapeHtml3(institution.text)}</h3>`,
879
+ `### ${escapeMarkdown2(institution.text)}
880
+
881
+ `
882
+ );
883
+ for (const detail of details)
884
+ await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
885
+
886
+ `);
609
887
  continue;
610
888
  }
611
889
  if (isUnmarkedList(block)) {
612
- await write(
613
- `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`
890
+ await output(
891
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
892
+ `${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
893
+
894
+ `
614
895
  );
615
896
  continue;
616
897
  }
@@ -618,27 +899,40 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
618
899
  continue;
619
900
  }
620
901
  if (block.type === "employment") {
621
- await write(
622
- `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`
902
+ await output(
903
+ `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
904
+ `### ${escapeMarkdown2(block.role)}
905
+
906
+ ${escapeMarkdown2(block.organization)}
907
+
908
+ ${escapeMarkdown2(block.date)}
909
+
910
+ `
623
911
  );
624
912
  employmentOpen = true;
625
913
  continue;
626
914
  }
627
- await write(semanticBlockHtml(block, defaultColor));
915
+ await write(semanticBlockOutput(block, defaultColor, format));
628
916
  }
629
917
  while (mediaIndex < page.media.length) {
630
- await flushPendingParagraph();
631
- const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
632
- if (activeTable) pendingMedia.push(html);
633
- else await write(html);
918
+ const item = page.media[mediaIndex];
919
+ if (item && !emittedMedia.has(item)) {
920
+ await flushPendingParagraph();
921
+ const html = markdown ? `${item.markdown}
922
+
923
+ ` : `<div class="pdf-semantic-visual">${item.html}</div>`;
924
+ if (activeTable) pendingMedia.push(html);
925
+ else await write(html);
926
+ }
634
927
  mediaIndex += 1;
635
928
  }
636
929
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
637
930
  };
638
931
  for await (const page of pages) {
639
- const media = semanticMedia(page);
932
+ const media = await prepareSemanticMedia(page, imageOptions, onImage);
640
933
  const structured = (0, import_structure.structurePage)(withoutSemanticMediaSpans(page, media));
641
934
  buffer.push({ width: page.width, height: page.height, structured, media });
935
+ restoreObservedHyphens(buffer);
642
936
  stats.pagesProcessed += 1;
643
937
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
644
938
  stats.peakBufferedLines = Math.max(
@@ -654,22 +948,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
654
948
  const ready = buffer.shift();
655
949
  if (ready) await emitPage(ready, buffer);
656
950
  }
657
- if (headerOpen) await write("</header>");
951
+ if (headerOpen) await output("</header>");
658
952
  await closeTable();
659
953
  await closeEmployment();
660
954
  if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
661
955
  await closeSections();
662
- await write(
663
- `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
956
+ await output(
957
+ `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
958
+ `---
959
+
960
+ ${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
664
961
  );
665
962
  pendingParagraph = void 0;
666
963
  } else {
667
964
  await flushPendingParagraph();
668
965
  await closeSections();
669
966
  }
670
- await write("</article>");
967
+ await output("</article>");
671
968
  return stats;
672
969
  }
970
+ function restoreObservedHyphens(buffer) {
971
+ const terms = new Set(
972
+ buffer.flatMap(
973
+ (page) => page.structured.lines.flatMap(
974
+ (line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
975
+ )
976
+ )
977
+ );
978
+ for (const page of buffer) {
979
+ for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
980
+ }
981
+ }
982
+ function restoreBlockHyphens(block, terms) {
983
+ const restore = (value) => restoreTextHyphens(value, terms);
984
+ if (block.type === "insetGroup") {
985
+ for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
986
+ } else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
987
+ block.text = restore(block.text);
988
+ } else if (block.type === "list") {
989
+ for (const item of block.items) item.text = restore(item.text);
990
+ } else if (block.type === "definitionList") {
991
+ for (const entry of block.entries) {
992
+ entry.term = restore(entry.term);
993
+ entry.description = restore(entry.description);
994
+ }
995
+ } else if (block.type === "cardList") {
996
+ for (const item of block.items) {
997
+ item.title = restore(item.title);
998
+ item.details = item.details.map(restore);
999
+ }
1000
+ } else if (block.type === "sectionGroup") {
1001
+ for (const item of block.items) {
1002
+ item.label = restore(item.label);
1003
+ item.content = item.content.map(restore);
1004
+ }
1005
+ } else if (block.type === "employment") {
1006
+ block.role = restore(block.role);
1007
+ block.organization = restore(block.organization);
1008
+ block.date = restore(block.date);
1009
+ }
1010
+ }
1011
+ function restoreTextHyphens(value, terms) {
1012
+ let output = value;
1013
+ for (const term of terms) {
1014
+ const collapsed = term.replace(/[-‐‑]/gu, "");
1015
+ if (collapsed === term || !output.includes(collapsed)) continue;
1016
+ const pattern = new RegExp(
1017
+ `(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
1018
+ "gu"
1019
+ );
1020
+ output = output.replace(pattern, term);
1021
+ }
1022
+ return output;
1023
+ }
1024
+ function escapeRegularExpression(value) {
1025
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1026
+ }
673
1027
  function isContactBlock(block) {
674
1028
  const text = block.text;
675
1029
  const signals = [
@@ -753,6 +1107,20 @@ function tableRow(row, header) {
753
1107
  const cell = header ? "th" : "td";
754
1108
  return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
755
1109
  }
1110
+ function markdownTableStart(rows, header) {
1111
+ const columns = rows[0]?.length ?? 0;
1112
+ if (columns === 0) return "";
1113
+ const heading = header ?? Array(columns).fill("");
1114
+ return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
1115
+ }
1116
+ function markdownTableRow(row, shouldEscape = true) {
1117
+ const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
1118
+ return `| ${cells.join(" | ")} |
1119
+ `;
1120
+ }
1121
+ function escapeMarkdownTableCell(value) {
1122
+ return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
1123
+ }
756
1124
  function isFinancialSummary(block) {
757
1125
  return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
758
1126
  }
@@ -800,6 +1168,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
800
1168
  const tag = block.ordered ? "ol" : "ul";
801
1169
  return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
802
1170
  }
1171
+ function semanticBlockOutput(block, defaultColor, format) {
1172
+ return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
1173
+ }
1174
+ function semanticBlockMarkdown(block, defaultColor = "#000000") {
1175
+ if (block.type === "insetGroup") {
1176
+ const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
1177
+ return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
1178
+
1179
+ `;
1180
+ }
1181
+ if (block.type === "table") {
1182
+ const rows = (0, import_structure.tableToRows)(block.table);
1183
+ const header = tableHeader(rows);
1184
+ return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
1185
+ `;
1186
+ }
1187
+ if (block.type === "heading") {
1188
+ return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
1189
+
1190
+ `;
1191
+ }
1192
+ if (block.type === "paragraph") {
1193
+ return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
1194
+
1195
+ `;
1196
+ }
1197
+ if (block.type === "preformatted") {
1198
+ const fence = block.text.includes("```") ? "````" : "```";
1199
+ return `${fence}
1200
+ ${block.text}
1201
+ ${fence}
1202
+
1203
+ `;
1204
+ }
1205
+ if (block.type === "definitionList") {
1206
+ return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
1207
+
1208
+ `;
1209
+ }
1210
+ if (block.type === "cardList") {
1211
+ const rows = [
1212
+ ["Item", "Quantity", "Amount"],
1213
+ ...block.items.map((item) => {
1214
+ const trailing = item.details.at(-1) ?? "";
1215
+ const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
1216
+ const detail = item.details.slice(0, -1).join(" ");
1217
+ return [
1218
+ `${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
1219
+ match?.[1] ?? "",
1220
+ match?.[2] ?? trailing
1221
+ ];
1222
+ })
1223
+ ];
1224
+ return `## Items ordered
1225
+
1226
+ ${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
1227
+ `;
1228
+ }
1229
+ if (block.type === "sectionGroup") {
1230
+ return block.items.map(
1231
+ (item) => `## ${escapeMarkdown2(titleCase(item.label))}
1232
+
1233
+ ${item.content.map(
1234
+ (content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
1235
+ ).join("\n\n")}
1236
+
1237
+ `
1238
+ ).join("");
1239
+ }
1240
+ if (block.type === "employment") {
1241
+ return `### ${escapeMarkdown2(block.role)}
1242
+
1243
+ ${escapeMarkdown2(block.organization)}
1244
+
1245
+ ${escapeMarkdown2(block.date)}
1246
+
1247
+ `;
1248
+ }
1249
+ return `${block.items.map(
1250
+ (item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
1251
+ ).join("\n")}
1252
+
1253
+ `;
1254
+ }
803
1255
  function semanticBlockY(block) {
804
1256
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
805
1257
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -832,6 +1284,9 @@ function titleCase(value) {
832
1284
  function escapeHtml3(value) {
833
1285
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
834
1286
  }
1287
+ function escapeMarkdown2(value) {
1288
+ return value.replace(/([\\`*_[\]<>])/g, "\\$1");
1289
+ }
835
1290
 
836
1291
  // src/index.ts
837
1292
  var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-semantic-document h1,.pdf-page--semantic h1{font-size:1.7em}.pdf-semantic-document h2,.pdf-page--semantic h2{font-size:1.5em}.pdf-semantic-document h3,.pdf-page--semantic h3{font-size:1.35em}.pdf-semantic-document h4,.pdf-page--semantic h4{font-size:1.1em}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
@@ -848,9 +1303,18 @@ async function writeHtmlDocument(pages, write, options = {}) {
848
1303
  await write("</head><body>");
849
1304
  }
850
1305
  await write('<main class="pdf-document">');
851
- if (resolveProfile(options) === "semantic") {
1306
+ const profile = resolveProfile(options);
1307
+ const imageOptions = resolveImageOptions(profile, options);
1308
+ validateImageOptions(imageOptions, options);
1309
+ if (profile === "semantic") {
852
1310
  const lookahead = semanticLookahead(options.semanticLookaheadPages);
853
- const stats = await writeSemanticDocument(pages, write, lookahead);
1311
+ const stats = await writeSemanticDocument(
1312
+ pages,
1313
+ write,
1314
+ lookahead,
1315
+ imageOptions,
1316
+ options.onImage
1317
+ );
854
1318
  options.onSemanticStats?.(stats);
855
1319
  } else {
856
1320
  for await (const page of pages) await writePage(page, write, options);
@@ -858,6 +1322,20 @@ async function writeHtmlDocument(pages, write, options = {}) {
858
1322
  await write("</main>");
859
1323
  if (includeDocument) await write("</body></html>");
860
1324
  }
1325
+ async function writeMarkdownDocument(pages, write, options = {}) {
1326
+ const imageOptions = options.imageOptions ?? "excluded";
1327
+ validateImageOptions(imageOptions, options);
1328
+ const lookahead = semanticLookahead(options.semanticLookaheadPages);
1329
+ const stats = await writeSemanticDocument(
1330
+ pages,
1331
+ write,
1332
+ lookahead,
1333
+ imageOptions,
1334
+ options.onImage,
1335
+ "markdown"
1336
+ );
1337
+ options.onSemanticStats?.(stats);
1338
+ }
861
1339
  function semanticLookahead(value) {
862
1340
  const lookahead = value ?? 4;
863
1341
  if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
@@ -866,7 +1344,9 @@ function semanticLookahead(value) {
866
1344
  return lookahead;
867
1345
  }
868
1346
  async function writePage(page, write, options = {}) {
869
- if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
1347
+ const profile = resolveProfile(options);
1348
+ validateImageOptions(resolveImageOptions(profile, options), options);
1349
+ if (profile === "semantic") await writeFlowPage(page, write, options);
870
1350
  else await writePositionedPage(page, write, options);
871
1351
  }
872
1352
  async function pageToHtml(page, options = {}) {
@@ -881,7 +1361,9 @@ async function pageToHtml(page, options = {}) {
881
1361
  return output;
882
1362
  }
883
1363
  async function writePositionedPage(page, write, options) {
884
- const visualSpans = page.visualSpans ?? page.spans;
1364
+ const imageOptions = resolveImageOptions("visual", options);
1365
+ const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
1366
+ const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
885
1367
  const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
886
1368
  const quarterTurn = page.rotate === 90 || page.rotate === 270;
887
1369
  const displayWidth = quarterTurn ? page.height : page.width;
@@ -893,25 +1375,32 @@ async function writePositionedPage(page, write, options) {
893
1375
  const type3Fonts = new Map(
894
1376
  (page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
895
1377
  );
1378
+ const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
896
1379
  if ((options.includeStyles ?? true) && page.fonts?.length) {
897
1380
  await write(
898
1381
  `<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
899
1382
  );
900
1383
  }
1384
+ if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
901
1385
  await write(
902
1386
  `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
903
1387
  );
904
1388
  await write(
905
1389
  `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
906
1390
  );
907
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
1391
+ const clipDefinitions = imageClipDefinitions(
1392
+ imageOptions === "excluded" ? [] : page.images ?? [],
1393
+ page.number,
1394
+ page.height
1395
+ ) + vectorPathClipDefinitions(
908
1396
  (page.paths ?? []).map((path, index) => ({ path, index })),
909
1397
  page.number
910
1398
  );
911
1399
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
912
1400
  if (reflectedOverlay) {
913
1401
  for (const [index, image] of (page.images ?? []).entries()) {
914
- 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));
915
1404
  }
916
1405
  }
917
1406
  if (page.fills?.length || page.paths?.length) {
@@ -924,14 +1413,29 @@ async function writePositionedPage(page, write, options) {
924
1413
  }
925
1414
  if (!reflectedOverlay) {
926
1415
  for (const [index, image] of (page.images ?? []).entries()) {
927
- await write(visualImage(image, page.height, page.number, index));
1416
+ const source = visualImages[index];
1417
+ if (source) await write(visualImage(image, page.height, page.number, index, source));
928
1418
  }
929
1419
  }
930
- for (const span of visualSpans) {
1420
+ for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
1421
+ const span = visualSpans[spanIndex];
1422
+ if (!span) continue;
931
1423
  if (!usesPositionedSpan(span)) {
932
1424
  const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
1425
+ const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
1426
+ if (line) {
1427
+ await write(line.html);
1428
+ spanIndex = line.endIndex;
1429
+ continue;
1430
+ }
933
1431
  await write(
934
- type3 ? visualType3Text(span, type3, page.height) : visualText(span, page.height, fontAliases, reflectedOverlay && page.rotate === 180)
1432
+ type3 ? visualType3Text(span, type3, page.height) : visualText(
1433
+ span,
1434
+ page.height,
1435
+ fontAliases,
1436
+ reflectedOverlay && page.rotate === 180,
1437
+ textClasses?.names
1438
+ )
935
1439
  );
936
1440
  }
937
1441
  }
@@ -941,23 +1445,131 @@ async function writePositionedPage(page, write, options) {
941
1445
  }
942
1446
  await write("</div></section>");
943
1447
  }
1448
+ function coalesceVisualSpans(spans) {
1449
+ const output = [];
1450
+ for (const span of spans) {
1451
+ const previous = output.at(-1);
1452
+ if (!previous || !canCoalesceVisualSpans(previous, span)) {
1453
+ output.push(span);
1454
+ continue;
1455
+ }
1456
+ output[output.length - 1] = {
1457
+ ...previous,
1458
+ text: previous.text + span.text,
1459
+ bounds: {
1460
+ ...previous.bounds,
1461
+ width: span.bounds.x + span.bounds.width - previous.bounds.x,
1462
+ height: Math.max(previous.bounds.height, span.bounds.height)
1463
+ }
1464
+ };
1465
+ }
1466
+ return output;
1467
+ }
1468
+ function canCoalesceVisualSpans(left, right) {
1469
+ if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
1470
+ if (left.direction !== "ltr" || right.direction !== "ltr") return false;
1471
+ if (/guardian/i.test(left.fontFamily ?? "")) return false;
1472
+ if (left.glyphCodes || right.glyphCodes) return false;
1473
+ if (!sameVisualTextState(left, right)) return false;
1474
+ const tolerance = Math.max(0.02, left.fontSize * 0.015);
1475
+ if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
1476
+ const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
1477
+ return !right.hasLeadingSpace && gap >= -tolerance && gap <= tolerance;
1478
+ }
1479
+ function sameVisualTextState(left, right) {
1480
+ return Math.abs(left.fontSize - right.fontSize) <= 1e-3 && left.fontName === right.fontName && left.fontFamily === right.fontFamily && left.fontAssetId === right.fontAssetId && left.color === right.color && left.fillOpacity === right.fillOpacity && left.strokeColor === right.strokeColor && left.strokeWidth === right.strokeWidth && left.strokeOpacity === right.strokeOpacity && left.renderingMode === right.renderingMode && sameTransform(left.transform, right.transform);
1481
+ }
1482
+ function sameTransform(left, right) {
1483
+ if (!left || !right) return left === right;
1484
+ return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
1485
+ }
1486
+ function visualTextClasses(pageNumber, spans, fontAliases) {
1487
+ const names = /* @__PURE__ */ new Map();
1488
+ let css = "";
1489
+ for (const span of spans) {
1490
+ if (usesPositionedSpan(span) || span.glyphCodes) {
1491
+ continue;
1492
+ }
1493
+ const style = visualTextClassStyle(span, fontAliases);
1494
+ if (!style || names.has(style)) continue;
1495
+ const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
1496
+ names.set(style, name);
1497
+ css += `.${name}{${style}}`;
1498
+ }
1499
+ return { css, names };
1500
+ }
1501
+ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
1502
+ const first = spans[startIndex];
1503
+ if (!first || !canGroupVisualTextLine(first)) return void 0;
1504
+ const style = visualTextClassStyle(first, fontAliases);
1505
+ const className = styleClasses.get(style);
1506
+ if (!className) return void 0;
1507
+ let endIndex = startIndex;
1508
+ while (endIndex + 1 < spans.length) {
1509
+ const next = spans[endIndex + 1];
1510
+ if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
1511
+ break;
1512
+ }
1513
+ endIndex += 1;
1514
+ }
1515
+ if (endIndex === startIndex) return void 0;
1516
+ const baseline = pageHeight - first.bounds.y;
1517
+ const lineSpans = spans.slice(startIndex, endIndex + 1);
1518
+ const content = lineSpans.map(
1519
+ (span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
1520
+ ).join("");
1521
+ return {
1522
+ html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
1523
+ endIndex
1524
+ };
1525
+ }
1526
+ function visualTextTspan(span, dx) {
1527
+ const extent = span.bounds.width;
1528
+ const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
1529
+ const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1530
+ return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
1531
+ }
1532
+ function textSpanGap(previous, current) {
1533
+ if (!previous) return 0;
1534
+ const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
1535
+ const adjustment = current.textAdjustmentBefore;
1536
+ return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
1537
+ }
1538
+ function canGroupVisualTextLine(span) {
1539
+ return !usesPositionedSpan(span) && !span.glyphCodes && span.direction === "ltr" && !isHebrewPaintOrder(span) && !hasNonIdentityTransform(span.transform) && span.renderingMode !== 3 && span.renderingMode !== 7 && (span.fontAssetId !== void 0 || !isAdobeCjkFont(span.fontFamily));
1540
+ }
944
1541
  function usesReflectedVisualOverlay(page, spans) {
945
1542
  return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
946
1543
  (span) => span.transform !== void 0 && Math.abs(span.transform[0] + 1) < 1e-6 && Math.abs(span.transform[1]) < 1e-6 && Math.abs(span.transform[2]) < 1e-6 && Math.abs(span.transform[3] - 1) < 1e-6
947
1544
  );
948
1545
  }
949
- function visualImage(image, pageHeight, pageNumber, imageIndex) {
1546
+ function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
950
1547
  const [a, b, c, d, e, f] = image.transform;
951
1548
  const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
952
1549
  const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
953
- const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
954
- const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
955
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
1550
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
956
1551
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
957
1552
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
958
1553
  }
959
1554
  return output;
960
1555
  }
1556
+ async function prepareVisualImages(page, imageOptions, onImage) {
1557
+ if (imageOptions === "excluded") return [];
1558
+ const sources = [];
1559
+ for (const [index, image] of (page.images ?? []).entries()) {
1560
+ const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
1561
+ const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
1562
+ if (imageOptions === "embedded") {
1563
+ sources.push(`data:${mimeType};base64,${base64(data)}`);
1564
+ continue;
1565
+ }
1566
+ const extension = image.format === "jpeg" ? "jpg" : "bmp";
1567
+ const name = `page-${page.number}-image-${index + 1}.${extension}`;
1568
+ await onImage?.({ name, mimeType, data });
1569
+ sources.push(name);
1570
+ }
1571
+ return sources;
1572
+ }
961
1573
  function imageClipDefinitions(images, pageNumber, pageHeight) {
962
1574
  return images.flatMap(
963
1575
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
@@ -1024,8 +1636,9 @@ function positionedSpan(span, fontAliases) {
1024
1636
  ].join(";");
1025
1637
  return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
1026
1638
  }
1027
- async function writeFlowPage(page, write) {
1028
- const media = semanticMedia(page);
1639
+ async function writeFlowPage(page, write, options) {
1640
+ const imageOptions = resolveImageOptions("semantic", options);
1641
+ const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
1029
1642
  const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
1030
1643
  const defaultColor = dominantTextColor(structured.lines);
1031
1644
  let mediaIndex = 0;
@@ -1037,18 +1650,22 @@ async function writeFlowPage(page, write) {
1037
1650
  structured.lines
1038
1651
  );
1039
1652
  const captionedMedia = new Set(captions.values());
1653
+ const emittedMedia = /* @__PURE__ */ new Set();
1040
1654
  await write(
1041
1655
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
1042
1656
  );
1043
1657
  for (const block of structured.blocks) {
1044
1658
  const blockY = semanticBlockY2(block);
1045
1659
  let emittedAsCaption = false;
1660
+ while (media[mediaIndex] && emittedMedia.has(media[mediaIndex]))
1661
+ mediaIndex += 1;
1046
1662
  while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
1047
1663
  const item = media[mediaIndex];
1048
1664
  if (item && captions.get(block) === item && block.type === "paragraph") {
1049
1665
  await write(
1050
1666
  `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
1051
1667
  );
1668
+ emittedMedia.add(item);
1052
1669
  mediaIndex += 1;
1053
1670
  emittedAsCaption = true;
1054
1671
  break;
@@ -1057,6 +1674,14 @@ async function writeFlowPage(page, write) {
1057
1674
  await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
1058
1675
  mediaIndex += 1;
1059
1676
  }
1677
+ const associatedMedia = captions.get(block);
1678
+ if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
1679
+ await write(
1680
+ `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
1681
+ );
1682
+ emittedMedia.add(associatedMedia);
1683
+ emittedAsCaption = true;
1684
+ }
1060
1685
  if (emittedAsCaption) continue;
1061
1686
  if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
1062
1687
  else if (block.type === "heading") {
@@ -1111,7 +1736,10 @@ async function writeFlowPage(page, write) {
1111
1736
  }
1112
1737
  }
1113
1738
  while (mediaIndex < media.length) {
1114
- await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
1739
+ const item = media[mediaIndex];
1740
+ if (item && !emittedMedia.has(item)) {
1741
+ await write(`<div class="pdf-semantic-visual">${item.html}</div>`);
1742
+ }
1115
1743
  mediaIndex += 1;
1116
1744
  }
1117
1745
  await write("</section>");
@@ -1147,10 +1775,37 @@ function semanticBlockY2(block) {
1147
1775
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
1148
1776
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
1149
1777
  }
1150
- function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
1778
+ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
1151
1779
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
1152
1780
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
1153
1781
  const direction = directionAttribute([span]);
1782
+ const style = visualTextStyle(span, fontAliases);
1783
+ const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
1784
+ const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
1785
+ const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
1786
+ const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
1787
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1788
+ const transform = counterRotateReflectedText && span.transform ? [
1789
+ span.transform[0],
1790
+ span.transform[1],
1791
+ span.transform[2],
1792
+ -span.transform[3]
1793
+ ] : span.transform;
1794
+ const transformed = hasNonIdentityTransform(transform);
1795
+ const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
1796
+ const basisX = transform?.[0] ?? 1;
1797
+ const basisY = transform?.[1] ?? 0;
1798
+ const anchorX = span.bounds.x + basisX * rtlOffset;
1799
+ const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1800
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1801
+ return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
1802
+ }
1803
+ function visualTextClassStyle(span, fontAliases) {
1804
+ const style = visualTextStyle(span, fontAliases);
1805
+ const fontSize = `font-size:${number3(span.fontSize)}px`;
1806
+ return style ? `${style};${fontSize}` : fontSize;
1807
+ }
1808
+ function visualTextStyle(span, fontAliases) {
1154
1809
  const font = visualFontStyles(
1155
1810
  span.fontFamily,
1156
1811
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -1160,7 +1815,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1160
1815
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
1161
1816
  const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
1162
1817
  const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
1163
- const style = [
1818
+ return [
1164
1819
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
1165
1820
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
1166
1821
  strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
@@ -1170,22 +1825,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
1170
1825
  strokeOpacity,
1171
1826
  font
1172
1827
  ].filter(Boolean).join(";");
1173
- const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
1174
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1175
- const transform = counterRotateReflectedText && span.transform ? [
1176
- span.transform[0],
1177
- span.transform[1],
1178
- span.transform[2],
1179
- -span.transform[3]
1180
- ] : span.transform;
1181
- const transformed = hasNonIdentityTransform(transform);
1182
- const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
1183
- const basisX = transform?.[0] ?? 1;
1184
- const basisY = transform?.[1] ?? 0;
1185
- const anchorX = span.bounds.x + basisX * rtlOffset;
1186
- const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1187
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1188
- return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
1189
1828
  }
1190
1829
  function isAdobeCjkFont(fontFamily) {
1191
1830
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -1280,10 +1919,19 @@ function resolveProfile(options) {
1280
1919
  }
1281
1920
  return options.profile ?? legacyProfile;
1282
1921
  }
1922
+ function resolveImageOptions(profile, options) {
1923
+ return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
1924
+ }
1925
+ function validateImageOptions(imageOptions, options) {
1926
+ if (imageOptions === "references" && !options.onImage) {
1927
+ throw new Error('imageOptions "references" requires an onImage callback');
1928
+ }
1929
+ }
1283
1930
  // Annotate the CommonJS export names for ESM import in node:
1284
1931
  0 && (module.exports = {
1285
1932
  pageToHtml,
1286
1933
  writeHtmlDocument,
1934
+ writeMarkdownDocument,
1287
1935
  writePage
1288
1936
  });
1289
1937
  //# sourceMappingURL=index.cjs.map