@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.cjs CHANGED
@@ -27,6 +27,64 @@ __export(index_exports, {
27
27
  module.exports = __toCommonJS(index_exports);
28
28
  var import_structure2 = require("@boxpdf/reader/structure");
29
29
 
30
+ // src/semantic-caption.ts
31
+ function isClearMediaCaption(media, block, pageWidth, pageHeight, pageLines) {
32
+ if (block.type !== "paragraph" || block.lines.length === 0) return false;
33
+ const bounds2 = unionLines(block.lines);
34
+ const lineHeight = median(block.lines.map((line) => line.bounds.height));
35
+ if (media.bounds.width < pageWidth * 0.2 || media.bounds.height < lineHeight * 10) return false;
36
+ if (media.bounds.x < -2 || media.bounds.y < -2 || media.bounds.x + media.bounds.width > pageWidth + 2 || media.bounds.y + media.bounds.height > pageHeight + 2)
37
+ return false;
38
+ const gap = media.bounds.y - (bounds2.y + bounds2.height);
39
+ if (gap < -lineHeight * 0.15 || gap > lineHeight * 1.25) return false;
40
+ const mediaCenter = media.bounds.x + media.bounds.width / 2;
41
+ const captionCenter = bounds2.x + bounds2.width / 2;
42
+ if (Math.abs(mediaCenter - captionCenter) > Math.max(3, media.bounds.width * 0.03)) return false;
43
+ if (bounds2.width < media.bounds.width * 0.45 || bounds2.width > media.bounds.width * 1.06) {
44
+ return false;
45
+ }
46
+ const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
47
+ if (!first) return false;
48
+ const otherLines = pageLines.filter((line) => !block.lines.includes(line));
49
+ return fontSignature(first) !== dominantFontSignature(otherLines);
50
+ }
51
+ function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
52
+ const associations = /* @__PURE__ */ new Map();
53
+ for (const item of media) {
54
+ const candidates = blocks.filter((block) => isClearMediaCaption(item, block, pageWidth, pageHeight, pageLines)).filter((block) => !associations.has(block)).sort((left, right) => captionGap(item, left) - captionGap(item, right));
55
+ const caption = candidates[0];
56
+ if (caption) associations.set(caption, item);
57
+ }
58
+ return associations;
59
+ }
60
+ function captionGap(media, block) {
61
+ if (block.type !== "paragraph") return Number.POSITIVE_INFINITY;
62
+ const bounds2 = unionLines(block.lines);
63
+ return Math.abs(media.bounds.y - bounds2.y - bounds2.height);
64
+ }
65
+ function unionLines(lines) {
66
+ const x = Math.min(...lines.map((line) => line.bounds.x));
67
+ const y = Math.min(...lines.map((line) => line.bounds.y));
68
+ const right = Math.max(...lines.map((line) => line.bounds.x + line.bounds.width));
69
+ const top = Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
70
+ return { x, y, width: right - x, height: top - y };
71
+ }
72
+ function dominantFontSignature(lines) {
73
+ const counts = /* @__PURE__ */ new Map();
74
+ for (const span of lines.flatMap((line) => line.spans)) {
75
+ const signature = fontSignature(span);
76
+ counts.set(signature, (counts.get(signature) ?? 0) + Math.max(1, [...span.text].length));
77
+ }
78
+ return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "";
79
+ }
80
+ function fontSignature(span) {
81
+ return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
82
+ }
83
+ function median(values) {
84
+ const ordered = [...values].sort((left, right) => left - right);
85
+ return ordered[Math.floor(ordered.length / 2)] ?? 1;
86
+ }
87
+
30
88
  // src/semantic-document.ts
31
89
  var import_structure = require("@boxpdf/reader/structure");
32
90
 
@@ -99,6 +157,85 @@ function escapeHtml(value) {
99
157
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
100
158
  }
101
159
 
160
+ // src/vector-svg.ts
161
+ function vectorFillSvg(fill) {
162
+ if (!isCssHexColor(fill.color)) return "";
163
+ const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
164
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
165
+ return `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
166
+ }
167
+ function vectorPathSvg(path, pageNumber, pathIndex) {
168
+ if (!isSvgPath(path.d)) return "";
169
+ const fill = isCssHexColor(path.fill) ? path.fill : "none";
170
+ const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
171
+ const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
172
+ const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
173
+ const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
174
+ const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
175
+ const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
176
+ const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
177
+ const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
178
+ const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
179
+ let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${dashoffset}${linecap}${linejoin}${rule}/>`;
180
+ for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
181
+ output = `<g clip-path="url(#${vectorPathClipId(pageNumber, pathIndex, index)})">${output}</g>`;
182
+ }
183
+ return output;
184
+ }
185
+ function vectorPathClipDefinitions(paths, pageNumber) {
186
+ return paths.flatMap(
187
+ ({ path, index: pathIndex }) => (path.clips ?? []).map((clip, clipIndex) => {
188
+ if (!isSvgPath(clip.d)) return "";
189
+ const rule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
190
+ return `<clipPath id="${vectorPathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${rule}/></clipPath>`;
191
+ })
192
+ ).join("");
193
+ }
194
+ function vectorPathBounds(path) {
195
+ if (!isSvgPath(path.d)) return void 0;
196
+ const values = [...path.d.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
197
+ (match) => Number(match[0])
198
+ );
199
+ if (values.length < 2) return void 0;
200
+ const xs = [];
201
+ const ys = [];
202
+ for (let index = 0; index + 1 < values.length; index += 2) {
203
+ xs.push(values[index] ?? 0);
204
+ ys.push(values[index + 1] ?? 0);
205
+ }
206
+ return bounds(xs, ys);
207
+ }
208
+ function vectorFillBounds(fill) {
209
+ if (fill.points.length === 0) return void 0;
210
+ return bounds(
211
+ fill.points.map(([x]) => x),
212
+ fill.points.map(([, y]) => y)
213
+ );
214
+ }
215
+ function isSvgPath(value) {
216
+ return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
217
+ }
218
+ function bounds(xs, ys) {
219
+ const x = Math.min(...xs);
220
+ const y = Math.min(...ys);
221
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
222
+ }
223
+ function vectorPathClipId(pageNumber, pathIndex, clipIndex) {
224
+ return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
225
+ }
226
+ function isCssHexColor(value) {
227
+ return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value);
228
+ }
229
+ function finiteNonnegative(value) {
230
+ return value !== void 0 && Number.isFinite(value) && value >= 0;
231
+ }
232
+ function isUnitInterval(value) {
233
+ return value !== void 0 && Number.isFinite(value) && value >= 0 && value <= 1;
234
+ }
235
+ function number(value) {
236
+ return Number(value.toFixed(4)).toString();
237
+ }
238
+
102
239
  // src/visual-font.ts
103
240
  function visualFontAliases(pageNumber, fonts) {
104
241
  return new Map(
@@ -148,46 +285,90 @@ function base64(bytes) {
148
285
  // src/semantic-media.ts
149
286
  function semanticMedia(page) {
150
287
  const output = (page.images ?? []).map((image) => rasterMedia(image));
151
- const vector = vectorMedia(page);
152
- if (vector) output.push(vector);
288
+ output.push(...vectorMedia(page));
153
289
  return output.sort((left, right) => right.bounds.y - left.bounds.y);
154
290
  }
155
291
  function rasterMedia(image) {
156
- const bounds = transformedUnitBounds(image.transform);
292
+ const bounds2 = transformedUnitBounds(image.transform);
157
293
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
158
294
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
159
- const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
295
+ const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
160
296
  return {
161
- bounds,
162
- 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}">`
297
+ bounds: bounds2,
298
+ html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`
163
299
  };
164
300
  }
165
301
  function vectorMedia(page) {
166
- const paths = (page.paths ?? []).filter((path) => safePath(path.d));
167
- const fills = page.fills ?? [];
168
- const bounds = unionBounds([
169
- ...paths.map((path) => pathBounds(path.d)),
170
- ...fills.map(fillBounds)
171
- ]);
172
- if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
302
+ const primitives = [
303
+ ...(page.paths ?? []).flatMap((path, index) => {
304
+ const bounds2 = vectorPathBounds(path);
305
+ return bounds2 ? [{ type: "path", value: path, index, bounds: bounds2 }] : [];
306
+ }),
307
+ ...(page.fills ?? []).flatMap((fill) => {
308
+ const bounds2 = vectorFillBounds(fill);
309
+ return bounds2 && !isPageBackground(fill, bounds2, page) ? [{ type: "fill", value: fill, bounds: bounds2 }] : [];
310
+ })
311
+ ];
312
+ const components = vectorComponents(primitives, Math.min(36, page.width * 0.06)).filter(
313
+ (component) => component.primitives.length >= 2 || component.bounds.width * component.bounds.height >= page.width * page.height * 2e-3
314
+ );
173
315
  const aliases = visualFontAliases(page.number, page.fonts ?? []);
174
316
  const visualCodeFonts = new Set(
175
317
  (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
176
318
  );
177
- const visualSpans = page.visualSpans ?? page.spans;
178
- const overlay = visualSpans.filter(
179
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
180
- );
181
- const consumedSpans = page.spans.filter(
182
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
183
- );
184
- const fontIds = new Set(overlay.map((span) => span.fontAssetId));
185
- const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
186
- return {
187
- bounds,
188
- 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>`,
189
- ...consumedSpans.length > 0 ? { consumedSpans } : {}
190
- };
319
+ return components.map((component) => {
320
+ const bounds2 = component.bounds;
321
+ const paths = component.primitives.flatMap(
322
+ (primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
323
+ );
324
+ const fills = component.primitives.flatMap(
325
+ (primitive) => primitive.type === "fill" ? [primitive.value] : []
326
+ );
327
+ const visualSpans = page.visualSpans ?? page.spans;
328
+ const overlay = visualSpans.filter(
329
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
330
+ );
331
+ const consumedSpans = page.spans.filter(
332
+ (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
333
+ );
334
+ const fontIds = new Set(overlay.map((span) => span.fontAssetId));
335
+ const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
336
+ return {
337
+ bounds: bounds2,
338
+ html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number2(bounds2.x)} ${number2(page.height - bounds2.y - bounds2.height)} ${number2(bounds2.width)} ${number2(bounds2.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}${paths.length ? `<defs>${vectorPathClipDefinitions(paths, page.number)}</defs>` : ""}<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">${fills.map(vectorFillSvg).join("") + paths.map(({ path, index }) => vectorPathSvg(path, page.number, index)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`,
339
+ ...consumedSpans.length > 0 ? { consumedSpans } : {}
340
+ };
341
+ });
342
+ }
343
+ function vectorComponents(primitives, padding) {
344
+ const components = [];
345
+ for (const primitive of primitives) {
346
+ const matches = components.filter(
347
+ (component) => nearby(component.bounds, primitive.bounds, padding)
348
+ );
349
+ if (matches.length === 0) {
350
+ components.push({ bounds: primitive.bounds, primitives: [primitive] });
351
+ continue;
352
+ }
353
+ const target = matches[0];
354
+ target.primitives.push(primitive);
355
+ target.bounds = unionBounds([target.bounds, primitive.bounds]);
356
+ for (const component of matches.slice(1)) {
357
+ target.primitives.push(...component.primitives);
358
+ target.bounds = unionBounds([target.bounds, component.bounds]);
359
+ components.splice(components.indexOf(component), 1);
360
+ }
361
+ }
362
+ return components;
363
+ }
364
+ function nearby(left, right, padding) {
365
+ 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);
366
+ }
367
+ function isPageBackground(fill, bounds2, page) {
368
+ if (!/^#f{6}$/i.test(fill.color)) return false;
369
+ const outside = bounds2.x < 0 || bounds2.y < 0 || bounds2.x + bounds2.width > page.width || bounds2.y + bounds2.height > page.height;
370
+ const large = bounds2.width * bounds2.height > page.width * page.height * 0.2;
371
+ return outside || large;
191
372
  }
192
373
  function withoutSemanticMediaSpans(page, media) {
193
374
  const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
@@ -197,7 +378,7 @@ function vectorText(span, pageHeight, aliases) {
197
378
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
198
379
  const styles2 = [
199
380
  cssColor(span.color) ? `fill:${span.color}` : "",
200
- unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
381
+ unitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "",
201
382
  ...visualFontStyles(
202
383
  span.fontFamily,
203
384
  span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
@@ -205,33 +386,16 @@ function vectorText(span, pageHeight, aliases) {
205
386
  ].filter(Boolean).join(";");
206
387
  const anchorY = pageHeight - span.bounds.y;
207
388
  const transform = span.transform;
208
- 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)}"`;
389
+ 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)}"`;
209
390
  const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
210
- const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
211
- return `<text${position} font-size="${number(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
391
+ const length = extent > 0 ? ` textLength="${number2(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
392
+ return `<text${position} font-size="${number2(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
212
393
  }
213
394
  function centerInside(inner, outer) {
214
395
  const x = inner.x + inner.width / 2;
215
396
  const y = inner.y + inner.height / 2;
216
397
  return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
217
398
  }
218
- function vectorFill(fill) {
219
- const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
220
- const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
221
- return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
222
- }
223
- function vectorPath(path) {
224
- const fill = cssColor(path.fill) ? path.fill : "none";
225
- const stroke = cssColor(path.stroke) ? path.stroke : "none";
226
- const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
227
- const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
228
- const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
229
- const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
230
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
231
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
232
- const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
233
- return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
234
- }
235
399
  function transformedUnitBounds([a, b, c, d, e, f]) {
236
400
  const points = [
237
401
  [e, f],
@@ -245,31 +409,8 @@ function transformedUnitBounds([a, b, c, d, e, f]) {
245
409
  const minY = Math.min(...ys);
246
410
  return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
247
411
  }
248
- function pathBounds(path) {
249
- const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
250
- (match) => Number(match[0])
251
- );
252
- if (values.length < 2) return void 0;
253
- const xs = [];
254
- const ys = [];
255
- for (let index = 0; index + 1 < values.length; index += 2) {
256
- xs.push(values[index] ?? 0);
257
- ys.push(values[index + 1] ?? 0);
258
- }
259
- const minX = Math.min(...xs);
260
- const minY = Math.min(...ys);
261
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
262
- }
263
- function fillBounds(fill) {
264
- if (fill.points.length === 0) return void 0;
265
- const xs = fill.points.map(([x]) => x);
266
- const ys = fill.points.map(([, y]) => y);
267
- const minX = Math.min(...xs);
268
- const minY = Math.min(...ys);
269
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
270
- }
271
- function unionBounds(bounds) {
272
- const values = bounds.filter((value) => Boolean(value));
412
+ function unionBounds(bounds2) {
413
+ const values = bounds2.filter((value) => Boolean(value));
273
414
  if (values.length === 0) return void 0;
274
415
  const x = Math.min(...values.map((value) => value.x));
275
416
  const y = Math.min(...values.map((value) => value.y));
@@ -300,19 +441,16 @@ function rgbBmp(image) {
300
441
  }
301
442
  return output;
302
443
  }
303
- function safePath(value) {
304
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
305
- }
306
444
  function cssColor(value) {
307
445
  return /^#[\da-f]{6}$/i.test(value ?? "");
308
446
  }
309
- function finiteNonnegative(value) {
447
+ function finiteNonnegative2(value) {
310
448
  return Number.isFinite(value) && (value ?? -1) >= 0;
311
449
  }
312
450
  function unitInterval(value) {
313
- return finiteNonnegative(value) && value <= 1;
451
+ return finiteNonnegative2(value) && value <= 1;
314
452
  }
315
- function number(value) {
453
+ function number2(value) {
316
454
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
317
455
  }
318
456
  function escapeHtml2(value) {
@@ -364,18 +502,38 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
364
502
  const emitPage = async (page, future) => {
365
503
  const defaultColor = dominantTextColor(page.structured.lines);
366
504
  let mediaIndex = 0;
505
+ const captions = clearMediaCaptionAssociations(
506
+ page.media,
507
+ page.structured.blocks,
508
+ page.width,
509
+ page.height,
510
+ page.structured.lines
511
+ );
512
+ const captionedMedia = new Set(captions.values());
367
513
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
368
514
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
369
515
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
370
516
  const nextBlock = page.structured.blocks[blockIndex + 1];
371
517
  const blockY = semanticBlockY(block);
518
+ let emittedAsCaption = false;
372
519
  while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
373
520
  await flushPendingParagraph();
374
- const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
521
+ const item = page.media[mediaIndex];
522
+ if (item && captions.get(block) === item && block.type === "paragraph") {
523
+ const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
524
+ if (activeTable) pendingMedia.push(html2);
525
+ else await write(html2);
526
+ mediaIndex += 1;
527
+ emittedAsCaption = true;
528
+ break;
529
+ }
530
+ if (item && captionedMedia.has(item)) break;
531
+ const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
375
532
  if (activeTable) pendingMedia.push(html);
376
533
  else await write(html);
377
534
  mediaIndex += 1;
378
535
  }
536
+ if (emittedAsCaption) continue;
379
537
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
380
538
  stats.suppressedFurniture += 1;
381
539
  continue;
@@ -606,6 +764,14 @@ function financialSummaryRow(entry, columns) {
606
764
  return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
607
765
  }
608
766
  function semanticBlockHtml(block, defaultColor = "#000000") {
767
+ if (block.type === "insetGroup") {
768
+ return `<div class="pdf-semantic-inset" style="margin-inline-start:${block.indentEm}em">${block.blocks.map((item) => semanticBlockHtml(item, defaultColor)).join("")}</div>`;
769
+ }
770
+ if (block.type === "table") {
771
+ const rows = (0, import_structure.tableToRows)(block.table);
772
+ const header = tableHeader(rows);
773
+ return `<table>${rows.map((row, index) => tableRow(row, Boolean(header && index === 0))).join("")}</table>`;
774
+ }
609
775
  if (block.type === "heading")
610
776
  return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
611
777
  if (block.type === "paragraph")
@@ -721,7 +887,7 @@ async function writePositionedPage(page, write, options) {
721
887
  const displayWidth = quarterTurn ? page.height : page.width;
722
888
  const displayHeight = quarterTurn ? page.width : page.height;
723
889
  await write(
724
- `<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">`
890
+ `<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">`
725
891
  );
726
892
  const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
727
893
  const type3Fonts = new Map(
@@ -733,44 +899,26 @@ async function writePositionedPage(page, write, options) {
733
899
  );
734
900
  }
735
901
  await write(
736
- `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
902
+ `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
737
903
  );
738
904
  await write(
739
- `<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)}">`
905
+ `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
906
+ );
907
+ const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
908
+ (page.paths ?? []).map((path, index) => ({ path, index })),
909
+ page.number
740
910
  );
741
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
742
911
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
743
912
  if (reflectedOverlay) {
744
913
  for (const [index, image] of (page.images ?? []).entries()) {
745
914
  await write(visualImage(image, page.height, page.number, index));
746
915
  }
747
916
  }
748
- for (const fill of page.fills ?? []) {
749
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
750
- if (isCssHexColor(fill.color)) {
751
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
752
- await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
753
- }
754
- }
755
- if (page.paths?.length) {
756
- await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
757
- for (const [pathIndex, path] of page.paths.entries()) {
758
- if (!isSvgPath(path.d)) continue;
759
- const fill = isCssHexColor(path.fill) ? path.fill : "none";
760
- const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
761
- const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
762
- const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
763
- const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
764
- const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
765
- const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
766
- const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
767
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
768
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
769
- let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
770
- for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
771
- output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
772
- }
773
- await write(output);
917
+ if (page.fills?.length || page.paths?.length) {
918
+ await write(`<g transform="translate(0 ${number3(page.height)}) scale(1 -1)">`);
919
+ for (const fill of page.fills ?? []) await write(vectorFillSvg(fill));
920
+ for (const [pathIndex, path] of (page.paths ?? []).entries()) {
921
+ await write(vectorPathSvg(path, page.number, pathIndex));
774
922
  }
775
923
  await write("</g>");
776
924
  }
@@ -800,8 +948,8 @@ function usesReflectedVisualOverlay(page, spans) {
800
948
  }
801
949
  function visualImage(image, pageHeight, pageNumber, imageIndex) {
802
950
  const [a, b, c, d, e, f] = image.transform;
803
- const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
804
- const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
951
+ const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
952
+ const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
805
953
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
806
954
  const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
807
955
  let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
@@ -815,25 +963,13 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
815
963
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
816
964
  if (!isSvgPath(clip.d)) return "";
817
965
  const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
818
- return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number2(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
966
+ return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number3(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
819
967
  })
820
968
  ).join("");
821
969
  }
822
970
  function imageClipId(pageNumber, imageIndex, clipIndex) {
823
971
  return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
824
972
  }
825
- function pathClipDefinitions(paths, pageNumber) {
826
- return paths.flatMap(
827
- (path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
828
- if (!isSvgPath(clip.d)) return "";
829
- const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
830
- return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
831
- })
832
- ).join("");
833
- }
834
- function pathClipId(pageNumber, pathIndex, clipIndex) {
835
- return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
836
- }
837
973
  function rgbBmp2(image) {
838
974
  const stride = Math.ceil(image.width * 3 / 4) * 4;
839
975
  const output = new Uint8Array(54 + stride * image.height);
@@ -862,11 +998,11 @@ function rgbBmp2(image) {
862
998
  function rotationTransform(page) {
863
999
  switch (page.rotate) {
864
1000
  case 90:
865
- return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
1001
+ return `;transform:translate(${number3(page.height)}pt,0) rotate(90deg)`;
866
1002
  case 180:
867
- return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
1003
+ return `;transform:translate(${number3(page.width)}pt,${number3(page.height)}pt) rotate(180deg)`;
868
1004
  case 270:
869
- return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
1005
+ return `;transform:translate(0,${number3(page.width)}pt) rotate(270deg)`;
870
1006
  default:
871
1007
  return "";
872
1008
  }
@@ -874,13 +1010,13 @@ function rotationTransform(page) {
874
1010
  function positionedSpan(span, fontAliases) {
875
1011
  const direction = directionAttribute([span]);
876
1012
  const style = [
877
- `left:${number2(span.bounds.x)}pt`,
878
- `bottom:${number2(span.bounds.y)}pt`,
879
- `width:${number2(span.bounds.width)}pt`,
880
- `height:${number2(span.bounds.height)}pt`,
881
- `font-size:${number2(span.fontSize)}pt`,
882
- ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
883
- ...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
1013
+ `left:${number3(span.bounds.x)}pt`,
1014
+ `bottom:${number3(span.bounds.y)}pt`,
1015
+ `width:${number3(span.bounds.width)}pt`,
1016
+ `height:${number3(span.bounds.height)}pt`,
1017
+ `font-size:${number3(span.fontSize)}pt`,
1018
+ ...isCssHexColor2(span.color) ? [`color:${span.color}`] : [],
1019
+ ...isUnitInterval2(span.fillOpacity) ? [`opacity:${number3(span.fillOpacity)}`] : [],
884
1020
  ...visualFontStyles(
885
1021
  span.fontFamily,
886
1022
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -893,15 +1029,35 @@ async function writeFlowPage(page, write) {
893
1029
  const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
894
1030
  const defaultColor = dominantTextColor(structured.lines);
895
1031
  let mediaIndex = 0;
1032
+ const captions = clearMediaCaptionAssociations(
1033
+ media,
1034
+ structured.blocks,
1035
+ page.width,
1036
+ page.height,
1037
+ structured.lines
1038
+ );
1039
+ const captionedMedia = new Set(captions.values());
896
1040
  await write(
897
1041
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
898
1042
  );
899
1043
  for (const block of structured.blocks) {
900
1044
  const blockY = semanticBlockY2(block);
1045
+ let emittedAsCaption = false;
901
1046
  while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
902
- await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
1047
+ const item = media[mediaIndex];
1048
+ if (item && captions.get(block) === item && block.type === "paragraph") {
1049
+ await write(
1050
+ `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
1051
+ );
1052
+ mediaIndex += 1;
1053
+ emittedAsCaption = true;
1054
+ break;
1055
+ }
1056
+ if (item && captionedMedia.has(item)) break;
1057
+ await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
903
1058
  mediaIndex += 1;
904
1059
  }
1060
+ if (emittedAsCaption) continue;
905
1061
  if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
906
1062
  else if (block.type === "heading") {
907
1063
  await write(
@@ -941,6 +1097,10 @@ async function writeFlowPage(page, write) {
941
1097
  await write(
942
1098
  `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
943
1099
  );
1100
+ } else if (block.type === "insetGroup") {
1101
+ await write(
1102
+ `<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`
1103
+ );
944
1104
  } else {
945
1105
  const tag = block.ordered ? "ol" : "ul";
946
1106
  await write(`<${tag}>`);
@@ -956,6 +1116,33 @@ async function writeFlowPage(page, write) {
956
1116
  }
957
1117
  await write("</section>");
958
1118
  }
1119
+ function nestedSemanticBlockHtml(block, defaultColor) {
1120
+ if (block.type === "insetGroup") {
1121
+ return `<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`;
1122
+ }
1123
+ if (block.type === "table") return (0, import_structure2.tableToHtml)(block.table);
1124
+ if (block.type === "heading") {
1125
+ return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
1126
+ }
1127
+ if (block.type === "paragraph") {
1128
+ return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
1129
+ }
1130
+ if (block.type === "preformatted") return `<pre>${escapeHtml4(block.text)}</pre>`;
1131
+ if (block.type === "definitionList") {
1132
+ return `<dl>${block.entries.map((entry) => `<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`).join("")}</dl>`;
1133
+ }
1134
+ if (block.type === "cardList") {
1135
+ 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>`;
1136
+ }
1137
+ if (block.type === "sectionGroup") {
1138
+ 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>`;
1139
+ }
1140
+ if (block.type === "employment") {
1141
+ return `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`;
1142
+ }
1143
+ const tag = block.ordered ? "ol" : "ul";
1144
+ return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
1145
+ }
959
1146
  function semanticBlockY2(block) {
960
1147
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
961
1148
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -968,15 +1155,15 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
968
1155
  span.fontFamily,
969
1156
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
970
1157
  ).join(";");
971
- const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
972
- const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number2(span.strokeWidth ?? 0)}` : "";
1158
+ const stroke = isCssHexColor2(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
1159
+ const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number3(span.strokeWidth ?? 0)}` : "";
973
1160
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
974
- const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
975
- const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number2(span.strokeOpacity)}` : "";
1161
+ const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
1162
+ const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
976
1163
  const style = [
977
1164
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
978
1165
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
979
- strokeOnly ? "fill:none" : isCssHexColor(span.color) ? `fill:${span.color}` : "",
1166
+ strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
980
1167
  stroke,
981
1168
  strokeWidth,
982
1169
  fillOpacity,
@@ -984,7 +1171,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
984
1171
  font
985
1172
  ].filter(Boolean).join(";");
986
1173
  const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
987
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number2(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
1174
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
988
1175
  const transform = counterRotateReflectedText && span.transform ? [
989
1176
  span.transform[0],
990
1177
  span.transform[1],
@@ -997,8 +1184,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
997
1184
  const basisY = transform?.[1] ?? 0;
998
1185
  const anchorX = span.bounds.x + basisX * rtlOffset;
999
1186
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
1000
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
1001
- return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
1187
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
1188
+ return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
1002
1189
  }
1003
1190
  function isAdobeCjkFont(fontFamily) {
1004
1191
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -1010,16 +1197,16 @@ function visualType3Text(span, font, pageHeight) {
1010
1197
  const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
1011
1198
  if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
1012
1199
  const transform = span.transform ?? [1, 0, 0, 1];
1013
- const outer = `matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(pageHeight - span.bounds.y)})`;
1200
+ const outer = `matrix(${transform.map(number3).join(" ")} ${number3(span.bounds.x)} ${number3(pageHeight - span.bounds.y)})`;
1014
1201
  const xScale = span.bounds.width / totalAdvance;
1015
1202
  let offset = 0;
1016
1203
  let content = "";
1017
1204
  for (const glyph of sequence) {
1018
1205
  if (!glyph) continue;
1019
- content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
1206
+ content += `<g transform="translate(${number3(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
1020
1207
  offset += glyph.advance;
1021
1208
  }
1022
- return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
1209
+ return `<g transform="${outer}"><g transform="scale(${number3(xScale)} ${number3(-span.fontSize)})">${content}</g></g>`;
1023
1210
  }
1024
1211
  function isHebrewPaintOrder(span) {
1025
1212
  return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
@@ -1030,30 +1217,27 @@ function usesSpacingAdjustment(span) {
1030
1217
  function type3Glyph(glyph, textColor) {
1031
1218
  let output = "";
1032
1219
  for (const fill of glyph.fills ?? []) {
1033
- const color = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : fill.color;
1034
- if (!isCssHexColor(color)) continue;
1035
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
1036
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
1220
+ const color = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : fill.color;
1221
+ if (!isCssHexColor2(color)) continue;
1222
+ const points = fill.points.map(([x, y]) => `${number3(x)},${number3(y)}`).join(" ");
1223
+ const opacity = isUnitInterval2(fill.opacity) ? ` fill-opacity="${number3(fill.opacity)}"` : "";
1037
1224
  output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
1038
1225
  }
1039
1226
  for (const path of glyph.paths ?? []) {
1040
1227
  if (!isSvgPath(path.d)) continue;
1041
- const fill = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : isCssHexColor(path.fill) ? path.fill : "none";
1042
- const stroke = glyph.usesTextColor && isCssHexColor(textColor) && path.stroke ? textColor : isCssHexColor(path.stroke) ? path.stroke : "none";
1043
- const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
1228
+ const fill = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : isCssHexColor2(path.fill) ? path.fill : "none";
1229
+ const stroke = glyph.usesTextColor && isCssHexColor2(textColor) && path.stroke ? textColor : isCssHexColor2(path.stroke) ? path.stroke : "none";
1230
+ const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number3(path.strokeWidth)}"` : "";
1044
1231
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
1045
1232
  }
1046
1233
  return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
1047
1234
  }
1048
- function isCssHexColor(value) {
1235
+ function isCssHexColor2(value) {
1049
1236
  return /^#[\da-f]{6}$/i.test(value ?? "");
1050
1237
  }
1051
- function isUnitInterval(value) {
1238
+ function isUnitInterval2(value) {
1052
1239
  return Number.isFinite(value) && (value ?? -1) >= 0 && (value ?? 2) <= 1;
1053
1240
  }
1054
- function isSvgPath(value) {
1055
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
1056
- }
1057
1241
  function isMonospace(fontFamily) {
1058
1242
  return /courier|mono/i.test(fontFamily ?? "");
1059
1243
  }
@@ -1071,7 +1255,7 @@ function directionAttribute(spans) {
1071
1255
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
1072
1256
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
1073
1257
  }
1074
- function number2(value) {
1258
+ function number3(value) {
1075
1259
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
1076
1260
  }
1077
1261
  function escapeAttribute(value) {