@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.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
 
@@ -39,7 +97,7 @@ function dominantTextColor(lines) {
39
97
  }
40
98
  return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
41
99
  }
42
- function semanticTextHtml(text, lines, defaultColor) {
100
+ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
43
101
  const ranges = [];
44
102
  let cursor = 0;
45
103
  for (const span of lines.flatMap((line) => line.spans)) {
@@ -48,14 +106,25 @@ function semanticTextHtml(text, lines, defaultColor) {
48
106
  if (start < 0) continue;
49
107
  cursor = start + span.text.length;
50
108
  const color = normalizedColor(span.color);
51
- if (color && color !== defaultColor) ranges.push({ start, end: cursor, color });
109
+ const bold = preserveWeight && /(?:bold|semibold|demi|medium|medi)/i.test(span.fontFamily ?? "");
110
+ const italic = /(?:italic|oblique|slanted|slant|ital)/i.test(span.fontFamily ?? "");
111
+ const nondefaultColor = color && color !== defaultColor ? color : void 0;
112
+ if (nondefaultColor || bold || italic) {
113
+ ranges.push({
114
+ start,
115
+ end: cursor,
116
+ ...nondefaultColor ? { color: nondefaultColor } : {},
117
+ bold,
118
+ italic
119
+ });
120
+ }
52
121
  }
53
122
  const merged = mergeRanges(ranges, text);
54
123
  let html = "";
55
124
  let offset = 0;
56
125
  for (const range of merged) {
57
126
  html += escapeHtml(text.slice(offset, range.start));
58
- html += `<span style="color:${range.color}">${escapeHtml(text.slice(range.start, range.end))}</span>`;
127
+ html += styledHtml(text.slice(range.start, range.end), range);
59
128
  offset = range.end;
60
129
  }
61
130
  return html + escapeHtml(text.slice(offset));
@@ -64,7 +133,7 @@ function mergeRanges(ranges, text) {
64
133
  const merged = [];
65
134
  for (const range of ranges) {
66
135
  const previous = merged.at(-1);
67
- if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
136
+ if (previous && previous.color === range.color && previous.bold === range.bold && previous.italic === range.italic && /^\s*$/.test(text.slice(previous.end, range.start))) {
68
137
  previous.end = range.end;
69
138
  } else {
70
139
  merged.push({ ...range });
@@ -72,6 +141,13 @@ function mergeRanges(ranges, text) {
72
141
  }
73
142
  return merged;
74
143
  }
144
+ function styledHtml(value, range) {
145
+ let html = escapeHtml(value);
146
+ if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
147
+ if (range.italic) html = `<em>${html}</em>`;
148
+ if (range.bold) html = `<strong>${html}</strong>`;
149
+ return html;
150
+ }
75
151
  function normalizedColor(value) {
76
152
  if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
77
153
  const color = value.toLowerCase();
@@ -81,6 +157,85 @@ function escapeHtml(value) {
81
157
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
82
158
  }
83
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
+
84
239
  // src/visual-font.ts
85
240
  function visualFontAliases(pageNumber, fonts) {
86
241
  return new Map(
@@ -130,46 +285,90 @@ function base64(bytes) {
130
285
  // src/semantic-media.ts
131
286
  function semanticMedia(page) {
132
287
  const output = (page.images ?? []).map((image) => rasterMedia(image));
133
- const vector = vectorMedia(page);
134
- if (vector) output.push(vector);
288
+ output.push(...vectorMedia(page));
135
289
  return output.sort((left, right) => right.bounds.y - left.bounds.y);
136
290
  }
137
291
  function rasterMedia(image) {
138
- const bounds = transformedUnitBounds(image.transform);
292
+ const bounds2 = transformedUnitBounds(image.transform);
139
293
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
140
294
  const data = image.format === "jpeg" ? image.data : rgbBmp(image);
141
- const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
295
+ const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
142
296
  return {
143
- bounds,
144
- 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}">`
145
299
  };
146
300
  }
147
301
  function vectorMedia(page) {
148
- const paths = (page.paths ?? []).filter((path) => safePath(path.d));
149
- const fills = page.fills ?? [];
150
- const bounds = unionBounds([
151
- ...paths.map((path) => pathBounds(path.d)),
152
- ...fills.map(fillBounds)
153
- ]);
154
- 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
+ );
155
315
  const aliases = visualFontAliases(page.number, page.fonts ?? []);
156
316
  const visualCodeFonts = new Set(
157
317
  (page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
158
318
  );
159
- const visualSpans = page.visualSpans ?? page.spans;
160
- const overlay = visualSpans.filter(
161
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
162
- );
163
- const consumedSpans = page.spans.filter(
164
- (span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds)
165
- );
166
- const fontIds = new Set(overlay.map((span) => span.fontAssetId));
167
- const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
168
- return {
169
- bounds,
170
- 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>`,
171
- ...consumedSpans.length > 0 ? { consumedSpans } : {}
172
- };
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;
173
372
  }
174
373
  function withoutSemanticMediaSpans(page, media) {
175
374
  const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
@@ -179,7 +378,7 @@ function vectorText(span, pageHeight, aliases) {
179
378
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
180
379
  const styles2 = [
181
380
  cssColor(span.color) ? `fill:${span.color}` : "",
182
- unitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "",
381
+ unitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "",
183
382
  ...visualFontStyles(
184
383
  span.fontFamily,
185
384
  span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
@@ -187,33 +386,16 @@ function vectorText(span, pageHeight, aliases) {
187
386
  ].filter(Boolean).join(";");
188
387
  const anchorY = pageHeight - span.bounds.y;
189
388
  const transform = span.transform;
190
- 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)}"`;
191
390
  const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
192
- const length = extent > 0 ? ` textLength="${number(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
193
- 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>`;
194
393
  }
195
394
  function centerInside(inner, outer) {
196
395
  const x = inner.x + inner.width / 2;
197
396
  const y = inner.y + inner.height / 2;
198
397
  return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
199
398
  }
200
- function vectorFill(fill) {
201
- const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
202
- const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
203
- return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
204
- }
205
- function vectorPath(path) {
206
- const fill = cssColor(path.fill) ? path.fill : "none";
207
- const stroke = cssColor(path.stroke) ? path.stroke : "none";
208
- const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
209
- const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
210
- const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
211
- const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
212
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
213
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
214
- const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
215
- return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
216
- }
217
399
  function transformedUnitBounds([a, b, c, d, e, f]) {
218
400
  const points = [
219
401
  [e, f],
@@ -227,31 +409,8 @@ function transformedUnitBounds([a, b, c, d, e, f]) {
227
409
  const minY = Math.min(...ys);
228
410
  return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
229
411
  }
230
- function pathBounds(path) {
231
- const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
232
- (match) => Number(match[0])
233
- );
234
- if (values.length < 2) return void 0;
235
- const xs = [];
236
- const ys = [];
237
- for (let index = 0; index + 1 < values.length; index += 2) {
238
- xs.push(values[index] ?? 0);
239
- ys.push(values[index + 1] ?? 0);
240
- }
241
- const minX = Math.min(...xs);
242
- const minY = Math.min(...ys);
243
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
244
- }
245
- function fillBounds(fill) {
246
- if (fill.points.length === 0) return void 0;
247
- const xs = fill.points.map(([x]) => x);
248
- const ys = fill.points.map(([, y]) => y);
249
- const minX = Math.min(...xs);
250
- const minY = Math.min(...ys);
251
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
252
- }
253
- function unionBounds(bounds) {
254
- const values = bounds.filter((value) => Boolean(value));
412
+ function unionBounds(bounds2) {
413
+ const values = bounds2.filter((value) => Boolean(value));
255
414
  if (values.length === 0) return void 0;
256
415
  const x = Math.min(...values.map((value) => value.x));
257
416
  const y = Math.min(...values.map((value) => value.y));
@@ -282,19 +441,16 @@ function rgbBmp(image) {
282
441
  }
283
442
  return output;
284
443
  }
285
- function safePath(value) {
286
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
287
- }
288
444
  function cssColor(value) {
289
445
  return /^#[\da-f]{6}$/i.test(value ?? "");
290
446
  }
291
- function finiteNonnegative(value) {
447
+ function finiteNonnegative2(value) {
292
448
  return Number.isFinite(value) && (value ?? -1) >= 0;
293
449
  }
294
450
  function unitInterval(value) {
295
- return finiteNonnegative(value) && value <= 1;
451
+ return finiteNonnegative2(value) && value <= 1;
296
452
  }
297
- function number(value) {
453
+ function number2(value) {
298
454
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
299
455
  }
300
456
  function escapeHtml2(value) {
@@ -346,18 +502,38 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
346
502
  const emitPage = async (page, future) => {
347
503
  const defaultColor = dominantTextColor(page.structured.lines);
348
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());
349
513
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
350
514
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
351
515
  for (const [blockIndex, block] of page.structured.blocks.entries()) {
352
516
  const nextBlock = page.structured.blocks[blockIndex + 1];
353
517
  const blockY = semanticBlockY(block);
518
+ let emittedAsCaption = false;
354
519
  while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
355
520
  await flushPendingParagraph();
356
- 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>`;
357
532
  if (activeTable) pendingMedia.push(html);
358
533
  else await write(html);
359
534
  mediaIndex += 1;
360
535
  }
536
+ if (emittedAsCaption) continue;
361
537
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
362
538
  stats.suppressedFurniture += 1;
363
539
  continue;
@@ -365,7 +541,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
365
541
  await flushPendingParagraph();
366
542
  if (employmentOpen && block.type !== "list") await closeEmployment();
367
543
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
368
- await write(`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor)}</h1>`);
544
+ await write(
545
+ `<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
546
+ );
369
547
  headerOpen = true;
370
548
  continue;
371
549
  }
@@ -380,7 +558,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
380
558
  }
381
559
  if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
382
560
  await write(
383
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
561
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
384
562
  );
385
563
  continue;
386
564
  }
@@ -418,7 +596,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
418
596
  const level = contentStarted && block.level === 1 ? 2 : block.level;
419
597
  await closeSections(level);
420
598
  await write(
421
- `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
599
+ `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
422
600
  );
423
601
  sectionLevels.push(level);
424
602
  continue;
@@ -586,8 +764,16 @@ function financialSummaryRow(entry, columns) {
586
764
  return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
587
765
  }
588
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
+ }
589
775
  if (block.type === "heading")
590
- return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
776
+ return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
591
777
  if (block.type === "paragraph")
592
778
  return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
593
779
  if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
@@ -612,7 +798,7 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
612
798
  return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
613
799
  }
614
800
  const tag = block.ordered ? "ol" : "ul";
615
- return `<${tag}>${block.items.map((item) => `<li>${escapeHtml3(item.text)}</li>`).join("")}</${tag}>`;
801
+ return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
616
802
  }
617
803
  function semanticBlockY(block) {
618
804
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
@@ -701,7 +887,7 @@ async function writePositionedPage(page, write, options) {
701
887
  const displayWidth = quarterTurn ? page.height : page.width;
702
888
  const displayHeight = quarterTurn ? page.width : page.height;
703
889
  await write(
704
- `<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">`
705
891
  );
706
892
  const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
707
893
  const type3Fonts = new Map(
@@ -713,44 +899,26 @@ async function writePositionedPage(page, write, options) {
713
899
  );
714
900
  }
715
901
  await write(
716
- `<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)}">`
717
903
  );
718
904
  await write(
719
- `<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
720
910
  );
721
- const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
722
911
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
723
912
  if (reflectedOverlay) {
724
913
  for (const [index, image] of (page.images ?? []).entries()) {
725
914
  await write(visualImage(image, page.height, page.number, index));
726
915
  }
727
916
  }
728
- for (const fill of page.fills ?? []) {
729
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
730
- if (isCssHexColor(fill.color)) {
731
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
732
- await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
733
- }
734
- }
735
- if (page.paths?.length) {
736
- await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
737
- for (const [pathIndex, path] of page.paths.entries()) {
738
- if (!isSvgPath(path.d)) continue;
739
- const fill = isCssHexColor(path.fill) ? path.fill : "none";
740
- const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
741
- const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
742
- const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
743
- const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
744
- const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
745
- const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
746
- const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
747
- const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
748
- const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
749
- let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
750
- for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
751
- output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
752
- }
753
- 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));
754
922
  }
755
923
  await write("</g>");
756
924
  }
@@ -780,8 +948,8 @@ function usesReflectedVisualOverlay(page, spans) {
780
948
  }
781
949
  function visualImage(image, pageHeight, pageNumber, imageIndex) {
782
950
  const [a, b, c, d, e, f] = image.transform;
783
- const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
784
- 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)}"` : "";
785
953
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
786
954
  const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
787
955
  let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
@@ -795,25 +963,13 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
795
963
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
796
964
  if (!isSvgPath(clip.d)) return "";
797
965
  const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
798
- 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>`;
799
967
  })
800
968
  ).join("");
801
969
  }
802
970
  function imageClipId(pageNumber, imageIndex, clipIndex) {
803
971
  return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
804
972
  }
805
- function pathClipDefinitions(paths, pageNumber) {
806
- return paths.flatMap(
807
- (path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
808
- if (!isSvgPath(clip.d)) return "";
809
- const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
810
- return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
811
- })
812
- ).join("");
813
- }
814
- function pathClipId(pageNumber, pathIndex, clipIndex) {
815
- return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
816
- }
817
973
  function rgbBmp2(image) {
818
974
  const stride = Math.ceil(image.width * 3 / 4) * 4;
819
975
  const output = new Uint8Array(54 + stride * image.height);
@@ -842,11 +998,11 @@ function rgbBmp2(image) {
842
998
  function rotationTransform(page) {
843
999
  switch (page.rotate) {
844
1000
  case 90:
845
- return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
1001
+ return `;transform:translate(${number3(page.height)}pt,0) rotate(90deg)`;
846
1002
  case 180:
847
- 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)`;
848
1004
  case 270:
849
- return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
1005
+ return `;transform:translate(0,${number3(page.width)}pt) rotate(270deg)`;
850
1006
  default:
851
1007
  return "";
852
1008
  }
@@ -854,13 +1010,13 @@ function rotationTransform(page) {
854
1010
  function positionedSpan(span, fontAliases) {
855
1011
  const direction = directionAttribute([span]);
856
1012
  const style = [
857
- `left:${number2(span.bounds.x)}pt`,
858
- `bottom:${number2(span.bounds.y)}pt`,
859
- `width:${number2(span.bounds.width)}pt`,
860
- `height:${number2(span.bounds.height)}pt`,
861
- `font-size:${number2(span.fontSize)}pt`,
862
- ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
863
- ...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)}`] : [],
864
1020
  ...visualFontStyles(
865
1021
  span.fontFamily,
866
1022
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
@@ -873,19 +1029,39 @@ async function writeFlowPage(page, write) {
873
1029
  const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
874
1030
  const defaultColor = dominantTextColor(structured.lines);
875
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());
876
1040
  await write(
877
1041
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
878
1042
  );
879
1043
  for (const block of structured.blocks) {
880
1044
  const blockY = semanticBlockY2(block);
1045
+ let emittedAsCaption = false;
881
1046
  while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
882
- 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>`);
883
1058
  mediaIndex += 1;
884
1059
  }
1060
+ if (emittedAsCaption) continue;
885
1061
  if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
886
1062
  else if (block.type === "heading") {
887
1063
  await write(
888
- `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
1064
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
889
1065
  );
890
1066
  } else if (block.type === "paragraph") {
891
1067
  await write(
@@ -921,10 +1097,16 @@ async function writeFlowPage(page, write) {
921
1097
  await write(
922
1098
  `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
923
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
+ );
924
1104
  } else {
925
1105
  const tag = block.ordered ? "ol" : "ul";
926
1106
  await write(`<${tag}>`);
927
- for (const item of block.items) await write(`<li>${escapeHtml4(item.text)}</li>`);
1107
+ for (const item of block.items) {
1108
+ await write(`<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`);
1109
+ }
928
1110
  await write(`</${tag}>`);
929
1111
  }
930
1112
  }
@@ -934,6 +1116,33 @@ async function writeFlowPage(page, write) {
934
1116
  }
935
1117
  await write("</section>");
936
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
+ }
937
1146
  function semanticBlockY2(block) {
938
1147
  const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
939
1148
  return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
@@ -946,15 +1155,15 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
946
1155
  span.fontFamily,
947
1156
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
948
1157
  ).join(";");
949
- const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
950
- 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)}` : "";
951
1160
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
952
- const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
953
- 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)}` : "";
954
1163
  const style = [
955
1164
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
956
1165
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
957
- strokeOnly ? "fill:none" : isCssHexColor(span.color) ? `fill:${span.color}` : "",
1166
+ strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
958
1167
  stroke,
959
1168
  strokeWidth,
960
1169
  fillOpacity,
@@ -962,7 +1171,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
962
1171
  font
963
1172
  ].filter(Boolean).join(";");
964
1173
  const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
965
- 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"}"` : "";
966
1175
  const transform = counterRotateReflectedText && span.transform ? [
967
1176
  span.transform[0],
968
1177
  span.transform[1],
@@ -975,8 +1184,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
975
1184
  const basisY = transform?.[1] ?? 0;
976
1185
  const anchorX = span.bounds.x + basisX * rtlOffset;
977
1186
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
978
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
979
- 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>`;
980
1189
  }
981
1190
  function isAdobeCjkFont(fontFamily) {
982
1191
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -988,16 +1197,16 @@ function visualType3Text(span, font, pageHeight) {
988
1197
  const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
989
1198
  if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
990
1199
  const transform = span.transform ?? [1, 0, 0, 1];
991
- 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)})`;
992
1201
  const xScale = span.bounds.width / totalAdvance;
993
1202
  let offset = 0;
994
1203
  let content = "";
995
1204
  for (const glyph of sequence) {
996
1205
  if (!glyph) continue;
997
- 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>`;
998
1207
  offset += glyph.advance;
999
1208
  }
1000
- 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>`;
1001
1210
  }
1002
1211
  function isHebrewPaintOrder(span) {
1003
1212
  return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
@@ -1008,30 +1217,27 @@ function usesSpacingAdjustment(span) {
1008
1217
  function type3Glyph(glyph, textColor) {
1009
1218
  let output = "";
1010
1219
  for (const fill of glyph.fills ?? []) {
1011
- const color = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : fill.color;
1012
- if (!isCssHexColor(color)) continue;
1013
- const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
1014
- 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)}"` : "";
1015
1224
  output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
1016
1225
  }
1017
1226
  for (const path of glyph.paths ?? []) {
1018
1227
  if (!isSvgPath(path.d)) continue;
1019
- const fill = glyph.usesTextColor && isCssHexColor(textColor) ? textColor : isCssHexColor(path.fill) ? path.fill : "none";
1020
- const stroke = glyph.usesTextColor && isCssHexColor(textColor) && path.stroke ? textColor : isCssHexColor(path.stroke) ? path.stroke : "none";
1021
- 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)}"` : "";
1022
1231
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
1023
1232
  }
1024
1233
  return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
1025
1234
  }
1026
- function isCssHexColor(value) {
1235
+ function isCssHexColor2(value) {
1027
1236
  return /^#[\da-f]{6}$/i.test(value ?? "");
1028
1237
  }
1029
- function isUnitInterval(value) {
1238
+ function isUnitInterval2(value) {
1030
1239
  return Number.isFinite(value) && (value ?? -1) >= 0 && (value ?? 2) <= 1;
1031
1240
  }
1032
- function isSvgPath(value) {
1033
- return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
1034
- }
1035
1241
  function isMonospace(fontFamily) {
1036
1242
  return /courier|mono/i.test(fontFamily ?? "");
1037
1243
  }
@@ -1049,7 +1255,7 @@ function directionAttribute(spans) {
1049
1255
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
1050
1256
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
1051
1257
  }
1052
- function number2(value) {
1258
+ function number3(value) {
1053
1259
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
1054
1260
  }
1055
1261
  function escapeAttribute(value) {