@boxpdf/html-writer 0.1.12 → 0.1.13

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
@@ -58,6 +58,152 @@ function escapeHtml(value) {
58
58
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
59
59
  }
60
60
 
61
+ // src/semantic-media.ts
62
+ function semanticMedia(page) {
63
+ const output = (page.images ?? []).map((image) => rasterMedia(image));
64
+ const vector = vectorMedia(page);
65
+ if (vector) output.push(vector);
66
+ return output.sort((left, right) => right.bounds.y - left.bounds.y);
67
+ }
68
+ function rasterMedia(image) {
69
+ const bounds = transformedUnitBounds(image.transform);
70
+ const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
71
+ const data = image.format === "jpeg" ? image.data : rgbBmp(image);
72
+ const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
73
+ return {
74
+ bounds,
75
+ html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${number(bounds.width)}" height="${number(bounds.height)}" alt="" style="max-width:100%;height:auto${opacity}">`
76
+ };
77
+ }
78
+ function vectorMedia(page) {
79
+ const paths = (page.paths ?? []).filter((path) => safePath(path.d));
80
+ const fills = page.fills ?? [];
81
+ const bounds = unionBounds([
82
+ ...paths.map((path) => pathBounds(path.d)),
83
+ ...fills.map(fillBounds)
84
+ ]);
85
+ if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
86
+ const content = fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("");
87
+ return {
88
+ bounds,
89
+ html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number(bounds.x)} ${number(page.height - bounds.y - bounds.height)} ${number(bounds.width)} ${number(bounds.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true"><g transform="translate(0 ${number(page.height)}) scale(1 -1)">${content}</g></svg>`
90
+ };
91
+ }
92
+ function vectorFill(fill) {
93
+ const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
94
+ const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
95
+ return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
96
+ }
97
+ function vectorPath(path) {
98
+ const fill = cssColor(path.fill) ? path.fill : "none";
99
+ const stroke = cssColor(path.stroke) ? path.stroke : "none";
100
+ const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
101
+ const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
102
+ const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
103
+ const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
104
+ const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
105
+ const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
106
+ const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
107
+ return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
108
+ }
109
+ function transformedUnitBounds([a, b, c, d, e, f]) {
110
+ const points = [
111
+ [e, f],
112
+ [a + e, b + f],
113
+ [c + e, d + f],
114
+ [a + c + e, b + d + f]
115
+ ];
116
+ const xs = points.map(([x]) => x ?? 0);
117
+ const ys = points.map(([, y]) => y ?? 0);
118
+ const minX = Math.min(...xs);
119
+ const minY = Math.min(...ys);
120
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
121
+ }
122
+ function pathBounds(path) {
123
+ const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
124
+ (match) => Number(match[0])
125
+ );
126
+ if (values.length < 2) return void 0;
127
+ const xs = [];
128
+ const ys = [];
129
+ for (let index = 0; index + 1 < values.length; index += 2) {
130
+ xs.push(values[index] ?? 0);
131
+ ys.push(values[index + 1] ?? 0);
132
+ }
133
+ const minX = Math.min(...xs);
134
+ const minY = Math.min(...ys);
135
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
136
+ }
137
+ function fillBounds(fill) {
138
+ if (fill.points.length === 0) return void 0;
139
+ const xs = fill.points.map(([x]) => x);
140
+ const ys = fill.points.map(([, y]) => y);
141
+ const minX = Math.min(...xs);
142
+ const minY = Math.min(...ys);
143
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
144
+ }
145
+ function unionBounds(bounds) {
146
+ const values = bounds.filter((value) => Boolean(value));
147
+ if (values.length === 0) return void 0;
148
+ const x = Math.min(...values.map((value) => value.x));
149
+ const y = Math.min(...values.map((value) => value.y));
150
+ const right = Math.max(...values.map((value) => value.x + value.width));
151
+ const top = Math.max(...values.map((value) => value.y + value.height));
152
+ return { x, y, width: right - x, height: top - y };
153
+ }
154
+ function rgbBmp(image) {
155
+ const stride = Math.ceil(image.width * 3 / 4) * 4;
156
+ const output = new Uint8Array(54 + stride * image.height);
157
+ const view = new DataView(output.buffer);
158
+ output.set([66, 77]);
159
+ view.setUint32(2, output.length, true);
160
+ view.setUint32(10, 54, true);
161
+ view.setUint32(14, 40, true);
162
+ view.setInt32(18, image.width, true);
163
+ view.setInt32(22, -image.height, true);
164
+ view.setUint16(26, 1, true);
165
+ view.setUint16(28, 24, true);
166
+ for (let row = 0; row < image.height; row += 1) {
167
+ for (let column = 0; column < image.width; column += 1) {
168
+ const source = (row * image.width + column) * 3;
169
+ const target = 54 + row * stride + column * 3;
170
+ output[target] = image.data[source + 2] ?? 0;
171
+ output[target + 1] = image.data[source + 1] ?? 0;
172
+ output[target + 2] = image.data[source] ?? 0;
173
+ }
174
+ }
175
+ return output;
176
+ }
177
+ function base64(bytes) {
178
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
179
+ let output = "";
180
+ for (let index = 0; index < bytes.length; index += 3) {
181
+ const first = bytes[index] ?? 0;
182
+ const second = bytes[index + 1] ?? 0;
183
+ const third = bytes[index + 2] ?? 0;
184
+ output += alphabet[first >> 2];
185
+ output += alphabet[(first & 3) << 4 | second >> 4];
186
+ output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
187
+ output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
188
+ }
189
+ return output;
190
+ }
191
+ function safePath(value) {
192
+ return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
193
+ }
194
+ function cssColor(value) {
195
+ return /^#[\da-f]{6}$/i.test(value ?? "");
196
+ }
197
+ function finiteNonnegative(value) {
198
+ return Number.isFinite(value) && (value ?? -1) >= 0;
199
+ }
200
+ function unitInterval(value) {
201
+ return finiteNonnegative(value) && value <= 1;
202
+ }
203
+ function number(value) {
204
+ return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
205
+ }
206
+
61
207
  // src/semantic-document.ts
62
208
  async function writeSemanticDocument(pages, write, lookaheadPages) {
63
209
  const stats = {
@@ -71,6 +217,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
71
217
  const seenFurniture = /* @__PURE__ */ new Set();
72
218
  const sectionLevels = [];
73
219
  let activeTable;
220
+ const pendingMedia = [];
74
221
  let headerOpen = false;
75
222
  let headerHasParagraph = false;
76
223
  let contentStarted = false;
@@ -81,6 +228,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
81
228
  if (!activeTable) return;
82
229
  await write("</table>");
83
230
  activeTable = void 0;
231
+ while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
84
232
  };
85
233
  const closeSections = async (minimumLevel = 0) => {
86
234
  while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
@@ -100,10 +248,19 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
100
248
  };
101
249
  const emitPage = async (page, future) => {
102
250
  const defaultColor = dominantTextColor(page.structured.lines);
251
+ let mediaIndex = 0;
103
252
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
104
253
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
105
254
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
106
255
  const nextBlock = page.structured.blocks[blockIndex + 1];
256
+ const blockY = semanticBlockY(block);
257
+ while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
258
+ await flushPendingParagraph();
259
+ const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
260
+ if (activeTable) pendingMedia.push(html);
261
+ else await write(html);
262
+ mediaIndex += 1;
263
+ }
107
264
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
108
265
  stats.suppressedFurniture += 1;
109
266
  continue;
@@ -194,11 +351,18 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
194
351
  }
195
352
  await write(semanticBlockHtml(block, defaultColor));
196
353
  }
354
+ while (mediaIndex < page.media.length) {
355
+ await flushPendingParagraph();
356
+ const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
357
+ if (activeTable) pendingMedia.push(html);
358
+ else await write(html);
359
+ mediaIndex += 1;
360
+ }
197
361
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
198
362
  };
199
363
  for await (const page of pages) {
200
364
  const structured = structurePage(page);
201
- buffer.push({ width: page.width, height: page.height, structured });
365
+ buffer.push({ width: page.width, height: page.height, structured, media: semanticMedia(page) });
202
366
  stats.pagesProcessed += 1;
203
367
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
204
368
  stats.peakBufferedLines = Math.max(
@@ -352,6 +516,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
352
516
  const tag = block.ordered ? "ol" : "ul";
353
517
  return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
354
518
  }
519
+ function semanticBlockY(block) {
520
+ const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
521
+ return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
522
+ }
355
523
  function cardTableRow(item) {
356
524
  const trailing = item.details.at(-1) ?? "";
357
525
  const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
@@ -435,7 +603,7 @@ async function writePositionedPage(page, write, options) {
435
603
  const displayWidth = quarterTurn ? page.height : page.width;
436
604
  const displayHeight = quarterTurn ? page.width : page.height;
437
605
  await write(
438
- `<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number(displayWidth)}pt;height:${number(displayHeight)}pt">`
606
+ `<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number2(displayWidth)}pt;height:${number2(displayHeight)}pt">`
439
607
  );
440
608
  const fontAliases = new Map(
441
609
  (page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
@@ -447,10 +615,10 @@ async function writePositionedPage(page, write, options) {
447
615
  await write(`<style>${page.fonts.map((font) => fontFace(font, fontAliases)).join("")}</style>`);
448
616
  }
449
617
  await write(
450
- `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number(page.width)}pt;height:${number(page.height)}pt${rotationTransform(page)}">`
618
+ `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
451
619
  );
452
620
  await write(
453
- `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number(page.width)}pt" height="${number(page.height)}pt" viewBox="0 0 ${number(page.width)} ${number(page.height)}">`
621
+ `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number2(page.width)}pt" height="${number2(page.height)}pt" viewBox="0 0 ${number2(page.width)} ${number2(page.height)}">`
454
622
  );
455
623
  const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
456
624
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
@@ -460,24 +628,24 @@ async function writePositionedPage(page, write, options) {
460
628
  }
461
629
  }
462
630
  for (const fill of page.fills ?? []) {
463
- const points = fill.points.map(([x, y]) => `${number(x)},${number(page.height - y)}`).join(" ");
631
+ const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
464
632
  if (isCssHexColor(fill.color)) {
465
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
633
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
466
634
  await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
467
635
  }
468
636
  }
469
637
  if (page.paths?.length) {
470
- await write(`<g transform="translate(0 ${number(page.height)}) scale(1 -1)">`);
638
+ await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
471
639
  for (const [pathIndex, path] of page.paths.entries()) {
472
640
  if (!isSvgPath(path.d)) continue;
473
641
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
474
642
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
475
- const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number(path.strokeWidth)}"` : "";
643
+ const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
476
644
  const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
477
- const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
478
- const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
479
- const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
480
- const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
645
+ const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
646
+ const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
647
+ const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
648
+ const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
481
649
  const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
482
650
  const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
483
651
  let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
@@ -514,11 +682,11 @@ function usesReflectedVisualOverlay(page, spans) {
514
682
  }
515
683
  function visualImage(image, pageHeight, pageNumber, imageIndex) {
516
684
  const [a, b, c, d, e, f] = image.transform;
517
- const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number).join(" ");
518
- const opacity = isUnitInterval(image.opacity) ? ` opacity="${number(image.opacity)}"` : "";
685
+ const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
686
+ const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
519
687
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
520
- const data = image.format === "jpeg" ? image.data : rgbBmp(image);
521
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
688
+ const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
689
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base642(data)}"${opacity}/>`;
522
690
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
523
691
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
524
692
  }
@@ -529,7 +697,7 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
529
697
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
530
698
  if (!isSvgPath(clip.d)) return "";
531
699
  const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
532
- return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
700
+ return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number2(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
533
701
  })
534
702
  ).join("");
535
703
  }
@@ -548,7 +716,7 @@ function pathClipDefinitions(paths, pageNumber) {
548
716
  function pathClipId(pageNumber, pathIndex, clipIndex) {
549
717
  return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
550
718
  }
551
- function rgbBmp(image) {
719
+ function rgbBmp2(image) {
552
720
  const stride = Math.ceil(image.width * 3 / 4) * 4;
553
721
  const output = new Uint8Array(54 + stride * image.height);
554
722
  const view = new DataView(output.buffer);
@@ -576,11 +744,11 @@ function rgbBmp(image) {
576
744
  function rotationTransform(page) {
577
745
  switch (page.rotate) {
578
746
  case 90:
579
- return `;transform:translate(${number(page.height)}pt,0) rotate(90deg)`;
747
+ return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
580
748
  case 180:
581
- return `;transform:translate(${number(page.width)}pt,${number(page.height)}pt) rotate(180deg)`;
749
+ return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
582
750
  case 270:
583
- return `;transform:translate(0,${number(page.width)}pt) rotate(270deg)`;
751
+ return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
584
752
  default:
585
753
  return "";
586
754
  }
@@ -588,13 +756,13 @@ function rotationTransform(page) {
588
756
  function positionedSpan(span, fontAliases) {
589
757
  const direction = directionAttribute([span]);
590
758
  const style = [
591
- `left:${number(span.bounds.x)}pt`,
592
- `bottom:${number(span.bounds.y)}pt`,
593
- `width:${number(span.bounds.width)}pt`,
594
- `height:${number(span.bounds.height)}pt`,
595
- `font-size:${number(span.fontSize)}pt`,
759
+ `left:${number2(span.bounds.x)}pt`,
760
+ `bottom:${number2(span.bounds.y)}pt`,
761
+ `width:${number2(span.bounds.width)}pt`,
762
+ `height:${number2(span.bounds.height)}pt`,
763
+ `font-size:${number2(span.fontSize)}pt`,
596
764
  ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
597
- ...isUnitInterval(span.fillOpacity) ? [`opacity:${number(span.fillOpacity)}`] : [],
765
+ ...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
598
766
  ...fontStyles(
599
767
  span.fontFamily,
600
768
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -605,10 +773,17 @@ function positionedSpan(span, fontAliases) {
605
773
  async function writeFlowPage(page, write) {
606
774
  const structured = structurePage2(page);
607
775
  const defaultColor = dominantTextColor(structured.lines);
776
+ const media = semanticMedia(page);
777
+ let mediaIndex = 0;
608
778
  await write(
609
779
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
610
780
  );
611
781
  for (const block of structured.blocks) {
782
+ const blockY = semanticBlockY2(block);
783
+ while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
784
+ await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
785
+ mediaIndex += 1;
786
+ }
612
787
  if (block.type === "table") await write(tableToHtml(block.table));
613
788
  else if (block.type === "heading") {
614
789
  await write(
@@ -655,8 +830,16 @@ async function writeFlowPage(page, write) {
655
830
  await write(`</${tag}>`);
656
831
  }
657
832
  }
833
+ while (mediaIndex < media.length) {
834
+ await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
835
+ mediaIndex += 1;
836
+ }
658
837
  await write("</section>");
659
838
  }
839
+ function semanticBlockY2(block) {
840
+ const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
841
+ return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
842
+ }
660
843
  function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
661
844
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
662
845
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
@@ -666,10 +849,10 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
666
849
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
667
850
  ).join(";");
668
851
  const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
669
- const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number(span.strokeWidth ?? 0)}` : "";
852
+ const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number2(span.strokeWidth ?? 0)}` : "";
670
853
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
671
- const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "";
672
- const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number(span.strokeOpacity)}` : "";
854
+ const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
855
+ const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number2(span.strokeOpacity)}` : "";
673
856
  const style = [
674
857
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
675
858
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
@@ -681,7 +864,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
681
864
  font
682
865
  ].filter(Boolean).join(";");
683
866
  const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
684
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
867
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number2(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
685
868
  const transform = counterRotateReflectedText && span.transform ? [
686
869
  span.transform[0],
687
870
  span.transform[1],
@@ -694,8 +877,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
694
877
  const basisY = transform?.[1] ?? 0;
695
878
  const anchorX = span.bounds.x + basisX * rtlOffset;
696
879
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
697
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
698
- return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
880
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
881
+ return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
699
882
  }
700
883
  function isAdobeCjkFont(fontFamily) {
701
884
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -707,16 +890,16 @@ function visualType3Text(span, font, pageHeight) {
707
890
  const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
708
891
  if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
709
892
  const transform = span.transform ?? [1, 0, 0, 1];
710
- const outer = `matrix(${transform.map(number).join(" ")} ${number(span.bounds.x)} ${number(pageHeight - span.bounds.y)})`;
893
+ const outer = `matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(pageHeight - span.bounds.y)})`;
711
894
  const xScale = span.bounds.width / totalAdvance;
712
895
  let offset = 0;
713
896
  let content = "";
714
897
  for (const glyph of sequence) {
715
898
  if (!glyph) continue;
716
- content += `<g transform="translate(${number(offset)} 0)">${type3Glyph(glyph)}</g>`;
899
+ content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph)}</g>`;
717
900
  offset += glyph.advance;
718
901
  }
719
- return `<g transform="${outer}"><g transform="scale(${number(xScale)} ${number(-span.fontSize)})">${content}</g></g>`;
902
+ return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
720
903
  }
721
904
  function isHebrewPaintOrder(span) {
722
905
  return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
@@ -728,15 +911,15 @@ function type3Glyph(glyph) {
728
911
  let output = "";
729
912
  for (const fill of glyph.fills ?? []) {
730
913
  if (!isCssHexColor(fill.color)) continue;
731
- const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
732
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
914
+ const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
915
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
733
916
  output += `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
734
917
  }
735
918
  for (const path of glyph.paths ?? []) {
736
919
  if (!isSvgPath(path.d)) continue;
737
920
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
738
921
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
739
- const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number(path.strokeWidth)}"` : "";
922
+ const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
740
923
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
741
924
  }
742
925
  return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
@@ -786,9 +969,9 @@ function fontFace(font, aliases) {
786
969
  const styles2 = fontStyles(font.family, alias).filter(
787
970
  (style) => !style.startsWith("font-family:")
788
971
  );
789
- return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
972
+ return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
790
973
  }
791
- function base64(bytes) {
974
+ function base642(bytes) {
792
975
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
793
976
  let output = "";
794
977
  for (let index = 0; index < bytes.length; index += 3) {
@@ -808,7 +991,7 @@ function directionAttribute(spans) {
808
991
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
809
992
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
810
993
  }
811
- function number(value) {
994
+ function number2(value) {
812
995
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
813
996
  }
814
997
  function escapeAttribute(value) {