@boxpdf/html-writer 0.1.18 → 0.1.21
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/README.md +45 -8
- package/dist/index.cjs +765 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +764 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,39 +2,150 @@
|
|
|
2
2
|
import { structurePage as structurePage2, tableToHtml } from "@boxpdf/reader/structure";
|
|
3
3
|
|
|
4
4
|
// src/semantic-caption.ts
|
|
5
|
-
|
|
6
|
-
if (block.type !== "paragraph" || block.lines.length === 0) return false;
|
|
7
|
-
const bounds2 = unionLines(block.lines);
|
|
8
|
-
const lineHeight = median(block.lines.map((line) => line.bounds.height));
|
|
9
|
-
if (media.bounds.width < pageWidth * 0.2 || media.bounds.height < lineHeight * 10) return false;
|
|
10
|
-
if (media.bounds.x < -2 || media.bounds.y < -2 || media.bounds.x + media.bounds.width > pageWidth + 2 || media.bounds.y + media.bounds.height > pageHeight + 2)
|
|
11
|
-
return false;
|
|
12
|
-
const gap = media.bounds.y - (bounds2.y + bounds2.height);
|
|
13
|
-
if (gap < -lineHeight * 0.15 || gap > lineHeight * 1.25) return false;
|
|
14
|
-
const mediaCenter = media.bounds.x + media.bounds.width / 2;
|
|
15
|
-
const captionCenter = bounds2.x + bounds2.width / 2;
|
|
16
|
-
if (Math.abs(mediaCenter - captionCenter) > Math.max(3, media.bounds.width * 0.03)) return false;
|
|
17
|
-
if (bounds2.width < media.bounds.width * 0.45 || bounds2.width > media.bounds.width * 1.06) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
|
|
21
|
-
if (!first) return false;
|
|
22
|
-
const otherLines = pageLines.filter((line) => !block.lines.includes(line));
|
|
23
|
-
return fontSignature(first) !== dominantFontSignature(otherLines);
|
|
24
|
-
}
|
|
5
|
+
var minimumCaptionScore = 0.72;
|
|
25
6
|
function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
|
|
7
|
+
const candidates = blocks.flatMap((block) => {
|
|
8
|
+
const candidate = captionCandidate(block);
|
|
9
|
+
return candidate ? [candidate] : [];
|
|
10
|
+
});
|
|
11
|
+
const preliminary = media.flatMap(
|
|
12
|
+
(item) => candidates.flatMap((candidate) => {
|
|
13
|
+
const evidence = scoreCaption(item, candidate, pageWidth, pageHeight, pageLines, 0);
|
|
14
|
+
return evidence ? [{ media: item, candidate, evidence }] : [];
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
const patterns = repeatedPatterns(preliminary);
|
|
18
|
+
const edges = preliminary.map((edge) => {
|
|
19
|
+
const evidence = scoreCaption(
|
|
20
|
+
edge.media,
|
|
21
|
+
edge.candidate,
|
|
22
|
+
pageWidth,
|
|
23
|
+
pageHeight,
|
|
24
|
+
pageLines,
|
|
25
|
+
patterns.get(patternKey(edge)) ?? 0
|
|
26
|
+
);
|
|
27
|
+
return evidence ? { ...edge, evidence } : void 0;
|
|
28
|
+
}).filter((edge) => Boolean(edge)).filter((edge) => edge.evidence.score >= minimumCaptionScore);
|
|
29
|
+
const bestForMedia = bestEdges(edges, (edge) => edge.media);
|
|
30
|
+
const bestForCaption = bestEdges(edges, (edge) => edge.candidate.block);
|
|
26
31
|
const associations = /* @__PURE__ */ new Map();
|
|
27
|
-
for (const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
for (const edge of edges) {
|
|
33
|
+
if (bestForMedia.get(edge.media) === edge && bestForCaption.get(edge.candidate.block) === edge) {
|
|
34
|
+
associations.set(edge.candidate.block, edge.media);
|
|
35
|
+
}
|
|
31
36
|
}
|
|
32
37
|
return associations;
|
|
33
38
|
}
|
|
34
|
-
function
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
function scoreCaption(media, candidate, pageWidth, pageHeight, pageLines, repeatedAlignment) {
|
|
40
|
+
if (!insidePage(media.bounds, pageWidth, pageHeight)) return void 0;
|
|
41
|
+
if (media.bounds.width < pageWidth * 0.06 || media.bounds.height < candidate.lineHeight * 1.5)
|
|
42
|
+
return void 0;
|
|
43
|
+
const relation = verticalRelation(media.bounds, candidate.bounds);
|
|
44
|
+
if (!relation) return void 0;
|
|
45
|
+
const maximumGap = Math.max(candidate.lineHeight * 3, pageHeight * 0.035);
|
|
46
|
+
if (relation.gap > maximumGap) return void 0;
|
|
47
|
+
const overlapWidth = overlap(
|
|
48
|
+
media.bounds.x,
|
|
49
|
+
media.bounds.width,
|
|
50
|
+
candidate.bounds.x,
|
|
51
|
+
candidate.bounds.width
|
|
52
|
+
);
|
|
53
|
+
const horizontalOverlap = overlapWidth / Math.max(1, Math.min(media.bounds.width, candidate.bounds.width));
|
|
54
|
+
const centerDistance = Math.abs(center(media.bounds) - center(candidate.bounds));
|
|
55
|
+
const centerAlignment = clamp01(
|
|
56
|
+
1 - centerDistance / Math.max(media.bounds.width, candidate.bounds.width)
|
|
57
|
+
);
|
|
58
|
+
if (horizontalOverlap < 0.45 && centerAlignment < 0.82) return void 0;
|
|
59
|
+
const relativeWidth = Math.min(media.bounds.width, candidate.bounds.width) / Math.max(media.bounds.width, candidate.bounds.width);
|
|
60
|
+
const gapRatio = clamp01(1 - relation.gap / maximumGap);
|
|
61
|
+
const interveningContent = interveningScore(media.bounds, candidate, relation.side, pageLines);
|
|
62
|
+
if (interveningContent === 0) return void 0;
|
|
63
|
+
if (relation.side === "above" && (gapRatio < 0.6 || interveningContent < 1)) return void 0;
|
|
64
|
+
const fontContrast = captionFontContrast(candidate, pageLines);
|
|
65
|
+
const surroundingWhitespace = whitespaceScore(candidate, relation.side, relation.gap, pageLines);
|
|
66
|
+
const score = gapRatio * 0.22 + horizontalOverlap * 0.17 + centerAlignment * 0.13 + relativeWidth * 0.09 + fontContrast * 0.13 + surroundingWhitespace * 0.09 + interveningContent * 0.09 + repeatedAlignment * 0.08;
|
|
67
|
+
return {
|
|
68
|
+
score,
|
|
69
|
+
side: relation.side,
|
|
70
|
+
gapRatio,
|
|
71
|
+
horizontalOverlap,
|
|
72
|
+
centerAlignment,
|
|
73
|
+
relativeWidth,
|
|
74
|
+
fontContrast,
|
|
75
|
+
surroundingWhitespace,
|
|
76
|
+
interveningContent,
|
|
77
|
+
repeatedAlignment
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function captionCandidate(block) {
|
|
81
|
+
if (block.type !== "paragraph" || block.lines.length === 0) return void 0;
|
|
82
|
+
const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
|
|
83
|
+
if (!first) return void 0;
|
|
84
|
+
return {
|
|
85
|
+
block,
|
|
86
|
+
bounds: unionLines(block.lines),
|
|
87
|
+
lineHeight: median(block.lines.map((line) => line.bounds.height)),
|
|
88
|
+
font: fontSignature(first)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function verticalRelation(media, caption) {
|
|
92
|
+
const belowGap = media.y - (caption.y + caption.height);
|
|
93
|
+
if (belowGap >= -caption.height * 0.15) return { side: "below", gap: Math.max(0, belowGap) };
|
|
94
|
+
const aboveGap = caption.y - (media.y + media.height);
|
|
95
|
+
if (aboveGap >= -caption.height * 0.15) return { side: "above", gap: Math.max(0, aboveGap) };
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
function interveningScore(media, candidate, side, pageLines) {
|
|
99
|
+
const lower = side === "below" ? candidate.bounds.y + candidate.bounds.height : media.y + media.height;
|
|
100
|
+
const upper = side === "below" ? media.y : candidate.bounds.y;
|
|
101
|
+
const blockers = pageLines.filter(
|
|
102
|
+
(line) => !candidate.block.lines.includes(line) && line.bounds.y < upper && line.bounds.y + line.bounds.height > lower && overlap(line.bounds.x, line.bounds.width, media.x, media.width) / Math.max(1, Math.min(line.bounds.width, media.width)) >= 0.25
|
|
103
|
+
);
|
|
104
|
+
return blockers.length === 0 ? 1 : blockers.length === 1 ? 0.35 : 0;
|
|
105
|
+
}
|
|
106
|
+
function captionFontContrast(candidate, pageLines) {
|
|
107
|
+
const otherLines = pageLines.filter((line) => !candidate.block.lines.includes(line));
|
|
108
|
+
if (candidate.font !== dominantFontSignature(otherLines)) return 1;
|
|
109
|
+
const candidateSize = median(
|
|
110
|
+
candidate.block.lines.flatMap((line) => line.spans.map((span) => span.fontSize))
|
|
111
|
+
);
|
|
112
|
+
const bodySize = median(otherLines.flatMap((line) => line.spans.map((span) => span.fontSize)));
|
|
113
|
+
return Math.abs(candidateSize - bodySize) >= 0.75 ? 0.65 : 0.15;
|
|
114
|
+
}
|
|
115
|
+
function whitespaceScore(candidate, side, mediaGap, pageLines) {
|
|
116
|
+
const awayGaps = pageLines.filter((line) => !candidate.block.lines.includes(line)).filter(
|
|
117
|
+
(line) => overlap(line.bounds.x, line.bounds.width, candidate.bounds.x, candidate.bounds.width) > 0
|
|
118
|
+
).flatMap((line) => {
|
|
119
|
+
if (side === "below" && line.bounds.y + line.bounds.height <= candidate.bounds.y)
|
|
120
|
+
return [candidate.bounds.y - line.bounds.y - line.bounds.height];
|
|
121
|
+
if (side === "above" && line.bounds.y >= candidate.bounds.y + candidate.bounds.height)
|
|
122
|
+
return [line.bounds.y - candidate.bounds.y - candidate.bounds.height];
|
|
123
|
+
return [];
|
|
124
|
+
});
|
|
125
|
+
const awayGap = Math.min(...awayGaps, Number.POSITIVE_INFINITY);
|
|
126
|
+
return Number.isFinite(awayGap) ? clamp01((awayGap + candidate.lineHeight * 0.25) / (mediaGap + candidate.lineHeight)) : 1;
|
|
127
|
+
}
|
|
128
|
+
function repeatedPatterns(edges) {
|
|
129
|
+
const counts = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const edge of edges.filter((item) => item.evidence.score >= minimumCaptionScore - 0.08)) {
|
|
131
|
+
counts.set(patternKey(edge), (counts.get(patternKey(edge)) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
return new Map([...counts].map(([key, count]) => [key, count >= 2 ? 1 : 0]));
|
|
134
|
+
}
|
|
135
|
+
function patternKey(edge) {
|
|
136
|
+
const widthRatio = edge.candidate.bounds.width / Math.max(1, edge.media.bounds.width);
|
|
137
|
+
return `${edge.candidate.font}|${edge.evidence.side}|${Math.round(widthRatio * 4) / 4}`;
|
|
138
|
+
}
|
|
139
|
+
function bestEdges(edges, key) {
|
|
140
|
+
const output = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const edge of edges) {
|
|
142
|
+
const existing = output.get(key(edge));
|
|
143
|
+
if (!existing || edge.evidence.score > existing.evidence.score) output.set(key(edge), edge);
|
|
144
|
+
}
|
|
145
|
+
return output;
|
|
146
|
+
}
|
|
147
|
+
function insidePage(bounds2, pageWidth, pageHeight) {
|
|
148
|
+
return bounds2.x >= -2 && bounds2.y >= -2 && bounds2.x + bounds2.width <= pageWidth + 2 && bounds2.y + bounds2.height <= pageHeight + 2;
|
|
38
149
|
}
|
|
39
150
|
function unionLines(lines) {
|
|
40
151
|
const x = Math.min(...lines.map((line) => line.bounds.x));
|
|
@@ -54,6 +165,15 @@ function dominantFontSignature(lines) {
|
|
|
54
165
|
function fontSignature(span) {
|
|
55
166
|
return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
|
|
56
167
|
}
|
|
168
|
+
function center(bounds2) {
|
|
169
|
+
return bounds2.x + bounds2.width / 2;
|
|
170
|
+
}
|
|
171
|
+
function overlap(left, leftSize, right, rightSize) {
|
|
172
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
173
|
+
}
|
|
174
|
+
function clamp01(value) {
|
|
175
|
+
return Math.max(0, Math.min(1, value));
|
|
176
|
+
}
|
|
57
177
|
function median(values) {
|
|
58
178
|
const ordered = [...values].sort((left, right) => left - right);
|
|
59
179
|
return ordered[Math.floor(ordered.length / 2)] ?? 1;
|
|
@@ -75,6 +195,12 @@ function dominantTextColor(lines) {
|
|
|
75
195
|
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
76
196
|
}
|
|
77
197
|
function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
198
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "html");
|
|
199
|
+
}
|
|
200
|
+
function semanticTextMarkdown(text, lines, defaultColor, preserveWeight = true) {
|
|
201
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "markdown");
|
|
202
|
+
}
|
|
203
|
+
function semanticText(text, lines, defaultColor, preserveWeight, format) {
|
|
78
204
|
const ranges = [];
|
|
79
205
|
let cursor = 0;
|
|
80
206
|
for (const span of lines.flatMap((line) => line.spans)) {
|
|
@@ -100,11 +226,11 @@ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
|
100
226
|
let html = "";
|
|
101
227
|
let offset = 0;
|
|
102
228
|
for (const range of merged) {
|
|
103
|
-
html +=
|
|
104
|
-
html +=
|
|
229
|
+
html += escapeText(text.slice(offset, range.start), format);
|
|
230
|
+
html += styledText(text.slice(range.start, range.end), range, format);
|
|
105
231
|
offset = range.end;
|
|
106
232
|
}
|
|
107
|
-
return html +
|
|
233
|
+
return html + escapeText(text.slice(offset), format);
|
|
108
234
|
}
|
|
109
235
|
function mergeRanges(ranges, text) {
|
|
110
236
|
const merged = [];
|
|
@@ -118,13 +244,19 @@ function mergeRanges(ranges, text) {
|
|
|
118
244
|
}
|
|
119
245
|
return merged;
|
|
120
246
|
}
|
|
121
|
-
function
|
|
122
|
-
let html =
|
|
247
|
+
function styledText(value, range, format) {
|
|
248
|
+
let html = escapeText(value, format);
|
|
123
249
|
if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
|
|
124
|
-
if (range.italic) html = `<em>${html}</em
|
|
125
|
-
if (range.bold) html = `<strong>${html}</strong
|
|
250
|
+
if (range.italic) html = format === "html" ? `<em>${html}</em>` : `_${html}_`;
|
|
251
|
+
if (range.bold) html = format === "html" ? `<strong>${html}</strong>` : `**${html}**`;
|
|
126
252
|
return html;
|
|
127
253
|
}
|
|
254
|
+
function escapeText(value, format) {
|
|
255
|
+
return format === "html" ? escapeHtml(value) : escapeMarkdown(value);
|
|
256
|
+
}
|
|
257
|
+
function escapeMarkdown(value) {
|
|
258
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
259
|
+
}
|
|
128
260
|
function normalizedColor(value) {
|
|
129
261
|
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
130
262
|
const color = value.toLowerCase();
|
|
@@ -260,22 +392,39 @@ function base64(bytes) {
|
|
|
260
392
|
}
|
|
261
393
|
|
|
262
394
|
// src/semantic-media.ts
|
|
263
|
-
function semanticMedia(page) {
|
|
264
|
-
|
|
265
|
-
output
|
|
266
|
-
|
|
395
|
+
function semanticMedia(page, imageOptions = "embedded") {
|
|
396
|
+
if (imageOptions === "excluded") return [];
|
|
397
|
+
const output = (page.images ?? []).map(
|
|
398
|
+
(image, index) => rasterMedia(image, page.number, index, imageOptions)
|
|
399
|
+
);
|
|
400
|
+
output.push(...vectorMedia(page, imageOptions));
|
|
401
|
+
return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
|
|
402
|
+
}
|
|
403
|
+
async function prepareSemanticMedia(page, imageOptions, onImage) {
|
|
404
|
+
const media = semanticMedia(page, imageOptions);
|
|
405
|
+
for (const item of media) {
|
|
406
|
+
for (const asset of item.assets ?? []) await onImage?.(asset);
|
|
407
|
+
delete item.assets;
|
|
408
|
+
}
|
|
409
|
+
return media;
|
|
267
410
|
}
|
|
268
|
-
function rasterMedia(image) {
|
|
411
|
+
function rasterMedia(image, pageNumber, index, imageOptions) {
|
|
269
412
|
const bounds2 = transformedUnitBounds(image.transform);
|
|
270
413
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
271
414
|
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
415
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
416
|
+
const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
|
|
417
|
+
const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
|
|
272
418
|
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
273
419
|
return {
|
|
274
420
|
bounds: bounds2,
|
|
275
|
-
|
|
421
|
+
kind: "raster",
|
|
422
|
+
html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
|
|
423
|
+
markdown: ``,
|
|
424
|
+
...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
|
|
276
425
|
};
|
|
277
426
|
}
|
|
278
|
-
function vectorMedia(page) {
|
|
427
|
+
function vectorMedia(page, imageOptions) {
|
|
279
428
|
const primitives = [
|
|
280
429
|
...(page.paths ?? []).flatMap((path, index) => {
|
|
281
430
|
const bounds2 = vectorPathBounds(path);
|
|
@@ -293,7 +442,7 @@ function vectorMedia(page) {
|
|
|
293
442
|
const visualCodeFonts = new Set(
|
|
294
443
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
295
444
|
);
|
|
296
|
-
return components.map((component) => {
|
|
445
|
+
return components.map((component, componentIndex) => {
|
|
297
446
|
const bounds2 = component.bounds;
|
|
298
447
|
const paths = component.primitives.flatMap(
|
|
299
448
|
(primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
|
|
@@ -310,13 +459,82 @@ function vectorMedia(page) {
|
|
|
310
459
|
);
|
|
311
460
|
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
312
461
|
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
462
|
+
const svg = `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number2(bounds2.x)} ${number2(page.height - bounds2.y - bounds2.height)} ${number2(bounds2.width)} ${number2(bounds2.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true">${fontFaces ? `<style>${fontFaces}</style>` : ""}${paths.length ? `<defs>${vectorPathClipDefinitions(paths, page.number)}</defs>` : ""}<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">${fills.map(vectorFillSvg).join("") + paths.map(({ path, index }) => vectorPathSvg(path, page.number, index)).join("")}</g>${overlay.map((span) => vectorText(span, page.height, aliases)).join("")}</svg>`;
|
|
463
|
+
const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
|
|
313
464
|
return {
|
|
314
465
|
bounds: bounds2,
|
|
315
|
-
|
|
466
|
+
kind: "vector",
|
|
467
|
+
html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
|
|
468
|
+
markdown: imageOptions === "references" ? `` : svg,
|
|
469
|
+
...imageOptions === "references" ? {
|
|
470
|
+
assets: [
|
|
471
|
+
{ name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
|
|
472
|
+
]
|
|
473
|
+
} : {},
|
|
316
474
|
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
317
475
|
};
|
|
318
476
|
});
|
|
319
477
|
}
|
|
478
|
+
function mediaComponents(media, page) {
|
|
479
|
+
const components = [];
|
|
480
|
+
for (const item of media) {
|
|
481
|
+
if (isPageBackdrop(item.bounds, page)) {
|
|
482
|
+
components.push([item]);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const matches = components.filter(
|
|
486
|
+
(component) => !component.some((member) => isPageBackdrop(member.bounds, page)) && component.some((member) => mediaPiecesTouch(member.bounds, item.bounds))
|
|
487
|
+
);
|
|
488
|
+
if (matches.length === 0) {
|
|
489
|
+
components.push([item]);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const target = matches[0];
|
|
493
|
+
target.push(item);
|
|
494
|
+
for (const component of matches.slice(1)) {
|
|
495
|
+
target.push(...component);
|
|
496
|
+
components.splice(components.indexOf(component), 1);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return components.map((component) => compositeMedia(component));
|
|
500
|
+
}
|
|
501
|
+
function compositeMedia(items) {
|
|
502
|
+
if (items.length === 1) return items[0];
|
|
503
|
+
const bounds2 = unionBounds(items.map((item) => item.bounds));
|
|
504
|
+
const layers = items.map((item) => {
|
|
505
|
+
const left = (item.bounds.x - bounds2.x) / bounds2.width * 100;
|
|
506
|
+
const top = (bounds2.y + bounds2.height - item.bounds.y - item.bounds.height) / bounds2.height * 100;
|
|
507
|
+
const width = item.bounds.width / bounds2.width * 100;
|
|
508
|
+
const height = item.bounds.height / bounds2.height * 100;
|
|
509
|
+
return `<div style="position:absolute;left:${number2(left)}%;top:${number2(top)}%;width:${number2(width)}%;height:${number2(height)}%;overflow:hidden">${item.html}</div>`;
|
|
510
|
+
}).join("");
|
|
511
|
+
return {
|
|
512
|
+
bounds: bounds2,
|
|
513
|
+
kind: "composite",
|
|
514
|
+
html: `<div class="pdf-semantic-media pdf-semantic-media-composite" style="position:relative;max-width:100%;width:${number2(bounds2.width)}px;aspect-ratio:${number2(bounds2.width)}/${number2(bounds2.height)}">${layers}</div>`,
|
|
515
|
+
markdown: items.map((item) => item.markdown).join("\n\n"),
|
|
516
|
+
consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
|
|
517
|
+
assets: items.flatMap((item) => item.assets ?? [])
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function mediaPiecesTouch(left, right) {
|
|
521
|
+
const xOverlap = overlap2(left.x, left.width, right.x, right.width);
|
|
522
|
+
const yOverlap = overlap2(left.y, left.height, right.y, right.height);
|
|
523
|
+
if (xOverlap > 0 && yOverlap > 0) return true;
|
|
524
|
+
const horizontalGap = axisGap(left.x, left.width, right.x, right.width);
|
|
525
|
+
const verticalGap = axisGap(left.y, left.height, right.y, right.height);
|
|
526
|
+
if (horizontalGap <= 2 && yOverlap / Math.min(left.height, right.height) >= 0.65) return true;
|
|
527
|
+
return verticalGap <= 2 && xOverlap / Math.min(left.width, right.width) >= 0.65;
|
|
528
|
+
}
|
|
529
|
+
function isPageBackdrop(bounds2, page) {
|
|
530
|
+
return bounds2.width * bounds2.height >= page.width * page.height * 0.7;
|
|
531
|
+
}
|
|
532
|
+
function overlap2(left, leftSize, right, rightSize) {
|
|
533
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
534
|
+
}
|
|
535
|
+
function axisGap(left, leftSize, right, rightSize) {
|
|
536
|
+
return Math.max(0, right - left - leftSize, left - right - rightSize);
|
|
537
|
+
}
|
|
320
538
|
function vectorComponents(primitives, padding) {
|
|
321
539
|
const components = [];
|
|
322
540
|
for (const primitive of primitives) {
|
|
@@ -435,7 +653,7 @@ function escapeHtml2(value) {
|
|
|
435
653
|
}
|
|
436
654
|
|
|
437
655
|
// src/semantic-document.ts
|
|
438
|
-
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
656
|
+
async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
|
|
439
657
|
const stats = {
|
|
440
658
|
pagesProcessed: 0,
|
|
441
659
|
peakBufferedPages: 0,
|
|
@@ -453,27 +671,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
453
671
|
let contentStarted = false;
|
|
454
672
|
let employmentOpen = false;
|
|
455
673
|
let pendingParagraph;
|
|
456
|
-
|
|
674
|
+
const markdown = format === "markdown";
|
|
675
|
+
const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
|
|
676
|
+
const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
|
|
677
|
+
await output('<article class="pdf-semantic-document">');
|
|
457
678
|
const closeTable = async () => {
|
|
458
679
|
if (!activeTable) return;
|
|
459
|
-
await
|
|
680
|
+
await output("</table>", "\n");
|
|
460
681
|
activeTable = void 0;
|
|
461
682
|
while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
|
|
462
683
|
};
|
|
463
684
|
const closeSections = async (minimumLevel = 0) => {
|
|
464
685
|
while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
|
|
465
|
-
await
|
|
686
|
+
await output("</section>");
|
|
466
687
|
sectionLevels.pop();
|
|
467
688
|
}
|
|
468
689
|
};
|
|
469
690
|
const flushPendingParagraph = async () => {
|
|
470
691
|
if (!pendingParagraph) return;
|
|
471
|
-
await write(
|
|
692
|
+
await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
|
|
472
693
|
pendingParagraph = void 0;
|
|
473
694
|
};
|
|
474
695
|
const closeEmployment = async () => {
|
|
475
696
|
if (!employmentOpen) return;
|
|
476
|
-
await
|
|
697
|
+
await output("</section>");
|
|
477
698
|
employmentOpen = false;
|
|
478
699
|
};
|
|
479
700
|
const emitPage = async (page, future) => {
|
|
@@ -487,29 +708,53 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
487
708
|
page.structured.lines
|
|
488
709
|
);
|
|
489
710
|
const captionedMedia = new Set(captions.values());
|
|
711
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
490
712
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
491
713
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
492
714
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
493
715
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
494
716
|
const blockY = semanticBlockY(block);
|
|
495
717
|
let emittedAsCaption = false;
|
|
718
|
+
while (page.media[mediaIndex] && emittedMedia.has(page.media[mediaIndex])) {
|
|
719
|
+
mediaIndex += 1;
|
|
720
|
+
}
|
|
496
721
|
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
497
722
|
await flushPendingParagraph();
|
|
498
723
|
const item = page.media[mediaIndex];
|
|
499
724
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
500
|
-
const html2 =
|
|
725
|
+
const html2 = markdown ? `${item.markdown}
|
|
726
|
+
|
|
727
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
728
|
+
|
|
729
|
+
` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
501
730
|
if (activeTable) pendingMedia.push(html2);
|
|
502
731
|
else await write(html2);
|
|
732
|
+
emittedMedia.add(item);
|
|
503
733
|
mediaIndex += 1;
|
|
504
734
|
emittedAsCaption = true;
|
|
505
735
|
break;
|
|
506
736
|
}
|
|
507
737
|
if (item && captionedMedia.has(item)) break;
|
|
508
|
-
const html =
|
|
738
|
+
const html = markdown ? `${item?.markdown ?? ""}
|
|
739
|
+
|
|
740
|
+
` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
|
|
509
741
|
if (activeTable) pendingMedia.push(html);
|
|
510
742
|
else await write(html);
|
|
511
743
|
mediaIndex += 1;
|
|
512
744
|
}
|
|
745
|
+
const associatedMedia = captions.get(block);
|
|
746
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
747
|
+
await flushPendingParagraph();
|
|
748
|
+
const html = markdown ? `${associatedMedia.markdown}
|
|
749
|
+
|
|
750
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
751
|
+
|
|
752
|
+
` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
753
|
+
if (activeTable) pendingMedia.push(html);
|
|
754
|
+
else await write(html);
|
|
755
|
+
emittedMedia.add(associatedMedia);
|
|
756
|
+
emittedAsCaption = true;
|
|
757
|
+
}
|
|
513
758
|
if (emittedAsCaption) continue;
|
|
514
759
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
515
760
|
stats.suppressedFurniture += 1;
|
|
@@ -518,8 +763,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
518
763
|
await flushPendingParagraph();
|
|
519
764
|
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
520
765
|
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
521
|
-
await
|
|
522
|
-
`<header><h1>${
|
|
766
|
+
await output(
|
|
767
|
+
`<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
|
|
768
|
+
`# ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
769
|
+
|
|
770
|
+
`
|
|
523
771
|
);
|
|
524
772
|
headerOpen = true;
|
|
525
773
|
continue;
|
|
@@ -527,19 +775,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
527
775
|
if (headerOpen) {
|
|
528
776
|
if (block.type === "paragraph") {
|
|
529
777
|
const tag = isContactBlock(block) ? "address" : "p";
|
|
530
|
-
await
|
|
531
|
-
`<${tag}>${
|
|
778
|
+
await output(
|
|
779
|
+
`<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
|
|
780
|
+
`${inlineText(block.text, block.lines, defaultColor)}
|
|
781
|
+
|
|
782
|
+
`
|
|
532
783
|
);
|
|
533
784
|
headerHasParagraph = true;
|
|
534
785
|
continue;
|
|
535
786
|
}
|
|
536
787
|
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
537
|
-
await
|
|
538
|
-
`<h${block.level}>${
|
|
788
|
+
await output(
|
|
789
|
+
`<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
|
|
790
|
+
`${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
791
|
+
|
|
792
|
+
`
|
|
539
793
|
);
|
|
540
794
|
continue;
|
|
541
795
|
}
|
|
542
|
-
await
|
|
796
|
+
await output("</header>");
|
|
543
797
|
headerOpen = false;
|
|
544
798
|
contentStarted = true;
|
|
545
799
|
}
|
|
@@ -547,23 +801,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
547
801
|
const rows = tableToRows(block.table);
|
|
548
802
|
if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
|
|
549
803
|
const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
|
|
550
|
-
for (const row of continuationRows)
|
|
804
|
+
for (const row of continuationRows)
|
|
805
|
+
await write(markdown ? markdownTableRow(row) : tableRow(row, false));
|
|
551
806
|
activeTable.table = block.table;
|
|
552
807
|
stats.mergedTables += 1;
|
|
553
808
|
continue;
|
|
554
809
|
}
|
|
555
810
|
await closeTable();
|
|
556
811
|
const header = tableHeader(rows);
|
|
557
|
-
await
|
|
558
|
-
|
|
559
|
-
|
|
812
|
+
await output("<table>", markdownTableStart(rows, header));
|
|
813
|
+
const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
|
|
814
|
+
for (const [index, row] of markdownRows.entries())
|
|
815
|
+
await write(
|
|
816
|
+
markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
|
|
817
|
+
);
|
|
560
818
|
activeTable = { table: block.table, header };
|
|
561
819
|
continue;
|
|
562
820
|
}
|
|
563
821
|
if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
|
|
564
822
|
const columns = activeTable.table.columns.length;
|
|
565
|
-
await
|
|
566
|
-
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot
|
|
823
|
+
await output(
|
|
824
|
+
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
|
|
825
|
+
block.entries.map(
|
|
826
|
+
(entry) => markdownTableRow([
|
|
827
|
+
entry.term,
|
|
828
|
+
...Array(Math.max(0, columns - 2)).fill(""),
|
|
829
|
+
entry.description
|
|
830
|
+
])
|
|
831
|
+
).join("")
|
|
567
832
|
);
|
|
568
833
|
await closeTable();
|
|
569
834
|
continue;
|
|
@@ -572,8 +837,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
572
837
|
if (block.type === "heading") {
|
|
573
838
|
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
574
839
|
await closeSections(level);
|
|
575
|
-
await
|
|
576
|
-
`<section data-level="${level}"><h${level}>${
|
|
840
|
+
await output(
|
|
841
|
+
`<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
|
|
842
|
+
`${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
843
|
+
|
|
844
|
+
`
|
|
577
845
|
);
|
|
578
846
|
sectionLevels.push(level);
|
|
579
847
|
continue;
|
|
@@ -581,13 +849,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
581
849
|
if (block.type === "paragraph") {
|
|
582
850
|
if (isTitledRecord(block)) {
|
|
583
851
|
const [institution, ...details] = block.lines;
|
|
584
|
-
if (institution)
|
|
585
|
-
|
|
852
|
+
if (institution)
|
|
853
|
+
await output(
|
|
854
|
+
`<h3>${escapeHtml3(institution.text)}</h3>`,
|
|
855
|
+
`### ${escapeMarkdown2(institution.text)}
|
|
856
|
+
|
|
857
|
+
`
|
|
858
|
+
);
|
|
859
|
+
for (const detail of details)
|
|
860
|
+
await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
|
|
861
|
+
|
|
862
|
+
`);
|
|
586
863
|
continue;
|
|
587
864
|
}
|
|
588
865
|
if (isUnmarkedList(block)) {
|
|
589
|
-
await
|
|
590
|
-
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul
|
|
866
|
+
await output(
|
|
867
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
|
|
868
|
+
`${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
|
|
869
|
+
|
|
870
|
+
`
|
|
591
871
|
);
|
|
592
872
|
continue;
|
|
593
873
|
}
|
|
@@ -595,27 +875,40 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
595
875
|
continue;
|
|
596
876
|
}
|
|
597
877
|
if (block.type === "employment") {
|
|
598
|
-
await
|
|
599
|
-
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p
|
|
878
|
+
await output(
|
|
879
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
|
|
880
|
+
`### ${escapeMarkdown2(block.role)}
|
|
881
|
+
|
|
882
|
+
${escapeMarkdown2(block.organization)}
|
|
883
|
+
|
|
884
|
+
${escapeMarkdown2(block.date)}
|
|
885
|
+
|
|
886
|
+
`
|
|
600
887
|
);
|
|
601
888
|
employmentOpen = true;
|
|
602
889
|
continue;
|
|
603
890
|
}
|
|
604
|
-
await write(
|
|
891
|
+
await write(semanticBlockOutput(block, defaultColor, format));
|
|
605
892
|
}
|
|
606
893
|
while (mediaIndex < page.media.length) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
894
|
+
const item = page.media[mediaIndex];
|
|
895
|
+
if (item && !emittedMedia.has(item)) {
|
|
896
|
+
await flushPendingParagraph();
|
|
897
|
+
const html = markdown ? `${item.markdown}
|
|
898
|
+
|
|
899
|
+
` : `<div class="pdf-semantic-visual">${item.html}</div>`;
|
|
900
|
+
if (activeTable) pendingMedia.push(html);
|
|
901
|
+
else await write(html);
|
|
902
|
+
}
|
|
611
903
|
mediaIndex += 1;
|
|
612
904
|
}
|
|
613
905
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
614
906
|
};
|
|
615
907
|
for await (const page of pages) {
|
|
616
|
-
const media =
|
|
908
|
+
const media = await prepareSemanticMedia(page, imageOptions, onImage);
|
|
617
909
|
const structured = structurePage(withoutSemanticMediaSpans(page, media));
|
|
618
910
|
buffer.push({ width: page.width, height: page.height, structured, media });
|
|
911
|
+
restoreObservedHyphens(buffer);
|
|
619
912
|
stats.pagesProcessed += 1;
|
|
620
913
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
621
914
|
stats.peakBufferedLines = Math.max(
|
|
@@ -631,22 +924,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
631
924
|
const ready = buffer.shift();
|
|
632
925
|
if (ready) await emitPage(ready, buffer);
|
|
633
926
|
}
|
|
634
|
-
if (headerOpen) await
|
|
927
|
+
if (headerOpen) await output("</header>");
|
|
635
928
|
await closeTable();
|
|
636
929
|
await closeEmployment();
|
|
637
930
|
if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
|
|
638
931
|
await closeSections();
|
|
639
|
-
await
|
|
640
|
-
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer
|
|
932
|
+
await output(
|
|
933
|
+
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
|
|
934
|
+
`---
|
|
935
|
+
|
|
936
|
+
${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
|
|
641
937
|
);
|
|
642
938
|
pendingParagraph = void 0;
|
|
643
939
|
} else {
|
|
644
940
|
await flushPendingParagraph();
|
|
645
941
|
await closeSections();
|
|
646
942
|
}
|
|
647
|
-
await
|
|
943
|
+
await output("</article>");
|
|
648
944
|
return stats;
|
|
649
945
|
}
|
|
946
|
+
function restoreObservedHyphens(buffer) {
|
|
947
|
+
const terms = new Set(
|
|
948
|
+
buffer.flatMap(
|
|
949
|
+
(page) => page.structured.lines.flatMap(
|
|
950
|
+
(line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
);
|
|
954
|
+
for (const page of buffer) {
|
|
955
|
+
for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function restoreBlockHyphens(block, terms) {
|
|
959
|
+
const restore = (value) => restoreTextHyphens(value, terms);
|
|
960
|
+
if (block.type === "insetGroup") {
|
|
961
|
+
for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
|
|
962
|
+
} else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
|
|
963
|
+
block.text = restore(block.text);
|
|
964
|
+
} else if (block.type === "list") {
|
|
965
|
+
for (const item of block.items) item.text = restore(item.text);
|
|
966
|
+
} else if (block.type === "definitionList") {
|
|
967
|
+
for (const entry of block.entries) {
|
|
968
|
+
entry.term = restore(entry.term);
|
|
969
|
+
entry.description = restore(entry.description);
|
|
970
|
+
}
|
|
971
|
+
} else if (block.type === "cardList") {
|
|
972
|
+
for (const item of block.items) {
|
|
973
|
+
item.title = restore(item.title);
|
|
974
|
+
item.details = item.details.map(restore);
|
|
975
|
+
}
|
|
976
|
+
} else if (block.type === "sectionGroup") {
|
|
977
|
+
for (const item of block.items) {
|
|
978
|
+
item.label = restore(item.label);
|
|
979
|
+
item.content = item.content.map(restore);
|
|
980
|
+
}
|
|
981
|
+
} else if (block.type === "employment") {
|
|
982
|
+
block.role = restore(block.role);
|
|
983
|
+
block.organization = restore(block.organization);
|
|
984
|
+
block.date = restore(block.date);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function restoreTextHyphens(value, terms) {
|
|
988
|
+
let output = value;
|
|
989
|
+
for (const term of terms) {
|
|
990
|
+
const collapsed = term.replace(/[-‐‑]/gu, "");
|
|
991
|
+
if (collapsed === term || !output.includes(collapsed)) continue;
|
|
992
|
+
const pattern = new RegExp(
|
|
993
|
+
`(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
|
|
994
|
+
"gu"
|
|
995
|
+
);
|
|
996
|
+
output = output.replace(pattern, term);
|
|
997
|
+
}
|
|
998
|
+
return output;
|
|
999
|
+
}
|
|
1000
|
+
function escapeRegularExpression(value) {
|
|
1001
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1002
|
+
}
|
|
650
1003
|
function isContactBlock(block) {
|
|
651
1004
|
const text = block.text;
|
|
652
1005
|
const signals = [
|
|
@@ -730,6 +1083,20 @@ function tableRow(row, header) {
|
|
|
730
1083
|
const cell = header ? "th" : "td";
|
|
731
1084
|
return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
|
|
732
1085
|
}
|
|
1086
|
+
function markdownTableStart(rows, header) {
|
|
1087
|
+
const columns = rows[0]?.length ?? 0;
|
|
1088
|
+
if (columns === 0) return "";
|
|
1089
|
+
const heading = header ?? Array(columns).fill("");
|
|
1090
|
+
return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
|
|
1091
|
+
}
|
|
1092
|
+
function markdownTableRow(row, shouldEscape = true) {
|
|
1093
|
+
const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
|
|
1094
|
+
return `| ${cells.join(" | ")} |
|
|
1095
|
+
`;
|
|
1096
|
+
}
|
|
1097
|
+
function escapeMarkdownTableCell(value) {
|
|
1098
|
+
return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
|
|
1099
|
+
}
|
|
733
1100
|
function isFinancialSummary(block) {
|
|
734
1101
|
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
735
1102
|
}
|
|
@@ -777,6 +1144,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
777
1144
|
const tag = block.ordered ? "ol" : "ul";
|
|
778
1145
|
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
779
1146
|
}
|
|
1147
|
+
function semanticBlockOutput(block, defaultColor, format) {
|
|
1148
|
+
return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
|
|
1149
|
+
}
|
|
1150
|
+
function semanticBlockMarkdown(block, defaultColor = "#000000") {
|
|
1151
|
+
if (block.type === "insetGroup") {
|
|
1152
|
+
const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
|
|
1153
|
+
return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
|
|
1154
|
+
|
|
1155
|
+
`;
|
|
1156
|
+
}
|
|
1157
|
+
if (block.type === "table") {
|
|
1158
|
+
const rows = tableToRows(block.table);
|
|
1159
|
+
const header = tableHeader(rows);
|
|
1160
|
+
return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
|
|
1161
|
+
`;
|
|
1162
|
+
}
|
|
1163
|
+
if (block.type === "heading") {
|
|
1164
|
+
return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
|
|
1165
|
+
|
|
1166
|
+
`;
|
|
1167
|
+
}
|
|
1168
|
+
if (block.type === "paragraph") {
|
|
1169
|
+
return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
|
|
1170
|
+
|
|
1171
|
+
`;
|
|
1172
|
+
}
|
|
1173
|
+
if (block.type === "preformatted") {
|
|
1174
|
+
const fence = block.text.includes("```") ? "````" : "```";
|
|
1175
|
+
return `${fence}
|
|
1176
|
+
${block.text}
|
|
1177
|
+
${fence}
|
|
1178
|
+
|
|
1179
|
+
`;
|
|
1180
|
+
}
|
|
1181
|
+
if (block.type === "definitionList") {
|
|
1182
|
+
return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
|
|
1183
|
+
|
|
1184
|
+
`;
|
|
1185
|
+
}
|
|
1186
|
+
if (block.type === "cardList") {
|
|
1187
|
+
const rows = [
|
|
1188
|
+
["Item", "Quantity", "Amount"],
|
|
1189
|
+
...block.items.map((item) => {
|
|
1190
|
+
const trailing = item.details.at(-1) ?? "";
|
|
1191
|
+
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
1192
|
+
const detail = item.details.slice(0, -1).join(" ");
|
|
1193
|
+
return [
|
|
1194
|
+
`${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
1195
|
+
match?.[1] ?? "",
|
|
1196
|
+
match?.[2] ?? trailing
|
|
1197
|
+
];
|
|
1198
|
+
})
|
|
1199
|
+
];
|
|
1200
|
+
return `## Items ordered
|
|
1201
|
+
|
|
1202
|
+
${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
|
|
1203
|
+
`;
|
|
1204
|
+
}
|
|
1205
|
+
if (block.type === "sectionGroup") {
|
|
1206
|
+
return block.items.map(
|
|
1207
|
+
(item) => `## ${escapeMarkdown2(titleCase(item.label))}
|
|
1208
|
+
|
|
1209
|
+
${item.content.map(
|
|
1210
|
+
(content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
|
|
1211
|
+
).join("\n\n")}
|
|
1212
|
+
|
|
1213
|
+
`
|
|
1214
|
+
).join("");
|
|
1215
|
+
}
|
|
1216
|
+
if (block.type === "employment") {
|
|
1217
|
+
return `### ${escapeMarkdown2(block.role)}
|
|
1218
|
+
|
|
1219
|
+
${escapeMarkdown2(block.organization)}
|
|
1220
|
+
|
|
1221
|
+
${escapeMarkdown2(block.date)}
|
|
1222
|
+
|
|
1223
|
+
`;
|
|
1224
|
+
}
|
|
1225
|
+
return `${block.items.map(
|
|
1226
|
+
(item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
|
|
1227
|
+
).join("\n")}
|
|
1228
|
+
|
|
1229
|
+
`;
|
|
1230
|
+
}
|
|
780
1231
|
function semanticBlockY(block) {
|
|
781
1232
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
782
1233
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
@@ -809,6 +1260,9 @@ function titleCase(value) {
|
|
|
809
1260
|
function escapeHtml3(value) {
|
|
810
1261
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
811
1262
|
}
|
|
1263
|
+
function escapeMarkdown2(value) {
|
|
1264
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
1265
|
+
}
|
|
812
1266
|
|
|
813
1267
|
// src/index.ts
|
|
814
1268
|
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}`;
|
|
@@ -825,9 +1279,18 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
825
1279
|
await write("</head><body>");
|
|
826
1280
|
}
|
|
827
1281
|
await write('<main class="pdf-document">');
|
|
828
|
-
|
|
1282
|
+
const profile = resolveProfile(options);
|
|
1283
|
+
const imageOptions = resolveImageOptions(profile, options);
|
|
1284
|
+
validateImageOptions(imageOptions, options);
|
|
1285
|
+
if (profile === "semantic") {
|
|
829
1286
|
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
830
|
-
const stats = await writeSemanticDocument(
|
|
1287
|
+
const stats = await writeSemanticDocument(
|
|
1288
|
+
pages,
|
|
1289
|
+
write,
|
|
1290
|
+
lookahead,
|
|
1291
|
+
imageOptions,
|
|
1292
|
+
options.onImage
|
|
1293
|
+
);
|
|
831
1294
|
options.onSemanticStats?.(stats);
|
|
832
1295
|
} else {
|
|
833
1296
|
for await (const page of pages) await writePage(page, write, options);
|
|
@@ -835,6 +1298,20 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
835
1298
|
await write("</main>");
|
|
836
1299
|
if (includeDocument) await write("</body></html>");
|
|
837
1300
|
}
|
|
1301
|
+
async function writeMarkdownDocument(pages, write, options = {}) {
|
|
1302
|
+
const imageOptions = options.imageOptions ?? "excluded";
|
|
1303
|
+
validateImageOptions(imageOptions, options);
|
|
1304
|
+
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
1305
|
+
const stats = await writeSemanticDocument(
|
|
1306
|
+
pages,
|
|
1307
|
+
write,
|
|
1308
|
+
lookahead,
|
|
1309
|
+
imageOptions,
|
|
1310
|
+
options.onImage,
|
|
1311
|
+
"markdown"
|
|
1312
|
+
);
|
|
1313
|
+
options.onSemanticStats?.(stats);
|
|
1314
|
+
}
|
|
838
1315
|
function semanticLookahead(value) {
|
|
839
1316
|
const lookahead = value ?? 4;
|
|
840
1317
|
if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
|
|
@@ -843,7 +1320,9 @@ function semanticLookahead(value) {
|
|
|
843
1320
|
return lookahead;
|
|
844
1321
|
}
|
|
845
1322
|
async function writePage(page, write, options = {}) {
|
|
846
|
-
|
|
1323
|
+
const profile = resolveProfile(options);
|
|
1324
|
+
validateImageOptions(resolveImageOptions(profile, options), options);
|
|
1325
|
+
if (profile === "semantic") await writeFlowPage(page, write, options);
|
|
847
1326
|
else await writePositionedPage(page, write, options);
|
|
848
1327
|
}
|
|
849
1328
|
async function pageToHtml(page, options = {}) {
|
|
@@ -858,7 +1337,9 @@ async function pageToHtml(page, options = {}) {
|
|
|
858
1337
|
return output;
|
|
859
1338
|
}
|
|
860
1339
|
async function writePositionedPage(page, write, options) {
|
|
861
|
-
const
|
|
1340
|
+
const imageOptions = resolveImageOptions("visual", options);
|
|
1341
|
+
const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
|
|
1342
|
+
const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
|
|
862
1343
|
const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
|
|
863
1344
|
const quarterTurn = page.rotate === 90 || page.rotate === 270;
|
|
864
1345
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
@@ -870,25 +1351,32 @@ async function writePositionedPage(page, write, options) {
|
|
|
870
1351
|
const type3Fonts = new Map(
|
|
871
1352
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
872
1353
|
);
|
|
1354
|
+
const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
|
|
873
1355
|
if ((options.includeStyles ?? true) && page.fonts?.length) {
|
|
874
1356
|
await write(
|
|
875
1357
|
`<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
876
1358
|
);
|
|
877
1359
|
}
|
|
1360
|
+
if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
|
|
878
1361
|
await write(
|
|
879
1362
|
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
|
|
880
1363
|
);
|
|
881
1364
|
await write(
|
|
882
1365
|
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
|
|
883
1366
|
);
|
|
884
|
-
const clipDefinitions = imageClipDefinitions(
|
|
1367
|
+
const clipDefinitions = imageClipDefinitions(
|
|
1368
|
+
imageOptions === "excluded" ? [] : page.images ?? [],
|
|
1369
|
+
page.number,
|
|
1370
|
+
page.height
|
|
1371
|
+
) + vectorPathClipDefinitions(
|
|
885
1372
|
(page.paths ?? []).map((path, index) => ({ path, index })),
|
|
886
1373
|
page.number
|
|
887
1374
|
);
|
|
888
1375
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
889
1376
|
if (reflectedOverlay) {
|
|
890
1377
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
891
|
-
|
|
1378
|
+
const source = visualImages[index];
|
|
1379
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
892
1380
|
}
|
|
893
1381
|
}
|
|
894
1382
|
if (page.fills?.length || page.paths?.length) {
|
|
@@ -901,14 +1389,29 @@ async function writePositionedPage(page, write, options) {
|
|
|
901
1389
|
}
|
|
902
1390
|
if (!reflectedOverlay) {
|
|
903
1391
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
904
|
-
|
|
1392
|
+
const source = visualImages[index];
|
|
1393
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
905
1394
|
}
|
|
906
1395
|
}
|
|
907
|
-
for (
|
|
1396
|
+
for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
|
|
1397
|
+
const span = visualSpans[spanIndex];
|
|
1398
|
+
if (!span) continue;
|
|
908
1399
|
if (!usesPositionedSpan(span)) {
|
|
909
1400
|
const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
|
|
1401
|
+
const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
|
|
1402
|
+
if (line) {
|
|
1403
|
+
await write(line.html);
|
|
1404
|
+
spanIndex = line.endIndex;
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
910
1407
|
await write(
|
|
911
|
-
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1408
|
+
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1409
|
+
span,
|
|
1410
|
+
page.height,
|
|
1411
|
+
fontAliases,
|
|
1412
|
+
reflectedOverlay && page.rotate === 180,
|
|
1413
|
+
textClasses?.names
|
|
1414
|
+
)
|
|
912
1415
|
);
|
|
913
1416
|
}
|
|
914
1417
|
}
|
|
@@ -918,23 +1421,131 @@ async function writePositionedPage(page, write, options) {
|
|
|
918
1421
|
}
|
|
919
1422
|
await write("</div></section>");
|
|
920
1423
|
}
|
|
1424
|
+
function coalesceVisualSpans(spans) {
|
|
1425
|
+
const output = [];
|
|
1426
|
+
for (const span of spans) {
|
|
1427
|
+
const previous = output.at(-1);
|
|
1428
|
+
if (!previous || !canCoalesceVisualSpans(previous, span)) {
|
|
1429
|
+
output.push(span);
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
output[output.length - 1] = {
|
|
1433
|
+
...previous,
|
|
1434
|
+
text: previous.text + span.text,
|
|
1435
|
+
bounds: {
|
|
1436
|
+
...previous.bounds,
|
|
1437
|
+
width: span.bounds.x + span.bounds.width - previous.bounds.x,
|
|
1438
|
+
height: Math.max(previous.bounds.height, span.bounds.height)
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
return output;
|
|
1443
|
+
}
|
|
1444
|
+
function canCoalesceVisualSpans(left, right) {
|
|
1445
|
+
if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
|
|
1446
|
+
if (left.direction !== "ltr" || right.direction !== "ltr") return false;
|
|
1447
|
+
if (/guardian/i.test(left.fontFamily ?? "")) return false;
|
|
1448
|
+
if (left.glyphCodes || right.glyphCodes) return false;
|
|
1449
|
+
if (!sameVisualTextState(left, right)) return false;
|
|
1450
|
+
const tolerance = Math.max(0.02, left.fontSize * 0.015);
|
|
1451
|
+
if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
|
|
1452
|
+
const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
|
|
1453
|
+
return !right.hasLeadingSpace && gap >= -tolerance && gap <= tolerance;
|
|
1454
|
+
}
|
|
1455
|
+
function sameVisualTextState(left, right) {
|
|
1456
|
+
return Math.abs(left.fontSize - right.fontSize) <= 1e-3 && left.fontName === right.fontName && left.fontFamily === right.fontFamily && left.fontAssetId === right.fontAssetId && left.color === right.color && left.fillOpacity === right.fillOpacity && left.strokeColor === right.strokeColor && left.strokeWidth === right.strokeWidth && left.strokeOpacity === right.strokeOpacity && left.renderingMode === right.renderingMode && sameTransform(left.transform, right.transform);
|
|
1457
|
+
}
|
|
1458
|
+
function sameTransform(left, right) {
|
|
1459
|
+
if (!left || !right) return left === right;
|
|
1460
|
+
return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
|
|
1461
|
+
}
|
|
1462
|
+
function visualTextClasses(pageNumber, spans, fontAliases) {
|
|
1463
|
+
const names = /* @__PURE__ */ new Map();
|
|
1464
|
+
let css = "";
|
|
1465
|
+
for (const span of spans) {
|
|
1466
|
+
if (usesPositionedSpan(span) || span.glyphCodes) {
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
const style = visualTextClassStyle(span, fontAliases);
|
|
1470
|
+
if (!style || names.has(style)) continue;
|
|
1471
|
+
const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
|
|
1472
|
+
names.set(style, name);
|
|
1473
|
+
css += `.${name}{${style}}`;
|
|
1474
|
+
}
|
|
1475
|
+
return { css, names };
|
|
1476
|
+
}
|
|
1477
|
+
function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
|
|
1478
|
+
const first = spans[startIndex];
|
|
1479
|
+
if (!first || !canGroupVisualTextLine(first)) return void 0;
|
|
1480
|
+
const style = visualTextClassStyle(first, fontAliases);
|
|
1481
|
+
const className = styleClasses.get(style);
|
|
1482
|
+
if (!className) return void 0;
|
|
1483
|
+
let endIndex = startIndex;
|
|
1484
|
+
while (endIndex + 1 < spans.length) {
|
|
1485
|
+
const next = spans[endIndex + 1];
|
|
1486
|
+
if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
endIndex += 1;
|
|
1490
|
+
}
|
|
1491
|
+
if (endIndex === startIndex) return void 0;
|
|
1492
|
+
const baseline = pageHeight - first.bounds.y;
|
|
1493
|
+
const lineSpans = spans.slice(startIndex, endIndex + 1);
|
|
1494
|
+
const content = lineSpans.map(
|
|
1495
|
+
(span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
|
|
1496
|
+
).join("");
|
|
1497
|
+
return {
|
|
1498
|
+
html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
|
|
1499
|
+
endIndex
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
function visualTextTspan(span, dx) {
|
|
1503
|
+
const extent = span.bounds.width;
|
|
1504
|
+
const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
|
|
1505
|
+
const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1506
|
+
return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
|
|
1507
|
+
}
|
|
1508
|
+
function textSpanGap(previous, current) {
|
|
1509
|
+
if (!previous) return 0;
|
|
1510
|
+
const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
|
|
1511
|
+
const adjustment = current.textAdjustmentBefore;
|
|
1512
|
+
return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
|
|
1513
|
+
}
|
|
1514
|
+
function canGroupVisualTextLine(span) {
|
|
1515
|
+
return !usesPositionedSpan(span) && !span.glyphCodes && span.direction === "ltr" && !isHebrewPaintOrder(span) && !hasNonIdentityTransform(span.transform) && span.renderingMode !== 3 && span.renderingMode !== 7 && (span.fontAssetId !== void 0 || !isAdobeCjkFont(span.fontFamily));
|
|
1516
|
+
}
|
|
921
1517
|
function usesReflectedVisualOverlay(page, spans) {
|
|
922
1518
|
return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
|
|
923
1519
|
(span) => span.transform !== void 0 && Math.abs(span.transform[0] + 1) < 1e-6 && Math.abs(span.transform[1]) < 1e-6 && Math.abs(span.transform[2]) < 1e-6 && Math.abs(span.transform[3] - 1) < 1e-6
|
|
924
1520
|
);
|
|
925
1521
|
}
|
|
926
|
-
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
1522
|
+
function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
|
|
927
1523
|
const [a, b, c, d, e, f] = image.transform;
|
|
928
1524
|
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
|
|
929
1525
|
const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
|
|
930
|
-
|
|
931
|
-
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
932
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
1526
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
|
|
933
1527
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
934
1528
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
935
1529
|
}
|
|
936
1530
|
return output;
|
|
937
1531
|
}
|
|
1532
|
+
async function prepareVisualImages(page, imageOptions, onImage) {
|
|
1533
|
+
if (imageOptions === "excluded") return [];
|
|
1534
|
+
const sources = [];
|
|
1535
|
+
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1536
|
+
const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
1537
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
1538
|
+
if (imageOptions === "embedded") {
|
|
1539
|
+
sources.push(`data:${mimeType};base64,${base64(data)}`);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
1543
|
+
const name = `page-${page.number}-image-${index + 1}.${extension}`;
|
|
1544
|
+
await onImage?.({ name, mimeType, data });
|
|
1545
|
+
sources.push(name);
|
|
1546
|
+
}
|
|
1547
|
+
return sources;
|
|
1548
|
+
}
|
|
938
1549
|
function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
939
1550
|
return images.flatMap(
|
|
940
1551
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
@@ -1001,8 +1612,9 @@ function positionedSpan(span, fontAliases) {
|
|
|
1001
1612
|
].join(";");
|
|
1002
1613
|
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
|
|
1003
1614
|
}
|
|
1004
|
-
async function writeFlowPage(page, write) {
|
|
1005
|
-
const
|
|
1615
|
+
async function writeFlowPage(page, write, options) {
|
|
1616
|
+
const imageOptions = resolveImageOptions("semantic", options);
|
|
1617
|
+
const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
|
|
1006
1618
|
const structured = structurePage2(withoutSemanticMediaSpans(page, media));
|
|
1007
1619
|
const defaultColor = dominantTextColor(structured.lines);
|
|
1008
1620
|
let mediaIndex = 0;
|
|
@@ -1014,18 +1626,22 @@ async function writeFlowPage(page, write) {
|
|
|
1014
1626
|
structured.lines
|
|
1015
1627
|
);
|
|
1016
1628
|
const captionedMedia = new Set(captions.values());
|
|
1629
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
1017
1630
|
await write(
|
|
1018
1631
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
1019
1632
|
);
|
|
1020
1633
|
for (const block of structured.blocks) {
|
|
1021
1634
|
const blockY = semanticBlockY2(block);
|
|
1022
1635
|
let emittedAsCaption = false;
|
|
1636
|
+
while (media[mediaIndex] && emittedMedia.has(media[mediaIndex]))
|
|
1637
|
+
mediaIndex += 1;
|
|
1023
1638
|
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
1024
1639
|
const item = media[mediaIndex];
|
|
1025
1640
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
1026
1641
|
await write(
|
|
1027
1642
|
`<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1028
1643
|
);
|
|
1644
|
+
emittedMedia.add(item);
|
|
1029
1645
|
mediaIndex += 1;
|
|
1030
1646
|
emittedAsCaption = true;
|
|
1031
1647
|
break;
|
|
@@ -1034,6 +1650,14 @@ async function writeFlowPage(page, write) {
|
|
|
1034
1650
|
await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
|
|
1035
1651
|
mediaIndex += 1;
|
|
1036
1652
|
}
|
|
1653
|
+
const associatedMedia = captions.get(block);
|
|
1654
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
1655
|
+
await write(
|
|
1656
|
+
`<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1657
|
+
);
|
|
1658
|
+
emittedMedia.add(associatedMedia);
|
|
1659
|
+
emittedAsCaption = true;
|
|
1660
|
+
}
|
|
1037
1661
|
if (emittedAsCaption) continue;
|
|
1038
1662
|
if (block.type === "table") await write(tableToHtml(block.table));
|
|
1039
1663
|
else if (block.type === "heading") {
|
|
@@ -1088,7 +1712,10 @@ async function writeFlowPage(page, write) {
|
|
|
1088
1712
|
}
|
|
1089
1713
|
}
|
|
1090
1714
|
while (mediaIndex < media.length) {
|
|
1091
|
-
|
|
1715
|
+
const item = media[mediaIndex];
|
|
1716
|
+
if (item && !emittedMedia.has(item)) {
|
|
1717
|
+
await write(`<div class="pdf-semantic-visual">${item.html}</div>`);
|
|
1718
|
+
}
|
|
1092
1719
|
mediaIndex += 1;
|
|
1093
1720
|
}
|
|
1094
1721
|
await write("</section>");
|
|
@@ -1124,10 +1751,37 @@ function semanticBlockY2(block) {
|
|
|
1124
1751
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
1125
1752
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
1126
1753
|
}
|
|
1127
|
-
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
|
|
1754
|
+
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
|
|
1128
1755
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
1129
1756
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
1130
1757
|
const direction = directionAttribute([span]);
|
|
1758
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1759
|
+
const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
|
|
1760
|
+
const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
|
|
1761
|
+
const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
|
|
1762
|
+
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1763
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1764
|
+
const transform = counterRotateReflectedText && span.transform ? [
|
|
1765
|
+
span.transform[0],
|
|
1766
|
+
span.transform[1],
|
|
1767
|
+
span.transform[2],
|
|
1768
|
+
-span.transform[3]
|
|
1769
|
+
] : span.transform;
|
|
1770
|
+
const transformed = hasNonIdentityTransform(transform);
|
|
1771
|
+
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1772
|
+
const basisX = transform?.[0] ?? 1;
|
|
1773
|
+
const basisY = transform?.[1] ?? 0;
|
|
1774
|
+
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1775
|
+
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1776
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1777
|
+
return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
|
|
1778
|
+
}
|
|
1779
|
+
function visualTextClassStyle(span, fontAliases) {
|
|
1780
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1781
|
+
const fontSize = `font-size:${number3(span.fontSize)}px`;
|
|
1782
|
+
return style ? `${style};${fontSize}` : fontSize;
|
|
1783
|
+
}
|
|
1784
|
+
function visualTextStyle(span, fontAliases) {
|
|
1131
1785
|
const font = visualFontStyles(
|
|
1132
1786
|
span.fontFamily,
|
|
1133
1787
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -1137,7 +1791,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1137
1791
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
1138
1792
|
const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
|
|
1139
1793
|
const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
|
|
1140
|
-
|
|
1794
|
+
return [
|
|
1141
1795
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
1142
1796
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
1143
1797
|
strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
|
|
@@ -1147,22 +1801,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1147
1801
|
strokeOpacity,
|
|
1148
1802
|
font
|
|
1149
1803
|
].filter(Boolean).join(";");
|
|
1150
|
-
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1151
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1152
|
-
const transform = counterRotateReflectedText && span.transform ? [
|
|
1153
|
-
span.transform[0],
|
|
1154
|
-
span.transform[1],
|
|
1155
|
-
span.transform[2],
|
|
1156
|
-
-span.transform[3]
|
|
1157
|
-
] : span.transform;
|
|
1158
|
-
const transformed = hasNonIdentityTransform(transform);
|
|
1159
|
-
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1160
|
-
const basisX = transform?.[0] ?? 1;
|
|
1161
|
-
const basisY = transform?.[1] ?? 0;
|
|
1162
|
-
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1163
|
-
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1164
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1165
|
-
return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
1166
1804
|
}
|
|
1167
1805
|
function isAdobeCjkFont(fontFamily) {
|
|
1168
1806
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -1257,9 +1895,18 @@ function resolveProfile(options) {
|
|
|
1257
1895
|
}
|
|
1258
1896
|
return options.profile ?? legacyProfile;
|
|
1259
1897
|
}
|
|
1898
|
+
function resolveImageOptions(profile, options) {
|
|
1899
|
+
return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
|
|
1900
|
+
}
|
|
1901
|
+
function validateImageOptions(imageOptions, options) {
|
|
1902
|
+
if (imageOptions === "references" && !options.onImage) {
|
|
1903
|
+
throw new Error('imageOptions "references" requires an onImage callback');
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1260
1906
|
export {
|
|
1261
1907
|
pageToHtml,
|
|
1262
1908
|
writeHtmlDocument,
|
|
1909
|
+
writeMarkdownDocument,
|
|
1263
1910
|
writePage
|
|
1264
1911
|
};
|
|
1265
1912
|
//# sourceMappingURL=index.js.map
|