@boxpdf/html-writer 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +246 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +246 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -28,39 +28,150 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
var import_structure2 = require("@boxpdf/reader/structure");
|
|
29
29
|
|
|
30
30
|
// 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
|
-
}
|
|
31
|
+
var minimumCaptionScore = 0.72;
|
|
51
32
|
function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
|
|
33
|
+
const candidates = blocks.flatMap((block) => {
|
|
34
|
+
const candidate = captionCandidate(block);
|
|
35
|
+
return candidate ? [candidate] : [];
|
|
36
|
+
});
|
|
37
|
+
const preliminary = media.flatMap(
|
|
38
|
+
(item) => candidates.flatMap((candidate) => {
|
|
39
|
+
const evidence = scoreCaption(item, candidate, pageWidth, pageHeight, pageLines, 0);
|
|
40
|
+
return evidence ? [{ media: item, candidate, evidence }] : [];
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
const patterns = repeatedPatterns(preliminary);
|
|
44
|
+
const edges = preliminary.map((edge) => {
|
|
45
|
+
const evidence = scoreCaption(
|
|
46
|
+
edge.media,
|
|
47
|
+
edge.candidate,
|
|
48
|
+
pageWidth,
|
|
49
|
+
pageHeight,
|
|
50
|
+
pageLines,
|
|
51
|
+
patterns.get(patternKey(edge)) ?? 0
|
|
52
|
+
);
|
|
53
|
+
return evidence ? { ...edge, evidence } : void 0;
|
|
54
|
+
}).filter((edge) => Boolean(edge)).filter((edge) => edge.evidence.score >= minimumCaptionScore);
|
|
55
|
+
const bestForMedia = bestEdges(edges, (edge) => edge.media);
|
|
56
|
+
const bestForCaption = bestEdges(edges, (edge) => edge.candidate.block);
|
|
52
57
|
const associations = /* @__PURE__ */ new Map();
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
for (const edge of edges) {
|
|
59
|
+
if (bestForMedia.get(edge.media) === edge && bestForCaption.get(edge.candidate.block) === edge) {
|
|
60
|
+
associations.set(edge.candidate.block, edge.media);
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
return associations;
|
|
59
64
|
}
|
|
60
|
-
function
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
function scoreCaption(media, candidate, pageWidth, pageHeight, pageLines, repeatedAlignment) {
|
|
66
|
+
if (!insidePage(media.bounds, pageWidth, pageHeight)) return void 0;
|
|
67
|
+
if (media.bounds.width < pageWidth * 0.06 || media.bounds.height < candidate.lineHeight * 1.5)
|
|
68
|
+
return void 0;
|
|
69
|
+
const relation = verticalRelation(media.bounds, candidate.bounds);
|
|
70
|
+
if (!relation) return void 0;
|
|
71
|
+
const maximumGap = Math.max(candidate.lineHeight * 3, pageHeight * 0.035);
|
|
72
|
+
if (relation.gap > maximumGap) return void 0;
|
|
73
|
+
const overlapWidth = overlap(
|
|
74
|
+
media.bounds.x,
|
|
75
|
+
media.bounds.width,
|
|
76
|
+
candidate.bounds.x,
|
|
77
|
+
candidate.bounds.width
|
|
78
|
+
);
|
|
79
|
+
const horizontalOverlap = overlapWidth / Math.max(1, Math.min(media.bounds.width, candidate.bounds.width));
|
|
80
|
+
const centerDistance = Math.abs(center(media.bounds) - center(candidate.bounds));
|
|
81
|
+
const centerAlignment = clamp01(
|
|
82
|
+
1 - centerDistance / Math.max(media.bounds.width, candidate.bounds.width)
|
|
83
|
+
);
|
|
84
|
+
if (horizontalOverlap < 0.45 && centerAlignment < 0.82) return void 0;
|
|
85
|
+
const relativeWidth = Math.min(media.bounds.width, candidate.bounds.width) / Math.max(media.bounds.width, candidate.bounds.width);
|
|
86
|
+
const gapRatio = clamp01(1 - relation.gap / maximumGap);
|
|
87
|
+
const interveningContent = interveningScore(media.bounds, candidate, relation.side, pageLines);
|
|
88
|
+
if (interveningContent === 0) return void 0;
|
|
89
|
+
if (relation.side === "above" && (gapRatio < 0.6 || interveningContent < 1)) return void 0;
|
|
90
|
+
const fontContrast = captionFontContrast(candidate, pageLines);
|
|
91
|
+
const surroundingWhitespace = whitespaceScore(candidate, relation.side, relation.gap, pageLines);
|
|
92
|
+
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;
|
|
93
|
+
return {
|
|
94
|
+
score,
|
|
95
|
+
side: relation.side,
|
|
96
|
+
gapRatio,
|
|
97
|
+
horizontalOverlap,
|
|
98
|
+
centerAlignment,
|
|
99
|
+
relativeWidth,
|
|
100
|
+
fontContrast,
|
|
101
|
+
surroundingWhitespace,
|
|
102
|
+
interveningContent,
|
|
103
|
+
repeatedAlignment
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function captionCandidate(block) {
|
|
107
|
+
if (block.type !== "paragraph" || block.lines.length === 0) return void 0;
|
|
108
|
+
const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
|
|
109
|
+
if (!first) return void 0;
|
|
110
|
+
return {
|
|
111
|
+
block,
|
|
112
|
+
bounds: unionLines(block.lines),
|
|
113
|
+
lineHeight: median(block.lines.map((line) => line.bounds.height)),
|
|
114
|
+
font: fontSignature(first)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function verticalRelation(media, caption) {
|
|
118
|
+
const belowGap = media.y - (caption.y + caption.height);
|
|
119
|
+
if (belowGap >= -caption.height * 0.15) return { side: "below", gap: Math.max(0, belowGap) };
|
|
120
|
+
const aboveGap = caption.y - (media.y + media.height);
|
|
121
|
+
if (aboveGap >= -caption.height * 0.15) return { side: "above", gap: Math.max(0, aboveGap) };
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
function interveningScore(media, candidate, side, pageLines) {
|
|
125
|
+
const lower = side === "below" ? candidate.bounds.y + candidate.bounds.height : media.y + media.height;
|
|
126
|
+
const upper = side === "below" ? media.y : candidate.bounds.y;
|
|
127
|
+
const blockers = pageLines.filter(
|
|
128
|
+
(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
|
|
129
|
+
);
|
|
130
|
+
return blockers.length === 0 ? 1 : blockers.length === 1 ? 0.35 : 0;
|
|
131
|
+
}
|
|
132
|
+
function captionFontContrast(candidate, pageLines) {
|
|
133
|
+
const otherLines = pageLines.filter((line) => !candidate.block.lines.includes(line));
|
|
134
|
+
if (candidate.font !== dominantFontSignature(otherLines)) return 1;
|
|
135
|
+
const candidateSize = median(
|
|
136
|
+
candidate.block.lines.flatMap((line) => line.spans.map((span) => span.fontSize))
|
|
137
|
+
);
|
|
138
|
+
const bodySize = median(otherLines.flatMap((line) => line.spans.map((span) => span.fontSize)));
|
|
139
|
+
return Math.abs(candidateSize - bodySize) >= 0.75 ? 0.65 : 0.15;
|
|
140
|
+
}
|
|
141
|
+
function whitespaceScore(candidate, side, mediaGap, pageLines) {
|
|
142
|
+
const awayGaps = pageLines.filter((line) => !candidate.block.lines.includes(line)).filter(
|
|
143
|
+
(line) => overlap(line.bounds.x, line.bounds.width, candidate.bounds.x, candidate.bounds.width) > 0
|
|
144
|
+
).flatMap((line) => {
|
|
145
|
+
if (side === "below" && line.bounds.y + line.bounds.height <= candidate.bounds.y)
|
|
146
|
+
return [candidate.bounds.y - line.bounds.y - line.bounds.height];
|
|
147
|
+
if (side === "above" && line.bounds.y >= candidate.bounds.y + candidate.bounds.height)
|
|
148
|
+
return [line.bounds.y - candidate.bounds.y - candidate.bounds.height];
|
|
149
|
+
return [];
|
|
150
|
+
});
|
|
151
|
+
const awayGap = Math.min(...awayGaps, Number.POSITIVE_INFINITY);
|
|
152
|
+
return Number.isFinite(awayGap) ? clamp01((awayGap + candidate.lineHeight * 0.25) / (mediaGap + candidate.lineHeight)) : 1;
|
|
153
|
+
}
|
|
154
|
+
function repeatedPatterns(edges) {
|
|
155
|
+
const counts = /* @__PURE__ */ new Map();
|
|
156
|
+
for (const edge of edges.filter((item) => item.evidence.score >= minimumCaptionScore - 0.08)) {
|
|
157
|
+
counts.set(patternKey(edge), (counts.get(patternKey(edge)) ?? 0) + 1);
|
|
158
|
+
}
|
|
159
|
+
return new Map([...counts].map(([key, count]) => [key, count >= 2 ? 1 : 0]));
|
|
160
|
+
}
|
|
161
|
+
function patternKey(edge) {
|
|
162
|
+
const widthRatio = edge.candidate.bounds.width / Math.max(1, edge.media.bounds.width);
|
|
163
|
+
return `${edge.candidate.font}|${edge.evidence.side}|${Math.round(widthRatio * 4) / 4}`;
|
|
164
|
+
}
|
|
165
|
+
function bestEdges(edges, key) {
|
|
166
|
+
const output = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const edge of edges) {
|
|
168
|
+
const existing = output.get(key(edge));
|
|
169
|
+
if (!existing || edge.evidence.score > existing.evidence.score) output.set(key(edge), edge);
|
|
170
|
+
}
|
|
171
|
+
return output;
|
|
172
|
+
}
|
|
173
|
+
function insidePage(bounds2, pageWidth, pageHeight) {
|
|
174
|
+
return bounds2.x >= -2 && bounds2.y >= -2 && bounds2.x + bounds2.width <= pageWidth + 2 && bounds2.y + bounds2.height <= pageHeight + 2;
|
|
64
175
|
}
|
|
65
176
|
function unionLines(lines) {
|
|
66
177
|
const x = Math.min(...lines.map((line) => line.bounds.x));
|
|
@@ -80,6 +191,15 @@ function dominantFontSignature(lines) {
|
|
|
80
191
|
function fontSignature(span) {
|
|
81
192
|
return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
|
|
82
193
|
}
|
|
194
|
+
function center(bounds2) {
|
|
195
|
+
return bounds2.x + bounds2.width / 2;
|
|
196
|
+
}
|
|
197
|
+
function overlap(left, leftSize, right, rightSize) {
|
|
198
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
199
|
+
}
|
|
200
|
+
function clamp01(value) {
|
|
201
|
+
return Math.max(0, Math.min(1, value));
|
|
202
|
+
}
|
|
83
203
|
function median(values) {
|
|
84
204
|
const ordered = [...values].sort((left, right) => left - right);
|
|
85
205
|
return ordered[Math.floor(ordered.length / 2)] ?? 1;
|
|
@@ -286,7 +406,7 @@ function base64(bytes) {
|
|
|
286
406
|
function semanticMedia(page) {
|
|
287
407
|
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
288
408
|
output.push(...vectorMedia(page));
|
|
289
|
-
return output.sort((left, right) => right.bounds.y - left.bounds.y);
|
|
409
|
+
return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
|
|
290
410
|
}
|
|
291
411
|
function rasterMedia(image) {
|
|
292
412
|
const bounds2 = transformedUnitBounds(image.transform);
|
|
@@ -295,6 +415,7 @@ function rasterMedia(image) {
|
|
|
295
415
|
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
296
416
|
return {
|
|
297
417
|
bounds: bounds2,
|
|
418
|
+
kind: "raster",
|
|
298
419
|
html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${number2(bounds2.width)}" height="${number2(bounds2.height)}" alt="" style="max-width:100%;height:auto${opacity}">`
|
|
299
420
|
};
|
|
300
421
|
}
|
|
@@ -335,11 +456,70 @@ function vectorMedia(page) {
|
|
|
335
456
|
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
336
457
|
return {
|
|
337
458
|
bounds: bounds2,
|
|
459
|
+
kind: "vector",
|
|
338
460
|
html: `<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>`,
|
|
339
461
|
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
340
462
|
};
|
|
341
463
|
});
|
|
342
464
|
}
|
|
465
|
+
function mediaComponents(media, page) {
|
|
466
|
+
const components = [];
|
|
467
|
+
for (const item of media) {
|
|
468
|
+
if (isPageBackdrop(item.bounds, page)) {
|
|
469
|
+
components.push([item]);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const matches = components.filter(
|
|
473
|
+
(component) => !component.some((member) => isPageBackdrop(member.bounds, page)) && component.some((member) => mediaPiecesTouch(member.bounds, item.bounds))
|
|
474
|
+
);
|
|
475
|
+
if (matches.length === 0) {
|
|
476
|
+
components.push([item]);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const target = matches[0];
|
|
480
|
+
target.push(item);
|
|
481
|
+
for (const component of matches.slice(1)) {
|
|
482
|
+
target.push(...component);
|
|
483
|
+
components.splice(components.indexOf(component), 1);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return components.map((component) => compositeMedia(component));
|
|
487
|
+
}
|
|
488
|
+
function compositeMedia(items) {
|
|
489
|
+
if (items.length === 1) return items[0];
|
|
490
|
+
const bounds2 = unionBounds(items.map((item) => item.bounds));
|
|
491
|
+
const layers = items.map((item) => {
|
|
492
|
+
const left = (item.bounds.x - bounds2.x) / bounds2.width * 100;
|
|
493
|
+
const top = (bounds2.y + bounds2.height - item.bounds.y - item.bounds.height) / bounds2.height * 100;
|
|
494
|
+
const width = item.bounds.width / bounds2.width * 100;
|
|
495
|
+
const height = item.bounds.height / bounds2.height * 100;
|
|
496
|
+
return `<div style="position:absolute;left:${number2(left)}%;top:${number2(top)}%;width:${number2(width)}%;height:${number2(height)}%;overflow:hidden">${item.html}</div>`;
|
|
497
|
+
}).join("");
|
|
498
|
+
return {
|
|
499
|
+
bounds: bounds2,
|
|
500
|
+
kind: "composite",
|
|
501
|
+
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>`,
|
|
502
|
+
consumedSpans: items.flatMap((item) => item.consumedSpans ?? [])
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function mediaPiecesTouch(left, right) {
|
|
506
|
+
const xOverlap = overlap2(left.x, left.width, right.x, right.width);
|
|
507
|
+
const yOverlap = overlap2(left.y, left.height, right.y, right.height);
|
|
508
|
+
if (xOverlap > 0 && yOverlap > 0) return true;
|
|
509
|
+
const horizontalGap = axisGap(left.x, left.width, right.x, right.width);
|
|
510
|
+
const verticalGap = axisGap(left.y, left.height, right.y, right.height);
|
|
511
|
+
if (horizontalGap <= 2 && yOverlap / Math.min(left.height, right.height) >= 0.65) return true;
|
|
512
|
+
return verticalGap <= 2 && xOverlap / Math.min(left.width, right.width) >= 0.65;
|
|
513
|
+
}
|
|
514
|
+
function isPageBackdrop(bounds2, page) {
|
|
515
|
+
return bounds2.width * bounds2.height >= page.width * page.height * 0.7;
|
|
516
|
+
}
|
|
517
|
+
function overlap2(left, leftSize, right, rightSize) {
|
|
518
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
519
|
+
}
|
|
520
|
+
function axisGap(left, leftSize, right, rightSize) {
|
|
521
|
+
return Math.max(0, right - left - leftSize, left - right - rightSize);
|
|
522
|
+
}
|
|
343
523
|
function vectorComponents(primitives, padding) {
|
|
344
524
|
const components = [];
|
|
345
525
|
for (const primitive of primitives) {
|
|
@@ -510,12 +690,16 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
510
690
|
page.structured.lines
|
|
511
691
|
);
|
|
512
692
|
const captionedMedia = new Set(captions.values());
|
|
693
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
513
694
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
514
695
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
515
696
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
516
697
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
517
698
|
const blockY = semanticBlockY(block);
|
|
518
699
|
let emittedAsCaption = false;
|
|
700
|
+
while (page.media[mediaIndex] && emittedMedia.has(page.media[mediaIndex])) {
|
|
701
|
+
mediaIndex += 1;
|
|
702
|
+
}
|
|
519
703
|
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
520
704
|
await flushPendingParagraph();
|
|
521
705
|
const item = page.media[mediaIndex];
|
|
@@ -523,6 +707,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
523
707
|
const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
524
708
|
if (activeTable) pendingMedia.push(html2);
|
|
525
709
|
else await write(html2);
|
|
710
|
+
emittedMedia.add(item);
|
|
526
711
|
mediaIndex += 1;
|
|
527
712
|
emittedAsCaption = true;
|
|
528
713
|
break;
|
|
@@ -533,6 +718,15 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
533
718
|
else await write(html);
|
|
534
719
|
mediaIndex += 1;
|
|
535
720
|
}
|
|
721
|
+
const associatedMedia = captions.get(block);
|
|
722
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
723
|
+
await flushPendingParagraph();
|
|
724
|
+
const html = `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
725
|
+
if (activeTable) pendingMedia.push(html);
|
|
726
|
+
else await write(html);
|
|
727
|
+
emittedMedia.add(associatedMedia);
|
|
728
|
+
emittedAsCaption = true;
|
|
729
|
+
}
|
|
536
730
|
if (emittedAsCaption) continue;
|
|
537
731
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
538
732
|
stats.suppressedFurniture += 1;
|
|
@@ -627,10 +821,13 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
627
821
|
await write(semanticBlockHtml(block, defaultColor));
|
|
628
822
|
}
|
|
629
823
|
while (mediaIndex < page.media.length) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
824
|
+
const item = page.media[mediaIndex];
|
|
825
|
+
if (item && !emittedMedia.has(item)) {
|
|
826
|
+
await flushPendingParagraph();
|
|
827
|
+
const html = `<div class="pdf-semantic-visual">${item.html}</div>`;
|
|
828
|
+
if (activeTable) pendingMedia.push(html);
|
|
829
|
+
else await write(html);
|
|
830
|
+
}
|
|
634
831
|
mediaIndex += 1;
|
|
635
832
|
}
|
|
636
833
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
@@ -1037,18 +1234,22 @@ async function writeFlowPage(page, write) {
|
|
|
1037
1234
|
structured.lines
|
|
1038
1235
|
);
|
|
1039
1236
|
const captionedMedia = new Set(captions.values());
|
|
1237
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
1040
1238
|
await write(
|
|
1041
1239
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
1042
1240
|
);
|
|
1043
1241
|
for (const block of structured.blocks) {
|
|
1044
1242
|
const blockY = semanticBlockY2(block);
|
|
1045
1243
|
let emittedAsCaption = false;
|
|
1244
|
+
while (media[mediaIndex] && emittedMedia.has(media[mediaIndex]))
|
|
1245
|
+
mediaIndex += 1;
|
|
1046
1246
|
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
1047
1247
|
const item = media[mediaIndex];
|
|
1048
1248
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
1049
1249
|
await write(
|
|
1050
1250
|
`<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1051
1251
|
);
|
|
1252
|
+
emittedMedia.add(item);
|
|
1052
1253
|
mediaIndex += 1;
|
|
1053
1254
|
emittedAsCaption = true;
|
|
1054
1255
|
break;
|
|
@@ -1057,6 +1258,14 @@ async function writeFlowPage(page, write) {
|
|
|
1057
1258
|
await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
|
|
1058
1259
|
mediaIndex += 1;
|
|
1059
1260
|
}
|
|
1261
|
+
const associatedMedia = captions.get(block);
|
|
1262
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
1263
|
+
await write(
|
|
1264
|
+
`<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1265
|
+
);
|
|
1266
|
+
emittedMedia.add(associatedMedia);
|
|
1267
|
+
emittedAsCaption = true;
|
|
1268
|
+
}
|
|
1060
1269
|
if (emittedAsCaption) continue;
|
|
1061
1270
|
if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
|
|
1062
1271
|
else if (block.type === "heading") {
|
|
@@ -1111,7 +1320,10 @@ async function writeFlowPage(page, write) {
|
|
|
1111
1320
|
}
|
|
1112
1321
|
}
|
|
1113
1322
|
while (mediaIndex < media.length) {
|
|
1114
|
-
|
|
1323
|
+
const item = media[mediaIndex];
|
|
1324
|
+
if (item && !emittedMedia.has(item)) {
|
|
1325
|
+
await write(`<div class="pdf-semantic-visual">${item.html}</div>`);
|
|
1326
|
+
}
|
|
1115
1327
|
mediaIndex += 1;
|
|
1116
1328
|
}
|
|
1117
1329
|
await write("</section>");
|