@boxpdf/html-writer 0.1.16 → 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,
@@ -16,7 +74,7 @@ function dominantTextColor(lines) {
16
74
  }
17
75
  return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
18
76
  }
19
- function semanticTextHtml(text, lines, defaultColor) {
77
+ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
20
78
  const ranges = [];
21
79
  let cursor = 0;
22
80
  for (const span of lines.flatMap((line) => line.spans)) {
@@ -25,14 +83,25 @@ function semanticTextHtml(text, lines, defaultColor) {
25
83
  if (start < 0) continue;
26
84
  cursor = start + span.text.length;
27
85
  const color = normalizedColor(span.color);
28
- if (color && color !== defaultColor) ranges.push({ start, end: cursor, color });
86
+ const bold = preserveWeight && /(?:bold|semibold|demi|medium|medi)/i.test(span.fontFamily ?? "");
87
+ const italic = /(?:italic|oblique|slanted|slant|ital)/i.test(span.fontFamily ?? "");
88
+ const nondefaultColor = color && color !== defaultColor ? color : void 0;
89
+ if (nondefaultColor || bold || italic) {
90
+ ranges.push({
91
+ start,
92
+ end: cursor,
93
+ ...nondefaultColor ? { color: nondefaultColor } : {},
94
+ bold,
95
+ italic
96
+ });
97
+ }
29
98
  }
30
99
  const merged = mergeRanges(ranges, text);
31
100
  let html = "";
32
101
  let offset = 0;
33
102
  for (const range of merged) {
34
103
  html += escapeHtml(text.slice(offset, range.start));
35
- html += `<span style="color:${range.color}">${escapeHtml(text.slice(range.start, range.end))}</span>`;
104
+ html += styledHtml(text.slice(range.start, range.end), range);
36
105
  offset = range.end;
37
106
  }
38
107
  return html + escapeHtml(text.slice(offset));
@@ -41,7 +110,7 @@ function mergeRanges(ranges, text) {
41
110
  const merged = [];
42
111
  for (const range of ranges) {
43
112
  const previous = merged.at(-1);
44
- if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
113
+ if (previous && previous.color === range.color && previous.bold === range.bold && previous.italic === range.italic && /^\s*$/.test(text.slice(previous.end, range.start))) {
45
114
  previous.end = range.end;
46
115
  } else {
47
116
  merged.push({ ...range });
@@ -49,6 +118,13 @@ function mergeRanges(ranges, text) {
49
118
  }
50
119
  return merged;
51
120
  }
121
+ function styledHtml(value, range) {
122
+ let html = escapeHtml(value);
123
+ if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
124
+ if (range.italic) html = `<em>${html}</em>`;
125
+ if (range.bold) html = `<strong>${html}</strong>`;
126
+ return html;
127
+ }
52
128
  function normalizedColor(value) {
53
129
  if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
54
130
  const color = value.toLowerCase();
@@ -58,6 +134,85 @@ function escapeHtml(value) {
58
134
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
59
135
  }
60
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
+
61
216
  // src/visual-font.ts
62
217
  function visualFontAliases(pageNumber, fonts) {
63
218
  return new Map(
@@ -107,46 +262,90 @@ function base64(bytes) {
107
262
  // src/semantic-media.ts
108
263
  function semanticMedia(page) {
109
264
  const output = (page.images ?? []).map((image) => rasterMedia(image));
110
- const vector = vectorMedia(page);
111
- if (vector) output.push(vector);
265
+ output.push(...vectorMedia(page));
112
266
  return output.sort((left, right) => right.bounds.y - left.bounds.y);
113
267
  }
114
268
  function rasterMedia(image) {
115
- const bounds = transformedUnitBounds(image.transform);
269
+ const bounds2 = transformedUnitBounds(image.transform);
116
270
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
117
271
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
118
- const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
272
+ const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
119
273
  return {
120
- bounds,
121
- 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}">`
122
276
  };
123
277
  }
124
278
  function vectorMedia(page) {
125
- const paths = (page.paths ?? []).filter((path) => safePath(path.d));
126
- const fills = page.fills ?? [];
127
- const bounds = unionBounds([
128
- ...paths.map((path) => pathBounds(path.d)),
129
- ...fills.map(fillBounds)
130
- ]);
131
- 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
+ );
132
292
  const aliases = visualFontAliases(page.number, page.fonts ?? []);
133
293
  const visualCodeFonts = new Set(
134
294
  (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
135
295
  );
136
- const visualSpans = page.visualSpans ?? page.spans;
137
- const overlay = visualSpans.filter(
138
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
139
- );
140
- const consumedSpans = page.spans.filter(
141
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
142
- );
143
- const fontIds = new Set(overlay.map((span) => span.fontAssetId));
144
- const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
145
- return {
146
- bounds,
147
- 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>`,
148
- ...consumedSpans.length > 0 ? { consumedSpans } : {}
149
- };
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;
150
349
  }
151
350
  function withoutSemanticMediaSpans(page, media) {
152
351
  const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
@@ -156,7 +355,7 @@ function vectorText(span, pageHeight, aliases) {
156
355
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
157
356
  const styles2 = [
158
357
  cssColor(span.color) ? `fill:${span.color}` : "",
159
- unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
358
+ unitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "",
160
359
  ...visualFontStyles(
161
360
  span.fontFamily,
162
361
  span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
@@ -164,33 +363,16 @@ function vectorText(span, pageHeight, aliases) {
164
363
  ].filter(Boolean).join(";");
165
364
  const anchorY = pageHeight - span.bounds.y;
166
365
  const transform = span.transform;
167
- 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)}"`;
168
367
  const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
169
- const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
170
- 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>`;
171
370
  }
172
371
  function centerInside(inner, outer) {
173
372
  const x = inner.x + inner.width / 2;
174
373
  const y = inner.y + inner.height / 2;
175
374
  return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
176
375
  }
177
- function vectorFill(fill) {
178
- const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
179
- const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
180
- return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
181
- }
182
- function vectorPath(path) {
183
- const fill = cssColor(path.fill) ? path.fill : "none";
184
- const stroke = cssColor(path.stroke) ? path.stroke : "none";
185
- const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
186
- const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
187
- const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
188
- const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
189
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
190
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
191
- const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
192
- return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
193
- }
194
376
  function transformedUnitBounds([a, b, c, d, e, f]) {
195
377
  const points = [
196
378
  [e, f],
@@ -204,31 +386,8 @@ function transformedUnitBounds([a, b, c, d, e, f]) {
204
386
  const minY = Math.min(...ys);
205
387
  return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
206
388
  }
207
- function pathBounds(path) {
208
- const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
209
- (match) => Number(match[0])
210
- );
211
- if (values.length < 2) return void 0;
212
- const xs = [];
213
- const ys = [];
214
- for (let index = 0; index + 1 < values.length; index += 2) {
215
- xs.push(values[index] ?? 0);
216
- ys.push(values[index + 1] ?? 0);
217
- }
218
- const minX = Math.min(...xs);
219
- const minY = Math.min(...ys);
220
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
221
- }
222
- function fillBounds(fill) {
223
- if (fill.points.length === 0) return void 0;
224
- const xs = fill.points.map(([x]) => x);
225
- const ys = fill.points.map(([, y]) => y);
226
- const minX = Math.min(...xs);
227
- const minY = Math.min(...ys);
228
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
229
- }
230
- function unionBounds(bounds) {
231
- const values = bounds.filter((value) => Boolean(value));
389
+ function unionBounds(bounds2) {
390
+ const values = bounds2.filter((value) => Boolean(value));
232
391
  if (values.length === 0) return void 0;
233
392
  const x = Math.min(...values.map((value) => value.x));
234
393
  const y = Math.min(...values.map((value) => value.y));
@@ -259,19 +418,16 @@ function rgbBmp(image) {
259
418
  }
260
419
  return output;
261
420
  }
262
- function safePath(value) {
263
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
264
- }
265
421
  function cssColor(value) {
266
422
  return /^#[\da-f]{6}$/i.test(value ?? "");
267
423
  }
268
- function finiteNonnegative(value) {
424
+ function finiteNonnegative2(value) {
269
425
  return Number.isFinite(value) && (value ?? -1) >= 0;
270
426
  }
271
427
  function unitInterval(value) {
272
- return finiteNonnegative(value) && value <= 1;
428
+ return finiteNonnegative2(value) && value <= 1;
273
429
  }
274
- function number(value) {
430
+ function number2(value) {
275
431
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
276
432
  }
277
433
  function escapeHtml2(value) {
@@ -323,18 +479,38 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
323
479
  const emitPage = async (page, future) => {
324
480
  const defaultColor = dominantTextColor(page.structured.lines);
325
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());
326
490
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
327
491
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
328
492
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
329
493
  const nextBlock = page.structured.blocks[blockIndex + 1];
330
494
  const blockY = semanticBlockY(block);
495
+ let emittedAsCaption = false;
331
496
  while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
332
497
  await flushPendingParagraph();
333
- 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>`;
334
509
  if (activeTable) pendingMedia.push(html);
335
510
  else await write(html);
336
511
  mediaIndex += 1;
337
512
  }
513
+ if (emittedAsCaption) continue;
338
514
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
339
515
  stats.suppressedFurniture += 1;
340
516
  continue;
@@ -342,7 +518,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
342
518
  await flushPendingParagraph();
343
519
  if (employmentOpen && block.type !== "list") await closeEmployment();
344
520
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
345
- await write(`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor)}</h1>`);
521
+ await write(
522
+ `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
523
+ );
346
524
  headerOpen = true;
347
525
  continue;
348
526
  }
@@ -357,7 +535,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
357
535
  }
358
536
  if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
359
537
  await write(
360
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
538
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
361
539
  );
362
540
  continue;
363
541
  }
@@ -395,7 +573,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
395
573
  const level = contentStarted && block.level === 1 ? 2 : block.level;
396
574
  await closeSections(level);
397
575
  await write(
398
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
576
+ `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
399
577
  );
400
578
  sectionLevels.push(level);
401
579
  continue;
@@ -563,8 +741,16 @@ function financialSummaryRow(entry, columns) {
563
741
  return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
564
742
  }
565
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
+ }
566
752
  if (block.type === "heading")
567
- return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
753
+ return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
568
754
  if (block.type === "paragraph")
569
755
  return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
570
756
  if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
@@ -589,7 +775,7 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
589
775
  return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
590
776
  }
591
777
  const tag = block.ordered ? "ol" : "ul";
592
- return `<${tag}>${block.items.map((item) => `<li>${escapeHtml3(item.text)}</li>`).join("")}</${tag}>`;
778
+ return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
593
779
  }
594
780
  function semanticBlockY(block) {
595
781
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
@@ -678,7 +864,7 @@ async function writePositionedPage(page, write, options) {
678
864
  const displayWidth = quarterTurn ? page.height : page.width;
679
865
  const displayHeight = quarterTurn ? page.width : page.height;
680
866
  await write(
681
- `<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">`
682
868
  );
683
869
  const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
684
870
  const type3Fonts = new Map(
@@ -690,44 +876,26 @@ async function writePositionedPage(page, write, options) {
690
876
  );
691
877
  }
692
878
  await write(
693
- `<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)}">`
694
880
  );
695
881
  await write(
696
- `<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
697
887
  );
698
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
699
888
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
700
889
  if (reflectedOverlay) {
701
890
  for (const [index, image] of (page.images ?? []).entries()) {
702
891
  await write(visualImage(image, page.height, page.number, index));
703
892
  }
704
893
  }
705
- for (const fill of page.fills ?? []) {
706
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
707
- if (isCssHexColor(fill.color)) {
708
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
709
- await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
710
- }
711
- }
712
- if (page.paths?.length) {
713
- await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
714
- for (const [pathIndex, path] of page.paths.entries()) {
715
- if (!isSvgPath(path.d)) continue;
716
- const fill = isCssHexColor(path.fill) ? path.fill : "none";
717
- const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
718
- const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
719
- const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
720
- const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
721
- const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
722
- const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
723
- const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
724
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
725
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
726
- let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
727
- for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
728
- output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
729
- }
730
- 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));
731
899
  }
732
900
  await write("</g>");
733
901
  }
@@ -757,8 +925,8 @@ function usesReflectedVisualOverlay(page, spans) {
757
925
  }
758
926
  function visualImage(image, pageHeight, pageNumber, imageIndex) {
759
927
  const [a, b, c, d, e, f] = image.transform;
760
- const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
761
- 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)}"` : "";
762
930
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
763
931
  const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
764
932
  let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
@@ -772,25 +940,13 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
772
940
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
773
941
  if (!isSvgPath(clip.d)) return "";
774
942
  const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
775
- 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>`;
776
944
  })
777
945
  ).join("");
778
946
  }
779
947
  function imageClipId(pageNumber, imageIndex, clipIndex) {
780
948
  return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
781
949
  }
782
- function pathClipDefinitions(paths, pageNumber) {
783
- return paths.flatMap(
784
- (path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
785
- if (!isSvgPath(clip.d)) return "";
786
- const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
787
- return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
788
- })
789
- ).join("");
790
- }
791
- function pathClipId(pageNumber, pathIndex, clipIndex) {
792
- return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
793
- }
794
950
  function rgbBmp2(image) {
795
951
  const stride = Math.ceil(image.width * 3 / 4) * 4;
796
952
  const output = new Uint8Array(54 + stride * image.height);
@@ -819,11 +975,11 @@ function rgbBmp2(image) {
819
975
  function rotationTransform(page) {
820
976
  switch (page.rotate) {
821
977
  case 90:
822
- return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
978
+ return `;transform:translate(${number3(page.height)}pt,0) rotate(90deg)`;
823
979
  case 180:
824
- 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)`;
825
981
  case 270:
826
- return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
982
+ return `;transform:translate(0,${number3(page.width)}pt) rotate(270deg)`;
827
983
  default:
828
984
  return "";
829
985
  }
@@ -831,13 +987,13 @@ function rotationTransform(page) {
831
987
  function positionedSpan(span, fontAliases) {
832
988
  const direction = directionAttribute([span]);
833
989
  const style = [
834
- `left:${number2(span.bounds.x)}pt`,
835
- `bottom:${number2(span.bounds.y)}pt`,
836
- `width:${number2(span.bounds.width)}pt`,
837
- `height:${number2(span.bounds.height)}pt`,
838
- `font-size:${number2(span.fontSize)}pt`,
839
- ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
840
- ...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)}`] : [],
841
997
  ...visualFontStyles(
842
998
  span.fontFamily,
843
999
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -850,19 +1006,39 @@ async function writeFlowPage(page, write) {
850
1006
  const structured = structurePage2(withoutSemanticMediaSpans(page, media));
851
1007
  const defaultColor = dominantTextColor(structured.lines);
852
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());
853
1017
  await write(
854
1018
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
855
1019
  );
856
1020
  for (const block of structured.blocks) {
857
1021
  const blockY = semanticBlockY2(block);
1022
+ let emittedAsCaption = false;
858
1023
  while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
859
- 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>`);
860
1035
  mediaIndex += 1;
861
1036
  }
1037
+ if (emittedAsCaption) continue;
862
1038
  if (block.type === "table") await write(tableToHtml(block.table));
863
1039
  else if (block.type === "heading") {
864
1040
  await write(
865
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
1041
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
866
1042
  );
867
1043
  } else if (block.type === "paragraph") {
868
1044
  await write(
@@ -898,10 +1074,16 @@ async function writeFlowPage(page, write) {
898
1074
  await write(
899
1075
  `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
900
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
+ );
901
1081
  } else {
902
1082
  const tag = block.ordered ? "ol" : "ul";
903
1083
  await write(`<${tag}>`);
904
- for (const item of block.items) await write(`<li>${escapeHtml4(item.text)}</li>`);
1084
+ for (const item of block.items) {
1085
+ await write(`<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`);
1086
+ }
905
1087
  await write(`</${tag}>`);
906
1088
  }
907
1089
  }
@@ -911,6 +1093,33 @@ async function writeFlowPage(page, write) {
911
1093
  }
912
1094
  await write("</section>");
913
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
+ }
914
1123
  function semanticBlockY2(block) {
915
1124
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
916
1125
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -923,15 +1132,15 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
923
1132
  span.fontFamily,
924
1133
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
925
1134
  ).join(";");
926
- const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
927
- 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)}` : "";
928
1137
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
929
- const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
930
- 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)}` : "";
931
1140
  const style = [
932
1141
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
933
1142
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
934
- strokeOnly ? "fill:none" : isCssHexColor(span.color) ? `fill:${span.color}` : "",
1143
+ strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
935
1144
  stroke,
936
1145
  strokeWidth,
937
1146
  fillOpacity,
@@ -939,7 +1148,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
939
1148
  font
940
1149
  ].filter(Boolean).join(";");
941
1150
  const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
942
- 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"}"` : "";
943
1152
  const transform = counterRotateReflectedText && span.transform ? [
944
1153
  span.transform[0],
945
1154
  span.transform[1],
@@ -952,8 +1161,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
952
1161
  const basisY = transform?.[1] ?? 0;
953
1162
  const anchorX = span.bounds.x + basisX * rtlOffset;
954
1163
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
955
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
956
- 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>`;
957
1166
  }
958
1167
  function isAdobeCjkFont(fontFamily) {
959
1168
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -965,16 +1174,16 @@ function visualType3Text(span, font, pageHeight) {
965
1174
  const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
966
1175
  if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
967
1176
  const transform = span.transform ?? [1, 0, 0, 1];
968
- 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)})`;
969
1178
  const xScale = span.bounds.width / totalAdvance;
970
1179
  let offset = 0;
971
1180
  let content = "";
972
1181
  for (const glyph of sequence) {
973
1182
  if (!glyph) continue;
974
- 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>`;
975
1184
  offset += glyph.advance;
976
1185
  }
977
- 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>`;
978
1187
  }
979
1188
  function isHebrewPaintOrder(span) {
980
1189
  return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
@@ -985,30 +1194,27 @@ function usesSpacingAdjustment(span) {
985
1194
  function type3Glyph(glyph, textColor) {
986
1195
  let output = "";
987
1196
  for (const fill of glyph.fills ?? []) {
988
- const color = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : fill.color;
989
- if (!isCssHexColor(color)) continue;
990
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
991
- 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)}"` : "";
992
1201
  output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
993
1202
  }
994
1203
  for (const path of glyph.paths ?? []) {
995
1204
  if (!isSvgPath(path.d)) continue;
996
- const fill = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : isCssHexColor(path.fill) ? path.fill : "none";
997
- const stroke = glyph.usesTextColor && isCssHexColor(textColor) && path.stroke ? textColor : isCssHexColor(path.stroke) ? path.stroke : "none";
998
- 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)}"` : "";
999
1208
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
1000
1209
  }
1001
1210
  return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
1002
1211
  }
1003
- function isCssHexColor(value) {
1212
+ function isCssHexColor2(value) {
1004
1213
  return /^#[\da-f]{6}$/i.test(value ?? "");
1005
1214
  }
1006
- function isUnitInterval(value) {
1215
+ function isUnitInterval2(value) {
1007
1216
  return Number.isFinite(value) && (value ?? -1) >= 0 && (value ?? 2) <= 1;
1008
1217
  }
1009
- function isSvgPath(value) {
1010
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
1011
- }
1012
1218
  function isMonospace(fontFamily) {
1013
1219
  return /courier|mono/i.test(fontFamily ?? "");
1014
1220
  }
@@ -1026,7 +1232,7 @@ function directionAttribute(spans) {
1026
1232
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
1027
1233
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
1028
1234
  }
1029
- function number2(value) {
1235
+ function number3(value) {
1030
1236
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
1031
1237
  }
1032
1238
  function escapeAttribute(value) {