@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 +337 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +337 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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>${
|
|
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(
|
|
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(
|
|
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}>${
|
|
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>${
|
|
117
|
-
for (const detail of details) await write(`<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>${
|
|
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>${
|
|
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(
|
|
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}>${
|
|
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}>${
|
|
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}>${
|
|
268
|
-
if (block.type === "paragraph")
|
|
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>${
|
|
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>${
|
|
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>${
|
|
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>${
|
|
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>${
|
|
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">${
|
|
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>${
|
|
307
|
-
return `<section><h2>${
|
|
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>${
|
|
310
|
-
(content, index) => `<p>${index === 0 ? `<strong>${
|
|
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
|
|
548
|
+
function escapeHtml2(value) {
|
|
318
549
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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>${
|
|
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:${
|
|
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:${
|
|
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="${
|
|
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]) => `${
|
|
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="${
|
|
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 ${
|
|
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="${
|
|
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="${
|
|
415
|
-
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${
|
|
416
|
-
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(
|
|
417
|
-
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${
|
|
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(
|
|
455
|
-
const opacity = isUnitInterval(image.opacity) ? ` 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 :
|
|
458
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${
|
|
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 ${
|
|
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
|
|
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(${
|
|
747
|
+
return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
|
|
517
748
|
case 180:
|
|
518
|
-
return `;transform:translate(${
|
|
749
|
+
return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
|
|
519
750
|
case 270:
|
|
520
|
-
return `;transform:translate(0,${
|
|
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:${
|
|
529
|
-
`bottom:${
|
|
530
|
-
`width:${
|
|
531
|
-
`height:${
|
|
532
|
-
`font-size:${
|
|
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:${
|
|
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}">${
|
|
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(
|
|
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))}>${
|
|
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>${
|
|
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>${
|
|
567
|
-
for (const detail of item.details) await write(`<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>${
|
|
575
|
-
for (const content of item.content) await write(`<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>${
|
|
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>${
|
|
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:${
|
|
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:${
|
|
604
|
-
const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${
|
|
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="${
|
|
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(
|
|
630
|
-
return `<text${direction}${position} font-size="${
|
|
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(
|
|
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(${
|
|
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(${
|
|
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]) => `${
|
|
664
|
-
const opacity = isUnitInterval(fill.opacity) ? ` 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="${
|
|
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,${
|
|
972
|
+
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
722
973
|
}
|
|
723
|
-
function
|
|
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
|
|
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
|
|
998
|
+
return escapeHtml3(value).replaceAll("`", "`");
|
|
748
999
|
}
|
|
749
|
-
function
|
|
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";
|