@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.js
CHANGED
|
@@ -2,39 +2,150 @@
|
|
|
2
2
|
import { structurePage as structurePage2, tableToHtml } from "@boxpdf/reader/structure";
|
|
3
3
|
|
|
4
4
|
// src/semantic-caption.ts
|
|
5
|
-
|
|
6
|
-
if (block.type !== "paragraph" || block.lines.length === 0) return false;
|
|
7
|
-
const bounds2 = unionLines(block.lines);
|
|
8
|
-
const lineHeight = median(block.lines.map((line) => line.bounds.height));
|
|
9
|
-
if (media.bounds.width < pageWidth * 0.2 || media.bounds.height < lineHeight * 10) return false;
|
|
10
|
-
if (media.bounds.x < -2 || media.bounds.y < -2 || media.bounds.x + media.bounds.width > pageWidth + 2 || media.bounds.y + media.bounds.height > pageHeight + 2)
|
|
11
|
-
return false;
|
|
12
|
-
const gap = media.bounds.y - (bounds2.y + bounds2.height);
|
|
13
|
-
if (gap < -lineHeight * 0.15 || gap > lineHeight * 1.25) return false;
|
|
14
|
-
const mediaCenter = media.bounds.x + media.bounds.width / 2;
|
|
15
|
-
const captionCenter = bounds2.x + bounds2.width / 2;
|
|
16
|
-
if (Math.abs(mediaCenter - captionCenter) > Math.max(3, media.bounds.width * 0.03)) return false;
|
|
17
|
-
if (bounds2.width < media.bounds.width * 0.45 || bounds2.width > media.bounds.width * 1.06) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
|
|
21
|
-
if (!first) return false;
|
|
22
|
-
const otherLines = pageLines.filter((line) => !block.lines.includes(line));
|
|
23
|
-
return fontSignature(first) !== dominantFontSignature(otherLines);
|
|
24
|
-
}
|
|
5
|
+
var minimumCaptionScore = 0.72;
|
|
25
6
|
function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
|
|
7
|
+
const candidates = blocks.flatMap((block) => {
|
|
8
|
+
const candidate = captionCandidate(block);
|
|
9
|
+
return candidate ? [candidate] : [];
|
|
10
|
+
});
|
|
11
|
+
const preliminary = media.flatMap(
|
|
12
|
+
(item) => candidates.flatMap((candidate) => {
|
|
13
|
+
const evidence = scoreCaption(item, candidate, pageWidth, pageHeight, pageLines, 0);
|
|
14
|
+
return evidence ? [{ media: item, candidate, evidence }] : [];
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
const patterns = repeatedPatterns(preliminary);
|
|
18
|
+
const edges = preliminary.map((edge) => {
|
|
19
|
+
const evidence = scoreCaption(
|
|
20
|
+
edge.media,
|
|
21
|
+
edge.candidate,
|
|
22
|
+
pageWidth,
|
|
23
|
+
pageHeight,
|
|
24
|
+
pageLines,
|
|
25
|
+
patterns.get(patternKey(edge)) ?? 0
|
|
26
|
+
);
|
|
27
|
+
return evidence ? { ...edge, evidence } : void 0;
|
|
28
|
+
}).filter((edge) => Boolean(edge)).filter((edge) => edge.evidence.score >= minimumCaptionScore);
|
|
29
|
+
const bestForMedia = bestEdges(edges, (edge) => edge.media);
|
|
30
|
+
const bestForCaption = bestEdges(edges, (edge) => edge.candidate.block);
|
|
26
31
|
const associations = /* @__PURE__ */ new Map();
|
|
27
|
-
for (const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
for (const edge of edges) {
|
|
33
|
+
if (bestForMedia.get(edge.media) === edge && bestForCaption.get(edge.candidate.block) === edge) {
|
|
34
|
+
associations.set(edge.candidate.block, edge.media);
|
|
35
|
+
}
|
|
31
36
|
}
|
|
32
37
|
return associations;
|
|
33
38
|
}
|
|
34
|
-
function
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
function scoreCaption(media, candidate, pageWidth, pageHeight, pageLines, repeatedAlignment) {
|
|
40
|
+
if (!insidePage(media.bounds, pageWidth, pageHeight)) return void 0;
|
|
41
|
+
if (media.bounds.width < pageWidth * 0.06 || media.bounds.height < candidate.lineHeight * 1.5)
|
|
42
|
+
return void 0;
|
|
43
|
+
const relation = verticalRelation(media.bounds, candidate.bounds);
|
|
44
|
+
if (!relation) return void 0;
|
|
45
|
+
const maximumGap = Math.max(candidate.lineHeight * 3, pageHeight * 0.035);
|
|
46
|
+
if (relation.gap > maximumGap) return void 0;
|
|
47
|
+
const overlapWidth = overlap(
|
|
48
|
+
media.bounds.x,
|
|
49
|
+
media.bounds.width,
|
|
50
|
+
candidate.bounds.x,
|
|
51
|
+
candidate.bounds.width
|
|
52
|
+
);
|
|
53
|
+
const horizontalOverlap = overlapWidth / Math.max(1, Math.min(media.bounds.width, candidate.bounds.width));
|
|
54
|
+
const centerDistance = Math.abs(center(media.bounds) - center(candidate.bounds));
|
|
55
|
+
const centerAlignment = clamp01(
|
|
56
|
+
1 - centerDistance / Math.max(media.bounds.width, candidate.bounds.width)
|
|
57
|
+
);
|
|
58
|
+
if (horizontalOverlap < 0.45 && centerAlignment < 0.82) return void 0;
|
|
59
|
+
const relativeWidth = Math.min(media.bounds.width, candidate.bounds.width) / Math.max(media.bounds.width, candidate.bounds.width);
|
|
60
|
+
const gapRatio = clamp01(1 - relation.gap / maximumGap);
|
|
61
|
+
const interveningContent = interveningScore(media.bounds, candidate, relation.side, pageLines);
|
|
62
|
+
if (interveningContent === 0) return void 0;
|
|
63
|
+
if (relation.side === "above" && (gapRatio < 0.6 || interveningContent < 1)) return void 0;
|
|
64
|
+
const fontContrast = captionFontContrast(candidate, pageLines);
|
|
65
|
+
const surroundingWhitespace = whitespaceScore(candidate, relation.side, relation.gap, pageLines);
|
|
66
|
+
const score = gapRatio * 0.22 + horizontalOverlap * 0.17 + centerAlignment * 0.13 + relativeWidth * 0.09 + fontContrast * 0.13 + surroundingWhitespace * 0.09 + interveningContent * 0.09 + repeatedAlignment * 0.08;
|
|
67
|
+
return {
|
|
68
|
+
score,
|
|
69
|
+
side: relation.side,
|
|
70
|
+
gapRatio,
|
|
71
|
+
horizontalOverlap,
|
|
72
|
+
centerAlignment,
|
|
73
|
+
relativeWidth,
|
|
74
|
+
fontContrast,
|
|
75
|
+
surroundingWhitespace,
|
|
76
|
+
interveningContent,
|
|
77
|
+
repeatedAlignment
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function captionCandidate(block) {
|
|
81
|
+
if (block.type !== "paragraph" || block.lines.length === 0) return void 0;
|
|
82
|
+
const first = block.lines.flatMap((line) => line.spans).find((span) => /\S/u.test(span.text));
|
|
83
|
+
if (!first) return void 0;
|
|
84
|
+
return {
|
|
85
|
+
block,
|
|
86
|
+
bounds: unionLines(block.lines),
|
|
87
|
+
lineHeight: median(block.lines.map((line) => line.bounds.height)),
|
|
88
|
+
font: fontSignature(first)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function verticalRelation(media, caption) {
|
|
92
|
+
const belowGap = media.y - (caption.y + caption.height);
|
|
93
|
+
if (belowGap >= -caption.height * 0.15) return { side: "below", gap: Math.max(0, belowGap) };
|
|
94
|
+
const aboveGap = caption.y - (media.y + media.height);
|
|
95
|
+
if (aboveGap >= -caption.height * 0.15) return { side: "above", gap: Math.max(0, aboveGap) };
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
function interveningScore(media, candidate, side, pageLines) {
|
|
99
|
+
const lower = side === "below" ? candidate.bounds.y + candidate.bounds.height : media.y + media.height;
|
|
100
|
+
const upper = side === "below" ? media.y : candidate.bounds.y;
|
|
101
|
+
const blockers = pageLines.filter(
|
|
102
|
+
(line) => !candidate.block.lines.includes(line) && line.bounds.y < upper && line.bounds.y + line.bounds.height > lower && overlap(line.bounds.x, line.bounds.width, media.x, media.width) / Math.max(1, Math.min(line.bounds.width, media.width)) >= 0.25
|
|
103
|
+
);
|
|
104
|
+
return blockers.length === 0 ? 1 : blockers.length === 1 ? 0.35 : 0;
|
|
105
|
+
}
|
|
106
|
+
function captionFontContrast(candidate, pageLines) {
|
|
107
|
+
const otherLines = pageLines.filter((line) => !candidate.block.lines.includes(line));
|
|
108
|
+
if (candidate.font !== dominantFontSignature(otherLines)) return 1;
|
|
109
|
+
const candidateSize = median(
|
|
110
|
+
candidate.block.lines.flatMap((line) => line.spans.map((span) => span.fontSize))
|
|
111
|
+
);
|
|
112
|
+
const bodySize = median(otherLines.flatMap((line) => line.spans.map((span) => span.fontSize)));
|
|
113
|
+
return Math.abs(candidateSize - bodySize) >= 0.75 ? 0.65 : 0.15;
|
|
114
|
+
}
|
|
115
|
+
function whitespaceScore(candidate, side, mediaGap, pageLines) {
|
|
116
|
+
const awayGaps = pageLines.filter((line) => !candidate.block.lines.includes(line)).filter(
|
|
117
|
+
(line) => overlap(line.bounds.x, line.bounds.width, candidate.bounds.x, candidate.bounds.width) > 0
|
|
118
|
+
).flatMap((line) => {
|
|
119
|
+
if (side === "below" && line.bounds.y + line.bounds.height <= candidate.bounds.y)
|
|
120
|
+
return [candidate.bounds.y - line.bounds.y - line.bounds.height];
|
|
121
|
+
if (side === "above" && line.bounds.y >= candidate.bounds.y + candidate.bounds.height)
|
|
122
|
+
return [line.bounds.y - candidate.bounds.y - candidate.bounds.height];
|
|
123
|
+
return [];
|
|
124
|
+
});
|
|
125
|
+
const awayGap = Math.min(...awayGaps, Number.POSITIVE_INFINITY);
|
|
126
|
+
return Number.isFinite(awayGap) ? clamp01((awayGap + candidate.lineHeight * 0.25) / (mediaGap + candidate.lineHeight)) : 1;
|
|
127
|
+
}
|
|
128
|
+
function repeatedPatterns(edges) {
|
|
129
|
+
const counts = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const edge of edges.filter((item) => item.evidence.score >= minimumCaptionScore - 0.08)) {
|
|
131
|
+
counts.set(patternKey(edge), (counts.get(patternKey(edge)) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
return new Map([...counts].map(([key, count]) => [key, count >= 2 ? 1 : 0]));
|
|
134
|
+
}
|
|
135
|
+
function patternKey(edge) {
|
|
136
|
+
const widthRatio = edge.candidate.bounds.width / Math.max(1, edge.media.bounds.width);
|
|
137
|
+
return `${edge.candidate.font}|${edge.evidence.side}|${Math.round(widthRatio * 4) / 4}`;
|
|
138
|
+
}
|
|
139
|
+
function bestEdges(edges, key) {
|
|
140
|
+
const output = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const edge of edges) {
|
|
142
|
+
const existing = output.get(key(edge));
|
|
143
|
+
if (!existing || edge.evidence.score > existing.evidence.score) output.set(key(edge), edge);
|
|
144
|
+
}
|
|
145
|
+
return output;
|
|
146
|
+
}
|
|
147
|
+
function insidePage(bounds2, pageWidth, pageHeight) {
|
|
148
|
+
return bounds2.x >= -2 && bounds2.y >= -2 && bounds2.x + bounds2.width <= pageWidth + 2 && bounds2.y + bounds2.height <= pageHeight + 2;
|
|
38
149
|
}
|
|
39
150
|
function unionLines(lines) {
|
|
40
151
|
const x = Math.min(...lines.map((line) => line.bounds.x));
|
|
@@ -54,6 +165,15 @@ function dominantFontSignature(lines) {
|
|
|
54
165
|
function fontSignature(span) {
|
|
55
166
|
return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
|
|
56
167
|
}
|
|
168
|
+
function center(bounds2) {
|
|
169
|
+
return bounds2.x + bounds2.width / 2;
|
|
170
|
+
}
|
|
171
|
+
function overlap(left, leftSize, right, rightSize) {
|
|
172
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
173
|
+
}
|
|
174
|
+
function clamp01(value) {
|
|
175
|
+
return Math.max(0, Math.min(1, value));
|
|
176
|
+
}
|
|
57
177
|
function median(values) {
|
|
58
178
|
const ordered = [...values].sort((left, right) => left - right);
|
|
59
179
|
return ordered[Math.floor(ordered.length / 2)] ?? 1;
|
|
@@ -263,7 +383,7 @@ function base64(bytes) {
|
|
|
263
383
|
function semanticMedia(page) {
|
|
264
384
|
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
265
385
|
output.push(...vectorMedia(page));
|
|
266
|
-
return output.sort((left, right) => right.bounds.y - left.bounds.y);
|
|
386
|
+
return mediaComponents(output, page).sort((left, right) => right.bounds.y - left.bounds.y);
|
|
267
387
|
}
|
|
268
388
|
function rasterMedia(image) {
|
|
269
389
|
const bounds2 = transformedUnitBounds(image.transform);
|
|
@@ -272,6 +392,7 @@ function rasterMedia(image) {
|
|
|
272
392
|
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
273
393
|
return {
|
|
274
394
|
bounds: bounds2,
|
|
395
|
+
kind: "raster",
|
|
275
396
|
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}">`
|
|
276
397
|
};
|
|
277
398
|
}
|
|
@@ -312,11 +433,70 @@ function vectorMedia(page) {
|
|
|
312
433
|
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
313
434
|
return {
|
|
314
435
|
bounds: bounds2,
|
|
436
|
+
kind: "vector",
|
|
315
437
|
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>`,
|
|
316
438
|
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
317
439
|
};
|
|
318
440
|
});
|
|
319
441
|
}
|
|
442
|
+
function mediaComponents(media, page) {
|
|
443
|
+
const components = [];
|
|
444
|
+
for (const item of media) {
|
|
445
|
+
if (isPageBackdrop(item.bounds, page)) {
|
|
446
|
+
components.push([item]);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const matches = components.filter(
|
|
450
|
+
(component) => !component.some((member) => isPageBackdrop(member.bounds, page)) && component.some((member) => mediaPiecesTouch(member.bounds, item.bounds))
|
|
451
|
+
);
|
|
452
|
+
if (matches.length === 0) {
|
|
453
|
+
components.push([item]);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const target = matches[0];
|
|
457
|
+
target.push(item);
|
|
458
|
+
for (const component of matches.slice(1)) {
|
|
459
|
+
target.push(...component);
|
|
460
|
+
components.splice(components.indexOf(component), 1);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return components.map((component) => compositeMedia(component));
|
|
464
|
+
}
|
|
465
|
+
function compositeMedia(items) {
|
|
466
|
+
if (items.length === 1) return items[0];
|
|
467
|
+
const bounds2 = unionBounds(items.map((item) => item.bounds));
|
|
468
|
+
const layers = items.map((item) => {
|
|
469
|
+
const left = (item.bounds.x - bounds2.x) / bounds2.width * 100;
|
|
470
|
+
const top = (bounds2.y + bounds2.height - item.bounds.y - item.bounds.height) / bounds2.height * 100;
|
|
471
|
+
const width = item.bounds.width / bounds2.width * 100;
|
|
472
|
+
const height = item.bounds.height / bounds2.height * 100;
|
|
473
|
+
return `<div style="position:absolute;left:${number2(left)}%;top:${number2(top)}%;width:${number2(width)}%;height:${number2(height)}%;overflow:hidden">${item.html}</div>`;
|
|
474
|
+
}).join("");
|
|
475
|
+
return {
|
|
476
|
+
bounds: bounds2,
|
|
477
|
+
kind: "composite",
|
|
478
|
+
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>`,
|
|
479
|
+
consumedSpans: items.flatMap((item) => item.consumedSpans ?? [])
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function mediaPiecesTouch(left, right) {
|
|
483
|
+
const xOverlap = overlap2(left.x, left.width, right.x, right.width);
|
|
484
|
+
const yOverlap = overlap2(left.y, left.height, right.y, right.height);
|
|
485
|
+
if (xOverlap > 0 && yOverlap > 0) return true;
|
|
486
|
+
const horizontalGap = axisGap(left.x, left.width, right.x, right.width);
|
|
487
|
+
const verticalGap = axisGap(left.y, left.height, right.y, right.height);
|
|
488
|
+
if (horizontalGap <= 2 && yOverlap / Math.min(left.height, right.height) >= 0.65) return true;
|
|
489
|
+
return verticalGap <= 2 && xOverlap / Math.min(left.width, right.width) >= 0.65;
|
|
490
|
+
}
|
|
491
|
+
function isPageBackdrop(bounds2, page) {
|
|
492
|
+
return bounds2.width * bounds2.height >= page.width * page.height * 0.7;
|
|
493
|
+
}
|
|
494
|
+
function overlap2(left, leftSize, right, rightSize) {
|
|
495
|
+
return Math.max(0, Math.min(left + leftSize, right + rightSize) - Math.max(left, right));
|
|
496
|
+
}
|
|
497
|
+
function axisGap(left, leftSize, right, rightSize) {
|
|
498
|
+
return Math.max(0, right - left - leftSize, left - right - rightSize);
|
|
499
|
+
}
|
|
320
500
|
function vectorComponents(primitives, padding) {
|
|
321
501
|
const components = [];
|
|
322
502
|
for (const primitive of primitives) {
|
|
@@ -487,12 +667,16 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
487
667
|
page.structured.lines
|
|
488
668
|
);
|
|
489
669
|
const captionedMedia = new Set(captions.values());
|
|
670
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
490
671
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
491
672
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
492
673
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
493
674
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
494
675
|
const blockY = semanticBlockY(block);
|
|
495
676
|
let emittedAsCaption = false;
|
|
677
|
+
while (page.media[mediaIndex] && emittedMedia.has(page.media[mediaIndex])) {
|
|
678
|
+
mediaIndex += 1;
|
|
679
|
+
}
|
|
496
680
|
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
497
681
|
await flushPendingParagraph();
|
|
498
682
|
const item = page.media[mediaIndex];
|
|
@@ -500,6 +684,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
500
684
|
const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
501
685
|
if (activeTable) pendingMedia.push(html2);
|
|
502
686
|
else await write(html2);
|
|
687
|
+
emittedMedia.add(item);
|
|
503
688
|
mediaIndex += 1;
|
|
504
689
|
emittedAsCaption = true;
|
|
505
690
|
break;
|
|
@@ -510,6 +695,15 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
510
695
|
else await write(html);
|
|
511
696
|
mediaIndex += 1;
|
|
512
697
|
}
|
|
698
|
+
const associatedMedia = captions.get(block);
|
|
699
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
700
|
+
await flushPendingParagraph();
|
|
701
|
+
const html = `<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
702
|
+
if (activeTable) pendingMedia.push(html);
|
|
703
|
+
else await write(html);
|
|
704
|
+
emittedMedia.add(associatedMedia);
|
|
705
|
+
emittedAsCaption = true;
|
|
706
|
+
}
|
|
513
707
|
if (emittedAsCaption) continue;
|
|
514
708
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
515
709
|
stats.suppressedFurniture += 1;
|
|
@@ -604,10 +798,13 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
604
798
|
await write(semanticBlockHtml(block, defaultColor));
|
|
605
799
|
}
|
|
606
800
|
while (mediaIndex < page.media.length) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
801
|
+
const item = page.media[mediaIndex];
|
|
802
|
+
if (item && !emittedMedia.has(item)) {
|
|
803
|
+
await flushPendingParagraph();
|
|
804
|
+
const html = `<div class="pdf-semantic-visual">${item.html}</div>`;
|
|
805
|
+
if (activeTable) pendingMedia.push(html);
|
|
806
|
+
else await write(html);
|
|
807
|
+
}
|
|
611
808
|
mediaIndex += 1;
|
|
612
809
|
}
|
|
613
810
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
@@ -1014,18 +1211,22 @@ async function writeFlowPage(page, write) {
|
|
|
1014
1211
|
structured.lines
|
|
1015
1212
|
);
|
|
1016
1213
|
const captionedMedia = new Set(captions.values());
|
|
1214
|
+
const emittedMedia = /* @__PURE__ */ new Set();
|
|
1017
1215
|
await write(
|
|
1018
1216
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
1019
1217
|
);
|
|
1020
1218
|
for (const block of structured.blocks) {
|
|
1021
1219
|
const blockY = semanticBlockY2(block);
|
|
1022
1220
|
let emittedAsCaption = false;
|
|
1221
|
+
while (media[mediaIndex] && emittedMedia.has(media[mediaIndex]))
|
|
1222
|
+
mediaIndex += 1;
|
|
1023
1223
|
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
1024
1224
|
const item = media[mediaIndex];
|
|
1025
1225
|
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
1026
1226
|
await write(
|
|
1027
1227
|
`<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1028
1228
|
);
|
|
1229
|
+
emittedMedia.add(item);
|
|
1029
1230
|
mediaIndex += 1;
|
|
1030
1231
|
emittedAsCaption = true;
|
|
1031
1232
|
break;
|
|
@@ -1034,6 +1235,14 @@ async function writeFlowPage(page, write) {
|
|
|
1034
1235
|
await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
|
|
1035
1236
|
mediaIndex += 1;
|
|
1036
1237
|
}
|
|
1238
|
+
const associatedMedia = captions.get(block);
|
|
1239
|
+
if (!emittedAsCaption && associatedMedia && block.type === "paragraph") {
|
|
1240
|
+
await write(
|
|
1241
|
+
`<figure class="pdf-semantic-figure">${associatedMedia.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1242
|
+
);
|
|
1243
|
+
emittedMedia.add(associatedMedia);
|
|
1244
|
+
emittedAsCaption = true;
|
|
1245
|
+
}
|
|
1037
1246
|
if (emittedAsCaption) continue;
|
|
1038
1247
|
if (block.type === "table") await write(tableToHtml(block.table));
|
|
1039
1248
|
else if (block.type === "heading") {
|
|
@@ -1088,7 +1297,10 @@ async function writeFlowPage(page, write) {
|
|
|
1088
1297
|
}
|
|
1089
1298
|
}
|
|
1090
1299
|
while (mediaIndex < media.length) {
|
|
1091
|
-
|
|
1300
|
+
const item = media[mediaIndex];
|
|
1301
|
+
if (item && !emittedMedia.has(item)) {
|
|
1302
|
+
await write(`<div class="pdf-semantic-visual">${item.html}</div>`);
|
|
1303
|
+
}
|
|
1092
1304
|
mediaIndex += 1;
|
|
1093
1305
|
}
|
|
1094
1306
|
await write("</section>");
|