@boxpdf/html-writer 0.1.17 → 0.1.18

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
@@ -1,6 +1,64 @@
1
1
  // src/index.ts
2
2
  import { structurePage as structurePage2, tableToHtml } from "@boxpdf/reader/structure";
3
3
 
4
+ // src/semantic-caption.ts
5
+ function isClearMediaCaption(media, block, pageWidth, pageHeight, pageLines) {
6
+ if (block.type !== "paragraph" || block.lines.length === 0) return false;
7
+ const bounds2 = unionLines(block.lines);
8
+ const lineHeight = median(block.lines.map((line) => line.bounds.height));
9
+ if (media.bounds.width < pageWidth * 0.2 || media.bounds.height < lineHeight * 10) return false;
10
+ if (media.bounds.x < -2 || media.bounds.y < -2 || media.bounds.x + media.bounds.width > pageWidth + 2 || media.bounds.y + media.bounds.height > pageHeight + 2)
11
+ return false;
12
+ const gap = media.bounds.y - (bounds2.y + bounds2.height);
13
+ if (gap < -lineHeight * 0.15 || gap > lineHeight * 1.25) return false;
14
+ const mediaCenter = media.bounds.x + media.bounds.width / 2;
15
+ const captionCenter = bounds2.x + bounds2.width / 2;
16
+ if (Math.abs(mediaCenter - captionCenter) > Math.max(3, media.bounds.width * 0.03)) return false;
17
+ if (bounds2.width < media.bounds.width * 0.45 || bounds2.width > media.bounds.width * 1.06) {
18
+ return false;
19
+ }
20
+ const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
21
+ if (!first) return false;
22
+ const otherLines = pageLines.filter((line) => !block.lines.includes(line));
23
+ return fontSignature(first) !== dominantFontSignature(otherLines);
24
+ }
25
+ function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
26
+ const associations = /* @__PURE__ */ new Map();
27
+ for (const item of media) {
28
+ const candidates = blocks.filter((block) => isClearMediaCaption(item, block, pageWidth, pageHeight, pageLines)).filter((block) => !associations.has(block)).sort((left, right) => captionGap(item, left) - captionGap(item, right));
29
+ const caption = candidates[0];
30
+ if (caption) associations.set(caption, item);
31
+ }
32
+ return associations;
33
+ }
34
+ function captionGap(media, block) {
35
+ if (block.type !== "paragraph") return Number.POSITIVE_INFINITY;
36
+ const bounds2 = unionLines(block.lines);
37
+ return Math.abs(media.bounds.y - bounds2.y - bounds2.height);
38
+ }
39
+ function unionLines(lines) {
40
+ const x = Math.min(...lines.map((line) => line.bounds.x));
41
+ const y = Math.min(...lines.map((line) => line.bounds.y));
42
+ const right = Math.max(...lines.map((line) => line.bounds.x + line.bounds.width));
43
+ const top = Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
44
+ return { x, y, width: right - x, height: top - y };
45
+ }
46
+ function dominantFontSignature(lines) {
47
+ const counts = /* @__PURE__ */ new Map();
48
+ for (const span of lines.flatMap((line) => line.spans)) {
49
+ const signature = fontSignature(span);
50
+ counts.set(signature, (counts.get(signature) ?? 0) + Math.max(1, [...span.text].length));
51
+ }
52
+ return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "";
53
+ }
54
+ function fontSignature(span) {
55
+ return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
56
+ }
57
+ function median(values) {
58
+ const ordered = [...values].sort((left, right) => left - right);
59
+ return ordered[Math.floor(ordered.length / 2)] ?? 1;
60
+ }
61
+
4
62
  // src/semantic-document.ts
5
63
  import {
6
64
  structurePage,
@@ -76,6 +134,85 @@ function escapeHtml(value) {
76
134
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
77
135
  }
78
136
 
137
+ // src/vector-svg.ts
138
+ function vectorFillSvg(fill) {
139
+ if (!isCssHexColor(fill.color)) return "";
140
+ const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
141
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
142
+ return `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
143
+ }
144
+ function vectorPathSvg(path, pageNumber, pathIndex) {
145
+ if (!isSvgPath(path.d)) return "";
146
+ const fill = isCssHexColor(path.fill) ? path.fill : "none";
147
+ const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
148
+ const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
149
+ const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
150
+ const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
151
+ const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
152
+ const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
153
+ const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
154
+ const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
155
+ const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
156
+ let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${dashoffset}${linecap}${linejoin}${rule}/>`;
157
+ for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
158
+ output = `<g clip-path="url(#${vectorPathClipId(pageNumber, pathIndex, index)})">${output}</g>`;
159
+ }
160
+ return output;
161
+ }
162
+ function vectorPathClipDefinitions(paths, pageNumber) {
163
+ return paths.flatMap(
164
+ ({ path, index: pathIndex }) => (path.clips ?? []).map((clip, clipIndex) => {
165
+ if (!isSvgPath(clip.d)) return "";
166
+ const rule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
167
+ return `<clipPath id="${vectorPathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${rule}/></clipPath>`;
168
+ })
169
+ ).join("");
170
+ }
171
+ function vectorPathBounds(path) {
172
+ if (!isSvgPath(path.d)) return void 0;
173
+ const values = [...path.d.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
174
+ (match) => Number(match[0])
175
+ );
176
+ if (values.length < 2) return void 0;
177
+ const xs = [];
178
+ const ys = [];
179
+ for (let index = 0; index + 1 < values.length; index += 2) {
180
+ xs.push(values[index] ?? 0);
181
+ ys.push(values[index + 1] ?? 0);
182
+ }
183
+ return bounds(xs, ys);
184
+ }
185
+ function vectorFillBounds(fill) {
186
+ if (fill.points.length === 0) return void 0;
187
+ return bounds(
188
+ fill.points.map(([x]) => x),
189
+ fill.points.map(([, y]) => y)
190
+ );
191
+ }
192
+ function isSvgPath(value) {
193
+ return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
194
+ }
195
+ function bounds(xs, ys) {
196
+ const x = Math.min(...xs);
197
+ const y = Math.min(...ys);
198
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
199
+ }
200
+ function vectorPathClipId(pageNumber, pathIndex, clipIndex) {
201
+ return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
202
+ }
203
+ function isCssHexColor(value) {
204
+ return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value);
205
+ }
206
+ function finiteNonnegative(value) {
207
+ return value !== void 0 && Number.isFinite(value) && value >= 0;
208
+ }
209
+ function isUnitInterval(value) {
210
+ return value !== void 0 && Number.isFinite(value) && value >= 0 && value <= 1;
211
+ }
212
+ function number(value) {
213
+ return Number(value.toFixed(4)).toString();
214
+ }
215
+
79
216
  // src/visual-font.ts
80
217
  function visualFontAliases(pageNumber, fonts) {
81
218
  return new Map(
@@ -125,46 +262,90 @@ function base64(bytes) {
125
262
  // src/semantic-media.ts
126
263
  function semanticMedia(page) {
127
264
  const output = (page.images ?? []).map((image) => rasterMedia(image));
128
- const vector = vectorMedia(page);
129
- if (vector) output.push(vector);
265
+ output.push(...vectorMedia(page));
130
266
  return output.sort((left, right) => right.bounds.y - left.bounds.y);
131
267
  }
132
268
  function rasterMedia(image) {
133
- const bounds = transformedUnitBounds(image.transform);
269
+ const bounds2 = transformedUnitBounds(image.transform);
134
270
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
135
271
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
136
- const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
272
+ const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
137
273
  return {
138
- bounds,
139
- 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}">`
274
+ bounds: bounds2,
275
+ html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`
140
276
  };
141
277
  }
142
278
  function vectorMedia(page) {
143
- const paths = (page.paths ?? []).filter((path) => safePath(path.d));
144
- const fills = page.fills ?? [];
145
- const bounds = unionBounds([
146
- ...paths.map((path) => pathBounds(path.d)),
147
- ...fills.map(fillBounds)
148
- ]);
149
- if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
279
+ const primitives = [
280
+ ...(page.paths ?? []).flatMap((path, index) => {
281
+ const bounds2 = vectorPathBounds(path);
282
+ return bounds2 ? [{ type: "path", value: path, index, bounds: bounds2 }] : [];
283
+ }),
284
+ ...(page.fills ?? []).flatMap((fill) => {
285
+ const bounds2 = vectorFillBounds(fill);
286
+ return bounds2 && !isPageBackground(fill, bounds2, page) ? [{ type: "fill", value: fill, bounds: bounds2 }] : [];
287
+ })
288
+ ];
289
+ const components = vectorComponents(primitives, Math.min(36, page.width * 0.06)).filter(
290
+ (component) => component.primitives.length >= 2 || component.bounds.width * component.bounds.height >= page.width * page.height * 2e-3
291
+ );
150
292
  const aliases = visualFontAliases(page.number, page.fonts ?? []);
151
293
  const visualCodeFonts = new Set(
152
294
  (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
153
295
  );
154
- const visualSpans = page.visualSpans ?? page.spans;
155
- const overlay = visualSpans.filter(
156
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
157
- );
158
- const consumedSpans = page.spans.filter(
159
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
160
- );
161
- const fontIds = new Set(overlay.map((span) => span.fontAssetId));
162
- const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
163
- return {
164
- bounds,
165
- 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">${fontFaces ? `<style>${fontFaces}</style>` : ""}<g transform="translate(0 ${number(page.height)}) scale(1 -1)">${fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`,
166
- ...consumedSpans.length > 0 ? { consumedSpans } : {}
167
- };
296
+ return components.map((component) => {
297
+ const bounds2 = component.bounds;
298
+ const paths = component.primitives.flatMap(
299
+ (primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
300
+ );
301
+ const fills = component.primitives.flatMap(
302
+ (primitive) => primitive.type === "fill" ? [primitive.value] : []
303
+ );
304
+ const visualSpans = page.visualSpans ?? page.spans;
305
+ const overlay = visualSpans.filter(
306
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
307
+ );
308
+ const consumedSpans = page.spans.filter(
309
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
310
+ );
311
+ const fontIds = new Set(overlay.map((span) => span.fontAssetId));
312
+ const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
313
+ return {
314
+ bounds: bounds2,
315
+ html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number2(bounds2.x)} ${number2(page.height - bounds2.y - bounds2.height)} ${number2(bounds2.width)} ${number2(bounds2.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}${paths.length ? `<defs>${vectorPathClipDefinitions(paths, page.number)}</defs>` : ""}<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">${fills.map(vectorFillSvg).join("") + paths.map(({ path, index }) => vectorPathSvg(path, page.number, index)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`,
316
+ ...consumedSpans.length > 0 ? { consumedSpans } : {}
317
+ };
318
+ });
319
+ }
320
+ function vectorComponents(primitives, padding) {
321
+ const components = [];
322
+ for (const primitive of primitives) {
323
+ const matches = components.filter(
324
+ (component) => nearby(component.bounds, primitive.bounds, padding)
325
+ );
326
+ if (matches.length === 0) {
327
+ components.push({ bounds: primitive.bounds, primitives: [primitive] });
328
+ continue;
329
+ }
330
+ const target = matches[0];
331
+ target.primitives.push(primitive);
332
+ target.bounds = unionBounds([target.bounds, primitive.bounds]);
333
+ for (const component of matches.slice(1)) {
334
+ target.primitives.push(...component.primitives);
335
+ target.bounds = unionBounds([target.bounds, component.bounds]);
336
+ components.splice(components.indexOf(component), 1);
337
+ }
338
+ }
339
+ return components;
340
+ }
341
+ function nearby(left, right, padding) {
342
+ return !(left.x + left.width + padding < right.x || right.x + right.width + padding < left.x || left.y + left.height + padding < right.y || right.y + right.height + padding < left.y);
343
+ }
344
+ function isPageBackground(fill, bounds2, page) {
345
+ if (!/^#f{6}$/i.test(fill.color)) return false;
346
+ const outside = bounds2.x < 0 || bounds2.y < 0 || bounds2.x + bounds2.width > page.width || bounds2.y + bounds2.height > page.height;
347
+ const large = bounds2.width * bounds2.height > page.width * page.height * 0.2;
348
+ return outside || large;
168
349
  }
169
350
  function withoutSemanticMediaSpans(page, media) {
170
351
  const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
@@ -174,7 +355,7 @@ function vectorText(span, pageHeight, aliases) {
174
355
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
175
356
  const styles2 = [
176
357
  cssColor(span.color) ? `fill:${span.color}` : "",
177
- unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
358
+ unitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "",
178
359
  ...visualFontStyles(
179
360
  span.fontFamily,
180
361
  span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
@@ -182,33 +363,16 @@ function vectorText(span, pageHeight, aliases) {
182
363
  ].filter(Boolean).join(";");
183
364
  const anchorY = pageHeight - span.bounds.y;
184
365
  const transform = span.transform;
185
- const position = transform ? ` x="0" y="0" transform="matrix(${transform.map(number).join(" ")} ${number(span.bounds.x)} ${number(anchorY)})"` : ` x="${number(span.bounds.x)}" y="${number(anchorY)}"`;
366
+ const position = transform ? ` x="0" y="0" transform="matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(anchorY)})"` : ` x="${number2(span.bounds.x)}" y="${number2(anchorY)}"`;
186
367
  const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
187
- const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
188
- return `<text${position} font-size="${number(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
368
+ const length = extent > 0 ? ` textLength="${number2(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
369
+ return `<text${position} font-size="${number2(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
189
370
  }
190
371
  function centerInside(inner, outer) {
191
372
  const x = inner.x + inner.width / 2;
192
373
  const y = inner.y + inner.height / 2;
193
374
  return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
194
375
  }
195
- function vectorFill(fill) {
196
- const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
197
- const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
198
- return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
199
- }
200
- function vectorPath(path) {
201
- const fill = cssColor(path.fill) ? path.fill : "none";
202
- const stroke = cssColor(path.stroke) ? path.stroke : "none";
203
- const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
204
- const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
205
- const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
206
- const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
207
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
208
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
209
- const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
210
- return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
211
- }
212
376
  function transformedUnitBounds([a, b, c, d, e, f]) {
213
377
  const points = [
214
378
  [e, f],
@@ -222,31 +386,8 @@ function transformedUnitBounds([a, b, c, d, e, f]) {
222
386
  const minY = Math.min(...ys);
223
387
  return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
224
388
  }
225
- function pathBounds(path) {
226
- const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
227
- (match) => Number(match[0])
228
- );
229
- if (values.length < 2) return void 0;
230
- const xs = [];
231
- const ys = [];
232
- for (let index = 0; index + 1 < values.length; index += 2) {
233
- xs.push(values[index] ?? 0);
234
- ys.push(values[index + 1] ?? 0);
235
- }
236
- const minX = Math.min(...xs);
237
- const minY = Math.min(...ys);
238
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
239
- }
240
- function fillBounds(fill) {
241
- if (fill.points.length === 0) return void 0;
242
- const xs = fill.points.map(([x]) => x);
243
- const ys = fill.points.map(([, y]) => y);
244
- const minX = Math.min(...xs);
245
- const minY = Math.min(...ys);
246
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
247
- }
248
- function unionBounds(bounds) {
249
- const values = bounds.filter((value) => Boolean(value));
389
+ function unionBounds(bounds2) {
390
+ const values = bounds2.filter((value) => Boolean(value));
250
391
  if (values.length === 0) return void 0;
251
392
  const x = Math.min(...values.map((value) => value.x));
252
393
  const y = Math.min(...values.map((value) => value.y));
@@ -277,19 +418,16 @@ function rgbBmp(image) {
277
418
  }
278
419
  return output;
279
420
  }
280
- function safePath(value) {
281
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
282
- }
283
421
  function cssColor(value) {
284
422
  return /^#[\da-f]{6}$/i.test(value ?? "");
285
423
  }
286
- function finiteNonnegative(value) {
424
+ function finiteNonnegative2(value) {
287
425
  return Number.isFinite(value) && (value ?? -1) >= 0;
288
426
  }
289
427
  function unitInterval(value) {
290
- return finiteNonnegative(value) && value <= 1;
428
+ return finiteNonnegative2(value) && value <= 1;
291
429
  }
292
- function number(value) {
430
+ function number2(value) {
293
431
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
294
432
  }
295
433
  function escapeHtml2(value) {
@@ -341,18 +479,38 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
341
479
  const emitPage = async (page, future) => {
342
480
  const defaultColor = dominantTextColor(page.structured.lines);
343
481
  let mediaIndex = 0;
482
+ const captions = clearMediaCaptionAssociations(
483
+ page.media,
484
+ page.structured.blocks,
485
+ page.width,
486
+ page.height,
487
+ page.structured.lines
488
+ );
489
+ const captionedMedia = new Set(captions.values());
344
490
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
345
491
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
346
492
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
347
493
  const nextBlock = page.structured.blocks[blockIndex + 1];
348
494
  const blockY = semanticBlockY(block);
495
+ let emittedAsCaption = false;
349
496
  while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
350
497
  await flushPendingParagraph();
351
- const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
498
+ const item = page.media[mediaIndex];
499
+ if (item && captions.get(block) === item && block.type === "paragraph") {
500
+ const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
501
+ if (activeTable) pendingMedia.push(html2);
502
+ else await write(html2);
503
+ mediaIndex += 1;
504
+ emittedAsCaption = true;
505
+ break;
506
+ }
507
+ if (item && captionedMedia.has(item)) break;
508
+ const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
352
509
  if (activeTable) pendingMedia.push(html);
353
510
  else await write(html);
354
511
  mediaIndex += 1;
355
512
  }
513
+ if (emittedAsCaption) continue;
356
514
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
357
515
  stats.suppressedFurniture += 1;
358
516
  continue;
@@ -583,6 +741,14 @@ function financialSummaryRow(entry, columns) {
583
741
  return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
584
742
  }
585
743
  function semanticBlockHtml(block, defaultColor = "#000000") {
744
+ if (block.type === "insetGroup") {
745
+ return `<div class="pdf-semantic-inset" style="margin-inline-start:${block.indentEm}em">${block.blocks.map((item) => semanticBlockHtml(item, defaultColor)).join("")}</div>`;
746
+ }
747
+ if (block.type === "table") {
748
+ const rows = tableToRows(block.table);
749
+ const header = tableHeader(rows);
750
+ return `<table>${rows.map((row, index) => tableRow(row, Boolean(header && index === 0))).join("")}</table>`;
751
+ }
586
752
  if (block.type === "heading")
587
753
  return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
588
754
  if (block.type === "paragraph")
@@ -698,7 +864,7 @@ async function writePositionedPage(page, write, options) {
698
864
  const displayWidth = quarterTurn ? page.height : page.width;
699
865
  const displayHeight = quarterTurn ? page.width : page.height;
700
866
  await write(
701
- `<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">`
867
+ `<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number3(displayWidth)}pt;height:${number3(displayHeight)}pt">`
702
868
  );
703
869
  const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
704
870
  const type3Fonts = new Map(
@@ -710,44 +876,26 @@ async function writePositionedPage(page, write, options) {
710
876
  );
711
877
  }
712
878
  await write(
713
- `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
879
+ `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
714
880
  );
715
881
  await write(
716
- `<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)}">`
882
+ `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
883
+ );
884
+ const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
885
+ (page.paths ?? []).map((path, index) => ({ path, index })),
886
+ page.number
717
887
  );
718
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
719
888
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
720
889
  if (reflectedOverlay) {
721
890
  for (const [index, image] of (page.images ?? []).entries()) {
722
891
  await write(visualImage(image, page.height, page.number, index));
723
892
  }
724
893
  }
725
- for (const fill of page.fills ?? []) {
726
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
727
- if (isCssHexColor(fill.color)) {
728
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
729
- await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
730
- }
731
- }
732
- if (page.paths?.length) {
733
- await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
734
- for (const [pathIndex, path] of page.paths.entries()) {
735
- if (!isSvgPath(path.d)) continue;
736
- const fill = isCssHexColor(path.fill) ? path.fill : "none";
737
- const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
738
- const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
739
- const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
740
- const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
741
- const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
742
- const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
743
- const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
744
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
745
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
746
- let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
747
- for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
748
- output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
749
- }
750
- await write(output);
894
+ if (page.fills?.length || page.paths?.length) {
895
+ await write(`<g transform="translate(0 ${number3(page.height)}) scale(1 -1)">`);
896
+ for (const fill of page.fills ?? []) await write(vectorFillSvg(fill));
897
+ for (const [pathIndex, path] of (page.paths ?? []).entries()) {
898
+ await write(vectorPathSvg(path, page.number, pathIndex));
751
899
  }
752
900
  await write("</g>");
753
901
  }
@@ -777,8 +925,8 @@ function usesReflectedVisualOverlay(page, spans) {
777
925
  }
778
926
  function visualImage(image, pageHeight, pageNumber, imageIndex) {
779
927
  const [a, b, c, d, e, f] = image.transform;
780
- const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
781
- const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
928
+ const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
929
+ const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
782
930
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
783
931
  const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
784
932
  let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
@@ -792,25 +940,13 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
792
940
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
793
941
  if (!isSvgPath(clip.d)) return "";
794
942
  const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
795
- return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number2(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
943
+ return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number3(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
796
944
  })
797
945
  ).join("");
798
946
  }
799
947
  function imageClipId(pageNumber, imageIndex, clipIndex) {
800
948
  return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
801
949
  }
802
- function pathClipDefinitions(paths, pageNumber) {
803
- return paths.flatMap(
804
- (path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
805
- if (!isSvgPath(clip.d)) return "";
806
- const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
807
- return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
808
- })
809
- ).join("");
810
- }
811
- function pathClipId(pageNumber, pathIndex, clipIndex) {
812
- return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
813
- }
814
950
  function rgbBmp2(image) {
815
951
  const stride = Math.ceil(image.width * 3 / 4) * 4;
816
952
  const output = new Uint8Array(54 + stride * image.height);
@@ -839,11 +975,11 @@ function rgbBmp2(image) {
839
975
  function rotationTransform(page) {
840
976
  switch (page.rotate) {
841
977
  case 90:
842
- return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
978
+ return `;transform:translate(${number3(page.height)}pt,0) rotate(90deg)`;
843
979
  case 180:
844
- return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
980
+ return `;transform:translate(${number3(page.width)}pt,${number3(page.height)}pt) rotate(180deg)`;
845
981
  case 270:
846
- return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
982
+ return `;transform:translate(0,${number3(page.width)}pt) rotate(270deg)`;
847
983
  default:
848
984
  return "";
849
985
  }
@@ -851,13 +987,13 @@ function rotationTransform(page) {
851
987
  function positionedSpan(span, fontAliases) {
852
988
  const direction = directionAttribute([span]);
853
989
  const style = [
854
- `left:${number2(span.bounds.x)}pt`,
855
- `bottom:${number2(span.bounds.y)}pt`,
856
- `width:${number2(span.bounds.width)}pt`,
857
- `height:${number2(span.bounds.height)}pt`,
858
- `font-size:${number2(span.fontSize)}pt`,
859
- ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
860
- ...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
990
+ `left:${number3(span.bounds.x)}pt`,
991
+ `bottom:${number3(span.bounds.y)}pt`,
992
+ `width:${number3(span.bounds.width)}pt`,
993
+ `height:${number3(span.bounds.height)}pt`,
994
+ `font-size:${number3(span.fontSize)}pt`,
995
+ ...isCssHexColor2(span.color) ? [`color:${span.color}`] : [],
996
+ ...isUnitInterval2(span.fillOpacity) ? [`opacity:${number3(span.fillOpacity)}`] : [],
861
997
  ...visualFontStyles(
862
998
  span.fontFamily,
863
999
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -870,15 +1006,35 @@ async function writeFlowPage(page, write) {
870
1006
  const structured = structurePage2(withoutSemanticMediaSpans(page, media));
871
1007
  const defaultColor = dominantTextColor(structured.lines);
872
1008
  let mediaIndex = 0;
1009
+ const captions = clearMediaCaptionAssociations(
1010
+ media,
1011
+ structured.blocks,
1012
+ page.width,
1013
+ page.height,
1014
+ structured.lines
1015
+ );
1016
+ const captionedMedia = new Set(captions.values());
873
1017
  await write(
874
1018
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
875
1019
  );
876
1020
  for (const block of structured.blocks) {
877
1021
  const blockY = semanticBlockY2(block);
1022
+ let emittedAsCaption = false;
878
1023
  while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
879
- await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
1024
+ const item = media[mediaIndex];
1025
+ if (item && captions.get(block) === item && block.type === "paragraph") {
1026
+ await write(
1027
+ `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
1028
+ );
1029
+ mediaIndex += 1;
1030
+ emittedAsCaption = true;
1031
+ break;
1032
+ }
1033
+ if (item && captionedMedia.has(item)) break;
1034
+ await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
880
1035
  mediaIndex += 1;
881
1036
  }
1037
+ if (emittedAsCaption) continue;
882
1038
  if (block.type === "table") await write(tableToHtml(block.table));
883
1039
  else if (block.type === "heading") {
884
1040
  await write(
@@ -918,6 +1074,10 @@ async function writeFlowPage(page, write) {
918
1074
  await write(
919
1075
  `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
920
1076
  );
1077
+ } else if (block.type === "insetGroup") {
1078
+ await write(
1079
+ `<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`
1080
+ );
921
1081
  } else {
922
1082
  const tag = block.ordered ? "ol" : "ul";
923
1083
  await write(`<${tag}>`);
@@ -933,6 +1093,33 @@ async function writeFlowPage(page, write) {
933
1093
  }
934
1094
  await write("</section>");
935
1095
  }
1096
+ function nestedSemanticBlockHtml(block, defaultColor) {
1097
+ if (block.type === "insetGroup") {
1098
+ return `<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`;
1099
+ }
1100
+ if (block.type === "table") return tableToHtml(block.table);
1101
+ if (block.type === "heading") {
1102
+ return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
1103
+ }
1104
+ if (block.type === "paragraph") {
1105
+ return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
1106
+ }
1107
+ if (block.type === "preformatted") return `<pre>${escapeHtml4(block.text)}</pre>`;
1108
+ if (block.type === "definitionList") {
1109
+ return `<dl>${block.entries.map((entry) => `<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`).join("")}</dl>`;
1110
+ }
1111
+ if (block.type === "cardList") {
1112
+ return `<div class="pdf-semantic-cards">${block.items.map((item) => `<article><h3>${escapeHtml4(item.title)}</h3>${item.details.map((detail) => `<p>${escapeHtml4(detail)}</p>`).join("")}</article>`).join("")}</div>`;
1113
+ }
1114
+ if (block.type === "sectionGroup") {
1115
+ return `<div class="pdf-semantic-sections">${block.items.map((item) => `<section><h3>${escapeHtml4(item.label)}</h3>${item.content.map((content) => `<p>${escapeHtml4(content)}</p>`).join("")}</section>`).join("")}</div>`;
1116
+ }
1117
+ if (block.type === "employment") {
1118
+ return `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`;
1119
+ }
1120
+ const tag = block.ordered ? "ol" : "ul";
1121
+ return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
1122
+ }
936
1123
  function semanticBlockY2(block) {
937
1124
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
938
1125
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -945,15 +1132,15 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
945
1132
  span.fontFamily,
946
1133
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
947
1134
  ).join(";");
948
- const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
949
- const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number2(span.strokeWidth ?? 0)}` : "";
1135
+ const stroke = isCssHexColor2(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
1136
+ const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number3(span.strokeWidth ?? 0)}` : "";
950
1137
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
951
- const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
952
- const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number2(span.strokeOpacity)}` : "";
1138
+ const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
1139
+ const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
953
1140
  const style = [
954
1141
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
955
1142
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
956
- strokeOnly ? "fill:none" : isCssHexColor(span.color) ? `fill:${span.color}` : "",
1143
+ strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
957
1144
  stroke,
958
1145
  strokeWidth,
959
1146
  fillOpacity,
@@ -961,7 +1148,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
961
1148
  font
962
1149
  ].filter(Boolean).join(";");
963
1150
  const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
964
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number2(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1151
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
965
1152
  const transform = counterRotateReflectedText && span.transform ? [
966
1153
  span.transform[0],
967
1154
  span.transform[1],
@@ -974,8 +1161,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
974
1161
  const basisY = transform?.[1] ?? 0;
975
1162
  const anchorX = span.bounds.x + basisX * rtlOffset;
976
1163
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
977
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
978
- return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
1164
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1165
+ return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
979
1166
  }
980
1167
  function isAdobeCjkFont(fontFamily) {
981
1168
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -987,16 +1174,16 @@ function visualType3Text(span, font, pageHeight) {
987
1174
  const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
988
1175
  if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
989
1176
  const transform = span.transform ?? [1, 0, 0, 1];
990
- const outer = `matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(pageHeight - span.bounds.y)})`;
1177
+ const outer = `matrix(${transform.map(number3).join(" ")} ${number3(span.bounds.x)} ${number3(pageHeight - span.bounds.y)})`;
991
1178
  const xScale = span.bounds.width / totalAdvance;
992
1179
  let offset = 0;
993
1180
  let content = "";
994
1181
  for (const glyph of sequence) {
995
1182
  if (!glyph) continue;
996
- content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
1183
+ content += `<g transform="translate(${number3(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
997
1184
  offset += glyph.advance;
998
1185
  }
999
- return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
1186
+ return `<g transform="${outer}"><g transform="scale(${number3(xScale)} ${number3(-span.fontSize)})">${content}</g></g>`;
1000
1187
  }
1001
1188
  function isHebrewPaintOrder(span) {
1002
1189
  return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
@@ -1007,30 +1194,27 @@ function usesSpacingAdjustment(span) {
1007
1194
  function type3Glyph(glyph, textColor) {
1008
1195
  let output = "";
1009
1196
  for (const fill of glyph.fills ?? []) {
1010
- const color = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : fill.color;
1011
- if (!isCssHexColor(color)) continue;
1012
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
1013
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
1197
+ const color = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : fill.color;
1198
+ if (!isCssHexColor2(color)) continue;
1199
+ const points = fill.points.map(([x, y]) => `${number3(x)},${number3(y)}`).join(" ");
1200
+ const opacity = isUnitInterval2(fill.opacity) ? ` fill-opacity="${number3(fill.opacity)}"` : "";
1014
1201
  output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
1015
1202
  }
1016
1203
  for (const path of glyph.paths ?? []) {
1017
1204
  if (!isSvgPath(path.d)) continue;
1018
- const fill = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : isCssHexColor(path.fill) ? path.fill : "none";
1019
- const stroke = glyph.usesTextColor && isCssHexColor(textColor) && path.stroke ? textColor : isCssHexColor(path.stroke) ? path.stroke : "none";
1020
- const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
1205
+ const fill = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : isCssHexColor2(path.fill) ? path.fill : "none";
1206
+ const stroke = glyph.usesTextColor && isCssHexColor2(textColor) && path.stroke ? textColor : isCssHexColor2(path.stroke) ? path.stroke : "none";
1207
+ const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number3(path.strokeWidth)}"` : "";
1021
1208
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
1022
1209
  }
1023
1210
  return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
1024
1211
  }
1025
- function isCssHexColor(value) {
1212
+ function isCssHexColor2(value) {
1026
1213
  return /^#[\da-f]{6}$/i.test(value ?? "");
1027
1214
  }
1028
- function isUnitInterval(value) {
1215
+ function isUnitInterval2(value) {
1029
1216
  return Number.isFinite(value) && (value ?? -1) >= 0 && (value ?? 2) <= 1;
1030
1217
  }
1031
- function isSvgPath(value) {
1032
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
1033
- }
1034
1218
  function isMonospace(fontFamily) {
1035
1219
  return /courier|mono/i.test(fontFamily ?? "");
1036
1220
  }
@@ -1048,7 +1232,7 @@ function directionAttribute(spans) {
1048
1232
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
1049
1233
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
1050
1234
  }
1051
- function number2(value) {
1235
+ function number3(value) {
1052
1236
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
1053
1237
  }
1054
1238
  function escapeAttribute(value) {