@boxpdf/html-writer 0.1.16 → 0.1.18
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 +377 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +377 -171
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { structurePage as structurePage2, tableToHtml } from "@boxpdf/reader/structure";
|
|
3
3
|
|
|
4
|
+
// src/semantic-caption.ts
|
|
5
|
+
function isClearMediaCaption(media, block, pageWidth, pageHeight, pageLines) {
|
|
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
|
+
}
|
|
25
|
+
function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
|
|
26
|
+
const associations = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const item of media) {
|
|
28
|
+
const candidates = blocks.filter((block) => isClearMediaCaption(item, block, pageWidth, pageHeight, pageLines)).filter((block) => !associations.has(block)).sort((left, right) => captionGap(item, left) - captionGap(item, right));
|
|
29
|
+
const caption = candidates[0];
|
|
30
|
+
if (caption) associations.set(caption, item);
|
|
31
|
+
}
|
|
32
|
+
return associations;
|
|
33
|
+
}
|
|
34
|
+
function captionGap(media, block) {
|
|
35
|
+
if (block.type !== "paragraph") return Number.POSITIVE_INFINITY;
|
|
36
|
+
const bounds2 = unionLines(block.lines);
|
|
37
|
+
return Math.abs(media.bounds.y - bounds2.y - bounds2.height);
|
|
38
|
+
}
|
|
39
|
+
function unionLines(lines) {
|
|
40
|
+
const x = Math.min(...lines.map((line) => line.bounds.x));
|
|
41
|
+
const y = Math.min(...lines.map((line) => line.bounds.y));
|
|
42
|
+
const right = Math.max(...lines.map((line) => line.bounds.x + line.bounds.width));
|
|
43
|
+
const top = Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
44
|
+
return { x, y, width: right - x, height: top - y };
|
|
45
|
+
}
|
|
46
|
+
function dominantFontSignature(lines) {
|
|
47
|
+
const counts = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const span of lines.flatMap((line) => line.spans)) {
|
|
49
|
+
const signature = fontSignature(span);
|
|
50
|
+
counts.set(signature, (counts.get(signature) ?? 0) + Math.max(1, [...span.text].length));
|
|
51
|
+
}
|
|
52
|
+
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "";
|
|
53
|
+
}
|
|
54
|
+
function fontSignature(span) {
|
|
55
|
+
return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
|
|
56
|
+
}
|
|
57
|
+
function median(values) {
|
|
58
|
+
const ordered = [...values].sort((left, right) => left - right);
|
|
59
|
+
return ordered[Math.floor(ordered.length / 2)] ?? 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
4
62
|
// src/semantic-document.ts
|
|
5
63
|
import {
|
|
6
64
|
structurePage,
|
|
@@ -16,7 +74,7 @@ function dominantTextColor(lines) {
|
|
|
16
74
|
}
|
|
17
75
|
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
18
76
|
}
|
|
19
|
-
function semanticTextHtml(text, lines, defaultColor) {
|
|
77
|
+
function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
20
78
|
const ranges = [];
|
|
21
79
|
let cursor = 0;
|
|
22
80
|
for (const span of lines.flatMap((line) => line.spans)) {
|
|
@@ -25,14 +83,25 @@ function semanticTextHtml(text, lines, defaultColor) {
|
|
|
25
83
|
if (start < 0) continue;
|
|
26
84
|
cursor = start + span.text.length;
|
|
27
85
|
const color = normalizedColor(span.color);
|
|
28
|
-
|
|
86
|
+
const bold = preserveWeight && /(?:bold|semibold|demi|medium|medi)/i.test(span.fontFamily ?? "");
|
|
87
|
+
const italic = /(?:italic|oblique|slanted|slant|ital)/i.test(span.fontFamily ?? "");
|
|
88
|
+
const nondefaultColor = color && color !== defaultColor ? color : void 0;
|
|
89
|
+
if (nondefaultColor || bold || italic) {
|
|
90
|
+
ranges.push({
|
|
91
|
+
start,
|
|
92
|
+
end: cursor,
|
|
93
|
+
...nondefaultColor ? { color: nondefaultColor } : {},
|
|
94
|
+
bold,
|
|
95
|
+
italic
|
|
96
|
+
});
|
|
97
|
+
}
|
|
29
98
|
}
|
|
30
99
|
const merged = mergeRanges(ranges, text);
|
|
31
100
|
let html = "";
|
|
32
101
|
let offset = 0;
|
|
33
102
|
for (const range of merged) {
|
|
34
103
|
html += escapeHtml(text.slice(offset, range.start));
|
|
35
|
-
html +=
|
|
104
|
+
html += styledHtml(text.slice(range.start, range.end), range);
|
|
36
105
|
offset = range.end;
|
|
37
106
|
}
|
|
38
107
|
return html + escapeHtml(text.slice(offset));
|
|
@@ -41,7 +110,7 @@ function mergeRanges(ranges, text) {
|
|
|
41
110
|
const merged = [];
|
|
42
111
|
for (const range of ranges) {
|
|
43
112
|
const previous = merged.at(-1);
|
|
44
|
-
if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
|
|
113
|
+
if (previous && previous.color === range.color && previous.bold === range.bold && previous.italic === range.italic && /^\s*$/.test(text.slice(previous.end, range.start))) {
|
|
45
114
|
previous.end = range.end;
|
|
46
115
|
} else {
|
|
47
116
|
merged.push({ ...range });
|
|
@@ -49,6 +118,13 @@ function mergeRanges(ranges, text) {
|
|
|
49
118
|
}
|
|
50
119
|
return merged;
|
|
51
120
|
}
|
|
121
|
+
function styledHtml(value, range) {
|
|
122
|
+
let html = escapeHtml(value);
|
|
123
|
+
if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
|
|
124
|
+
if (range.italic) html = `<em>${html}</em>`;
|
|
125
|
+
if (range.bold) html = `<strong>${html}</strong>`;
|
|
126
|
+
return html;
|
|
127
|
+
}
|
|
52
128
|
function normalizedColor(value) {
|
|
53
129
|
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
54
130
|
const color = value.toLowerCase();
|
|
@@ -58,6 +134,85 @@ function escapeHtml(value) {
|
|
|
58
134
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
59
135
|
}
|
|
60
136
|
|
|
137
|
+
// src/vector-svg.ts
|
|
138
|
+
function vectorFillSvg(fill) {
|
|
139
|
+
if (!isCssHexColor(fill.color)) return "";
|
|
140
|
+
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
141
|
+
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
142
|
+
return `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
|
|
143
|
+
}
|
|
144
|
+
function vectorPathSvg(path, pageNumber, pathIndex) {
|
|
145
|
+
if (!isSvgPath(path.d)) return "";
|
|
146
|
+
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
147
|
+
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
148
|
+
const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
|
|
149
|
+
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
|
|
150
|
+
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
|
|
151
|
+
const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
|
|
152
|
+
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
|
|
153
|
+
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
154
|
+
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
155
|
+
const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
156
|
+
let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${dashoffset}${linecap}${linejoin}${rule}/>`;
|
|
157
|
+
for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
158
|
+
output = `<g clip-path="url(#${vectorPathClipId(pageNumber, pathIndex, index)})">${output}</g>`;
|
|
159
|
+
}
|
|
160
|
+
return output;
|
|
161
|
+
}
|
|
162
|
+
function vectorPathClipDefinitions(paths, pageNumber) {
|
|
163
|
+
return paths.flatMap(
|
|
164
|
+
({ path, index: pathIndex }) => (path.clips ?? []).map((clip, clipIndex) => {
|
|
165
|
+
if (!isSvgPath(clip.d)) return "";
|
|
166
|
+
const rule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
167
|
+
return `<clipPath id="${vectorPathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${rule}/></clipPath>`;
|
|
168
|
+
})
|
|
169
|
+
).join("");
|
|
170
|
+
}
|
|
171
|
+
function vectorPathBounds(path) {
|
|
172
|
+
if (!isSvgPath(path.d)) return void 0;
|
|
173
|
+
const values = [...path.d.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
|
|
174
|
+
(match) => Number(match[0])
|
|
175
|
+
);
|
|
176
|
+
if (values.length < 2) return void 0;
|
|
177
|
+
const xs = [];
|
|
178
|
+
const ys = [];
|
|
179
|
+
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
180
|
+
xs.push(values[index] ?? 0);
|
|
181
|
+
ys.push(values[index + 1] ?? 0);
|
|
182
|
+
}
|
|
183
|
+
return bounds(xs, ys);
|
|
184
|
+
}
|
|
185
|
+
function vectorFillBounds(fill) {
|
|
186
|
+
if (fill.points.length === 0) return void 0;
|
|
187
|
+
return bounds(
|
|
188
|
+
fill.points.map(([x]) => x),
|
|
189
|
+
fill.points.map(([, y]) => y)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
function isSvgPath(value) {
|
|
193
|
+
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
194
|
+
}
|
|
195
|
+
function bounds(xs, ys) {
|
|
196
|
+
const x = Math.min(...xs);
|
|
197
|
+
const y = Math.min(...ys);
|
|
198
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
199
|
+
}
|
|
200
|
+
function vectorPathClipId(pageNumber, pathIndex, clipIndex) {
|
|
201
|
+
return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
|
|
202
|
+
}
|
|
203
|
+
function isCssHexColor(value) {
|
|
204
|
+
return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value);
|
|
205
|
+
}
|
|
206
|
+
function finiteNonnegative(value) {
|
|
207
|
+
return value !== void 0 && Number.isFinite(value) && value >= 0;
|
|
208
|
+
}
|
|
209
|
+
function isUnitInterval(value) {
|
|
210
|
+
return value !== void 0 && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
211
|
+
}
|
|
212
|
+
function number(value) {
|
|
213
|
+
return Number(value.toFixed(4)).toString();
|
|
214
|
+
}
|
|
215
|
+
|
|
61
216
|
// src/visual-font.ts
|
|
62
217
|
function visualFontAliases(pageNumber, fonts) {
|
|
63
218
|
return new Map(
|
|
@@ -107,46 +262,90 @@ function base64(bytes) {
|
|
|
107
262
|
// src/semantic-media.ts
|
|
108
263
|
function semanticMedia(page) {
|
|
109
264
|
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
110
|
-
|
|
111
|
-
if (vector) output.push(vector);
|
|
265
|
+
output.push(...vectorMedia(page));
|
|
112
266
|
return output.sort((left, right) => right.bounds.y - left.bounds.y);
|
|
113
267
|
}
|
|
114
268
|
function rasterMedia(image) {
|
|
115
|
-
const
|
|
269
|
+
const bounds2 = transformedUnitBounds(image.transform);
|
|
116
270
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
117
271
|
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
118
|
-
const opacity = unitInterval(image.opacity) ? `;opacity:${
|
|
272
|
+
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
119
273
|
return {
|
|
120
|
-
bounds,
|
|
121
|
-
html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${
|
|
274
|
+
bounds: bounds2,
|
|
275
|
+
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}">`
|
|
122
276
|
};
|
|
123
277
|
}
|
|
124
278
|
function vectorMedia(page) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
279
|
+
const primitives = [
|
|
280
|
+
...(page.paths ?? []).flatMap((path, index) => {
|
|
281
|
+
const bounds2 = vectorPathBounds(path);
|
|
282
|
+
return bounds2 ? [{ type: "path", value: path, index, bounds: bounds2 }] : [];
|
|
283
|
+
}),
|
|
284
|
+
...(page.fills ?? []).flatMap((fill) => {
|
|
285
|
+
const bounds2 = vectorFillBounds(fill);
|
|
286
|
+
return bounds2 && !isPageBackground(fill, bounds2, page) ? [{ type: "fill", value: fill, bounds: bounds2 }] : [];
|
|
287
|
+
})
|
|
288
|
+
];
|
|
289
|
+
const components = vectorComponents(primitives, Math.min(36, page.width * 0.06)).filter(
|
|
290
|
+
(component) => component.primitives.length >= 2 || component.bounds.width * component.bounds.height >= page.width * page.height * 2e-3
|
|
291
|
+
);
|
|
132
292
|
const aliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
133
293
|
const visualCodeFonts = new Set(
|
|
134
294
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
135
295
|
);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
296
|
+
return components.map((component) => {
|
|
297
|
+
const bounds2 = component.bounds;
|
|
298
|
+
const paths = component.primitives.flatMap(
|
|
299
|
+
(primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
|
|
300
|
+
);
|
|
301
|
+
const fills = component.primitives.flatMap(
|
|
302
|
+
(primitive) => primitive.type === "fill" ? [primitive.value] : []
|
|
303
|
+
);
|
|
304
|
+
const visualSpans = page.visualSpans ?? page.spans;
|
|
305
|
+
const overlay = visualSpans.filter(
|
|
306
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
|
|
307
|
+
);
|
|
308
|
+
const consumedSpans = page.spans.filter(
|
|
309
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
|
|
310
|
+
);
|
|
311
|
+
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
312
|
+
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
313
|
+
return {
|
|
314
|
+
bounds: bounds2,
|
|
315
|
+
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
|
+
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function vectorComponents(primitives, padding) {
|
|
321
|
+
const components = [];
|
|
322
|
+
for (const primitive of primitives) {
|
|
323
|
+
const matches = components.filter(
|
|
324
|
+
(component) => nearby(component.bounds, primitive.bounds, padding)
|
|
325
|
+
);
|
|
326
|
+
if (matches.length === 0) {
|
|
327
|
+
components.push({ bounds: primitive.bounds, primitives: [primitive] });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const target = matches[0];
|
|
331
|
+
target.primitives.push(primitive);
|
|
332
|
+
target.bounds = unionBounds([target.bounds, primitive.bounds]);
|
|
333
|
+
for (const component of matches.slice(1)) {
|
|
334
|
+
target.primitives.push(...component.primitives);
|
|
335
|
+
target.bounds = unionBounds([target.bounds, component.bounds]);
|
|
336
|
+
components.splice(components.indexOf(component), 1);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return components;
|
|
340
|
+
}
|
|
341
|
+
function nearby(left, right, padding) {
|
|
342
|
+
return !(left.x + left.width + padding < right.x || right.x + right.width + padding < left.x || left.y + left.height + padding < right.y || right.y + right.height + padding < left.y);
|
|
343
|
+
}
|
|
344
|
+
function isPageBackground(fill, bounds2, page) {
|
|
345
|
+
if (!/^#f{6}$/i.test(fill.color)) return false;
|
|
346
|
+
const outside = bounds2.x < 0 || bounds2.y < 0 || bounds2.x + bounds2.width > page.width || bounds2.y + bounds2.height > page.height;
|
|
347
|
+
const large = bounds2.width * bounds2.height > page.width * page.height * 0.2;
|
|
348
|
+
return outside || large;
|
|
150
349
|
}
|
|
151
350
|
function withoutSemanticMediaSpans(page, media) {
|
|
152
351
|
const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
|
|
@@ -156,7 +355,7 @@ function vectorText(span, pageHeight, aliases) {
|
|
|
156
355
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
157
356
|
const styles2 = [
|
|
158
357
|
cssColor(span.color) ? `fill:${span.color}` : "",
|
|
159
|
-
unitInterval(span.fillOpacity) ? `fill-opacity:${
|
|
358
|
+
unitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "",
|
|
160
359
|
...visualFontStyles(
|
|
161
360
|
span.fontFamily,
|
|
162
361
|
span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
|
|
@@ -164,33 +363,16 @@ function vectorText(span, pageHeight, aliases) {
|
|
|
164
363
|
].filter(Boolean).join(";");
|
|
165
364
|
const anchorY = pageHeight - span.bounds.y;
|
|
166
365
|
const transform = span.transform;
|
|
167
|
-
const position = transform ? ` x="0" y="0" transform="matrix(${transform.map(
|
|
366
|
+
const position = transform ? ` x="0" y="0" transform="matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(anchorY)})"` : ` x="${number2(span.bounds.x)}" y="${number2(anchorY)}"`;
|
|
168
367
|
const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
169
|
-
const length = extent > 0 ? ` textLength="${
|
|
170
|
-
return `<text${position} font-size="${
|
|
368
|
+
const length = extent > 0 ? ` textLength="${number2(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
|
|
369
|
+
return `<text${position} font-size="${number2(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
|
|
171
370
|
}
|
|
172
371
|
function centerInside(inner, outer) {
|
|
173
372
|
const x = inner.x + inner.width / 2;
|
|
174
373
|
const y = inner.y + inner.height / 2;
|
|
175
374
|
return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
|
|
176
375
|
}
|
|
177
|
-
function vectorFill(fill) {
|
|
178
|
-
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
179
|
-
const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
180
|
-
return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
|
|
181
|
-
}
|
|
182
|
-
function vectorPath(path) {
|
|
183
|
-
const fill = cssColor(path.fill) ? path.fill : "none";
|
|
184
|
-
const stroke = cssColor(path.stroke) ? path.stroke : "none";
|
|
185
|
-
const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
|
|
186
|
-
const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
|
|
187
|
-
const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
|
|
188
|
-
const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
|
|
189
|
-
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
190
|
-
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
191
|
-
const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
192
|
-
return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
|
|
193
|
-
}
|
|
194
376
|
function transformedUnitBounds([a, b, c, d, e, f]) {
|
|
195
377
|
const points = [
|
|
196
378
|
[e, f],
|
|
@@ -204,31 +386,8 @@ function transformedUnitBounds([a, b, c, d, e, f]) {
|
|
|
204
386
|
const minY = Math.min(...ys);
|
|
205
387
|
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
206
388
|
}
|
|
207
|
-
function
|
|
208
|
-
const values =
|
|
209
|
-
(match) => Number(match[0])
|
|
210
|
-
);
|
|
211
|
-
if (values.length < 2) return void 0;
|
|
212
|
-
const xs = [];
|
|
213
|
-
const ys = [];
|
|
214
|
-
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
215
|
-
xs.push(values[index] ?? 0);
|
|
216
|
-
ys.push(values[index + 1] ?? 0);
|
|
217
|
-
}
|
|
218
|
-
const minX = Math.min(...xs);
|
|
219
|
-
const minY = Math.min(...ys);
|
|
220
|
-
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
221
|
-
}
|
|
222
|
-
function fillBounds(fill) {
|
|
223
|
-
if (fill.points.length === 0) return void 0;
|
|
224
|
-
const xs = fill.points.map(([x]) => x);
|
|
225
|
-
const ys = fill.points.map(([, y]) => y);
|
|
226
|
-
const minX = Math.min(...xs);
|
|
227
|
-
const minY = Math.min(...ys);
|
|
228
|
-
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
229
|
-
}
|
|
230
|
-
function unionBounds(bounds) {
|
|
231
|
-
const values = bounds.filter((value) => Boolean(value));
|
|
389
|
+
function unionBounds(bounds2) {
|
|
390
|
+
const values = bounds2.filter((value) => Boolean(value));
|
|
232
391
|
if (values.length === 0) return void 0;
|
|
233
392
|
const x = Math.min(...values.map((value) => value.x));
|
|
234
393
|
const y = Math.min(...values.map((value) => value.y));
|
|
@@ -259,19 +418,16 @@ function rgbBmp(image) {
|
|
|
259
418
|
}
|
|
260
419
|
return output;
|
|
261
420
|
}
|
|
262
|
-
function safePath(value) {
|
|
263
|
-
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
264
|
-
}
|
|
265
421
|
function cssColor(value) {
|
|
266
422
|
return /^#[\da-f]{6}$/i.test(value ?? "");
|
|
267
423
|
}
|
|
268
|
-
function
|
|
424
|
+
function finiteNonnegative2(value) {
|
|
269
425
|
return Number.isFinite(value) && (value ?? -1) >= 0;
|
|
270
426
|
}
|
|
271
427
|
function unitInterval(value) {
|
|
272
|
-
return
|
|
428
|
+
return finiteNonnegative2(value) && value <= 1;
|
|
273
429
|
}
|
|
274
|
-
function
|
|
430
|
+
function number2(value) {
|
|
275
431
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
276
432
|
}
|
|
277
433
|
function escapeHtml2(value) {
|
|
@@ -323,18 +479,38 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
323
479
|
const emitPage = async (page, future) => {
|
|
324
480
|
const defaultColor = dominantTextColor(page.structured.lines);
|
|
325
481
|
let mediaIndex = 0;
|
|
482
|
+
const captions = clearMediaCaptionAssociations(
|
|
483
|
+
page.media,
|
|
484
|
+
page.structured.blocks,
|
|
485
|
+
page.width,
|
|
486
|
+
page.height,
|
|
487
|
+
page.structured.lines
|
|
488
|
+
);
|
|
489
|
+
const captionedMedia = new Set(captions.values());
|
|
326
490
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
327
491
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
328
492
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
329
493
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
330
494
|
const blockY = semanticBlockY(block);
|
|
495
|
+
let emittedAsCaption = false;
|
|
331
496
|
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
332
497
|
await flushPendingParagraph();
|
|
333
|
-
const
|
|
498
|
+
const item = page.media[mediaIndex];
|
|
499
|
+
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
500
|
+
const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
501
|
+
if (activeTable) pendingMedia.push(html2);
|
|
502
|
+
else await write(html2);
|
|
503
|
+
mediaIndex += 1;
|
|
504
|
+
emittedAsCaption = true;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (item && captionedMedia.has(item)) break;
|
|
508
|
+
const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
|
|
334
509
|
if (activeTable) pendingMedia.push(html);
|
|
335
510
|
else await write(html);
|
|
336
511
|
mediaIndex += 1;
|
|
337
512
|
}
|
|
513
|
+
if (emittedAsCaption) continue;
|
|
338
514
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
339
515
|
stats.suppressedFurniture += 1;
|
|
340
516
|
continue;
|
|
@@ -342,7 +518,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
342
518
|
await flushPendingParagraph();
|
|
343
519
|
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
344
520
|
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
345
|
-
await write(
|
|
521
|
+
await write(
|
|
522
|
+
`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
|
|
523
|
+
);
|
|
346
524
|
headerOpen = true;
|
|
347
525
|
continue;
|
|
348
526
|
}
|
|
@@ -357,7 +535,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
357
535
|
}
|
|
358
536
|
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
359
537
|
await write(
|
|
360
|
-
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
538
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
|
|
361
539
|
);
|
|
362
540
|
continue;
|
|
363
541
|
}
|
|
@@ -395,7 +573,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
395
573
|
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
396
574
|
await closeSections(level);
|
|
397
575
|
await write(
|
|
398
|
-
`<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
|
|
576
|
+
`<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
|
|
399
577
|
);
|
|
400
578
|
sectionLevels.push(level);
|
|
401
579
|
continue;
|
|
@@ -563,8 +741,16 @@ function financialSummaryRow(entry, columns) {
|
|
|
563
741
|
return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
|
|
564
742
|
}
|
|
565
743
|
function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
744
|
+
if (block.type === "insetGroup") {
|
|
745
|
+
return `<div class="pdf-semantic-inset" style="margin-inline-start:${block.indentEm}em">${block.blocks.map((item) => semanticBlockHtml(item, defaultColor)).join("")}</div>`;
|
|
746
|
+
}
|
|
747
|
+
if (block.type === "table") {
|
|
748
|
+
const rows = tableToRows(block.table);
|
|
749
|
+
const header = tableHeader(rows);
|
|
750
|
+
return `<table>${rows.map((row, index) => tableRow(row, Boolean(header && index === 0))).join("")}</table>`;
|
|
751
|
+
}
|
|
566
752
|
if (block.type === "heading")
|
|
567
|
-
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
|
|
753
|
+
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
|
|
568
754
|
if (block.type === "paragraph")
|
|
569
755
|
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
570
756
|
if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
|
|
@@ -589,7 +775,7 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
589
775
|
return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
|
|
590
776
|
}
|
|
591
777
|
const tag = block.ordered ? "ol" : "ul";
|
|
592
|
-
return `<${tag}>${block.items.map((item) => `<li>${
|
|
778
|
+
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
593
779
|
}
|
|
594
780
|
function semanticBlockY(block) {
|
|
595
781
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
@@ -678,7 +864,7 @@ async function writePositionedPage(page, write, options) {
|
|
|
678
864
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
679
865
|
const displayHeight = quarterTurn ? page.width : page.height;
|
|
680
866
|
await write(
|
|
681
|
-
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${
|
|
867
|
+
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number3(displayWidth)}pt;height:${number3(displayHeight)}pt">`
|
|
682
868
|
);
|
|
683
869
|
const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
684
870
|
const type3Fonts = new Map(
|
|
@@ -690,44 +876,26 @@ async function writePositionedPage(page, write, options) {
|
|
|
690
876
|
);
|
|
691
877
|
}
|
|
692
878
|
await write(
|
|
693
|
-
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${
|
|
879
|
+
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
|
|
694
880
|
);
|
|
695
881
|
await write(
|
|
696
|
-
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${
|
|
882
|
+
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
|
|
883
|
+
);
|
|
884
|
+
const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
|
|
885
|
+
(page.paths ?? []).map((path, index) => ({ path, index })),
|
|
886
|
+
page.number
|
|
697
887
|
);
|
|
698
|
-
const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
|
|
699
888
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
700
889
|
if (reflectedOverlay) {
|
|
701
890
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
702
891
|
await write(visualImage(image, page.height, page.number, index));
|
|
703
892
|
}
|
|
704
893
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
await write(
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
if (page.paths?.length) {
|
|
713
|
-
await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
|
|
714
|
-
for (const [pathIndex, path] of page.paths.entries()) {
|
|
715
|
-
if (!isSvgPath(path.d)) continue;
|
|
716
|
-
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
717
|
-
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
718
|
-
const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
|
|
719
|
-
const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
720
|
-
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
|
|
721
|
-
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
|
|
722
|
-
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
|
|
723
|
-
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
|
|
724
|
-
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
725
|
-
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
726
|
-
let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
|
|
727
|
-
for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
728
|
-
output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
|
|
729
|
-
}
|
|
730
|
-
await write(output);
|
|
894
|
+
if (page.fills?.length || page.paths?.length) {
|
|
895
|
+
await write(`<g transform="translate(0 ${number3(page.height)}) scale(1 -1)">`);
|
|
896
|
+
for (const fill of page.fills ?? []) await write(vectorFillSvg(fill));
|
|
897
|
+
for (const [pathIndex, path] of (page.paths ?? []).entries()) {
|
|
898
|
+
await write(vectorPathSvg(path, page.number, pathIndex));
|
|
731
899
|
}
|
|
732
900
|
await write("</g>");
|
|
733
901
|
}
|
|
@@ -757,8 +925,8 @@ function usesReflectedVisualOverlay(page, spans) {
|
|
|
757
925
|
}
|
|
758
926
|
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
759
927
|
const [a, b, c, d, e, f] = image.transform;
|
|
760
|
-
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(
|
|
761
|
-
const opacity =
|
|
928
|
+
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
|
|
929
|
+
const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
|
|
762
930
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
763
931
|
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
764
932
|
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
@@ -772,25 +940,13 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
|
772
940
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
773
941
|
if (!isSvgPath(clip.d)) return "";
|
|
774
942
|
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
775
|
-
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${
|
|
943
|
+
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number3(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
|
|
776
944
|
})
|
|
777
945
|
).join("");
|
|
778
946
|
}
|
|
779
947
|
function imageClipId(pageNumber, imageIndex, clipIndex) {
|
|
780
948
|
return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
|
|
781
949
|
}
|
|
782
|
-
function pathClipDefinitions(paths, pageNumber) {
|
|
783
|
-
return paths.flatMap(
|
|
784
|
-
(path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
|
|
785
|
-
if (!isSvgPath(clip.d)) return "";
|
|
786
|
-
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
787
|
-
return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
|
|
788
|
-
})
|
|
789
|
-
).join("");
|
|
790
|
-
}
|
|
791
|
-
function pathClipId(pageNumber, pathIndex, clipIndex) {
|
|
792
|
-
return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
|
|
793
|
-
}
|
|
794
950
|
function rgbBmp2(image) {
|
|
795
951
|
const stride = Math.ceil(image.width * 3 / 4) * 4;
|
|
796
952
|
const output = new Uint8Array(54 + stride * image.height);
|
|
@@ -819,11 +975,11 @@ function rgbBmp2(image) {
|
|
|
819
975
|
function rotationTransform(page) {
|
|
820
976
|
switch (page.rotate) {
|
|
821
977
|
case 90:
|
|
822
|
-
return `;transform:translate(${
|
|
978
|
+
return `;transform:translate(${number3(page.height)}pt,0) rotate(90deg)`;
|
|
823
979
|
case 180:
|
|
824
|
-
return `;transform:translate(${
|
|
980
|
+
return `;transform:translate(${number3(page.width)}pt,${number3(page.height)}pt) rotate(180deg)`;
|
|
825
981
|
case 270:
|
|
826
|
-
return `;transform:translate(0,${
|
|
982
|
+
return `;transform:translate(0,${number3(page.width)}pt) rotate(270deg)`;
|
|
827
983
|
default:
|
|
828
984
|
return "";
|
|
829
985
|
}
|
|
@@ -831,13 +987,13 @@ function rotationTransform(page) {
|
|
|
831
987
|
function positionedSpan(span, fontAliases) {
|
|
832
988
|
const direction = directionAttribute([span]);
|
|
833
989
|
const style = [
|
|
834
|
-
`left:${
|
|
835
|
-
`bottom:${
|
|
836
|
-
`width:${
|
|
837
|
-
`height:${
|
|
838
|
-
`font-size:${
|
|
839
|
-
...
|
|
840
|
-
...
|
|
990
|
+
`left:${number3(span.bounds.x)}pt`,
|
|
991
|
+
`bottom:${number3(span.bounds.y)}pt`,
|
|
992
|
+
`width:${number3(span.bounds.width)}pt`,
|
|
993
|
+
`height:${number3(span.bounds.height)}pt`,
|
|
994
|
+
`font-size:${number3(span.fontSize)}pt`,
|
|
995
|
+
...isCssHexColor2(span.color) ? [`color:${span.color}`] : [],
|
|
996
|
+
...isUnitInterval2(span.fillOpacity) ? [`opacity:${number3(span.fillOpacity)}`] : [],
|
|
841
997
|
...visualFontStyles(
|
|
842
998
|
span.fontFamily,
|
|
843
999
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -850,19 +1006,39 @@ async function writeFlowPage(page, write) {
|
|
|
850
1006
|
const structured = structurePage2(withoutSemanticMediaSpans(page, media));
|
|
851
1007
|
const defaultColor = dominantTextColor(structured.lines);
|
|
852
1008
|
let mediaIndex = 0;
|
|
1009
|
+
const captions = clearMediaCaptionAssociations(
|
|
1010
|
+
media,
|
|
1011
|
+
structured.blocks,
|
|
1012
|
+
page.width,
|
|
1013
|
+
page.height,
|
|
1014
|
+
structured.lines
|
|
1015
|
+
);
|
|
1016
|
+
const captionedMedia = new Set(captions.values());
|
|
853
1017
|
await write(
|
|
854
1018
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
855
1019
|
);
|
|
856
1020
|
for (const block of structured.blocks) {
|
|
857
1021
|
const blockY = semanticBlockY2(block);
|
|
1022
|
+
let emittedAsCaption = false;
|
|
858
1023
|
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
859
|
-
|
|
1024
|
+
const item = media[mediaIndex];
|
|
1025
|
+
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
1026
|
+
await write(
|
|
1027
|
+
`<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1028
|
+
);
|
|
1029
|
+
mediaIndex += 1;
|
|
1030
|
+
emittedAsCaption = true;
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
if (item && captionedMedia.has(item)) break;
|
|
1034
|
+
await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
|
|
860
1035
|
mediaIndex += 1;
|
|
861
1036
|
}
|
|
1037
|
+
if (emittedAsCaption) continue;
|
|
862
1038
|
if (block.type === "table") await write(tableToHtml(block.table));
|
|
863
1039
|
else if (block.type === "heading") {
|
|
864
1040
|
await write(
|
|
865
|
-
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
1041
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
|
|
866
1042
|
);
|
|
867
1043
|
} else if (block.type === "paragraph") {
|
|
868
1044
|
await write(
|
|
@@ -898,10 +1074,16 @@ async function writeFlowPage(page, write) {
|
|
|
898
1074
|
await write(
|
|
899
1075
|
`<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
|
|
900
1076
|
);
|
|
1077
|
+
} else if (block.type === "insetGroup") {
|
|
1078
|
+
await write(
|
|
1079
|
+
`<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`
|
|
1080
|
+
);
|
|
901
1081
|
} else {
|
|
902
1082
|
const tag = block.ordered ? "ol" : "ul";
|
|
903
1083
|
await write(`<${tag}>`);
|
|
904
|
-
for (const item of block.items)
|
|
1084
|
+
for (const item of block.items) {
|
|
1085
|
+
await write(`<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`);
|
|
1086
|
+
}
|
|
905
1087
|
await write(`</${tag}>`);
|
|
906
1088
|
}
|
|
907
1089
|
}
|
|
@@ -911,6 +1093,33 @@ async function writeFlowPage(page, write) {
|
|
|
911
1093
|
}
|
|
912
1094
|
await write("</section>");
|
|
913
1095
|
}
|
|
1096
|
+
function nestedSemanticBlockHtml(block, defaultColor) {
|
|
1097
|
+
if (block.type === "insetGroup") {
|
|
1098
|
+
return `<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`;
|
|
1099
|
+
}
|
|
1100
|
+
if (block.type === "table") return tableToHtml(block.table);
|
|
1101
|
+
if (block.type === "heading") {
|
|
1102
|
+
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
|
|
1103
|
+
}
|
|
1104
|
+
if (block.type === "paragraph") {
|
|
1105
|
+
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
1106
|
+
}
|
|
1107
|
+
if (block.type === "preformatted") return `<pre>${escapeHtml4(block.text)}</pre>`;
|
|
1108
|
+
if (block.type === "definitionList") {
|
|
1109
|
+
return `<dl>${block.entries.map((entry) => `<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`).join("")}</dl>`;
|
|
1110
|
+
}
|
|
1111
|
+
if (block.type === "cardList") {
|
|
1112
|
+
return `<div class="pdf-semantic-cards">${block.items.map((item) => `<article><h3>${escapeHtml4(item.title)}</h3>${item.details.map((detail) => `<p>${escapeHtml4(detail)}</p>`).join("")}</article>`).join("")}</div>`;
|
|
1113
|
+
}
|
|
1114
|
+
if (block.type === "sectionGroup") {
|
|
1115
|
+
return `<div class="pdf-semantic-sections">${block.items.map((item) => `<section><h3>${escapeHtml4(item.label)}</h3>${item.content.map((content) => `<p>${escapeHtml4(content)}</p>`).join("")}</section>`).join("")}</div>`;
|
|
1116
|
+
}
|
|
1117
|
+
if (block.type === "employment") {
|
|
1118
|
+
return `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`;
|
|
1119
|
+
}
|
|
1120
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
1121
|
+
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
1122
|
+
}
|
|
914
1123
|
function semanticBlockY2(block) {
|
|
915
1124
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
916
1125
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
@@ -923,15 +1132,15 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
923
1132
|
span.fontFamily,
|
|
924
1133
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
925
1134
|
).join(";");
|
|
926
|
-
const stroke =
|
|
927
|
-
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${
|
|
1135
|
+
const stroke = isCssHexColor2(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
|
|
1136
|
+
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number3(span.strokeWidth ?? 0)}` : "";
|
|
928
1137
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
929
|
-
const fillOpacity =
|
|
930
|
-
const strokeOpacity =
|
|
1138
|
+
const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
|
|
1139
|
+
const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
|
|
931
1140
|
const style = [
|
|
932
1141
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
933
1142
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
934
|
-
strokeOnly ? "fill:none" :
|
|
1143
|
+
strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
|
|
935
1144
|
stroke,
|
|
936
1145
|
strokeWidth,
|
|
937
1146
|
fillOpacity,
|
|
@@ -939,7 +1148,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
939
1148
|
font
|
|
940
1149
|
].filter(Boolean).join(";");
|
|
941
1150
|
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
942
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${
|
|
1151
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
943
1152
|
const transform = counterRotateReflectedText && span.transform ? [
|
|
944
1153
|
span.transform[0],
|
|
945
1154
|
span.transform[1],
|
|
@@ -952,8 +1161,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
952
1161
|
const basisY = transform?.[1] ?? 0;
|
|
953
1162
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
954
1163
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
955
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(
|
|
956
|
-
return `<text${direction}${position} font-size="${
|
|
1164
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1165
|
+
return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
957
1166
|
}
|
|
958
1167
|
function isAdobeCjkFont(fontFamily) {
|
|
959
1168
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -965,16 +1174,16 @@ function visualType3Text(span, font, pageHeight) {
|
|
|
965
1174
|
const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
|
|
966
1175
|
if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
|
|
967
1176
|
const transform = span.transform ?? [1, 0, 0, 1];
|
|
968
|
-
const outer = `matrix(${transform.map(
|
|
1177
|
+
const outer = `matrix(${transform.map(number3).join(" ")} ${number3(span.bounds.x)} ${number3(pageHeight - span.bounds.y)})`;
|
|
969
1178
|
const xScale = span.bounds.width / totalAdvance;
|
|
970
1179
|
let offset = 0;
|
|
971
1180
|
let content = "";
|
|
972
1181
|
for (const glyph of sequence) {
|
|
973
1182
|
if (!glyph) continue;
|
|
974
|
-
content += `<g transform="translate(${
|
|
1183
|
+
content += `<g transform="translate(${number3(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
|
|
975
1184
|
offset += glyph.advance;
|
|
976
1185
|
}
|
|
977
|
-
return `<g transform="${outer}"><g transform="scale(${
|
|
1186
|
+
return `<g transform="${outer}"><g transform="scale(${number3(xScale)} ${number3(-span.fontSize)})">${content}</g></g>`;
|
|
978
1187
|
}
|
|
979
1188
|
function isHebrewPaintOrder(span) {
|
|
980
1189
|
return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
|
|
@@ -985,30 +1194,27 @@ function usesSpacingAdjustment(span) {
|
|
|
985
1194
|
function type3Glyph(glyph, textColor) {
|
|
986
1195
|
let output = "";
|
|
987
1196
|
for (const fill of glyph.fills ?? []) {
|
|
988
|
-
const color = glyph.usesTextColor &&
|
|
989
|
-
if (!
|
|
990
|
-
const points = fill.points.map(([x, y]) => `${
|
|
991
|
-
const opacity =
|
|
1197
|
+
const color = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : fill.color;
|
|
1198
|
+
if (!isCssHexColor2(color)) continue;
|
|
1199
|
+
const points = fill.points.map(([x, y]) => `${number3(x)},${number3(y)}`).join(" ");
|
|
1200
|
+
const opacity = isUnitInterval2(fill.opacity) ? ` fill-opacity="${number3(fill.opacity)}"` : "";
|
|
992
1201
|
output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
|
|
993
1202
|
}
|
|
994
1203
|
for (const path of glyph.paths ?? []) {
|
|
995
1204
|
if (!isSvgPath(path.d)) continue;
|
|
996
|
-
const fill = glyph.usesTextColor &&
|
|
997
|
-
const stroke = glyph.usesTextColor &&
|
|
998
|
-
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${
|
|
1205
|
+
const fill = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : isCssHexColor2(path.fill) ? path.fill : "none";
|
|
1206
|
+
const stroke = glyph.usesTextColor && isCssHexColor2(textColor) && path.stroke ? textColor : isCssHexColor2(path.stroke) ? path.stroke : "none";
|
|
1207
|
+
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number3(path.strokeWidth)}"` : "";
|
|
999
1208
|
output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
|
|
1000
1209
|
}
|
|
1001
1210
|
return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
|
|
1002
1211
|
}
|
|
1003
|
-
function
|
|
1212
|
+
function isCssHexColor2(value) {
|
|
1004
1213
|
return /^#[\da-f]{6}$/i.test(value ?? "");
|
|
1005
1214
|
}
|
|
1006
|
-
function
|
|
1215
|
+
function isUnitInterval2(value) {
|
|
1007
1216
|
return Number.isFinite(value) && (value ?? -1) >= 0 && (value ?? 2) <= 1;
|
|
1008
1217
|
}
|
|
1009
|
-
function isSvgPath(value) {
|
|
1010
|
-
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
1011
|
-
}
|
|
1012
1218
|
function isMonospace(fontFamily) {
|
|
1013
1219
|
return /courier|mono/i.test(fontFamily ?? "");
|
|
1014
1220
|
}
|
|
@@ -1026,7 +1232,7 @@ function directionAttribute(spans) {
|
|
|
1026
1232
|
if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
|
|
1027
1233
|
return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
|
|
1028
1234
|
}
|
|
1029
|
-
function
|
|
1235
|
+
function number3(value) {
|
|
1030
1236
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
1031
1237
|
}
|
|
1032
1238
|
function escapeAttribute(value) {
|