@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.cjs
CHANGED
|
@@ -22,45 +22,157 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
pageToHtml: () => pageToHtml,
|
|
24
24
|
writeHtmlDocument: () => writeHtmlDocument,
|
|
25
|
+
writeMarkdownDocument: () => writeMarkdownDocument,
|
|
25
26
|
writePage: () => writePage
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
var import_structure2 = require("@boxpdf/reader/structure");
|
|
29
30
|
|
|
30
31
|
// src/semantic-caption.ts
|
|
31
|
-
|
|
32
|
-
if (block.type !== "paragraph" || block.lines.length === 0) return false;
|
|
33
|
-
const bounds2 = unionLines(block.lines);
|
|
34
|
-
const lineHeight = median(block.lines.map((line) => line.bounds.height));
|
|
35
|
-
if (media.bounds.width < pageWidth * 0.2 || media.bounds.height < lineHeight * 10) return false;
|
|
36
|
-
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)
|
|
37
|
-
return false;
|
|
38
|
-
const gap = media.bounds.y - (bounds2.y + bounds2.height);
|
|
39
|
-
if (gap < -lineHeight * 0.15 || gap > lineHeight * 1.25) return false;
|
|
40
|
-
const mediaCenter = media.bounds.x + media.bounds.width / 2;
|
|
41
|
-
const captionCenter = bounds2.x + bounds2.width / 2;
|
|
42
|
-
if (Math.abs(mediaCenter - captionCenter) > Math.max(3, media.bounds.width * 0.03)) return false;
|
|
43
|
-
if (bounds2.width < media.bounds.width * 0.45 || bounds2.width > media.bounds.width * 1.06) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
|
|
47
|
-
if (!first) return false;
|
|
48
|
-
const otherLines = pageLines.filter((line) => !block.lines.includes(line));
|
|
49
|
-
return fontSignature(first) !== dominantFontSignature(otherLines);
|
|
50
|
-
}
|
|
32
|
+
var minimumCaptionScore = 0.72;
|
|
51
33
|
function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
|
|
34
|
+
const candidates = blocks.flatMap((block) => {
|
|
35
|
+
const candidate = captionCandidate(block);
|
|
36
|
+
return candidate ? [candidate] : [];
|
|
37
|
+
});
|
|
38
|
+
const preliminary = media.flatMap(
|
|
39
|
+
(item) => candidates.flatMap((candidate) => {
|
|
40
|
+
const evidence = scoreCaption(item, candidate, pageWidth, pageHeight, pageLines, 0);
|
|
41
|
+
return evidence ? [{ media: item, candidate, evidence }] : [];
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
const patterns = repeatedPatterns(preliminary);
|
|
45
|
+
const edges = preliminary.map((edge) => {
|
|
46
|
+
const evidence = scoreCaption(
|
|
47
|
+
edge.media,
|
|
48
|
+
edge.candidate,
|
|
49
|
+
pageWidth,
|
|
50
|
+
pageHeight,
|
|
51
|
+
pageLines,
|
|
52
|
+
patterns.get(patternKey(edge)) ?? 0
|
|
53
|
+
);
|
|
54
|
+
return evidence ? { ...edge, evidence } : void 0;
|
|
55
|
+
}).filter((edge) => Boolean(edge)).filter((edge) => edge.evidence.score >= minimumCaptionScore);
|
|
56
|
+
const bestForMedia = bestEdges(edges, (edge) => edge.media);
|
|
57
|
+
const bestForCaption = bestEdges(edges, (edge) => edge.candidate.block);
|
|
52
58
|
const associations = /* @__PURE__ */ new Map();
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
for (const edge of edges) {
|
|
60
|
+
if (bestForMedia.get(edge.media) === edge && bestForCaption.get(edge.candidate.block) === edge) {
|
|
61
|
+
associations.set(edge.candidate.block, edge.media);
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
return associations;
|
|
59
65
|
}
|
|
60
|
-
function
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
function scoreCaption(media, candidate, pageWidth, pageHeight, pageLines, repeatedAlignment) {
|
|
67
|
+
if (!insidePage(media.bounds, pageWidth, pageHeight)) return void 0;
|
|
68
|
+
if (media.bounds.width < pageWidth * 0.06 || media.bounds.height < candidate.lineHeight * 1.5)
|
|
69
|
+
return void 0;
|
|
70
|
+
const relation = verticalRelation(media.bounds, candidate.bounds);
|
|
71
|
+
if (!relation) return void 0;
|
|
72
|
+
const maximumGap = Math.max(candidate.lineHeight * 3, pageHeight * 0.035);
|
|
73
|
+
if (relation.gap > maximumGap) return void 0;
|
|
74
|
+
const overlapWidth = overlap(
|
|
75
|
+
media.bounds.x,
|
|
76
|
+
media.bounds.width,
|
|
77
|
+
candidate.bounds.x,
|
|
78
|
+
candidate.bounds.width
|
|
79
|
+
);
|
|
80
|
+
const horizontalOverlap = overlapWidth / Math.max(1, Math.min(media.bounds.width, candidate.bounds.width));
|
|
81
|
+
const centerDistance = Math.abs(center(media.bounds) - center(candidate.bounds));
|
|
82
|
+
const centerAlignment = clamp01(
|
|
83
|
+
1 - centerDistance / Math.max(media.bounds.width, candidate.bounds.width)
|
|
84
|
+
);
|
|
85
|
+
if (horizontalOverlap < 0.45 && centerAlignment < 0.82) return void 0;
|
|
86
|
+
const relativeWidth = Math.min(media.bounds.width, candidate.bounds.width) / Math.max(media.bounds.width, candidate.bounds.width);
|
|
87
|
+
const gapRatio = clamp01(1 - relation.gap / maximumGap);
|
|
88
|
+
const interveningContent = interveningScore(media.bounds, candidate, relation.side, pageLines);
|
|
89
|
+
if (interveningContent === 0) return void 0;
|
|
90
|
+
if (relation.side === "above" && (gapRatio < 0.6 || interveningContent < 1)) return void 0;
|
|
91
|
+
const fontContrast = captionFontContrast(candidate, pageLines);
|
|
92
|
+
const surroundingWhitespace = whitespaceScore(candidate, relation.side, relation.gap, pageLines);
|
|
93
|
+
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;
|
|
94
|
+
return {
|
|
95
|
+
score,
|
|
96
|
+
side: relation.side,
|
|
97
|
+
gapRatio,
|
|
98
|
+
horizontalOverlap,
|
|
99
|
+
centerAlignment,
|
|
100
|
+
relativeWidth,
|
|
101
|
+
fontContrast,
|
|
102
|
+
surroundingWhitespace,
|
|
103
|
+
interveningContent,
|
|
104
|
+
repeatedAlignment
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function captionCandidate(block) {
|
|
108
|
+
if (block.type !== "paragraph" || block.lines.length === 0) return void 0;
|
|
109
|
+
const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
|
|
110
|
+
if (!first) return void 0;
|
|
111
|
+
return {
|
|
112
|
+
block,
|
|
113
|
+
bounds: unionLines(block.lines),
|
|
114
|
+
lineHeight: median(block.lines.map((line) => line.bounds.height)),
|
|
115
|
+
font: fontSignature(first)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function verticalRelation(media, caption) {
|
|
119
|
+
const belowGap = media.y - (caption.y + caption.height);
|
|
120
|
+
if (belowGap >= -caption.height * 0.15) return { side: "below", gap: Math.max(0, belowGap) };
|
|
121
|
+
const aboveGap = caption.y - (media.y + media.height);
|
|
122
|
+
if (aboveGap >= -caption.height * 0.15) return { side: "above", gap: Math.max(0, aboveGap) };
|
|
123
|
+
return void 0;
|
|
124
|
+
}
|
|
125
|
+
function interveningScore(media, candidate, side, pageLines) {
|
|
126
|
+
const lower = side === "below" ? candidate.bounds.y + candidate.bounds.height : media.y + media.height;
|
|
127
|
+
const upper = side === "below" ? media.y : candidate.bounds.y;
|
|
128
|
+
const blockers = pageLines.filter(
|
|
129
|
+
(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
|
|
130
|
+
);
|
|
131
|
+
return blockers.length === 0 ? 1 : blockers.length === 1 ? 0.35 : 0;
|
|
132
|
+
}
|
|
133
|
+
function captionFontContrast(candidate, pageLines) {
|
|
134
|
+
const otherLines = pageLines.filter((line) => !candidate.block.lines.includes(line));
|
|
135
|
+
if (candidate.font !== dominantFontSignature(otherLines)) return 1;
|
|
136
|
+
const candidateSize = median(
|
|
137
|
+
candidate.block.lines.flatMap((line) => line.spans.map((span) => span.fontSize))
|
|
138
|
+
);
|
|
139
|
+
const bodySize = median(otherLines.flatMap((line) => line.spans.map((span) => span.fontSize)));
|
|
140
|
+
return Math.abs(candidateSize - bodySize) >= 0.75 ? 0.65 : 0.15;
|
|
141
|
+
}
|
|
142
|
+
function whitespaceScore(candidate, side, mediaGap, pageLines) {
|
|
143
|
+
const awayGaps = pageLines.filter((line) => !candidate.block.lines.includes(line)).filter(
|
|
144
|
+
(line) => overlap(line.bounds.x, line.bounds.width, candidate.bounds.x, candidate.bounds.width) > 0
|
|
145
|
+
).flatMap((line) => {
|
|
146
|
+
if (side === "below" && line.bounds.y + line.bounds.height <= candidate.bounds.y)
|
|
147
|
+
return [candidate.bounds.y - line.bounds.y - line.bounds.height];
|
|
148
|
+
if (side === "above" && line.bounds.y >= candidate.bounds.y + candidate.bounds.height)
|
|
149
|
+
return [line.bounds.y - candidate.bounds.y - candidate.bounds.height];
|
|
150
|
+
return [];
|
|
151
|
+
});
|
|
152
|
+
const awayGap = Math.min(...awayGaps, Number.POSITIVE_INFINITY);
|
|
153
|
+
return Number.isFinite(awayGap) ? clamp01((awayGap + candidate.lineHeight * 0.25) / (mediaGap + candidate.lineHeight)) : 1;
|
|
154
|
+
}
|
|
155
|
+
function repeatedPatterns(edges) {
|
|
156
|
+
const counts = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const edge of edges.filter((item) => item.evidence.score >= minimumCaptionScore - 0.08)) {
|
|
158
|
+
counts.set(patternKey(edge), (counts.get(patternKey(edge)) ?? 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
return new Map([...counts].map(([key, count]) => [key, count >= 2 ? 1 : 0]));
|
|
161
|
+
}
|
|
162
|
+
function patternKey(edge) {
|
|
163
|
+
const widthRatio = edge.candidate.bounds.width / Math.max(1, edge.media.bounds.width);
|
|
164
|
+
return `${edge.candidate.font}|${edge.evidence.side}|${Math.round(widthRatio * 4) / 4}`;
|
|
165
|
+
}
|
|
166
|
+
function bestEdges(edges, key) {
|
|
167
|
+
const output = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const edge of edges) {
|
|
169
|
+
const existing = output.get(key(edge));
|
|
170
|
+
if (!existing || edge.evidence.score > existing.evidence.score) output.set(key(edge), edge);
|
|
171
|
+
}
|
|
172
|
+
return output;
|
|
173
|
+
}
|
|
174
|
+
function insidePage(bounds2, pageWidth, pageHeight) {
|
|
175
|
+
return bounds2.x >= -2 && bounds2.y >= -2 && bounds2.x + bounds2.width <= pageWidth + 2 && bounds2.y + bounds2.height <= pageHeight + 2;
|
|
64
176
|
}
|
|
65
177
|
function unionLines(lines) {
|
|
66
178
|
const x = Math.min(...lines.map((line) => line.bounds.x));
|
|
@@ -80,6 +192,15 @@ function dominantFontSignature(lines) {
|
|
|
80
192
|
function fontSignature(span) {
|
|
81
193
|
return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
|
|
82
194
|
}
|
|
195
|
+
function center(bounds2) {
|
|
196
|
+
return bounds2.x + bounds2.width / 2;
|
|
197
|
+
}
|
|
198
|
+
function overlap(left, leftSize, right, rightSize) {
|
|
199
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
200
|
+
}
|
|
201
|
+
function clamp01(value) {
|
|
202
|
+
return Math.max(0, Math.min(1, value));
|
|
203
|
+
}
|
|
83
204
|
function median(values) {
|
|
84
205
|
const ordered = [...values].sort((left, right) => left - right);
|
|
85
206
|
return ordered[Math.floor(ordered.length / 2)] ?? 1;
|
|
@@ -98,6 +219,12 @@ function dominantTextColor(lines) {
|
|
|
98
219
|
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
99
220
|
}
|
|
100
221
|
function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
222
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "html");
|
|
223
|
+
}
|
|
224
|
+
function semanticTextMarkdown(text, lines, defaultColor, preserveWeight = true) {
|
|
225
|
+
return semanticText(text, lines, defaultColor, preserveWeight, "markdown");
|
|
226
|
+
}
|
|
227
|
+
function semanticText(text, lines, defaultColor, preserveWeight, format) {
|
|
101
228
|
const ranges = [];
|
|
102
229
|
let cursor = 0;
|
|
103
230
|
for (const span of lines.flatMap((line) => line.spans)) {
|
|
@@ -123,11 +250,11 @@ function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
|
123
250
|
let html = "";
|
|
124
251
|
let offset = 0;
|
|
125
252
|
for (const range of merged) {
|
|
126
|
-
html +=
|
|
127
|
-
html +=
|
|
253
|
+
html += escapeText(text.slice(offset, range.start), format);
|
|
254
|
+
html += styledText(text.slice(range.start, range.end), range, format);
|
|
128
255
|
offset = range.end;
|
|
129
256
|
}
|
|
130
|
-
return html +
|
|
257
|
+
return html + escapeText(text.slice(offset), format);
|
|
131
258
|
}
|
|
132
259
|
function mergeRanges(ranges, text) {
|
|
133
260
|
const merged = [];
|
|
@@ -141,13 +268,19 @@ function mergeRanges(ranges, text) {
|
|
|
141
268
|
}
|
|
142
269
|
return merged;
|
|
143
270
|
}
|
|
144
|
-
function
|
|
145
|
-
let html =
|
|
271
|
+
function styledText(value, range, format) {
|
|
272
|
+
let html = escapeText(value, format);
|
|
146
273
|
if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
|
|
147
|
-
if (range.italic) html = `<em>${html}</em
|
|
148
|
-
if (range.bold) html = `<strong>${html}</strong
|
|
274
|
+
if (range.italic) html = format === "html" ? `<em>${html}</em>` : `_${html}_`;
|
|
275
|
+
if (range.bold) html = format === "html" ? `<strong>${html}</strong>` : `**${html}**`;
|
|
149
276
|
return html;
|
|
150
277
|
}
|
|
278
|
+
function escapeText(value, format) {
|
|
279
|
+
return format === "html" ? escapeHtml(value) : escapeMarkdown(value);
|
|
280
|
+
}
|
|
281
|
+
function escapeMarkdown(value) {
|
|
282
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
283
|
+
}
|
|
151
284
|
function normalizedColor(value) {
|
|
152
285
|
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
153
286
|
const color = value.toLowerCase();
|
|
@@ -283,22 +416,39 @@ function base64(bytes) {
|
|
|
283
416
|
}
|
|
284
417
|
|
|
285
418
|
// src/semantic-media.ts
|
|
286
|
-
function semanticMedia(page) {
|
|
287
|
-
|
|
288
|
-
output
|
|
289
|
-
|
|
419
|
+
function semanticMedia(page, imageOptions = "embedded") {
|
|
420
|
+
if (imageOptions === "excluded") return [];
|
|
421
|
+
const output = (page.images ?? []).map(
|
|
422
|
+
(image, index) => rasterMedia(image, page.number, index, imageOptions)
|
|
423
|
+
);
|
|
424
|
+
output.push(...vectorMedia(page, imageOptions));
|
|
425
|
+
return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
|
|
426
|
+
}
|
|
427
|
+
async function prepareSemanticMedia(page, imageOptions, onImage) {
|
|
428
|
+
const media = semanticMedia(page, imageOptions);
|
|
429
|
+
for (const item of media) {
|
|
430
|
+
for (const asset of item.assets ?? []) await onImage?.(asset);
|
|
431
|
+
delete item.assets;
|
|
432
|
+
}
|
|
433
|
+
return media;
|
|
290
434
|
}
|
|
291
|
-
function rasterMedia(image) {
|
|
435
|
+
function rasterMedia(image, pageNumber, index, imageOptions) {
|
|
292
436
|
const bounds2 = transformedUnitBounds(image.transform);
|
|
293
437
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
294
438
|
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
439
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
440
|
+
const name = `page-${pageNumber}-image-${index + 1}.${extension}`;
|
|
441
|
+
const source = imageOptions === "references" ? name : `data:${mime};base64,${base64(data)}`;
|
|
295
442
|
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
296
443
|
return {
|
|
297
444
|
bounds: bounds2,
|
|
298
|
-
|
|
445
|
+
kind: "raster",
|
|
446
|
+
html: `<img class="pdf-semantic-media" src="${source}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`,
|
|
447
|
+
markdown: ``,
|
|
448
|
+
...imageOptions === "references" ? { assets: [{ name, mimeType: mime, data }] } : {}
|
|
299
449
|
};
|
|
300
450
|
}
|
|
301
|
-
function vectorMedia(page) {
|
|
451
|
+
function vectorMedia(page, imageOptions) {
|
|
302
452
|
const primitives = [
|
|
303
453
|
...(page.paths ?? []).flatMap((path, index) => {
|
|
304
454
|
const bounds2 = vectorPathBounds(path);
|
|
@@ -316,7 +466,7 @@ function vectorMedia(page) {
|
|
|
316
466
|
const visualCodeFonts = new Set(
|
|
317
467
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
318
468
|
);
|
|
319
|
-
return components.map((component) => {
|
|
469
|
+
return components.map((component, componentIndex) => {
|
|
320
470
|
const bounds2 = component.bounds;
|
|
321
471
|
const paths = component.primitives.flatMap(
|
|
322
472
|
(primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
|
|
@@ -333,13 +483,82 @@ function vectorMedia(page) {
|
|
|
333
483
|
);
|
|
334
484
|
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
335
485
|
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
486
|
+
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>`;
|
|
487
|
+
const name = `page-${page.number}-vector-${componentIndex + 1}.svg`;
|
|
336
488
|
return {
|
|
337
489
|
bounds: bounds2,
|
|
338
|
-
|
|
490
|
+
kind: "vector",
|
|
491
|
+
html: imageOptions === "references" ? `<img class="pdf-semantic-media" src="${name}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="">` : svg,
|
|
492
|
+
markdown: imageOptions === "references" ? `` : svg,
|
|
493
|
+
...imageOptions === "references" ? {
|
|
494
|
+
assets: [
|
|
495
|
+
{ name, mimeType: "image/svg+xml", data: new TextEncoder().encode(svg) }
|
|
496
|
+
]
|
|
497
|
+
} : {},
|
|
339
498
|
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
340
499
|
};
|
|
341
500
|
});
|
|
342
501
|
}
|
|
502
|
+
function mediaComponents(media, page) {
|
|
503
|
+
const components = [];
|
|
504
|
+
for (const item of media) {
|
|
505
|
+
if (isPageBackdrop(item.bounds, page)) {
|
|
506
|
+
components.push([item]);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const matches = components.filter(
|
|
510
|
+
(component) => !component.some((member) => isPageBackdrop(member.bounds, page)) && component.some((member) => mediaPiecesTouch(member.bounds, item.bounds))
|
|
511
|
+
);
|
|
512
|
+
if (matches.length === 0) {
|
|
513
|
+
components.push([item]);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const target = matches[0];
|
|
517
|
+
target.push(item);
|
|
518
|
+
for (const component of matches.slice(1)) {
|
|
519
|
+
target.push(...component);
|
|
520
|
+
components.splice(components.indexOf(component), 1);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return components.map((component) => compositeMedia(component));
|
|
524
|
+
}
|
|
525
|
+
function compositeMedia(items) {
|
|
526
|
+
if (items.length === 1) return items[0];
|
|
527
|
+
const bounds2 = unionBounds(items.map((item) => item.bounds));
|
|
528
|
+
const layers = items.map((item) => {
|
|
529
|
+
const left = (item.bounds.x - bounds2.x) / bounds2.width * 100;
|
|
530
|
+
const top = (bounds2.y + bounds2.height - item.bounds.y - item.bounds.height) / bounds2.height * 100;
|
|
531
|
+
const width = item.bounds.width / bounds2.width * 100;
|
|
532
|
+
const height = item.bounds.height / bounds2.height * 100;
|
|
533
|
+
return `<div style="position:absolute;left:${number2(left)}%;top:${number2(top)}%;width:${number2(width)}%;height:${number2(height)}%;overflow:hidden">${item.html}</div>`;
|
|
534
|
+
}).join("");
|
|
535
|
+
return {
|
|
536
|
+
bounds: bounds2,
|
|
537
|
+
kind: "composite",
|
|
538
|
+
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>`,
|
|
539
|
+
markdown: items.map((item) => item.markdown).join("\n\n"),
|
|
540
|
+
consumedSpans: items.flatMap((item) => item.consumedSpans ?? []),
|
|
541
|
+
assets: items.flatMap((item) => item.assets ?? [])
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function mediaPiecesTouch(left, right) {
|
|
545
|
+
const xOverlap = overlap2(left.x, left.width, right.x, right.width);
|
|
546
|
+
const yOverlap = overlap2(left.y, left.height, right.y, right.height);
|
|
547
|
+
if (xOverlap > 0 && yOverlap > 0) return true;
|
|
548
|
+
const horizontalGap = axisGap(left.x, left.width, right.x, right.width);
|
|
549
|
+
const verticalGap = axisGap(left.y, left.height, right.y, right.height);
|
|
550
|
+
if (horizontalGap <= 2 && yOverlap / Math.min(left.height, right.height) >= 0.65) return true;
|
|
551
|
+
return verticalGap <= 2 && xOverlap / Math.min(left.width, right.width) >= 0.65;
|
|
552
|
+
}
|
|
553
|
+
function isPageBackdrop(bounds2, page) {
|
|
554
|
+
return bounds2.width * bounds2.height >= page.width * page.height * 0.7;
|
|
555
|
+
}
|
|
556
|
+
function overlap2(left, leftSize, right, rightSize) {
|
|
557
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
558
|
+
}
|
|
559
|
+
function axisGap(left, leftSize, right, rightSize) {
|
|
560
|
+
return Math.max(0, right - left - leftSize, left - right - rightSize);
|
|
561
|
+
}
|
|
343
562
|
function vectorComponents(primitives, padding) {
|
|
344
563
|
const components = [];
|
|
345
564
|
for (const primitive of primitives) {
|
|
@@ -458,7 +677,7 @@ function escapeHtml2(value) {
|
|
|
458
677
|
}
|
|
459
678
|
|
|
460
679
|
// src/semantic-document.ts
|
|
461
|
-
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
680
|
+
async function writeSemanticDocument(pages, write, lookaheadPages, imageOptions, onImage, format = "html") {
|
|
462
681
|
const stats = {
|
|
463
682
|
pagesProcessed: 0,
|
|
464
683
|
peakBufferedPages: 0,
|
|
@@ -476,27 +695,30 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
476
695
|
let contentStarted = false;
|
|
477
696
|
let employmentOpen = false;
|
|
478
697
|
let pendingParagraph;
|
|
479
|
-
|
|
698
|
+
const markdown = format === "markdown";
|
|
699
|
+
const output = (html, markdownValue = "") => write(markdown ? markdownValue : html);
|
|
700
|
+
const inlineText = (text, lines, defaultColor, preserveWeight = true) => markdown ? semanticTextMarkdown(text, lines, defaultColor, preserveWeight) : semanticTextHtml(text, lines, defaultColor, preserveWeight);
|
|
701
|
+
await output('<article class="pdf-semantic-document">');
|
|
480
702
|
const closeTable = async () => {
|
|
481
703
|
if (!activeTable) return;
|
|
482
|
-
await
|
|
704
|
+
await output("</table>", "\n");
|
|
483
705
|
activeTable = void 0;
|
|
484
706
|
while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
|
|
485
707
|
};
|
|
486
708
|
const closeSections = async (minimumLevel = 0) => {
|
|
487
709
|
while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
|
|
488
|
-
await
|
|
710
|
+
await output("</section>");
|
|
489
711
|
sectionLevels.pop();
|
|
490
712
|
}
|
|
491
713
|
};
|
|
492
714
|
const flushPendingParagraph = async () => {
|
|
493
715
|
if (!pendingParagraph) return;
|
|
494
|
-
await write(
|
|
716
|
+
await write(semanticBlockOutput(pendingParagraph.block, pendingParagraph.defaultColor, format));
|
|
495
717
|
pendingParagraph = void 0;
|
|
496
718
|
};
|
|
497
719
|
const closeEmployment = async () => {
|
|
498
720
|
if (!employmentOpen) return;
|
|
499
|
-
await
|
|
721
|
+
await output("</section>");
|
|
500
722
|
employmentOpen = false;
|
|
501
723
|
};
|
|
502
724
|
const emitPage = async (page, future) => {
|
|
@@ -510,29 +732,53 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
510
732
|
page.structured.lines
|
|
511
733
|
);
|
|
512
734
|
const captionedMedia = new Set(captions.values());
|
|
735
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
513
736
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
514
737
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
515
738
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
516
739
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
517
740
|
const blockY = semanticBlockY(block);
|
|
518
741
|
let emittedAsCaption = false;
|
|
742
|
+
while (page.media[mediaIndex] && emittedMedia.has(page.media[mediaIndex])) {
|
|
743
|
+
mediaIndex += 1;
|
|
744
|
+
}
|
|
519
745
|
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
520
746
|
await flushPendingParagraph();
|
|
521
747
|
const item = page.media[mediaIndex];
|
|
522
748
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
523
|
-
const html2 =
|
|
749
|
+
const html2 = markdown ? `${item.markdown}
|
|
750
|
+
|
|
751
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
752
|
+
|
|
753
|
+
` : `<figure class="pdf-semantic-figure">${item.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
524
754
|
if (activeTable) pendingMedia.push(html2);
|
|
525
755
|
else await write(html2);
|
|
756
|
+
emittedMedia.add(item);
|
|
526
757
|
mediaIndex += 1;
|
|
527
758
|
emittedAsCaption = true;
|
|
528
759
|
break;
|
|
529
760
|
}
|
|
530
761
|
if (item && captionedMedia.has(item)) break;
|
|
531
|
-
const html =
|
|
762
|
+
const html = markdown ? `${item?.markdown ?? ""}
|
|
763
|
+
|
|
764
|
+
` : `<div class="pdf-semantic-visual">${item?.html}</div>`;
|
|
532
765
|
if (activeTable) pendingMedia.push(html);
|
|
533
766
|
else await write(html);
|
|
534
767
|
mediaIndex += 1;
|
|
535
768
|
}
|
|
769
|
+
const associatedMedia = captions.get(block);
|
|
770
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
771
|
+
await flushPendingParagraph();
|
|
772
|
+
const html = markdown ? `${associatedMedia.markdown}
|
|
773
|
+
|
|
774
|
+
*${inlineText(block.text, block.lines, defaultColor)}*
|
|
775
|
+
|
|
776
|
+
` : `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${inlineText(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
777
|
+
if (activeTable) pendingMedia.push(html);
|
|
778
|
+
else await write(html);
|
|
779
|
+
emittedMedia.add(associatedMedia);
|
|
780
|
+
emittedAsCaption = true;
|
|
781
|
+
}
|
|
536
782
|
if (emittedAsCaption) continue;
|
|
537
783
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
538
784
|
stats.suppressedFurniture += 1;
|
|
@@ -541,8 +787,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
541
787
|
await flushPendingParagraph();
|
|
542
788
|
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
543
789
|
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
544
|
-
await
|
|
545
|
-
`<header><h1>${
|
|
790
|
+
await output(
|
|
791
|
+
`<header><h1>${inlineText(block.text, block.lines, defaultColor, false)}</h1>`,
|
|
792
|
+
`# ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
793
|
+
|
|
794
|
+
`
|
|
546
795
|
);
|
|
547
796
|
headerOpen = true;
|
|
548
797
|
continue;
|
|
@@ -550,19 +799,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
550
799
|
if (headerOpen) {
|
|
551
800
|
if (block.type === "paragraph") {
|
|
552
801
|
const tag = isContactBlock(block) ? "address" : "p";
|
|
553
|
-
await
|
|
554
|
-
`<${tag}>${
|
|
802
|
+
await output(
|
|
803
|
+
`<${tag}>${inlineText(block.text, block.lines, defaultColor)}</${tag}>`,
|
|
804
|
+
`${inlineText(block.text, block.lines, defaultColor)}
|
|
805
|
+
|
|
806
|
+
`
|
|
555
807
|
);
|
|
556
808
|
headerHasParagraph = true;
|
|
557
809
|
continue;
|
|
558
810
|
}
|
|
559
811
|
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
560
|
-
await
|
|
561
|
-
`<h${block.level}>${
|
|
812
|
+
await output(
|
|
813
|
+
`<h${block.level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${block.level}>`,
|
|
814
|
+
`${"#".repeat(block.level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
815
|
+
|
|
816
|
+
`
|
|
562
817
|
);
|
|
563
818
|
continue;
|
|
564
819
|
}
|
|
565
|
-
await
|
|
820
|
+
await output("</header>");
|
|
566
821
|
headerOpen = false;
|
|
567
822
|
contentStarted = true;
|
|
568
823
|
}
|
|
@@ -570,23 +825,34 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
570
825
|
const rows = (0, import_structure.tableToRows)(block.table);
|
|
571
826
|
if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
|
|
572
827
|
const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
|
|
573
|
-
for (const row of continuationRows)
|
|
828
|
+
for (const row of continuationRows)
|
|
829
|
+
await write(markdown ? markdownTableRow(row) : tableRow(row, false));
|
|
574
830
|
activeTable.table = block.table;
|
|
575
831
|
stats.mergedTables += 1;
|
|
576
832
|
continue;
|
|
577
833
|
}
|
|
578
834
|
await closeTable();
|
|
579
835
|
const header = tableHeader(rows);
|
|
580
|
-
await
|
|
581
|
-
|
|
582
|
-
|
|
836
|
+
await output("<table>", markdownTableStart(rows, header));
|
|
837
|
+
const markdownRows = markdown ? header ? rows.slice(1) : rows : rows;
|
|
838
|
+
for (const [index, row] of markdownRows.entries())
|
|
839
|
+
await write(
|
|
840
|
+
markdown ? markdownTableRow(row) : tableRow(row, Boolean(header && index === 0))
|
|
841
|
+
);
|
|
583
842
|
activeTable = { table: block.table, header };
|
|
584
843
|
continue;
|
|
585
844
|
}
|
|
586
845
|
if (activeTable && block.type === "definitionList" && isFinancialSummary(block)) {
|
|
587
846
|
const columns = activeTable.table.columns.length;
|
|
588
|
-
await
|
|
589
|
-
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot
|
|
847
|
+
await output(
|
|
848
|
+
`<tfoot>${block.entries.map((entry) => financialSummaryRow(entry, columns)).join("")}</tfoot>`,
|
|
849
|
+
block.entries.map(
|
|
850
|
+
(entry) => markdownTableRow([
|
|
851
|
+
entry.term,
|
|
852
|
+
...Array(Math.max(0, columns - 2)).fill(""),
|
|
853
|
+
entry.description
|
|
854
|
+
])
|
|
855
|
+
).join("")
|
|
590
856
|
);
|
|
591
857
|
await closeTable();
|
|
592
858
|
continue;
|
|
@@ -595,8 +861,11 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
595
861
|
if (block.type === "heading") {
|
|
596
862
|
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
597
863
|
await closeSections(level);
|
|
598
|
-
await
|
|
599
|
-
`<section data-level="${level}"><h${level}>${
|
|
864
|
+
await output(
|
|
865
|
+
`<section data-level="${level}"><h${level}>${inlineText(block.text, block.lines, defaultColor, false)}</h${level}>`,
|
|
866
|
+
`${"#".repeat(level)} ${inlineText(block.text, block.lines, defaultColor, false)}
|
|
867
|
+
|
|
868
|
+
`
|
|
600
869
|
);
|
|
601
870
|
sectionLevels.push(level);
|
|
602
871
|
continue;
|
|
@@ -604,13 +873,25 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
604
873
|
if (block.type === "paragraph") {
|
|
605
874
|
if (isTitledRecord(block)) {
|
|
606
875
|
const [institution, ...details] = block.lines;
|
|
607
|
-
if (institution)
|
|
608
|
-
|
|
876
|
+
if (institution)
|
|
877
|
+
await output(
|
|
878
|
+
`<h3>${escapeHtml3(institution.text)}</h3>`,
|
|
879
|
+
`### ${escapeMarkdown2(institution.text)}
|
|
880
|
+
|
|
881
|
+
`
|
|
882
|
+
);
|
|
883
|
+
for (const detail of details)
|
|
884
|
+
await output(`<p>${escapeHtml3(detail.text)}</p>`, `${escapeMarkdown2(detail.text)}
|
|
885
|
+
|
|
886
|
+
`);
|
|
609
887
|
continue;
|
|
610
888
|
}
|
|
611
889
|
if (isUnmarkedList(block)) {
|
|
612
|
-
await
|
|
613
|
-
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul
|
|
890
|
+
await output(
|
|
891
|
+
`<ul>${block.lines.map((line) => `<li>${escapeHtml3(line.text)}</li>`).join("")}</ul>`,
|
|
892
|
+
`${block.lines.map((line) => `- ${escapeMarkdown2(line.text)}`).join("\n")}
|
|
893
|
+
|
|
894
|
+
`
|
|
614
895
|
);
|
|
615
896
|
continue;
|
|
616
897
|
}
|
|
@@ -618,27 +899,40 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
618
899
|
continue;
|
|
619
900
|
}
|
|
620
901
|
if (block.type === "employment") {
|
|
621
|
-
await
|
|
622
|
-
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p
|
|
902
|
+
await output(
|
|
903
|
+
`<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p>`,
|
|
904
|
+
`### ${escapeMarkdown2(block.role)}
|
|
905
|
+
|
|
906
|
+
${escapeMarkdown2(block.organization)}
|
|
907
|
+
|
|
908
|
+
${escapeMarkdown2(block.date)}
|
|
909
|
+
|
|
910
|
+
`
|
|
623
911
|
);
|
|
624
912
|
employmentOpen = true;
|
|
625
913
|
continue;
|
|
626
914
|
}
|
|
627
|
-
await write(
|
|
915
|
+
await write(semanticBlockOutput(block, defaultColor, format));
|
|
628
916
|
}
|
|
629
917
|
while (mediaIndex < page.media.length) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
918
|
+
const item = page.media[mediaIndex];
|
|
919
|
+
if (item && !emittedMedia.has(item)) {
|
|
920
|
+
await flushPendingParagraph();
|
|
921
|
+
const html = markdown ? `${item.markdown}
|
|
922
|
+
|
|
923
|
+
` : `<div class="pdf-semantic-visual">${item.html}</div>`;
|
|
924
|
+
if (activeTable) pendingMedia.push(html);
|
|
925
|
+
else await write(html);
|
|
926
|
+
}
|
|
634
927
|
mediaIndex += 1;
|
|
635
928
|
}
|
|
636
929
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
637
930
|
};
|
|
638
931
|
for await (const page of pages) {
|
|
639
|
-
const media =
|
|
932
|
+
const media = await prepareSemanticMedia(page, imageOptions, onImage);
|
|
640
933
|
const structured = (0, import_structure.structurePage)(withoutSemanticMediaSpans(page, media));
|
|
641
934
|
buffer.push({ width: page.width, height: page.height, structured, media });
|
|
935
|
+
restoreObservedHyphens(buffer);
|
|
642
936
|
stats.pagesProcessed += 1;
|
|
643
937
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
644
938
|
stats.peakBufferedLines = Math.max(
|
|
@@ -654,22 +948,82 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
654
948
|
const ready = buffer.shift();
|
|
655
949
|
if (ready) await emitPage(ready, buffer);
|
|
656
950
|
}
|
|
657
|
-
if (headerOpen) await
|
|
951
|
+
if (headerOpen) await output("</header>");
|
|
658
952
|
await closeTable();
|
|
659
953
|
await closeEmployment();
|
|
660
954
|
if (pendingParagraph && isFooterParagraph(pendingParagraph.block, pendingParagraph.height)) {
|
|
661
955
|
await closeSections();
|
|
662
|
-
await
|
|
663
|
-
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer
|
|
956
|
+
await output(
|
|
957
|
+
`<footer>${semanticBlockHtml(pendingParagraph.block, pendingParagraph.defaultColor)}</footer>`,
|
|
958
|
+
`---
|
|
959
|
+
|
|
960
|
+
${semanticBlockMarkdown(pendingParagraph.block, pendingParagraph.defaultColor)}`
|
|
664
961
|
);
|
|
665
962
|
pendingParagraph = void 0;
|
|
666
963
|
} else {
|
|
667
964
|
await flushPendingParagraph();
|
|
668
965
|
await closeSections();
|
|
669
966
|
}
|
|
670
|
-
await
|
|
967
|
+
await output("</article>");
|
|
671
968
|
return stats;
|
|
672
969
|
}
|
|
970
|
+
function restoreObservedHyphens(buffer) {
|
|
971
|
+
const terms = new Set(
|
|
972
|
+
buffer.flatMap(
|
|
973
|
+
(page) => page.structured.lines.flatMap(
|
|
974
|
+
(line) => line.text.match(/[\p{L}\p{N}]+(?:[-‐‑][\p{L}\p{N}]+)+/gu) ?? []
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
for (const page of buffer) {
|
|
979
|
+
for (const block of page.structured.blocks) restoreBlockHyphens(block, terms);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function restoreBlockHyphens(block, terms) {
|
|
983
|
+
const restore = (value) => restoreTextHyphens(value, terms);
|
|
984
|
+
if (block.type === "insetGroup") {
|
|
985
|
+
for (const nested of block.blocks) restoreBlockHyphens(nested, terms);
|
|
986
|
+
} else if (block.type === "heading" || block.type === "paragraph" || block.type === "preformatted") {
|
|
987
|
+
block.text = restore(block.text);
|
|
988
|
+
} else if (block.type === "list") {
|
|
989
|
+
for (const item of block.items) item.text = restore(item.text);
|
|
990
|
+
} else if (block.type === "definitionList") {
|
|
991
|
+
for (const entry of block.entries) {
|
|
992
|
+
entry.term = restore(entry.term);
|
|
993
|
+
entry.description = restore(entry.description);
|
|
994
|
+
}
|
|
995
|
+
} else if (block.type === "cardList") {
|
|
996
|
+
for (const item of block.items) {
|
|
997
|
+
item.title = restore(item.title);
|
|
998
|
+
item.details = item.details.map(restore);
|
|
999
|
+
}
|
|
1000
|
+
} else if (block.type === "sectionGroup") {
|
|
1001
|
+
for (const item of block.items) {
|
|
1002
|
+
item.label = restore(item.label);
|
|
1003
|
+
item.content = item.content.map(restore);
|
|
1004
|
+
}
|
|
1005
|
+
} else if (block.type === "employment") {
|
|
1006
|
+
block.role = restore(block.role);
|
|
1007
|
+
block.organization = restore(block.organization);
|
|
1008
|
+
block.date = restore(block.date);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function restoreTextHyphens(value, terms) {
|
|
1012
|
+
let output = value;
|
|
1013
|
+
for (const term of terms) {
|
|
1014
|
+
const collapsed = term.replace(/[-‐‑]/gu, "");
|
|
1015
|
+
if (collapsed === term || !output.includes(collapsed)) continue;
|
|
1016
|
+
const pattern = new RegExp(
|
|
1017
|
+
`(?<![\\p{L}\\p{N}])${escapeRegularExpression(collapsed)}(?![\\p{L}\\p{N}])`,
|
|
1018
|
+
"gu"
|
|
1019
|
+
);
|
|
1020
|
+
output = output.replace(pattern, term);
|
|
1021
|
+
}
|
|
1022
|
+
return output;
|
|
1023
|
+
}
|
|
1024
|
+
function escapeRegularExpression(value) {
|
|
1025
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1026
|
+
}
|
|
673
1027
|
function isContactBlock(block) {
|
|
674
1028
|
const text = block.text;
|
|
675
1029
|
const signals = [
|
|
@@ -753,6 +1107,20 @@ function tableRow(row, header) {
|
|
|
753
1107
|
const cell = header ? "th" : "td";
|
|
754
1108
|
return `<tr>${row.map((value) => `<${cell}>${escapeHtml3(value)}</${cell}>`).join("")}</tr>`;
|
|
755
1109
|
}
|
|
1110
|
+
function markdownTableStart(rows, header) {
|
|
1111
|
+
const columns = rows[0]?.length ?? 0;
|
|
1112
|
+
if (columns === 0) return "";
|
|
1113
|
+
const heading = header ?? Array(columns).fill("");
|
|
1114
|
+
return `${markdownTableRow(heading)}${markdownTableRow(Array(columns).fill("---"), false)}`;
|
|
1115
|
+
}
|
|
1116
|
+
function markdownTableRow(row, shouldEscape = true) {
|
|
1117
|
+
const cells = row.map((value) => shouldEscape ? escapeMarkdownTableCell(value) : value);
|
|
1118
|
+
return `| ${cells.join(" | ")} |
|
|
1119
|
+
`;
|
|
1120
|
+
}
|
|
1121
|
+
function escapeMarkdownTableCell(value) {
|
|
1122
|
+
return escapeMarkdown2(value).replaceAll("|", "\\|").replace(/\s*\n\s*/g, "<br>");
|
|
1123
|
+
}
|
|
756
1124
|
function isFinancialSummary(block) {
|
|
757
1125
|
return block.entries.length > 0 && block.entries.every((entry) => isNumericValue(entry.description)) && block.entries.some((entry) => /\p{Sc}|%/u.test(entry.description));
|
|
758
1126
|
}
|
|
@@ -800,6 +1168,90 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
800
1168
|
const tag = block.ordered ? "ol" : "ul";
|
|
801
1169
|
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
802
1170
|
}
|
|
1171
|
+
function semanticBlockOutput(block, defaultColor, format) {
|
|
1172
|
+
return format === "markdown" ? semanticBlockMarkdown(block, defaultColor) : semanticBlockHtml(block, defaultColor);
|
|
1173
|
+
}
|
|
1174
|
+
function semanticBlockMarkdown(block, defaultColor = "#000000") {
|
|
1175
|
+
if (block.type === "insetGroup") {
|
|
1176
|
+
const content = block.blocks.map((item) => semanticBlockMarkdown(item, defaultColor)).join("");
|
|
1177
|
+
return `${content.trimEnd().split("\n").map((line) => line ? `> ${line}` : ">").join("\n")}
|
|
1178
|
+
|
|
1179
|
+
`;
|
|
1180
|
+
}
|
|
1181
|
+
if (block.type === "table") {
|
|
1182
|
+
const rows = (0, import_structure.tableToRows)(block.table);
|
|
1183
|
+
const header = tableHeader(rows);
|
|
1184
|
+
return `${markdownTableStart(rows, header)}${(header ? rows.slice(1) : rows).map((row) => markdownTableRow(row)).join("")}
|
|
1185
|
+
`;
|
|
1186
|
+
}
|
|
1187
|
+
if (block.type === "heading") {
|
|
1188
|
+
return `${"#".repeat(block.level)} ${semanticTextMarkdown(block.text, block.lines, defaultColor, false)}
|
|
1189
|
+
|
|
1190
|
+
`;
|
|
1191
|
+
}
|
|
1192
|
+
if (block.type === "paragraph") {
|
|
1193
|
+
return `${semanticTextMarkdown(block.text, block.lines, defaultColor)}
|
|
1194
|
+
|
|
1195
|
+
`;
|
|
1196
|
+
}
|
|
1197
|
+
if (block.type === "preformatted") {
|
|
1198
|
+
const fence = block.text.includes("```") ? "````" : "```";
|
|
1199
|
+
return `${fence}
|
|
1200
|
+
${block.text}
|
|
1201
|
+
${fence}
|
|
1202
|
+
|
|
1203
|
+
`;
|
|
1204
|
+
}
|
|
1205
|
+
if (block.type === "definitionList") {
|
|
1206
|
+
return `${block.entries.map((entry) => `**${escapeMarkdown2(entry.term)}:** ${escapeMarkdown2(entry.description)}`).join("\n\n")}
|
|
1207
|
+
|
|
1208
|
+
`;
|
|
1209
|
+
}
|
|
1210
|
+
if (block.type === "cardList") {
|
|
1211
|
+
const rows = [
|
|
1212
|
+
["Item", "Quantity", "Amount"],
|
|
1213
|
+
...block.items.map((item) => {
|
|
1214
|
+
const trailing = item.details.at(-1) ?? "";
|
|
1215
|
+
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
1216
|
+
const detail = item.details.slice(0, -1).join(" ");
|
|
1217
|
+
return [
|
|
1218
|
+
`${item.title}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
1219
|
+
match?.[1] ?? "",
|
|
1220
|
+
match?.[2] ?? trailing
|
|
1221
|
+
];
|
|
1222
|
+
})
|
|
1223
|
+
];
|
|
1224
|
+
return `## Items ordered
|
|
1225
|
+
|
|
1226
|
+
${markdownTableStart(rows, rows[0])}${rows.slice(1).map((row) => markdownTableRow(row)).join("")}
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
if (block.type === "sectionGroup") {
|
|
1230
|
+
return block.items.map(
|
|
1231
|
+
(item) => `## ${escapeMarkdown2(titleCase(item.label))}
|
|
1232
|
+
|
|
1233
|
+
${item.content.map(
|
|
1234
|
+
(content, index) => index === 0 ? `**${escapeMarkdown2(content)}**` : escapeMarkdown2(content)
|
|
1235
|
+
).join("\n\n")}
|
|
1236
|
+
|
|
1237
|
+
`
|
|
1238
|
+
).join("");
|
|
1239
|
+
}
|
|
1240
|
+
if (block.type === "employment") {
|
|
1241
|
+
return `### ${escapeMarkdown2(block.role)}
|
|
1242
|
+
|
|
1243
|
+
${escapeMarkdown2(block.organization)}
|
|
1244
|
+
|
|
1245
|
+
${escapeMarkdown2(block.date)}
|
|
1246
|
+
|
|
1247
|
+
`;
|
|
1248
|
+
}
|
|
1249
|
+
return `${block.items.map(
|
|
1250
|
+
(item, index) => `${block.ordered ? `${index + 1}.` : "-"} ${semanticTextMarkdown(item.text, item.lines, defaultColor)}`
|
|
1251
|
+
).join("\n")}
|
|
1252
|
+
|
|
1253
|
+
`;
|
|
1254
|
+
}
|
|
803
1255
|
function semanticBlockY(block) {
|
|
804
1256
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
805
1257
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
@@ -832,6 +1284,9 @@ function titleCase(value) {
|
|
|
832
1284
|
function escapeHtml3(value) {
|
|
833
1285
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
834
1286
|
}
|
|
1287
|
+
function escapeMarkdown2(value) {
|
|
1288
|
+
return value.replace(/([\\`*_[\]<>])/g, "\\$1");
|
|
1289
|
+
}
|
|
835
1290
|
|
|
836
1291
|
// src/index.ts
|
|
837
1292
|
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}`;
|
|
@@ -848,9 +1303,18 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
848
1303
|
await write("</head><body>");
|
|
849
1304
|
}
|
|
850
1305
|
await write('<main class="pdf-document">');
|
|
851
|
-
|
|
1306
|
+
const profile = resolveProfile(options);
|
|
1307
|
+
const imageOptions = resolveImageOptions(profile, options);
|
|
1308
|
+
validateImageOptions(imageOptions, options);
|
|
1309
|
+
if (profile === "semantic") {
|
|
852
1310
|
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
853
|
-
const stats = await writeSemanticDocument(
|
|
1311
|
+
const stats = await writeSemanticDocument(
|
|
1312
|
+
pages,
|
|
1313
|
+
write,
|
|
1314
|
+
lookahead,
|
|
1315
|
+
imageOptions,
|
|
1316
|
+
options.onImage
|
|
1317
|
+
);
|
|
854
1318
|
options.onSemanticStats?.(stats);
|
|
855
1319
|
} else {
|
|
856
1320
|
for await (const page of pages) await writePage(page, write, options);
|
|
@@ -858,6 +1322,20 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
858
1322
|
await write("</main>");
|
|
859
1323
|
if (includeDocument) await write("</body></html>");
|
|
860
1324
|
}
|
|
1325
|
+
async function writeMarkdownDocument(pages, write, options = {}) {
|
|
1326
|
+
const imageOptions = options.imageOptions ?? "excluded";
|
|
1327
|
+
validateImageOptions(imageOptions, options);
|
|
1328
|
+
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
1329
|
+
const stats = await writeSemanticDocument(
|
|
1330
|
+
pages,
|
|
1331
|
+
write,
|
|
1332
|
+
lookahead,
|
|
1333
|
+
imageOptions,
|
|
1334
|
+
options.onImage,
|
|
1335
|
+
"markdown"
|
|
1336
|
+
);
|
|
1337
|
+
options.onSemanticStats?.(stats);
|
|
1338
|
+
}
|
|
861
1339
|
function semanticLookahead(value) {
|
|
862
1340
|
const lookahead = value ?? 4;
|
|
863
1341
|
if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
|
|
@@ -866,7 +1344,9 @@ function semanticLookahead(value) {
|
|
|
866
1344
|
return lookahead;
|
|
867
1345
|
}
|
|
868
1346
|
async function writePage(page, write, options = {}) {
|
|
869
|
-
|
|
1347
|
+
const profile = resolveProfile(options);
|
|
1348
|
+
validateImageOptions(resolveImageOptions(profile, options), options);
|
|
1349
|
+
if (profile === "semantic") await writeFlowPage(page, write, options);
|
|
870
1350
|
else await writePositionedPage(page, write, options);
|
|
871
1351
|
}
|
|
872
1352
|
async function pageToHtml(page, options = {}) {
|
|
@@ -881,7 +1361,9 @@ async function pageToHtml(page, options = {}) {
|
|
|
881
1361
|
return output;
|
|
882
1362
|
}
|
|
883
1363
|
async function writePositionedPage(page, write, options) {
|
|
884
|
-
const
|
|
1364
|
+
const imageOptions = resolveImageOptions("visual", options);
|
|
1365
|
+
const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
|
|
1366
|
+
const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
|
|
885
1367
|
const reflectedOverlay = usesReflectedVisualOverlay(page, visualSpans);
|
|
886
1368
|
const quarterTurn = page.rotate === 90 || page.rotate === 270;
|
|
887
1369
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
@@ -893,25 +1375,32 @@ async function writePositionedPage(page, write, options) {
|
|
|
893
1375
|
const type3Fonts = new Map(
|
|
894
1376
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
895
1377
|
);
|
|
1378
|
+
const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
|
|
896
1379
|
if ((options.includeStyles ?? true) && page.fonts?.length) {
|
|
897
1380
|
await write(
|
|
898
1381
|
`<style>${page.fonts.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
899
1382
|
);
|
|
900
1383
|
}
|
|
1384
|
+
if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
|
|
901
1385
|
await write(
|
|
902
1386
|
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
|
|
903
1387
|
);
|
|
904
1388
|
await write(
|
|
905
1389
|
`<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)}">`
|
|
906
1390
|
);
|
|
907
|
-
const clipDefinitions = imageClipDefinitions(
|
|
1391
|
+
const clipDefinitions = imageClipDefinitions(
|
|
1392
|
+
imageOptions === "excluded" ? [] : page.images ?? [],
|
|
1393
|
+
page.number,
|
|
1394
|
+
page.height
|
|
1395
|
+
) + vectorPathClipDefinitions(
|
|
908
1396
|
(page.paths ?? []).map((path, index) => ({ path, index })),
|
|
909
1397
|
page.number
|
|
910
1398
|
);
|
|
911
1399
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
912
1400
|
if (reflectedOverlay) {
|
|
913
1401
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
914
|
-
|
|
1402
|
+
const source = visualImages[index];
|
|
1403
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
915
1404
|
}
|
|
916
1405
|
}
|
|
917
1406
|
if (page.fills?.length || page.paths?.length) {
|
|
@@ -924,14 +1413,29 @@ async function writePositionedPage(page, write, options) {
|
|
|
924
1413
|
}
|
|
925
1414
|
if (!reflectedOverlay) {
|
|
926
1415
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
927
|
-
|
|
1416
|
+
const source = visualImages[index];
|
|
1417
|
+
if (source) await write(visualImage(image, page.height, page.number, index, source));
|
|
928
1418
|
}
|
|
929
1419
|
}
|
|
930
|
-
for (
|
|
1420
|
+
for (let spanIndex = 0; spanIndex < visualSpans.length; spanIndex += 1) {
|
|
1421
|
+
const span = visualSpans[spanIndex];
|
|
1422
|
+
if (!span) continue;
|
|
931
1423
|
if (!usesPositionedSpan(span)) {
|
|
932
1424
|
const type3 = span.fontAssetId ? type3Fonts.get(span.fontAssetId) : void 0;
|
|
1425
|
+
const line = !type3 && textClasses ? visualTextLine(visualSpans, spanIndex, textClasses.names, page.height, fontAliases) : void 0;
|
|
1426
|
+
if (line) {
|
|
1427
|
+
await write(line.html);
|
|
1428
|
+
spanIndex = line.endIndex;
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
933
1431
|
await write(
|
|
934
|
-
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1432
|
+
type3 ? visualType3Text(span, type3, page.height) : visualText(
|
|
1433
|
+
span,
|
|
1434
|
+
page.height,
|
|
1435
|
+
fontAliases,
|
|
1436
|
+
reflectedOverlay && page.rotate === 180,
|
|
1437
|
+
textClasses?.names
|
|
1438
|
+
)
|
|
935
1439
|
);
|
|
936
1440
|
}
|
|
937
1441
|
}
|
|
@@ -941,23 +1445,131 @@ async function writePositionedPage(page, write, options) {
|
|
|
941
1445
|
}
|
|
942
1446
|
await write("</div></section>");
|
|
943
1447
|
}
|
|
1448
|
+
function coalesceVisualSpans(spans) {
|
|
1449
|
+
const output = [];
|
|
1450
|
+
for (const span of spans) {
|
|
1451
|
+
const previous = output.at(-1);
|
|
1452
|
+
if (!previous || !canCoalesceVisualSpans(previous, span)) {
|
|
1453
|
+
output.push(span);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
output[output.length - 1] = {
|
|
1457
|
+
...previous,
|
|
1458
|
+
text: previous.text + span.text,
|
|
1459
|
+
bounds: {
|
|
1460
|
+
...previous.bounds,
|
|
1461
|
+
width: span.bounds.x + span.bounds.width - previous.bounds.x,
|
|
1462
|
+
height: Math.max(previous.bounds.height, span.bounds.height)
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
return output;
|
|
1467
|
+
}
|
|
1468
|
+
function canCoalesceVisualSpans(left, right) {
|
|
1469
|
+
if (usesPositionedSpan(left) || usesPositionedSpan(right)) return false;
|
|
1470
|
+
if (left.direction !== "ltr" || right.direction !== "ltr") return false;
|
|
1471
|
+
if (/guardian/i.test(left.fontFamily ?? "")) return false;
|
|
1472
|
+
if (left.glyphCodes || right.glyphCodes) return false;
|
|
1473
|
+
if (!sameVisualTextState(left, right)) return false;
|
|
1474
|
+
const tolerance = Math.max(0.02, left.fontSize * 0.015);
|
|
1475
|
+
if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
|
|
1476
|
+
const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
|
|
1477
|
+
return !right.hasLeadingSpace && gap >= -tolerance && gap <= tolerance;
|
|
1478
|
+
}
|
|
1479
|
+
function sameVisualTextState(left, right) {
|
|
1480
|
+
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);
|
|
1481
|
+
}
|
|
1482
|
+
function sameTransform(left, right) {
|
|
1483
|
+
if (!left || !right) return left === right;
|
|
1484
|
+
return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) <= 1e-6);
|
|
1485
|
+
}
|
|
1486
|
+
function visualTextClasses(pageNumber, spans, fontAliases) {
|
|
1487
|
+
const names = /* @__PURE__ */ new Map();
|
|
1488
|
+
let css = "";
|
|
1489
|
+
for (const span of spans) {
|
|
1490
|
+
if (usesPositionedSpan(span) || span.glyphCodes) {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const style = visualTextClassStyle(span, fontAliases);
|
|
1494
|
+
if (!style || names.has(style)) continue;
|
|
1495
|
+
const name = `boxpdf-p${number3(pageNumber)}-t${names.size + 1}`;
|
|
1496
|
+
names.set(style, name);
|
|
1497
|
+
css += `.${name}{${style}}`;
|
|
1498
|
+
}
|
|
1499
|
+
return { css, names };
|
|
1500
|
+
}
|
|
1501
|
+
function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases) {
|
|
1502
|
+
const first = spans[startIndex];
|
|
1503
|
+
if (!first || !canGroupVisualTextLine(first)) return void 0;
|
|
1504
|
+
const style = visualTextClassStyle(first, fontAliases);
|
|
1505
|
+
const className = styleClasses.get(style);
|
|
1506
|
+
if (!className) return void 0;
|
|
1507
|
+
let endIndex = startIndex;
|
|
1508
|
+
while (endIndex + 1 < spans.length) {
|
|
1509
|
+
const next = spans[endIndex + 1];
|
|
1510
|
+
if (!next || !canGroupVisualTextLine(next) || Math.abs(next.bounds.y - first.bounds.y) > 1e-3 || visualTextClassStyle(next, fontAliases) !== style) {
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
endIndex += 1;
|
|
1514
|
+
}
|
|
1515
|
+
if (endIndex === startIndex) return void 0;
|
|
1516
|
+
const baseline = pageHeight - first.bounds.y;
|
|
1517
|
+
const lineSpans = spans.slice(startIndex, endIndex + 1);
|
|
1518
|
+
const content = lineSpans.map(
|
|
1519
|
+
(span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
|
|
1520
|
+
).join("");
|
|
1521
|
+
return {
|
|
1522
|
+
html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}">${content}</text>`,
|
|
1523
|
+
endIndex
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function visualTextTspan(span, dx) {
|
|
1527
|
+
const extent = span.bounds.width;
|
|
1528
|
+
const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
|
|
1529
|
+
const length = extent > 0 ? ` textLength="${number3(extent)}" lengthAdjust="${usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1530
|
+
return `<tspan${offset}${length}>${escapeHtml4(span.text)}</tspan>`;
|
|
1531
|
+
}
|
|
1532
|
+
function textSpanGap(previous, current) {
|
|
1533
|
+
if (!previous) return 0;
|
|
1534
|
+
const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
|
|
1535
|
+
const adjustment = current.textAdjustmentBefore;
|
|
1536
|
+
return adjustment !== void 0 && Math.abs(adjustment - gap) <= 1e-3 ? adjustment : gap;
|
|
1537
|
+
}
|
|
1538
|
+
function canGroupVisualTextLine(span) {
|
|
1539
|
+
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));
|
|
1540
|
+
}
|
|
944
1541
|
function usesReflectedVisualOverlay(page, spans) {
|
|
945
1542
|
return Boolean(page.images?.length) && Boolean(page.paths?.length || page.fills?.length) && spans.length > 0 && spans.every(
|
|
946
1543
|
(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
|
|
947
1544
|
);
|
|
948
1545
|
}
|
|
949
|
-
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
1546
|
+
function visualImage(image, pageHeight, pageNumber, imageIndex, source) {
|
|
950
1547
|
const [a, b, c, d, e, f] = image.transform;
|
|
951
1548
|
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
|
|
952
1549
|
const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
|
|
953
|
-
|
|
954
|
-
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
955
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
1550
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="${source}"${opacity}/>`;
|
|
956
1551
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
957
1552
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
958
1553
|
}
|
|
959
1554
|
return output;
|
|
960
1555
|
}
|
|
1556
|
+
async function prepareVisualImages(page, imageOptions, onImage) {
|
|
1557
|
+
if (imageOptions === "excluded") return [];
|
|
1558
|
+
const sources = [];
|
|
1559
|
+
for (const [index, image] of (page.images ?? []).entries()) {
|
|
1560
|
+
const mimeType = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
1561
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
1562
|
+
if (imageOptions === "embedded") {
|
|
1563
|
+
sources.push(`data:${mimeType};base64,${base64(data)}`);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const extension = image.format === "jpeg" ? "jpg" : "bmp";
|
|
1567
|
+
const name = `page-${page.number}-image-${index + 1}.${extension}`;
|
|
1568
|
+
await onImage?.({ name, mimeType, data });
|
|
1569
|
+
sources.push(name);
|
|
1570
|
+
}
|
|
1571
|
+
return sources;
|
|
1572
|
+
}
|
|
961
1573
|
function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
962
1574
|
return images.flatMap(
|
|
963
1575
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
@@ -1024,8 +1636,9 @@ function positionedSpan(span, fontAliases) {
|
|
|
1024
1636
|
].join(";");
|
|
1025
1637
|
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml4(span.text)}</span>`;
|
|
1026
1638
|
}
|
|
1027
|
-
async function writeFlowPage(page, write) {
|
|
1028
|
-
const
|
|
1639
|
+
async function writeFlowPage(page, write, options) {
|
|
1640
|
+
const imageOptions = resolveImageOptions("semantic", options);
|
|
1641
|
+
const media = await prepareSemanticMedia(page, imageOptions, options.onImage);
|
|
1029
1642
|
const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
|
|
1030
1643
|
const defaultColor = dominantTextColor(structured.lines);
|
|
1031
1644
|
let mediaIndex = 0;
|
|
@@ -1037,18 +1650,22 @@ async function writeFlowPage(page, write) {
|
|
|
1037
1650
|
structured.lines
|
|
1038
1651
|
);
|
|
1039
1652
|
const captionedMedia = new Set(captions.values());
|
|
1653
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
1040
1654
|
await write(
|
|
1041
1655
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
1042
1656
|
);
|
|
1043
1657
|
for (const block of structured.blocks) {
|
|
1044
1658
|
const blockY = semanticBlockY2(block);
|
|
1045
1659
|
let emittedAsCaption = false;
|
|
1660
|
+
while (media[mediaIndex] && emittedMedia.has(media[mediaIndex]))
|
|
1661
|
+
mediaIndex += 1;
|
|
1046
1662
|
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
1047
1663
|
const item = media[mediaIndex];
|
|
1048
1664
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
1049
1665
|
await write(
|
|
1050
1666
|
`<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1051
1667
|
);
|
|
1668
|
+
emittedMedia.add(item);
|
|
1052
1669
|
mediaIndex += 1;
|
|
1053
1670
|
emittedAsCaption = true;
|
|
1054
1671
|
break;
|
|
@@ -1057,6 +1674,14 @@ async function writeFlowPage(page, write) {
|
|
|
1057
1674
|
await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
|
|
1058
1675
|
mediaIndex += 1;
|
|
1059
1676
|
}
|
|
1677
|
+
const associatedMedia = captions.get(block);
|
|
1678
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
1679
|
+
await write(
|
|
1680
|
+
`<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1681
|
+
);
|
|
1682
|
+
emittedMedia.add(associatedMedia);
|
|
1683
|
+
emittedAsCaption = true;
|
|
1684
|
+
}
|
|
1060
1685
|
if (emittedAsCaption) continue;
|
|
1061
1686
|
if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
|
|
1062
1687
|
else if (block.type === "heading") {
|
|
@@ -1111,7 +1736,10 @@ async function writeFlowPage(page, write) {
|
|
|
1111
1736
|
}
|
|
1112
1737
|
}
|
|
1113
1738
|
while (mediaIndex < media.length) {
|
|
1114
|
-
|
|
1739
|
+
const item = media[mediaIndex];
|
|
1740
|
+
if (item && !emittedMedia.has(item)) {
|
|
1741
|
+
await write(`<div class="pdf-semantic-visual">${item.html}</div>`);
|
|
1742
|
+
}
|
|
1115
1743
|
mediaIndex += 1;
|
|
1116
1744
|
}
|
|
1117
1745
|
await write("</section>");
|
|
@@ -1147,10 +1775,37 @@ function semanticBlockY2(block) {
|
|
|
1147
1775
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
1148
1776
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
1149
1777
|
}
|
|
1150
|
-
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
|
|
1778
|
+
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false, styleClasses) {
|
|
1151
1779
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
1152
1780
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
1153
1781
|
const direction = directionAttribute([span]);
|
|
1782
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1783
|
+
const styleClass = styleClasses?.get(visualTextClassStyle(span, fontAliases));
|
|
1784
|
+
const presentation = styleClass ? ` class="${styleClass}"` : style ? ` style="${style}"` : "";
|
|
1785
|
+
const fontSize = styleClass ? "" : ` font-size="${number3(span.fontSize)}"`;
|
|
1786
|
+
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1787
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1788
|
+
const transform = counterRotateReflectedText && span.transform ? [
|
|
1789
|
+
span.transform[0],
|
|
1790
|
+
span.transform[1],
|
|
1791
|
+
span.transform[2],
|
|
1792
|
+
-span.transform[3]
|
|
1793
|
+
] : span.transform;
|
|
1794
|
+
const transformed = hasNonIdentityTransform(transform);
|
|
1795
|
+
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1796
|
+
const basisX = transform?.[0] ?? 1;
|
|
1797
|
+
const basisY = transform?.[1] ?? 0;
|
|
1798
|
+
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1799
|
+
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1800
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1801
|
+
return `<text${direction}${position}${fontSize}${textLength}${presentation}>${escapeHtml4(span.text)}</text>`;
|
|
1802
|
+
}
|
|
1803
|
+
function visualTextClassStyle(span, fontAliases) {
|
|
1804
|
+
const style = visualTextStyle(span, fontAliases);
|
|
1805
|
+
const fontSize = `font-size:${number3(span.fontSize)}px`;
|
|
1806
|
+
return style ? `${style};${fontSize}` : fontSize;
|
|
1807
|
+
}
|
|
1808
|
+
function visualTextStyle(span, fontAliases) {
|
|
1154
1809
|
const font = visualFontStyles(
|
|
1155
1810
|
span.fontFamily,
|
|
1156
1811
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -1160,7 +1815,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1160
1815
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
1161
1816
|
const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
|
|
1162
1817
|
const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
|
|
1163
|
-
|
|
1818
|
+
return [
|
|
1164
1819
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
1165
1820
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
1166
1821
|
strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
|
|
@@ -1170,22 +1825,6 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
1170
1825
|
strokeOpacity,
|
|
1171
1826
|
font
|
|
1172
1827
|
].filter(Boolean).join(";");
|
|
1173
|
-
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
1174
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
1175
|
-
const transform = counterRotateReflectedText && span.transform ? [
|
|
1176
|
-
span.transform[0],
|
|
1177
|
-
span.transform[1],
|
|
1178
|
-
span.transform[2],
|
|
1179
|
-
-span.transform[3]
|
|
1180
|
-
] : span.transform;
|
|
1181
|
-
const transformed = hasNonIdentityTransform(transform);
|
|
1182
|
-
const rtlOffset = span.direction === "rtl" ? span.bounds.width : 0;
|
|
1183
|
-
const basisX = transform?.[0] ?? 1;
|
|
1184
|
-
const basisY = transform?.[1] ?? 0;
|
|
1185
|
-
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
1186
|
-
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
1187
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1188
|
-
return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
1189
1828
|
}
|
|
1190
1829
|
function isAdobeCjkFont(fontFamily) {
|
|
1191
1830
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -1280,10 +1919,19 @@ function resolveProfile(options) {
|
|
|
1280
1919
|
}
|
|
1281
1920
|
return options.profile ?? legacyProfile;
|
|
1282
1921
|
}
|
|
1922
|
+
function resolveImageOptions(profile, options) {
|
|
1923
|
+
return options.imageOptions ?? (profile === "semantic" ? "excluded" : "embedded");
|
|
1924
|
+
}
|
|
1925
|
+
function validateImageOptions(imageOptions, options) {
|
|
1926
|
+
if (imageOptions === "references" && !options.onImage) {
|
|
1927
|
+
throw new Error('imageOptions "references" requires an onImage callback');
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1283
1930
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1284
1931
|
0 && (module.exports = {
|
|
1285
1932
|
pageToHtml,
|
|
1286
1933
|
writeHtmlDocument,
|
|
1934
|
+
writeMarkdownDocument,
|
|
1287
1935
|
writePage
|
|
1288
1936
|
});
|
|
1289
1937
|
//# sourceMappingURL=index.cjs.map
|