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