@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.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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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>${
|
|
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(
|
|
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(
|
|
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}>${
|
|
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>${
|
|
140
|
-
for (const detail of details) await write(`<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>${
|
|
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>${
|
|
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(
|
|
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}>${
|
|
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}>${
|
|
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}>${
|
|
291
|
-
if (block.type === "paragraph")
|
|
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>${
|
|
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>${
|
|
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>${
|
|
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>${
|
|
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>${
|
|
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">${
|
|
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>${
|
|
330
|
-
return `<section><h2>${
|
|
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>${
|
|
333
|
-
(content, index) => `<p>${index === 0 ? `<strong>${
|
|
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
|
|
571
|
+
function escapeHtml2(value) {
|
|
341
572
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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>${
|
|
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:${
|
|
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:${
|
|
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="${
|
|
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]) => `${
|
|
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="${
|
|
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 ${
|
|
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="${
|
|
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="${
|
|
438
|
-
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${
|
|
439
|
-
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(
|
|
440
|
-
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${
|
|
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(
|
|
478
|
-
const opacity = isUnitInterval(image.opacity) ? ` 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 :
|
|
481
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${
|
|
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 ${
|
|
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
|
|
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(${
|
|
770
|
+
return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
|
|
540
771
|
case 180:
|
|
541
|
-
return `;transform:translate(${
|
|
772
|
+
return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
|
|
542
773
|
case 270:
|
|
543
|
-
return `;transform:translate(0,${
|
|
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:${
|
|
552
|
-
`bottom:${
|
|
553
|
-
`width:${
|
|
554
|
-
`height:${
|
|
555
|
-
`font-size:${
|
|
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:${
|
|
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}">${
|
|
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(
|
|
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))}>${
|
|
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>${
|
|
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>${
|
|
590
|
-
for (const detail of item.details) await write(`<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>${
|
|
598
|
-
for (const content of item.content) await write(`<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>${
|
|
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>${
|
|
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:${
|
|
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:${
|
|
627
|
-
const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${
|
|
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="${
|
|
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(
|
|
653
|
-
return `<text${direction}${position} font-size="${
|
|
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(
|
|
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(${
|
|
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(${
|
|
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]) => `${
|
|
687
|
-
const opacity = isUnitInterval(fill.opacity) ? ` 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="${
|
|
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,${
|
|
995
|
+
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
745
996
|
}
|
|
746
|
-
function
|
|
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
|
|
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
|
|
1021
|
+
return escapeHtml3(value).replaceAll("`", "`");
|
|
771
1022
|
}
|
|
772
|
-
function
|
|
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";
|