@boxpdf/html-writer 0.1.11 → 0.1.13

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
@@ -6,6 +6,205 @@ import {
6
6
  structurePage,
7
7
  tableToRows
8
8
  } from "@boxpdf/reader/structure";
9
+
10
+ // src/semantic-inline.ts
11
+ function dominantTextColor(lines) {
12
+ const counts = /* @__PURE__ */ new Map();
13
+ for (const span of lines.flatMap((line) => line.spans)) {
14
+ const color = normalizedColor(span.color) ?? "#000000";
15
+ counts.set(color, (counts.get(color) ?? 0) + Math.max(1, [...span.text].length));
16
+ }
17
+ return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
18
+ }
19
+ function semanticTextHtml(text, lines, defaultColor) {
20
+ const ranges = [];
21
+ let cursor = 0;
22
+ for (const span of lines.flatMap((line) => line.spans)) {
23
+ if (!span.text) continue;
24
+ const start = text.indexOf(span.text, cursor);
25
+ if (start < 0) continue;
26
+ cursor = start + span.text.length;
27
+ const color = normalizedColor(span.color);
28
+ if (color && color !== defaultColor) ranges.push({ start, end: cursor, color });
29
+ }
30
+ const merged = mergeRanges(ranges, text);
31
+ let html = "";
32
+ let offset = 0;
33
+ for (const range of merged) {
34
+ html += escapeHtml(text.slice(offset, range.start));
35
+ html += `<span style="color:${range.color}">${escapeHtml(text.slice(range.start, range.end))}</span>`;
36
+ offset = range.end;
37
+ }
38
+ return html + escapeHtml(text.slice(offset));
39
+ }
40
+ function mergeRanges(ranges, text) {
41
+ const merged = [];
42
+ for (const range of ranges) {
43
+ const previous = merged.at(-1);
44
+ if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
45
+ previous.end = range.end;
46
+ } else {
47
+ merged.push({ ...range });
48
+ }
49
+ }
50
+ return merged;
51
+ }
52
+ function normalizedColor(value) {
53
+ if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
54
+ const color = value.toLowerCase();
55
+ return color === "#000000" || color === "#000" ? "#000000" : color;
56
+ }
57
+ function escapeHtml(value) {
58
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
59
+ }
60
+
61
+ // src/semantic-media.ts
62
+ function semanticMedia(page) {
63
+ const output = (page.images ?? []).map((image) => rasterMedia(image));
64
+ const vector = vectorMedia(page);
65
+ if (vector) output.push(vector);
66
+ return output.sort((left, right) => right.bounds.y - left.bounds.y);
67
+ }
68
+ function rasterMedia(image) {
69
+ const bounds = transformedUnitBounds(image.transform);
70
+ const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
71
+ const data = image.format === "jpeg" ? image.data : rgbBmp(image);
72
+ const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
73
+ return {
74
+ bounds,
75
+ 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}">`
76
+ };
77
+ }
78
+ function vectorMedia(page) {
79
+ const paths = (page.paths ?? []).filter((path) => safePath(path.d));
80
+ const fills = page.fills ?? [];
81
+ const bounds = unionBounds([
82
+ ...paths.map((path) => pathBounds(path.d)),
83
+ ...fills.map(fillBounds)
84
+ ]);
85
+ if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
86
+ const content = fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("");
87
+ return {
88
+ bounds,
89
+ html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number(bounds.x)} ${number(page.height - bounds.y - bounds.height)} ${number(bounds.width)} ${number(bounds.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true"><g transform="translate(0 ${number(page.height)}) scale(1 -1)">${content}</g></svg>`
90
+ };
91
+ }
92
+ function vectorFill(fill) {
93
+ const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
94
+ const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
95
+ return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
96
+ }
97
+ function vectorPath(path) {
98
+ const fill = cssColor(path.fill) ? path.fill : "none";
99
+ const stroke = cssColor(path.stroke) ? path.stroke : "none";
100
+ const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
101
+ const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
102
+ const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
103
+ const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
104
+ const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
105
+ const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
106
+ const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
107
+ return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
108
+ }
109
+ function transformedUnitBounds([a, b, c, d, e, f]) {
110
+ const points = [
111
+ [e, f],
112
+ [a + e, b + f],
113
+ [c + e, d + f],
114
+ [a + c + e, b + d + f]
115
+ ];
116
+ const xs = points.map(([x]) => x ?? 0);
117
+ const ys = points.map(([, y]) => y ?? 0);
118
+ const minX = Math.min(...xs);
119
+ const minY = Math.min(...ys);
120
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
121
+ }
122
+ function pathBounds(path) {
123
+ const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
124
+ (match) => Number(match[0])
125
+ );
126
+ if (values.length < 2) return void 0;
127
+ const xs = [];
128
+ const ys = [];
129
+ for (let index = 0; index + 1 < values.length; index += 2) {
130
+ xs.push(values[index] ?? 0);
131
+ ys.push(values[index + 1] ?? 0);
132
+ }
133
+ const minX = Math.min(...xs);
134
+ const minY = Math.min(...ys);
135
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
136
+ }
137
+ function fillBounds(fill) {
138
+ if (fill.points.length === 0) return void 0;
139
+ const xs = fill.points.map(([x]) => x);
140
+ const ys = fill.points.map(([, y]) => y);
141
+ const minX = Math.min(...xs);
142
+ const minY = Math.min(...ys);
143
+ return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
144
+ }
145
+ function unionBounds(bounds) {
146
+ const values = bounds.filter((value) => Boolean(value));
147
+ if (values.length === 0) return void 0;
148
+ const x = Math.min(...values.map((value) => value.x));
149
+ const y = Math.min(...values.map((value) => value.y));
150
+ const right = Math.max(...values.map((value) => value.x + value.width));
151
+ const top = Math.max(...values.map((value) => value.y + value.height));
152
+ return { x, y, width: right - x, height: top - y };
153
+ }
154
+ function rgbBmp(image) {
155
+ const stride = Math.ceil(image.width * 3 / 4) * 4;
156
+ const output = new Uint8Array(54 + stride * image.height);
157
+ const view = new DataView(output.buffer);
158
+ output.set([66, 77]);
159
+ view.setUint32(2, output.length, true);
160
+ view.setUint32(10, 54, true);
161
+ view.setUint32(14, 40, true);
162
+ view.setInt32(18, image.width, true);
163
+ view.setInt32(22, -image.height, true);
164
+ view.setUint16(26, 1, true);
165
+ view.setUint16(28, 24, true);
166
+ for (let row = 0; row < image.height; row += 1) {
167
+ for (let column = 0; column < image.width; column += 1) {
168
+ const source = (row * image.width + column) * 3;
169
+ const target = 54 + row * stride + column * 3;
170
+ output[target] = image.data[source + 2] ?? 0;
171
+ output[target + 1] = image.data[source + 1] ?? 0;
172
+ output[target + 2] = image.data[source] ?? 0;
173
+ }
174
+ }
175
+ return output;
176
+ }
177
+ function base64(bytes) {
178
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
179
+ let output = "";
180
+ for (let index = 0; index < bytes.length; index += 3) {
181
+ const first = bytes[index] ?? 0;
182
+ const second = bytes[index + 1] ?? 0;
183
+ const third = bytes[index + 2] ?? 0;
184
+ output += alphabet[first >> 2];
185
+ output += alphabet[(first & 3) << 4 | second >> 4];
186
+ output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
187
+ output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
188
+ }
189
+ return output;
190
+ }
191
+ function safePath(value) {
192
+ return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
193
+ }
194
+ function cssColor(value) {
195
+ return /^#[\da-f]{6}$/i.test(value ?? "");
196
+ }
197
+ function finiteNonnegative(value) {
198
+ return Number.isFinite(value) && (value ?? -1) >= 0;
199
+ }
200
+ function unitInterval(value) {
201
+ return finiteNonnegative(value) && value <= 1;
202
+ }
203
+ function number(value) {
204
+ return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
205
+ }
206
+
207
+ // src/semantic-document.ts
9
208
  async function writeSemanticDocument(pages, write, lookaheadPages) {
10
209
  const stats = {
11
210
  pagesProcessed: 0,
@@ -18,6 +217,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
18
217
  const seenFurniture = /* @__PURE__ */ new Set();
19
218
  const sectionLevels = [];
20
219
  let activeTable;
220
+ const pendingMedia = [];
21
221
  let headerOpen = false;
22
222
  let headerHasParagraph = false;
23
223
  let contentStarted = false;
@@ -28,6 +228,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
28
228
  if (!activeTable) return;
29
229
  await write("</table>");
30
230
  activeTable = void 0;
231
+ while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
31
232
  };
32
233
  const closeSections = async (minimumLevel = 0) => {
33
234
  while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
@@ -37,7 +238,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
37
238
  };
38
239
  const flushPendingParagraph = async () => {
39
240
  if (!pendingParagraph) return;
40
- await write(semanticBlockHtml(pendingParagraph.block));
241
+ await write(semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor));
41
242
  pendingParagraph = void 0;
42
243
  };
43
244
  const closeEmployment = async () => {
@@ -46,9 +247,20 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
46
247
  employmentOpen = false;
47
248
  };
48
249
  const emitPage = async (page, future) => {
250
+ const defaultColor = dominantTextColor(page.structured.lines);
251
+ let mediaIndex = 0;
49
252
  const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
50
253
  const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
51
- for (const block of page.structured.blocks) {
254
+ for (const [blockIndex, block] of page.structured.blocks.entries()) {
255
+ const nextBlock = page.structured.blocks[blockIndex + 1];
256
+ const blockY = semanticBlockY(block);
257
+ while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
258
+ await flushPendingParagraph();
259
+ const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
260
+ if (activeTable) pendingMedia.push(html);
261
+ else await write(html);
262
+ mediaIndex += 1;
263
+ }
52
264
  if (isRepeatedFurniture(block, page, repeatedFurniture)) {
53
265
  stats.suppressedFurniture += 1;
54
266
  continue;
@@ -56,19 +268,23 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
56
268
  await flushPendingParagraph();
57
269
  if (employmentOpen && block.type !== "list") await closeEmployment();
58
270
  if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
59
- await write(`<header><h1>${escapeHtml(block.text)}</h1>`);
271
+ await write(`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor)}</h1>`);
60
272
  headerOpen = true;
61
273
  continue;
62
274
  }
63
275
  if (headerOpen) {
64
276
  if (block.type === "paragraph") {
65
277
  const tag = isContactBlock(block) ? "address" : "p";
66
- await write(`<${tag}>${escapeHtml(block.text)}</${tag}>`);
278
+ await write(
279
+ `<${tag}>${semanticTextHtml(block.text, block.lines, defaultColor)}</${tag}>`
280
+ );
67
281
  headerHasParagraph = true;
68
282
  continue;
69
283
  }
70
- if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#"))) {
71
- await write(`<h${block.level}>${escapeHtml(block.text)}</h${block.level}>`);
284
+ if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
285
+ await write(
286
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
287
+ );
72
288
  continue;
73
289
  }
74
290
  await write("</header>");
@@ -105,7 +321,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
105
321
  const level = contentStarted && block.level === 1 ? 2 : block.level;
106
322
  await closeSections(level);
107
323
  await write(
108
- `<section data-level="${level}"><h${level}>${escapeHtml(block.text)}</h${level}>`
324
+ `<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
109
325
  );
110
326
  sectionLevels.push(level);
111
327
  continue;
@@ -113,33 +329,40 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
113
329
  if (block.type === "paragraph") {
114
330
  if (isTitledRecord(block)) {
115
331
  const [institution, ...details] = block.lines;
116
- if (institution) await write(`<h3>${escapeHtml(institution.text)}</h3>`);
117
- for (const detail of details) await write(`<p>${escapeHtml(detail.text)}</p>`);
332
+ if (institution) await write(`<h3>${escapeHtml2(institution.text)}</h3>`);
333
+ for (const detail of details) await write(`<p>${escapeHtml2(detail.text)}</p>`);
118
334
  continue;
119
335
  }
120
336
  if (isUnmarkedList(block)) {
121
337
  await write(
122
- `<ul>${block.lines.map((line) => `<li>${escapeHtml(line.text)}</li>`).join("")}</ul>`
338
+ `<ul>${block.lines.map((line) => `<li>${escapeHtml2(line.text)}</li>`).join("")}</ul>`
123
339
  );
124
340
  continue;
125
341
  }
126
- pendingParagraph = { block, height: page.height };
342
+ pendingParagraph = { block, height: page.height, defaultColor };
127
343
  continue;
128
344
  }
129
345
  if (block.type === "employment") {
130
346
  await write(
131
- `<section><h3>${escapeHtml(block.role)}</h3><p>${escapeHtml(block.organization)}</p><p>${escapeHtml(block.date)}</p>`
347
+ `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p>`
132
348
  );
133
349
  employmentOpen = true;
134
350
  continue;
135
351
  }
136
- await write(semanticBlockHtml(block));
352
+ await write(semanticBlockHtml(block, defaultColor));
353
+ }
354
+ while (mediaIndex < page.media.length) {
355
+ await flushPendingParagraph();
356
+ const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
357
+ if (activeTable) pendingMedia.push(html);
358
+ else await write(html);
359
+ mediaIndex += 1;
137
360
  }
138
361
  for (const signature of marginSignatures(page)) seenFurniture.add(signature);
139
362
  };
140
363
  for await (const page of pages) {
141
364
  const structured = structurePage(page);
142
- buffer.push({ width: page.width, height: page.height, structured });
365
+ buffer.push({ width: page.width, height: page.height, structured, media: semanticMedia(page) });
143
366
  stats.pagesProcessed += 1;
144
367
  stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
145
368
  stats.peakBufferedLines = Math.max(
@@ -160,7 +383,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
160
383
  await closeEmployment();
161
384
  if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
162
385
  await closeSections();
163
- await write(`<footer>${semanticBlockHtml(pendingParagraph.block)}</footer>`);
386
+ await write(
387
+ `<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`
388
+ );
164
389
  pendingParagraph = void 0;
165
390
  } else {
166
391
  await flushPendingParagraph();
@@ -250,7 +475,7 @@ function sameRow(left, right) {
250
475
  }
251
476
  function tableRow(row, header) {
252
477
  const cell = header ? "th" : "td";
253
- return `<tr>${row.map((value) => `<${cell}>${escapeHtml(value)}</${cell}>`).join("")}</tr>`;
478
+ return `<tr>${row.map((value) => `<${cell}>${escapeHtml2(value)}</${cell}>`).join("")}</tr>`;
254
479
  }
255
480
  function isFinancialSummary(block) {
256
481
  return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
@@ -260,20 +485,22 @@ function isNumericValue(value) {
260
485
  }
261
486
  function financialSummaryRow(entry, columns) {
262
487
  const colspan = columns > 2 ? ` colspan="${columns - 1}"` : "";
263
- return `<tr><th scope="row"${colspan}>${escapeHtml(entry.term)}</th><td>${escapeHtml(entry.description)}</td></tr>`;
488
+ return `<tr><th scope="row"${colspan}>${escapeHtml2(entry.term)}</th><td>${escapeHtml2(entry.description)}</td></tr>`;
264
489
  }
265
- function semanticBlockHtml(block) {
490
+ function semanticBlockHtml(block, defaultColor = "#000000") {
266
491
  if (block.type === "heading")
267
- return `<h${block.level}>${escapeHtml(block.text)}</h${block.level}>`;
268
- if (block.type === "paragraph") return `<p>${escapeHtml(block.text)}</p>`;
492
+ return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
493
+ if (block.type === "paragraph")
494
+ return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
495
+ if (block.type === "preformatted") return `<pre>${escapeHtml2(block.text)}</pre>`;
269
496
  if (block.type === "definitionList") {
270
497
  if (block.entries.length <= 3 && block.entries.some((entry) => entry.description.trim().split(/\s+/).length >= 5) && block.entries.every((entry) => /^[A-Z][A-Z\s/-]*$/.test(entry.term.trim()))) {
271
498
  return block.entries.map(
272
- (entry) => `<section><h2>${escapeHtml(titleCase(entry.term))}</h2><p>${escapeHtml(entry.description)}</p></section>`
499
+ (entry) => `<section><h2>${escapeHtml2(titleCase(entry.term))}</h2><p>${escapeHtml2(entry.description)}</p></section>`
273
500
  ).join("");
274
501
  }
275
502
  const list = `<dl>${block.entries.map(
276
- (entry) => `<div><dt>${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.description)}</dd></div>`
503
+ (entry) => `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
277
504
  ).join("")}</dl>`;
278
505
  return isFinancialSummary(block) ? `<section>${list}</section>` : list;
279
506
  }
@@ -284,42 +511,46 @@ function semanticBlockHtml(block) {
284
511
  return block.items.map(labeledSectionHtml).join("");
285
512
  }
286
513
  if (block.type === "employment") {
287
- return `<section><h3>${escapeHtml(block.role)}</h3><p>${escapeHtml(block.organization)}</p><p>${escapeHtml(block.date)}</p></section>`;
514
+ return `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`;
288
515
  }
289
516
  const tag = block.ordered ? "ol" : "ul";
290
- return `<${tag}>${block.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("")}</${tag}>`;
517
+ return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
518
+ }
519
+ function semanticBlockY(block) {
520
+ const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
521
+ return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
291
522
  }
292
523
  function cardTableRow(item) {
293
524
  const trailing = item.details.at(-1) ?? "";
294
525
  const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
295
526
  const description = item.details.slice(0, -1).join(" ");
296
- const detail = description ? `<br><span>${escapeHtml(description)}</span>` : "";
527
+ const detail = description ? `<br><span>${escapeHtml2(description)}</span>` : "";
297
528
  const quantity = match?.[1] ?? "";
298
529
  const amount = match?.[2] ?? trailing;
299
- return `<tr><th scope="row">${escapeHtml(item.title)}${detail}</th><td>${escapeHtml(quantity)}</td><td>${escapeHtml(amount)}</td></tr>`;
530
+ return `<tr><th scope="row">${escapeHtml2(item.title)}${detail}</th><td>${escapeHtml2(quantity)}</td><td>${escapeHtml2(amount)}</td></tr>`;
300
531
  }
301
532
  function labeledSectionHtml(item) {
302
533
  const heading = titleCase(item.label);
303
534
  const postal = /\b(?:ship|deliver|mail)(?:ed)?\b/i.test(item.label);
304
535
  if (postal) {
305
536
  const [name, ...address] = item.content;
306
- const content = [name ? `<strong>${escapeHtml(name)}</strong>` : "", ...address.map(escapeHtml)].filter(Boolean).join("<br>");
307
- return `<section><h2>${escapeHtml(heading)}</h2><address>${content}</address></section>`;
537
+ const content = [name ? `<strong>${escapeHtml2(name)}</strong>` : "", ...address.map(escapeHtml2)].filter(Boolean).join("<br>");
538
+ return `<section><h2>${escapeHtml2(heading)}</h2><address>${content}</address></section>`;
308
539
  }
309
- return `<section><h2>${escapeHtml(heading)}</h2>${item.content.map(
310
- (content, index) => `<p>${index === 0 ? `<strong>${escapeHtml(content)}</strong>` : escapeHtml(content)}</p>`
540
+ return `<section><h2>${escapeHtml2(heading)}</h2>${item.content.map(
541
+ (content, index) => `<p>${index === 0 ? `<strong>${escapeHtml2(content)}</strong>` : escapeHtml2(content)}</p>`
311
542
  ).join("")}</section>`;
312
543
  }
313
544
  function titleCase(value) {
314
545
  const normalized = value.trim().toLocaleLowerCase("en");
315
546
  return normalized.replace(/^\p{L}/u, (letter) => letter.toLocaleUpperCase("en"));
316
547
  }
317
- function escapeHtml(value) {
548
+ function escapeHtml2(value) {
318
549
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
319
550
  }
320
551
 
321
552
  // src/index.ts
322
- var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
553
+ var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-semantic-document h1,.pdf-page--semantic h1{font-size:1.7em}.pdf-semantic-document h2,.pdf-page--semantic h2{font-size:1.5em}.pdf-semantic-document h3,.pdf-page--semantic h3{font-size:1.35em}.pdf-semantic-document h4,.pdf-page--semantic h4{font-size:1.1em}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
323
554
  async function writeHtmlDocument(pages, write, options = {}) {
324
555
  const includeDocument = options.includeDocument ?? true;
325
556
  if (includeDocument) {
@@ -328,7 +559,7 @@ async function writeHtmlDocument(pages, write, options = {}) {
328
559
  ` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
329
560
  );
330
561
  await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
331
- await write(`<title>${escapeHtml2(options.title ?? "PDF document")}</title>`);
562
+ await write(`<title>${escapeHtml3(options.title ?? "PDF document")}</title>`);
332
563
  if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
333
564
  await write("</head><body>");
334
565
  }
@@ -372,7 +603,7 @@ async function writePositionedPage(page, write, options) {
372
603
  const displayWidth = quarterTurn ? page.height : page.width;
373
604
  const displayHeight = quarterTurn ? page.width : page.height;
374
605
  await write(
375
- `<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number(displayWidth)}pt;height:${number(displayHeight)}pt">`
606
+ `<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">`
376
607
  );
377
608
  const fontAliases = new Map(
378
609
  (page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
@@ -384,10 +615,10 @@ async function writePositionedPage(page, write, options) {
384
615
  await write(`<style>${page.fonts.map((font) => fontFace(font, fontAliases)).join("")}</style>`);
385
616
  }
386
617
  await write(
387
- `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number(page.width)}pt;height:${number(page.height)}pt${rotationTransform(page)}">`
618
+ `<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
388
619
  );
389
620
  await write(
390
- `<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number(page.width)}pt" height="${number(page.height)}pt" viewBox="0 0 ${number(page.width)} ${number(page.height)}">`
621
+ `<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)}">`
391
622
  );
392
623
  const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
393
624
  if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
@@ -397,24 +628,24 @@ async function writePositionedPage(page, write, options) {
397
628
  }
398
629
  }
399
630
  for (const fill of page.fills ?? []) {
400
- const points = fill.points.map(([x, y]) => `${number(x)},${number(page.height - y)}`).join(" ");
631
+ const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
401
632
  if (isCssHexColor(fill.color)) {
402
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
633
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
403
634
  await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
404
635
  }
405
636
  }
406
637
  if (page.paths?.length) {
407
- await write(`<g transform="translate(0 ${number(page.height)}) scale(1 -1)">`);
638
+ await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
408
639
  for (const [pathIndex, path] of page.paths.entries()) {
409
640
  if (!isSvgPath(path.d)) continue;
410
641
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
411
642
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
412
- const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number(path.strokeWidth)}"` : "";
643
+ const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
413
644
  const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
414
- const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
415
- const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
416
- const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
417
- const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
645
+ const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
646
+ const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
647
+ const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
648
+ const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
418
649
  const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
419
650
  const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
420
651
  let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
@@ -451,11 +682,11 @@ function usesReflectedVisualOverlay(page, spans) {
451
682
  }
452
683
  function visualImage(image, pageHeight, pageNumber, imageIndex) {
453
684
  const [a, b, c, d, e, f] = image.transform;
454
- const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number).join(" ");
455
- const opacity = isUnitInterval(image.opacity) ? ` opacity="${number(image.opacity)}"` : "";
685
+ const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
686
+ const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
456
687
  const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
457
- const data = image.format === "jpeg" ? image.data : rgbBmp(image);
458
- let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
688
+ const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
689
+ let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base642(data)}"${opacity}/>`;
459
690
  for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
460
691
  output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
461
692
  }
@@ -466,7 +697,7 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
466
697
  (image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
467
698
  if (!isSvgPath(clip.d)) return "";
468
699
  const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
469
- return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
700
+ return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number2(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
470
701
  })
471
702
  ).join("");
472
703
  }
@@ -485,7 +716,7 @@ function pathClipDefinitions(paths, pageNumber) {
485
716
  function pathClipId(pageNumber, pathIndex, clipIndex) {
486
717
  return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
487
718
  }
488
- function rgbBmp(image) {
719
+ function rgbBmp2(image) {
489
720
  const stride = Math.ceil(image.width * 3 / 4) * 4;
490
721
  const output = new Uint8Array(54 + stride * image.height);
491
722
  const view = new DataView(output.buffer);
@@ -513,11 +744,11 @@ function rgbBmp(image) {
513
744
  function rotationTransform(page) {
514
745
  switch (page.rotate) {
515
746
  case 90:
516
- return `;transform:translate(${number(page.height)}pt,0) rotate(90deg)`;
747
+ return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
517
748
  case 180:
518
- return `;transform:translate(${number(page.width)}pt,${number(page.height)}pt) rotate(180deg)`;
749
+ return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
519
750
  case 270:
520
- return `;transform:translate(0,${number(page.width)}pt) rotate(270deg)`;
751
+ return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
521
752
  default:
522
753
  return "";
523
754
  }
@@ -525,70 +756,90 @@ function rotationTransform(page) {
525
756
  function positionedSpan(span, fontAliases) {
526
757
  const direction = directionAttribute([span]);
527
758
  const style = [
528
- `left:${number(span.bounds.x)}pt`,
529
- `bottom:${number(span.bounds.y)}pt`,
530
- `width:${number(span.bounds.width)}pt`,
531
- `height:${number(span.bounds.height)}pt`,
532
- `font-size:${number(span.fontSize)}pt`,
759
+ `left:${number2(span.bounds.x)}pt`,
760
+ `bottom:${number2(span.bounds.y)}pt`,
761
+ `width:${number2(span.bounds.width)}pt`,
762
+ `height:${number2(span.bounds.height)}pt`,
763
+ `font-size:${number2(span.fontSize)}pt`,
533
764
  ...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
534
- ...isUnitInterval(span.fillOpacity) ? [`opacity:${number(span.fillOpacity)}`] : [],
765
+ ...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
535
766
  ...fontStyles(
536
767
  span.fontFamily,
537
768
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
538
769
  )
539
770
  ].join(";");
540
- return `<span class="pdf-span"${direction} style="${style}">${escapeHtml2(span.text)}</span>`;
771
+ return `<span class="pdf-span"${direction} style="${style}">${escapeHtml3(span.text)}</span>`;
541
772
  }
542
773
  async function writeFlowPage(page, write) {
543
774
  const structured = structurePage2(page);
775
+ const defaultColor = dominantTextColor(structured.lines);
776
+ const media = semanticMedia(page);
777
+ let mediaIndex = 0;
544
778
  await write(
545
779
  `<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
546
780
  );
547
781
  for (const block of structured.blocks) {
782
+ const blockY = semanticBlockY2(block);
783
+ while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
784
+ await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
785
+ mediaIndex += 1;
786
+ }
548
787
  if (block.type === "table") await write(tableToHtml(block.table));
549
788
  else if (block.type === "heading") {
550
- await write(`<h${block.level}>${escapeHtml2(block.text)}</h${block.level}>`);
789
+ await write(
790
+ `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
791
+ );
551
792
  } else if (block.type === "paragraph") {
552
793
  await write(
553
- `<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${escapeHtml2(block.text)}</p>`
794
+ `<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`
554
795
  );
796
+ } else if (block.type === "preformatted") {
797
+ await write(`<pre>${escapeHtml3(block.text)}</pre>`);
555
798
  } else if (block.type === "definitionList") {
556
799
  await write("<dl>");
557
800
  for (const entry of block.entries) {
558
801
  await write(
559
- `<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
802
+ `<div><dt>${escapeHtml3(entry.term)}</dt><dd>${escapeHtml3(entry.description)}</dd></div>`
560
803
  );
561
804
  }
562
805
  await write("</dl>");
563
806
  } else if (block.type === "cardList") {
564
807
  await write('<div class="pdf-semantic-cards">');
565
808
  for (const item of block.items) {
566
- await write(`<article><h3>${escapeHtml2(item.title)}</h3>`);
567
- for (const detail of item.details) await write(`<p>${escapeHtml2(detail)}</p>`);
809
+ await write(`<article><h3>${escapeHtml3(item.title)}</h3>`);
810
+ for (const detail of item.details) await write(`<p>${escapeHtml3(detail)}</p>`);
568
811
  await write("</article>");
569
812
  }
570
813
  await write("</div>");
571
814
  } else if (block.type === "sectionGroup") {
572
815
  await write('<div class="pdf-semantic-sections">');
573
816
  for (const item of block.items) {
574
- await write(`<section><h3>${escapeHtml2(item.label)}</h3>`);
575
- for (const content of item.content) await write(`<p>${escapeHtml2(content)}</p>`);
817
+ await write(`<section><h3>${escapeHtml3(item.label)}</h3>`);
818
+ for (const content of item.content) await write(`<p>${escapeHtml3(content)}</p>`);
576
819
  await write("</section>");
577
820
  }
578
821
  await write("</div>");
579
822
  } else if (block.type === "employment") {
580
823
  await write(
581
- `<section><h3>${escapeHtml2(block.role)}</h3><p>${escapeHtml2(block.organization)}</p><p>${escapeHtml2(block.date)}</p></section>`
824
+ `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`
582
825
  );
583
826
  } else {
584
827
  const tag = block.ordered ? "ol" : "ul";
585
828
  await write(`<${tag}>`);
586
- for (const item of block.items) await write(`<li>${escapeHtml2(item.text)}</li>`);
829
+ for (const item of block.items) await write(`<li>${escapeHtml3(item.text)}</li>`);
587
830
  await write(`</${tag}>`);
588
831
  }
589
832
  }
833
+ while (mediaIndex < media.length) {
834
+ await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
835
+ mediaIndex += 1;
836
+ }
590
837
  await write("</section>");
591
838
  }
839
+ function semanticBlockY2(block) {
840
+ const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
841
+ return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
842
+ }
592
843
  function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
593
844
  if (span.renderingMode === 3 || span.renderingMode === 7) return "";
594
845
  if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
@@ -598,10 +849,10 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
598
849
  span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
599
850
  ).join(";");
600
851
  const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
601
- const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number(span.strokeWidth ?? 0)}` : "";
852
+ const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number2(span.strokeWidth ?? 0)}` : "";
602
853
  const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
603
- const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number(span.fillOpacity)}` : "";
604
- const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number(span.strokeOpacity)}` : "";
854
+ const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
855
+ const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number2(span.strokeOpacity)}` : "";
605
856
  const style = [
606
857
  isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
607
858
  span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
@@ -613,7 +864,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
613
864
  font
614
865
  ].filter(Boolean).join(";");
615
866
  const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
616
- const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
867
+ const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number2(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
617
868
  const transform = counterRotateReflectedText && span.transform ? [
618
869
  span.transform[0],
619
870
  span.transform[1],
@@ -626,8 +877,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
626
877
  const basisY = transform?.[1] ?? 0;
627
878
  const anchorX = span.bounds.x + basisX * rtlOffset;
628
879
  const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
629
- const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
630
- return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml2(span.text)}</text>`;
880
+ const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
881
+ return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
631
882
  }
632
883
  function isAdobeCjkFont(fontFamily) {
633
884
  return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
@@ -639,16 +890,16 @@ function visualType3Text(span, font, pageHeight) {
639
890
  const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
640
891
  if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
641
892
  const transform = span.transform ?? [1, 0, 0, 1];
642
- const outer = `matrix(${transform.map(number).join(" ")} ${number(span.bounds.x)} ${number(pageHeight - span.bounds.y)})`;
893
+ const outer = `matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(pageHeight - span.bounds.y)})`;
643
894
  const xScale = span.bounds.width / totalAdvance;
644
895
  let offset = 0;
645
896
  let content = "";
646
897
  for (const glyph of sequence) {
647
898
  if (!glyph) continue;
648
- content += `<g transform="translate(${number(offset)} 0)">${type3Glyph(glyph)}</g>`;
899
+ content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph)}</g>`;
649
900
  offset += glyph.advance;
650
901
  }
651
- return `<g transform="${outer}"><g transform="scale(${number(xScale)} ${number(-span.fontSize)})">${content}</g></g>`;
902
+ return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
652
903
  }
653
904
  function isHebrewPaintOrder(span) {
654
905
  return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
@@ -660,15 +911,15 @@ function type3Glyph(glyph) {
660
911
  let output = "";
661
912
  for (const fill of glyph.fills ?? []) {
662
913
  if (!isCssHexColor(fill.color)) continue;
663
- const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
664
- const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
914
+ const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
915
+ const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
665
916
  output += `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
666
917
  }
667
918
  for (const path of glyph.paths ?? []) {
668
919
  if (!isSvgPath(path.d)) continue;
669
920
  const fill = isCssHexColor(path.fill) ? path.fill : "none";
670
921
  const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
671
- const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number(path.strokeWidth)}"` : "";
922
+ const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
672
923
  output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
673
924
  }
674
925
  return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
@@ -718,9 +969,9 @@ function fontFace(font, aliases) {
718
969
  const styles2 = fontStyles(font.family, alias).filter(
719
970
  (style) => !style.startsWith("font-family:")
720
971
  );
721
- return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base64(font.data)}) format("truetype");${styles2.join(";")}}`;
972
+ return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
722
973
  }
723
- function base64(bytes) {
974
+ function base642(bytes) {
724
975
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
725
976
  let output = "";
726
977
  for (let index = 0; index < bytes.length; index += 3) {
@@ -740,13 +991,13 @@ function directionAttribute(spans) {
740
991
  if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
741
992
  return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
742
993
  }
743
- function number(value) {
994
+ function number2(value) {
744
995
  return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
745
996
  }
746
997
  function escapeAttribute(value) {
747
- return escapeHtml2(value).replaceAll("`", "&#96;");
998
+ return escapeHtml3(value).replaceAll("`", "&#96;");
748
999
  }
749
- function escapeHtml2(value) {
1000
+ function escapeHtml3(value) {
750
1001
  return [...value].map((character) => {
751
1002
  const codePoint = character.codePointAt(0) ?? 0;
752
1003
  if (codePoint === 13) return "\n";