@boxpdf/html-writer 0.1.12 → 0.1.14

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
@@ -81,6 +81,152 @@ function escapeHtml(value) {
81
81
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
82
82
  }
83
83
 
84
+ // src/semantic-media.ts
85
+ function semanticMedia(page) {
86
+ const output = (page.images ?? []).map((image) => rasterMedia(image));
87
+ const vector = vectorMedia(page);
88
+ if (vector) output.push(vector);
89
+ return output.sort((left, right) => right.bounds.y - left.bounds.y);
90
+ }
91
+ function rasterMedia(image) {
92
+ const bounds = transformedUnitBounds(image.transform);
93
+ const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
94
+ const data = image.format === "jpeg" ? image.data : rgbBmp(image);
95
+ const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
96
+ return {
97
+ bounds,
98
+ 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}">`
99
+ };
100
+ }
101
+ function vectorMedia(page) {
102
+ const paths = (page.paths ?? []).filter((path) => safePath(path.d));
103
+ const fills = page.fills ?? [];
104
+ const bounds = unionBounds([
105
+ ...paths.map((path) => pathBounds(path.d)),
106
+ ...fills.map(fillBounds)
107
+ ]);
108
+ if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
109
+ const content = fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("");
110
+ return {
111
+ bounds,
112
+ 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>`
113
+ };
114
+ }
115
+ function vectorFill(fill) {
116
+ const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
117
+ const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
118
+ return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
119
+ }
120
+ function vectorPath(path) {
121
+ const fill = cssColor(path.fill) ? path.fill : "none";
122
+ const stroke = cssColor(path.stroke) ? path.stroke : "none";
123
+ const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
124
+ const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
125
+ const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
126
+ const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
127
+ const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
128
+ const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
129
+ const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
130
+ return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
131
+ }
132
+ function transformedUnitBounds([a, b, c, d, e, f]) {
133
+ const points = [
134
+ [e, f],
135
+ [a + e, b + f],
136
+ [c + e, d + f],
137
+ [a + c + e, b + d + f]
138
+ ];
139
+ const xs = points.map(([x]) => x ?? 0);
140
+ const ys = points.map(([, y]) => y ?? 0);
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 pathBounds(path) {
146
+ const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
147
+ (match) => Number(match[0])
148
+ );
149
+ if (values.length < 2) return void 0;
150
+ const xs = [];
151
+ const ys = [];
152
+ for (let index = 0; index + 1 < values.length; index += 2) {
153
+ xs.push(values[index] ?? 0);
154
+ ys.push(values[index + 1] ?? 0);
155
+ }
156
+ const minX = Math.min(...xs);
157
+ const minY = Math.min(...ys);
158
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
159
+ }
160
+ function fillBounds(fill) {
161
+ if (fill.points.length === 0) return void 0;
162
+ const xs = fill.points.map(([x]) => x);
163
+ const ys = fill.points.map(([, y]) => y);
164
+ const minX = Math.min(...xs);
165
+ const minY = Math.min(...ys);
166
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
167
+ }
168
+ function unionBounds(bounds) {
169
+ const values = bounds.filter((value) => Boolean(value));
170
+ if (values.length === 0) return void 0;
171
+ const x = Math.min(...values.map((value) => value.x));
172
+ const y = Math.min(...values.map((value) => value.y));
173
+ const right = Math.max(...values.map((value) => value.x + value.width));
174
+ const top = Math.max(...values.map((value) => value.y + value.height));
175
+ return { x, y, width: right - x, height: top - y };
176
+ }
177
+ function rgbBmp(image) {
178
+ const stride = Math.ceil(image.width * 3 / 4) * 4;
179
+ const output = new Uint8Array(54 + stride * image.height);
180
+ const view = new DataView(output.buffer);
181
+ output.set([66, 77]);
182
+ view.setUint32(2, output.length, true);
183
+ view.setUint32(10, 54, true);
184
+ view.setUint32(14, 40, true);
185
+ view.setInt32(18, image.width, true);
186
+ view.setInt32(22, -image.height, true);
187
+ view.setUint16(26, 1, true);
188
+ view.setUint16(28, 24, true);
189
+ for (let row = 0; row < image.height; row += 1) {
190
+ for (let column = 0; column < image.width; column += 1) {
191
+ const source = (row * image.width + column) * 3;
192
+ const target = 54 + row * stride + column * 3;
193
+ output[target] = image.data[source + 2] ?? 0;
194
+ output[target + 1] = image.data[source + 1] ?? 0;
195
+ output[target + 2] = image.data[source] ?? 0;
196
+ }
197
+ }
198
+ return output;
199
+ }
200
+ function base64(bytes) {
201
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
202
+ let output = "";
203
+ for (let index = 0; index < bytes.length; index += 3) {
204
+ const first = bytes[index] ?? 0;
205
+ const second = bytes[index + 1] ?? 0;
206
+ const third = bytes[index + 2] ?? 0;
207
+ output += alphabet[first >> 2];
208
+ output += alphabet[(first & 3) << 4 | second >> 4];
209
+ output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
210
+ output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
211
+ }
212
+ return output;
213
+ }
214
+ function safePath(value) {
215
+ return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
216
+ }
217
+ function cssColor(value) {
218
+ return /^#[\da-f]{6}$/i.test(value ?? "");
219
+ }
220
+ function finiteNonnegative(value) {
221
+ return Number.isFinite(value) && (value ?? -1) >= 0;
222
+ }
223
+ function unitInterval(value) {
224
+ return finiteNonnegative(value) && value <= 1;
225
+ }
226
+ function number(value) {
227
+ return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
228
+ }
229
+
84
230
  // src/semantic-document.ts
85
231
  async function writeSemanticDocument(pages, write, lookaheadPages) {
86
232
  const stats = {
@@ -94,6 +240,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
94
240
  const seenFurniture = /* @__PURE__ */ new Set();
95
241
  const sectionLevels = [];
96
242
  let activeTable;
243
+ const pendingMedia = [];
97
244
  let headerOpen = false;
98
245
  let headerHasParagraph = false;
99
246
  let contentStarted = false;
@@ -104,6 +251,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
104
251
  if (!activeTable) return;
105
252
  await write("</table>");
106
253
  activeTable = void 0;
254
+ while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
107
255
  };
108
256
  const closeSections = async (minimumLevel = 0) => {
109
257
  while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
@@ -123,10 +271,19 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
123
271
  };
124
272
  const emitPage = async (page, future) => {
125
273
  const defaultColor = dominantTextColor(page.structured.lines);
274
+ let mediaIndex = 0;
126
275
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
127
276
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
128
277
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
129
278
  const nextBlock = page.structured.blocks[blockIndex + 1];
279
+ const blockY = semanticBlockY(block);
280
+ while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
281
+ await flushPendingParagraph();
282
+ const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
283
+ if (activeTable) pendingMedia.push(html);
284
+ else await write(html);
285
+ mediaIndex += 1;
286
+ }
130
287
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
131
288
  stats.suppressedFurniture += 1;
132
289
  continue;
@@ -217,11 +374,18 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
217
374
  }
218
375
  await write(semanticBlockHtml(block, defaultColor));
219
376
  }
377
+ while (mediaIndex < page.media.length) {
378
+ await flushPendingParagraph();
379
+ const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
380
+ if (activeTable) pendingMedia.push(html);
381
+ else await write(html);
382
+ mediaIndex += 1;
383
+ }
220
384
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
221
385
  };
222
386
  for await (const page of pages) {
223
387
  const structured = (0, import_structure.structurePage)(page);
224
- buffer.push({ width: page.width, height: page.height, structured });
388
+ buffer.push({ width: page.width, height: page.height, structured, media: semanticMedia(page) });
225
389
  stats.pagesProcessed += 1;
226
390
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
227
391
  stats.peakBufferedLines = Math.max(
@@ -375,6 +539,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
375
539
  const tag = block.ordered ? "ol" : "ul";
376
540
  return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
377
541
  }
542
+ function semanticBlockY(block) {
543
+ const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
544
+ return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
545
+ }
378
546
  function cardTableRow(item) {
379
547
  const trailing = item.details.at(-1) ?? "";
380
548
  const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
@@ -458,7 +626,7 @@ async function writePositionedPage(page, write, options) {
458
626
  const displayWidth = quarterTurn ? page.height : page.width;
459
627
  const displayHeight = quarterTurn ? page.width : page.height;
460
628
  await write(
461
- `<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">`
629
+ `<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">`
462
630
  );
463
631
  const fontAliases = new Map(
464
632
  (page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
@@ -470,10 +638,10 @@ async function writePositionedPage(page, write, options) {
470
638
  await write(`<style>${page.fonts.map((font) => fontFace(font, fontAliases)).join("")}</style>`);
471
639
  }
472
640
  await write(
473
- `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number(page.width)}pt;height:${number(page.height)}pt${rotationTransform(page)}">`
641
+ `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
474
642
  );
475
643
  await write(
476
- `<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)}">`
644
+ `<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)}">`
477
645
  );
478
646
  const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
479
647
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
@@ -483,24 +651,24 @@ async function writePositionedPage(page, write, options) {
483
651
  }
484
652
  }
485
653
  for (const fill of page.fills ?? []) {
486
- const points = fill.points.map(([x, y]) => `${number(x)},${number(page.height - y)}`).join(" ");
654
+ const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
487
655
  if (isCssHexColor(fill.color)) {
488
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
656
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
489
657
  await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
490
658
  }
491
659
  }
492
660
  if (page.paths?.length) {
493
- await write(`<g transform="translate(0 ${number(page.height)}) scale(1 -1)">`);
661
+ await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
494
662
  for (const [pathIndex, path] of page.paths.entries()) {
495
663
  if (!isSvgPath(path.d)) continue;
496
664
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
497
665
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
498
- const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number(path.strokeWidth)}"` : "";
666
+ const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
499
667
  const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
500
- const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
501
- const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
502
- const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
503
- const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
668
+ const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
669
+ const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
670
+ const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
671
+ const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
504
672
  const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
505
673
  const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
506
674
  let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
@@ -537,11 +705,11 @@ function usesReflectedVisualOverlay(page, spans) {
537
705
  }
538
706
  function visualImage(image, pageHeight, pageNumber, imageIndex) {
539
707
  const [a, b, c, d, e, f] = image.transform;
540
- const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number).join(" ");
541
- const opacity = isUnitInterval(image.opacity) ? ` opacity="${number(image.opacity)}"` : "";
708
+ const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
709
+ const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
542
710
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
543
- const data = image.format === "jpeg" ? image.data : rgbBmp(image);
544
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
711
+ const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
712
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base642(data)}"${opacity}/>`;
545
713
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
546
714
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
547
715
  }
@@ -552,7 +720,7 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
552
720
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
553
721
  if (!isSvgPath(clip.d)) return "";
554
722
  const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
555
- return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
723
+ return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number2(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
556
724
  })
557
725
  ).join("");
558
726
  }
@@ -571,7 +739,7 @@ function pathClipDefinitions(paths, pageNumber) {
571
739
  function pathClipId(pageNumber, pathIndex, clipIndex) {
572
740
  return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
573
741
  }
574
- function rgbBmp(image) {
742
+ function rgbBmp2(image) {
575
743
  const stride = Math.ceil(image.width * 3 / 4) * 4;
576
744
  const output = new Uint8Array(54 + stride * image.height);
577
745
  const view = new DataView(output.buffer);
@@ -599,11 +767,11 @@ function rgbBmp(image) {
599
767
  function rotationTransform(page) {
600
768
  switch (page.rotate) {
601
769
  case 90:
602
- return `;transform:translate(${number(page.height)}pt,0) rotate(90deg)`;
770
+ return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
603
771
  case 180:
604
- return `;transform:translate(${number(page.width)}pt,${number(page.height)}pt) rotate(180deg)`;
772
+ return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
605
773
  case 270:
606
- return `;transform:translate(0,${number(page.width)}pt) rotate(270deg)`;
774
+ return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
607
775
  default:
608
776
  return "";
609
777
  }
@@ -611,13 +779,13 @@ function rotationTransform(page) {
611
779
  function positionedSpan(span, fontAliases) {
612
780
  const direction = directionAttribute([span]);
613
781
  const style = [
614
- `left:${number(span.bounds.x)}pt`,
615
- `bottom:${number(span.bounds.y)}pt`,
616
- `width:${number(span.bounds.width)}pt`,
617
- `height:${number(span.bounds.height)}pt`,
618
- `font-size:${number(span.fontSize)}pt`,
782
+ `left:${number2(span.bounds.x)}pt`,
783
+ `bottom:${number2(span.bounds.y)}pt`,
784
+ `width:${number2(span.bounds.width)}pt`,
785
+ `height:${number2(span.bounds.height)}pt`,
786
+ `font-size:${number2(span.fontSize)}pt`,
619
787
  ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
620
- ...isUnitInterval(span.fillOpacity) ? [`opacity:${number(span.fillOpacity)}`] : [],
788
+ ...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
621
789
  ...fontStyles(
622
790
  span.fontFamily,
623
791
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -628,10 +796,17 @@ function positionedSpan(span, fontAliases) {
628
796
  async function writeFlowPage(page, write) {
629
797
  const structured = (0, import_structure2.structurePage)(page);
630
798
  const defaultColor = dominantTextColor(structured.lines);
799
+ const media = semanticMedia(page);
800
+ let mediaIndex = 0;
631
801
  await write(
632
802
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
633
803
  );
634
804
  for (const block of structured.blocks) {
805
+ const blockY = semanticBlockY2(block);
806
+ while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
807
+ await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
808
+ mediaIndex += 1;
809
+ }
635
810
  if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
636
811
  else if (block.type === "heading") {
637
812
  await write(
@@ -678,8 +853,16 @@ async function writeFlowPage(page, write) {
678
853
  await write(`</${tag}>`);
679
854
  }
680
855
  }
856
+ while (mediaIndex < media.length) {
857
+ await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
858
+ mediaIndex += 1;
859
+ }
681
860
  await write("</section>");
682
861
  }
862
+ function semanticBlockY2(block) {
863
+ const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
864
+ return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
865
+ }
683
866
  function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
684
867
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
685
868
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
@@ -689,10 +872,10 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
689
872
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
690
873
  ).join(";");
691
874
  const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
692
- const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number(span.strokeWidth ?? 0)}` : "";
875
+ const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number2(span.strokeWidth ?? 0)}` : "";
693
876
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
694
- const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "";
695
- const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number(span.strokeOpacity)}` : "";
877
+ const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
878
+ const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number2(span.strokeOpacity)}` : "";
696
879
  const style = [
697
880
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
698
881
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
@@ -704,7 +887,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
704
887
  font
705
888
  ].filter(Boolean).join(";");
706
889
  const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
707
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
890
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number2(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
708
891
  const transform = counterRotateReflectedText && span.transform ? [
709
892
  span.transform[0],
710
893
  span.transform[1],
@@ -717,8 +900,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
717
900
  const basisY = transform?.[1] ?? 0;
718
901
  const anchorX = span.bounds.x + basisX * rtlOffset;
719
902
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
720
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
721
- return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
903
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
904
+ return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
722
905
  }
723
906
  function isAdobeCjkFont(fontFamily) {
724
907
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -730,16 +913,16 @@ function visualType3Text(span, font, pageHeight) {
730
913
  const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
731
914
  if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
732
915
  const transform = span.transform ?? [1, 0, 0, 1];
733
- const outer = `matrix(${transform.map(number).join(" ")} ${number(span.bounds.x)} ${number(pageHeight - span.bounds.y)})`;
916
+ const outer = `matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(pageHeight - span.bounds.y)})`;
734
917
  const xScale = span.bounds.width / totalAdvance;
735
918
  let offset = 0;
736
919
  let content = "";
737
920
  for (const glyph of sequence) {
738
921
  if (!glyph) continue;
739
- content += `<g transform="translate(${number(offset)} 0)">${type3Glyph(glyph)}</g>`;
922
+ content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph)}</g>`;
740
923
  offset += glyph.advance;
741
924
  }
742
- return `<g transform="${outer}"><g transform="scale(${number(xScale)} ${number(-span.fontSize)})">${content}</g></g>`;
925
+ return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
743
926
  }
744
927
  function isHebrewPaintOrder(span) {
745
928
  return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
@@ -751,15 +934,15 @@ function type3Glyph(glyph) {
751
934
  let output = "";
752
935
  for (const fill of glyph.fills ?? []) {
753
936
  if (!isCssHexColor(fill.color)) continue;
754
- const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
755
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
937
+ const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
938
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
756
939
  output += `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
757
940
  }
758
941
  for (const path of glyph.paths ?? []) {
759
942
  if (!isSvgPath(path.d)) continue;
760
943
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
761
944
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
762
- const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number(path.strokeWidth)}"` : "";
945
+ const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
763
946
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
764
947
  }
765
948
  return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
@@ -809,9 +992,9 @@ function fontFace(font, aliases) {
809
992
  const styles2 = fontStyles(font.family, alias).filter(
810
993
  (style) => !style.startsWith("font-family:")
811
994
  );
812
- return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
995
+ return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
813
996
  }
814
- function base64(bytes) {
997
+ function base642(bytes) {
815
998
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
816
999
  let output = "";
817
1000
  for (let index = 0; index < bytes.length; index += 3) {
@@ -831,7 +1014,7 @@ function directionAttribute(spans) {
831
1014
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
832
1015
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
833
1016
  }
834
- function number(value) {
1017
+ function number2(value) {
835
1018
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
836
1019
  }
837
1020
  function escapeAttribute(value) {