@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.cjs
CHANGED
|
@@ -27,6 +27,64 @@ __export(index_exports, {
|
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
var import_structure2 = require("@boxpdf/reader/structure");
|
|
29
29
|
|
|
30
|
+
// src/semantic-caption.ts
|
|
31
|
+
function isClearMediaCaption(media, block, pageWidth, pageHeight, pageLines) {
|
|
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
|
+
}
|
|
51
|
+
function clearMediaCaptionAssociations(media, blocks, pageWidth, pageHeight, pageLines) {
|
|
52
|
+
const associations = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const item of media) {
|
|
54
|
+
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));
|
|
55
|
+
const caption = candidates[0];
|
|
56
|
+
if (caption) associations.set(caption, item);
|
|
57
|
+
}
|
|
58
|
+
return associations;
|
|
59
|
+
}
|
|
60
|
+
function captionGap(media, block) {
|
|
61
|
+
if (block.type !== "paragraph") return Number.POSITIVE_INFINITY;
|
|
62
|
+
const bounds2 = unionLines(block.lines);
|
|
63
|
+
return Math.abs(media.bounds.y - bounds2.y - bounds2.height);
|
|
64
|
+
}
|
|
65
|
+
function unionLines(lines) {
|
|
66
|
+
const x = Math.min(...lines.map((line) => line.bounds.x));
|
|
67
|
+
const y = Math.min(...lines.map((line) => line.bounds.y));
|
|
68
|
+
const right = Math.max(...lines.map((line) => line.bounds.x + line.bounds.width));
|
|
69
|
+
const top = Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
70
|
+
return { x, y, width: right - x, height: top - y };
|
|
71
|
+
}
|
|
72
|
+
function dominantFontSignature(lines) {
|
|
73
|
+
const counts = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const span of lines.flatMap((line) => line.spans)) {
|
|
75
|
+
const signature = fontSignature(span);
|
|
76
|
+
counts.set(signature, (counts.get(signature) ?? 0) + Math.max(1, [...span.text].length));
|
|
77
|
+
}
|
|
78
|
+
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "";
|
|
79
|
+
}
|
|
80
|
+
function fontSignature(span) {
|
|
81
|
+
return `${(span.fontFamily ?? span.fontName ?? "").toLocaleLowerCase("en")}|${Math.round(span.fontSize * 2) / 2}|${span.color ?? ""}`;
|
|
82
|
+
}
|
|
83
|
+
function median(values) {
|
|
84
|
+
const ordered = [...values].sort((left, right) => left - right);
|
|
85
|
+
return ordered[Math.floor(ordered.length / 2)] ?? 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
30
88
|
// src/semantic-document.ts
|
|
31
89
|
var import_structure = require("@boxpdf/reader/structure");
|
|
32
90
|
|
|
@@ -39,7 +97,7 @@ function dominantTextColor(lines) {
|
|
|
39
97
|
}
|
|
40
98
|
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "#000000";
|
|
41
99
|
}
|
|
42
|
-
function semanticTextHtml(text, lines, defaultColor) {
|
|
100
|
+
function semanticTextHtml(text, lines, defaultColor, preserveWeight = true) {
|
|
43
101
|
const ranges = [];
|
|
44
102
|
let cursor = 0;
|
|
45
103
|
for (const span of lines.flatMap((line) => line.spans)) {
|
|
@@ -48,14 +106,25 @@ function semanticTextHtml(text, lines, defaultColor) {
|
|
|
48
106
|
if (start < 0) continue;
|
|
49
107
|
cursor = start + span.text.length;
|
|
50
108
|
const color = normalizedColor(span.color);
|
|
51
|
-
|
|
109
|
+
const bold = preserveWeight && /(?:bold|semibold|demi|medium|medi)/i.test(span.fontFamily ?? "");
|
|
110
|
+
const italic = /(?:italic|oblique|slanted|slant|ital)/i.test(span.fontFamily ?? "");
|
|
111
|
+
const nondefaultColor = color && color !== defaultColor ? color : void 0;
|
|
112
|
+
if (nondefaultColor || bold || italic) {
|
|
113
|
+
ranges.push({
|
|
114
|
+
start,
|
|
115
|
+
end: cursor,
|
|
116
|
+
...nondefaultColor ? { color: nondefaultColor } : {},
|
|
117
|
+
bold,
|
|
118
|
+
italic
|
|
119
|
+
});
|
|
120
|
+
}
|
|
52
121
|
}
|
|
53
122
|
const merged = mergeRanges(ranges, text);
|
|
54
123
|
let html = "";
|
|
55
124
|
let offset = 0;
|
|
56
125
|
for (const range of merged) {
|
|
57
126
|
html += escapeHtml(text.slice(offset, range.start));
|
|
58
|
-
html +=
|
|
127
|
+
html += styledHtml(text.slice(range.start, range.end), range);
|
|
59
128
|
offset = range.end;
|
|
60
129
|
}
|
|
61
130
|
return html + escapeHtml(text.slice(offset));
|
|
@@ -64,7 +133,7 @@ function mergeRanges(ranges, text) {
|
|
|
64
133
|
const merged = [];
|
|
65
134
|
for (const range of ranges) {
|
|
66
135
|
const previous = merged.at(-1);
|
|
67
|
-
if (previous && previous.color === range.color && /^\s*$/.test(text.slice(previous.end, range.start))) {
|
|
136
|
+
if (previous && previous.color === range.color && previous.bold === range.bold && previous.italic === range.italic && /^\s*$/.test(text.slice(previous.end, range.start))) {
|
|
68
137
|
previous.end = range.end;
|
|
69
138
|
} else {
|
|
70
139
|
merged.push({ ...range });
|
|
@@ -72,6 +141,13 @@ function mergeRanges(ranges, text) {
|
|
|
72
141
|
}
|
|
73
142
|
return merged;
|
|
74
143
|
}
|
|
144
|
+
function styledHtml(value, range) {
|
|
145
|
+
let html = escapeHtml(value);
|
|
146
|
+
if (range.color) html = `<span style="color:${range.color}">${html}</span>`;
|
|
147
|
+
if (range.italic) html = `<em>${html}</em>`;
|
|
148
|
+
if (range.bold) html = `<strong>${html}</strong>`;
|
|
149
|
+
return html;
|
|
150
|
+
}
|
|
75
151
|
function normalizedColor(value) {
|
|
76
152
|
if (!value || !/^#[\da-f]{6}$/i.test(value)) return void 0;
|
|
77
153
|
const color = value.toLowerCase();
|
|
@@ -81,6 +157,85 @@ function escapeHtml(value) {
|
|
|
81
157
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
82
158
|
}
|
|
83
159
|
|
|
160
|
+
// src/vector-svg.ts
|
|
161
|
+
function vectorFillSvg(fill) {
|
|
162
|
+
if (!isCssHexColor(fill.color)) return "";
|
|
163
|
+
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
164
|
+
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
165
|
+
return `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
|
|
166
|
+
}
|
|
167
|
+
function vectorPathSvg(path, pageNumber, pathIndex) {
|
|
168
|
+
if (!isSvgPath(path.d)) return "";
|
|
169
|
+
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
170
|
+
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
171
|
+
const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
|
|
172
|
+
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
|
|
173
|
+
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
|
|
174
|
+
const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
|
|
175
|
+
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
|
|
176
|
+
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
177
|
+
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
178
|
+
const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
179
|
+
let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${dashoffset}${linecap}${linejoin}${rule}/>`;
|
|
180
|
+
for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
181
|
+
output = `<g clip-path="url(#${vectorPathClipId(pageNumber, pathIndex, index)})">${output}</g>`;
|
|
182
|
+
}
|
|
183
|
+
return output;
|
|
184
|
+
}
|
|
185
|
+
function vectorPathClipDefinitions(paths, pageNumber) {
|
|
186
|
+
return paths.flatMap(
|
|
187
|
+
({ path, index: pathIndex }) => (path.clips ?? []).map((clip, clipIndex) => {
|
|
188
|
+
if (!isSvgPath(clip.d)) return "";
|
|
189
|
+
const rule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
190
|
+
return `<clipPath id="${vectorPathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${rule}/></clipPath>`;
|
|
191
|
+
})
|
|
192
|
+
).join("");
|
|
193
|
+
}
|
|
194
|
+
function vectorPathBounds(path) {
|
|
195
|
+
if (!isSvgPath(path.d)) return void 0;
|
|
196
|
+
const values = [...path.d.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
|
|
197
|
+
(match) => Number(match[0])
|
|
198
|
+
);
|
|
199
|
+
if (values.length < 2) return void 0;
|
|
200
|
+
const xs = [];
|
|
201
|
+
const ys = [];
|
|
202
|
+
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
203
|
+
xs.push(values[index] ?? 0);
|
|
204
|
+
ys.push(values[index + 1] ?? 0);
|
|
205
|
+
}
|
|
206
|
+
return bounds(xs, ys);
|
|
207
|
+
}
|
|
208
|
+
function vectorFillBounds(fill) {
|
|
209
|
+
if (fill.points.length === 0) return void 0;
|
|
210
|
+
return bounds(
|
|
211
|
+
fill.points.map(([x]) => x),
|
|
212
|
+
fill.points.map(([, y]) => y)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function isSvgPath(value) {
|
|
216
|
+
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
217
|
+
}
|
|
218
|
+
function bounds(xs, ys) {
|
|
219
|
+
const x = Math.min(...xs);
|
|
220
|
+
const y = Math.min(...ys);
|
|
221
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
222
|
+
}
|
|
223
|
+
function vectorPathClipId(pageNumber, pathIndex, clipIndex) {
|
|
224
|
+
return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
|
|
225
|
+
}
|
|
226
|
+
function isCssHexColor(value) {
|
|
227
|
+
return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value);
|
|
228
|
+
}
|
|
229
|
+
function finiteNonnegative(value) {
|
|
230
|
+
return value !== void 0 && Number.isFinite(value) && value >= 0;
|
|
231
|
+
}
|
|
232
|
+
function isUnitInterval(value) {
|
|
233
|
+
return value !== void 0 && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
234
|
+
}
|
|
235
|
+
function number(value) {
|
|
236
|
+
return Number(value.toFixed(4)).toString();
|
|
237
|
+
}
|
|
238
|
+
|
|
84
239
|
// src/visual-font.ts
|
|
85
240
|
function visualFontAliases(pageNumber, fonts) {
|
|
86
241
|
return new Map(
|
|
@@ -130,46 +285,90 @@ function base64(bytes) {
|
|
|
130
285
|
// src/semantic-media.ts
|
|
131
286
|
function semanticMedia(page) {
|
|
132
287
|
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
133
|
-
|
|
134
|
-
if (vector) output.push(vector);
|
|
288
|
+
output.push(...vectorMedia(page));
|
|
135
289
|
return output.sort((left, right) => right.bounds.y - left.bounds.y);
|
|
136
290
|
}
|
|
137
291
|
function rasterMedia(image) {
|
|
138
|
-
const
|
|
292
|
+
const bounds2 = transformedUnitBounds(image.transform);
|
|
139
293
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
140
294
|
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
141
|
-
const opacity = unitInterval(image.opacity) ? `;opacity:${
|
|
295
|
+
const opacity = unitInterval(image.opacity) ? `;opacity:${number2(image.opacity)}` : "";
|
|
142
296
|
return {
|
|
143
|
-
bounds,
|
|
144
|
-
html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${
|
|
297
|
+
bounds: bounds2,
|
|
298
|
+
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}">`
|
|
145
299
|
};
|
|
146
300
|
}
|
|
147
301
|
function vectorMedia(page) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
302
|
+
const primitives = [
|
|
303
|
+
...(page.paths ?? []).flatMap((path, index) => {
|
|
304
|
+
const bounds2 = vectorPathBounds(path);
|
|
305
|
+
return bounds2 ? [{ type: "path", value: path, index, bounds: bounds2 }] : [];
|
|
306
|
+
}),
|
|
307
|
+
...(page.fills ?? []).flatMap((fill) => {
|
|
308
|
+
const bounds2 = vectorFillBounds(fill);
|
|
309
|
+
return bounds2 && !isPageBackground(fill, bounds2, page) ? [{ type: "fill", value: fill, bounds: bounds2 }] : [];
|
|
310
|
+
})
|
|
311
|
+
];
|
|
312
|
+
const components = vectorComponents(primitives, Math.min(36, page.width * 0.06)).filter(
|
|
313
|
+
(component) => component.primitives.length >= 2 || component.bounds.width * component.bounds.height >= page.width * page.height * 2e-3
|
|
314
|
+
);
|
|
155
315
|
const aliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
156
316
|
const visualCodeFonts = new Set(
|
|
157
317
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && font.visualCodeMapping).map((font) => font.id)
|
|
158
318
|
);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
319
|
+
return components.map((component) => {
|
|
320
|
+
const bounds2 = component.bounds;
|
|
321
|
+
const paths = component.primitives.flatMap(
|
|
322
|
+
(primitive) => primitive.type === "path" ? [{ path: primitive.value, index: primitive.index }] : []
|
|
323
|
+
);
|
|
324
|
+
const fills = component.primitives.flatMap(
|
|
325
|
+
(primitive) => primitive.type === "fill" ? [primitive.value] : []
|
|
326
|
+
);
|
|
327
|
+
const visualSpans = page.visualSpans ?? page.spans;
|
|
328
|
+
const overlay = visualSpans.filter(
|
|
329
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
|
|
330
|
+
);
|
|
331
|
+
const consumedSpans = page.spans.filter(
|
|
332
|
+
(span) => span.fontAssetId && visualCodeFonts.has(span.fontAssetId) && centerInside(span.bounds, bounds2)
|
|
333
|
+
);
|
|
334
|
+
const fontIds = new Set(overlay.map((span) => span.fontAssetId));
|
|
335
|
+
const fontFaces = (page.fonts ?? []).filter((font) => fontIds.has(font.id)).map((font) => visualFontFace(font, aliases)).join("");
|
|
336
|
+
return {
|
|
337
|
+
bounds: bounds2,
|
|
338
|
+
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
|
+
...consumedSpans.length > 0 ? { consumedSpans } : {}
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function vectorComponents(primitives, padding) {
|
|
344
|
+
const components = [];
|
|
345
|
+
for (const primitive of primitives) {
|
|
346
|
+
const matches = components.filter(
|
|
347
|
+
(component) => nearby(component.bounds, primitive.bounds, padding)
|
|
348
|
+
);
|
|
349
|
+
if (matches.length === 0) {
|
|
350
|
+
components.push({ bounds: primitive.bounds, primitives: [primitive] });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const target = matches[0];
|
|
354
|
+
target.primitives.push(primitive);
|
|
355
|
+
target.bounds = unionBounds([target.bounds, primitive.bounds]);
|
|
356
|
+
for (const component of matches.slice(1)) {
|
|
357
|
+
target.primitives.push(...component.primitives);
|
|
358
|
+
target.bounds = unionBounds([target.bounds, component.bounds]);
|
|
359
|
+
components.splice(components.indexOf(component), 1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return components;
|
|
363
|
+
}
|
|
364
|
+
function nearby(left, right, padding) {
|
|
365
|
+
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);
|
|
366
|
+
}
|
|
367
|
+
function isPageBackground(fill, bounds2, page) {
|
|
368
|
+
if (!/^#f{6}$/i.test(fill.color)) return false;
|
|
369
|
+
const outside = bounds2.x < 0 || bounds2.y < 0 || bounds2.x + bounds2.width > page.width || bounds2.y + bounds2.height > page.height;
|
|
370
|
+
const large = bounds2.width * bounds2.height > page.width * page.height * 0.2;
|
|
371
|
+
return outside || large;
|
|
173
372
|
}
|
|
174
373
|
function withoutSemanticMediaSpans(page, media) {
|
|
175
374
|
const consumed = new Set(media.flatMap((item) => item.consumedSpans ?? []));
|
|
@@ -179,7 +378,7 @@ function vectorText(span, pageHeight, aliases) {
|
|
|
179
378
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
180
379
|
const styles2 = [
|
|
181
380
|
cssColor(span.color) ? `fill:${span.color}` : "",
|
|
182
|
-
unitInterval(span.fillOpacity) ? `fill-opacity:${
|
|
381
|
+
unitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "",
|
|
183
382
|
...visualFontStyles(
|
|
184
383
|
span.fontFamily,
|
|
185
384
|
span.fontAssetId ? aliases.get(span.fontAssetId) : void 0
|
|
@@ -187,33 +386,16 @@ function vectorText(span, pageHeight, aliases) {
|
|
|
187
386
|
].filter(Boolean).join(";");
|
|
188
387
|
const anchorY = pageHeight - span.bounds.y;
|
|
189
388
|
const transform = span.transform;
|
|
190
|
-
const position = transform ? ` x="0" y="0" transform="matrix(${transform.map(
|
|
389
|
+
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)}"`;
|
|
191
390
|
const extent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
192
|
-
const length = extent > 0 ? ` textLength="${
|
|
193
|
-
return `<text${position} font-size="${
|
|
391
|
+
const length = extent > 0 ? ` textLength="${number2(extent)}" lengthAdjust="spacingAndGlyphs"` : "";
|
|
392
|
+
return `<text${position} font-size="${number2(span.fontSize)}"${length}${styles2 ? ` style="${styles2}"` : ""}>${escapeHtml2(span.text)}</text>`;
|
|
194
393
|
}
|
|
195
394
|
function centerInside(inner, outer) {
|
|
196
395
|
const x = inner.x + inner.width / 2;
|
|
197
396
|
const y = inner.y + inner.height / 2;
|
|
198
397
|
return x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height;
|
|
199
398
|
}
|
|
200
|
-
function vectorFill(fill) {
|
|
201
|
-
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
202
|
-
const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
203
|
-
return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
|
|
204
|
-
}
|
|
205
|
-
function vectorPath(path) {
|
|
206
|
-
const fill = cssColor(path.fill) ? path.fill : "none";
|
|
207
|
-
const stroke = cssColor(path.stroke) ? path.stroke : "none";
|
|
208
|
-
const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
|
|
209
|
-
const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
|
|
210
|
-
const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
|
|
211
|
-
const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
|
|
212
|
-
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
213
|
-
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
214
|
-
const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
215
|
-
return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
|
|
216
|
-
}
|
|
217
399
|
function transformedUnitBounds([a, b, c, d, e, f]) {
|
|
218
400
|
const points = [
|
|
219
401
|
[e, f],
|
|
@@ -227,31 +409,8 @@ function transformedUnitBounds([a, b, c, d, e, f]) {
|
|
|
227
409
|
const minY = Math.min(...ys);
|
|
228
410
|
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
229
411
|
}
|
|
230
|
-
function
|
|
231
|
-
const values =
|
|
232
|
-
(match) => Number(match[0])
|
|
233
|
-
);
|
|
234
|
-
if (values.length < 2) return void 0;
|
|
235
|
-
const xs = [];
|
|
236
|
-
const ys = [];
|
|
237
|
-
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
238
|
-
xs.push(values[index] ?? 0);
|
|
239
|
-
ys.push(values[index + 1] ?? 0);
|
|
240
|
-
}
|
|
241
|
-
const minX = Math.min(...xs);
|
|
242
|
-
const minY = Math.min(...ys);
|
|
243
|
-
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
244
|
-
}
|
|
245
|
-
function fillBounds(fill) {
|
|
246
|
-
if (fill.points.length === 0) return void 0;
|
|
247
|
-
const xs = fill.points.map(([x]) => x);
|
|
248
|
-
const ys = fill.points.map(([, y]) => y);
|
|
249
|
-
const minX = Math.min(...xs);
|
|
250
|
-
const minY = Math.min(...ys);
|
|
251
|
-
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
252
|
-
}
|
|
253
|
-
function unionBounds(bounds) {
|
|
254
|
-
const values = bounds.filter((value) => Boolean(value));
|
|
412
|
+
function unionBounds(bounds2) {
|
|
413
|
+
const values = bounds2.filter((value) => Boolean(value));
|
|
255
414
|
if (values.length === 0) return void 0;
|
|
256
415
|
const x = Math.min(...values.map((value) => value.x));
|
|
257
416
|
const y = Math.min(...values.map((value) => value.y));
|
|
@@ -282,19 +441,16 @@ function rgbBmp(image) {
|
|
|
282
441
|
}
|
|
283
442
|
return output;
|
|
284
443
|
}
|
|
285
|
-
function safePath(value) {
|
|
286
|
-
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
287
|
-
}
|
|
288
444
|
function cssColor(value) {
|
|
289
445
|
return /^#[\da-f]{6}$/i.test(value ?? "");
|
|
290
446
|
}
|
|
291
|
-
function
|
|
447
|
+
function finiteNonnegative2(value) {
|
|
292
448
|
return Number.isFinite(value) && (value ?? -1) >= 0;
|
|
293
449
|
}
|
|
294
450
|
function unitInterval(value) {
|
|
295
|
-
return
|
|
451
|
+
return finiteNonnegative2(value) && value <= 1;
|
|
296
452
|
}
|
|
297
|
-
function
|
|
453
|
+
function number2(value) {
|
|
298
454
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
299
455
|
}
|
|
300
456
|
function escapeHtml2(value) {
|
|
@@ -346,18 +502,38 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
346
502
|
const emitPage = async (page, future) => {
|
|
347
503
|
const defaultColor = dominantTextColor(page.structured.lines);
|
|
348
504
|
let mediaIndex = 0;
|
|
505
|
+
const captions = clearMediaCaptionAssociations(
|
|
506
|
+
page.media,
|
|
507
|
+
page.structured.blocks,
|
|
508
|
+
page.width,
|
|
509
|
+
page.height,
|
|
510
|
+
page.structured.lines
|
|
511
|
+
);
|
|
512
|
+
const captionedMedia = new Set(captions.values());
|
|
349
513
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
350
514
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
351
515
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
352
516
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
353
517
|
const blockY = semanticBlockY(block);
|
|
518
|
+
let emittedAsCaption = false;
|
|
354
519
|
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
355
520
|
await flushPendingParagraph();
|
|
356
|
-
const
|
|
521
|
+
const item = page.media[mediaIndex];
|
|
522
|
+
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
523
|
+
const html2 = `<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`;
|
|
524
|
+
if (activeTable) pendingMedia.push(html2);
|
|
525
|
+
else await write(html2);
|
|
526
|
+
mediaIndex += 1;
|
|
527
|
+
emittedAsCaption = true;
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
if (item && captionedMedia.has(item)) break;
|
|
531
|
+
const html = `<div class="pdf-semantic-visual">${item?.html}</div>`;
|
|
357
532
|
if (activeTable) pendingMedia.push(html);
|
|
358
533
|
else await write(html);
|
|
359
534
|
mediaIndex += 1;
|
|
360
535
|
}
|
|
536
|
+
if (emittedAsCaption) continue;
|
|
361
537
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
362
538
|
stats.suppressedFurniture += 1;
|
|
363
539
|
continue;
|
|
@@ -365,7 +541,9 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
365
541
|
await flushPendingParagraph();
|
|
366
542
|
if (employmentOpen && block.type !== "list") await closeEmployment();
|
|
367
543
|
if (!contentStarted && !headerOpen && block.type === "heading" && block.level === 1) {
|
|
368
|
-
await write(
|
|
544
|
+
await write(
|
|
545
|
+
`<header><h1>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h1>`
|
|
546
|
+
);
|
|
369
547
|
headerOpen = true;
|
|
370
548
|
continue;
|
|
371
549
|
}
|
|
@@ -380,7 +558,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
380
558
|
}
|
|
381
559
|
if (block.type === "heading" && !headerHasParagraph && (block.level === 1 || block.text.trimStart().startsWith("#") || block.level === 4 && nextBlock?.type === "paragraph" && isContactBlock(nextBlock))) {
|
|
382
560
|
await write(
|
|
383
|
-
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
561
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
|
|
384
562
|
);
|
|
385
563
|
continue;
|
|
386
564
|
}
|
|
@@ -418,7 +596,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
418
596
|
const level = contentStarted && block.level === 1 ? 2 : block.level;
|
|
419
597
|
await closeSections(level);
|
|
420
598
|
await write(
|
|
421
|
-
`<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${level}>`
|
|
599
|
+
`<section data-level="${level}"><h${level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${level}>`
|
|
422
600
|
);
|
|
423
601
|
sectionLevels.push(level);
|
|
424
602
|
continue;
|
|
@@ -586,8 +764,16 @@ function financialSummaryRow(entry, columns) {
|
|
|
586
764
|
return `<tr><th scope="row"${colspan}>${escapeHtml3(entry.term)}</th><td>${escapeHtml3(entry.description)}</td></tr>`;
|
|
587
765
|
}
|
|
588
766
|
function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
767
|
+
if (block.type === "insetGroup") {
|
|
768
|
+
return `<div class="pdf-semantic-inset" style="margin-inline-start:${block.indentEm}em">${block.blocks.map((item) => semanticBlockHtml(item, defaultColor)).join("")}</div>`;
|
|
769
|
+
}
|
|
770
|
+
if (block.type === "table") {
|
|
771
|
+
const rows = (0, import_structure.tableToRows)(block.table);
|
|
772
|
+
const header = tableHeader(rows);
|
|
773
|
+
return `<table>${rows.map((row, index) => tableRow(row, Boolean(header && index === 0))).join("")}</table>`;
|
|
774
|
+
}
|
|
589
775
|
if (block.type === "heading")
|
|
590
|
-
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`;
|
|
776
|
+
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
|
|
591
777
|
if (block.type === "paragraph")
|
|
592
778
|
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
593
779
|
if (block.type === "preformatted") return `<pre>${escapeHtml3(block.text)}</pre>`;
|
|
@@ -612,7 +798,7 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
612
798
|
return `<section><h3>${escapeHtml3(block.role)}</h3><p>${escapeHtml3(block.organization)}</p><p>${escapeHtml3(block.date)}</p></section>`;
|
|
613
799
|
}
|
|
614
800
|
const tag = block.ordered ? "ol" : "ul";
|
|
615
|
-
return `<${tag}>${block.items.map((item) => `<li>${
|
|
801
|
+
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
616
802
|
}
|
|
617
803
|
function semanticBlockY(block) {
|
|
618
804
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
@@ -701,7 +887,7 @@ async function writePositionedPage(page, write, options) {
|
|
|
701
887
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
702
888
|
const displayHeight = quarterTurn ? page.width : page.height;
|
|
703
889
|
await write(
|
|
704
|
-
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${
|
|
890
|
+
`<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">`
|
|
705
891
|
);
|
|
706
892
|
const fontAliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
707
893
|
const type3Fonts = new Map(
|
|
@@ -713,44 +899,26 @@ async function writePositionedPage(page, write, options) {
|
|
|
713
899
|
);
|
|
714
900
|
}
|
|
715
901
|
await write(
|
|
716
|
-
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${
|
|
902
|
+
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number3(page.width)}pt;height:${number3(page.height)}pt${rotationTransform(page)}">`
|
|
717
903
|
);
|
|
718
904
|
await write(
|
|
719
|
-
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${
|
|
905
|
+
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number3(page.width)}pt" height="${number3(page.height)}pt" viewBox="0 0 ${number3(page.width)} ${number3(page.height)}">`
|
|
906
|
+
);
|
|
907
|
+
const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + vectorPathClipDefinitions(
|
|
908
|
+
(page.paths ?? []).map((path, index) => ({ path, index })),
|
|
909
|
+
page.number
|
|
720
910
|
);
|
|
721
|
-
const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
|
|
722
911
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
723
912
|
if (reflectedOverlay) {
|
|
724
913
|
for (const [index, image] of (page.images ?? []).entries()) {
|
|
725
914
|
await write(visualImage(image, page.height, page.number, index));
|
|
726
915
|
}
|
|
727
916
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
await write(
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
if (page.paths?.length) {
|
|
736
|
-
await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
|
|
737
|
-
for (const [pathIndex, path] of page.paths.entries()) {
|
|
738
|
-
if (!isSvgPath(path.d)) continue;
|
|
739
|
-
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
740
|
-
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
741
|
-
const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
|
|
742
|
-
const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
743
|
-
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
|
|
744
|
-
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
|
|
745
|
-
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
|
|
746
|
-
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
|
|
747
|
-
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
748
|
-
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
749
|
-
let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
|
|
750
|
-
for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
751
|
-
output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
|
|
752
|
-
}
|
|
753
|
-
await write(output);
|
|
917
|
+
if (page.fills?.length || page.paths?.length) {
|
|
918
|
+
await write(`<g transform="translate(0 ${number3(page.height)}) scale(1 -1)">`);
|
|
919
|
+
for (const fill of page.fills ?? []) await write(vectorFillSvg(fill));
|
|
920
|
+
for (const [pathIndex, path] of (page.paths ?? []).entries()) {
|
|
921
|
+
await write(vectorPathSvg(path, page.number, pathIndex));
|
|
754
922
|
}
|
|
755
923
|
await write("</g>");
|
|
756
924
|
}
|
|
@@ -780,8 +948,8 @@ function usesReflectedVisualOverlay(page, spans) {
|
|
|
780
948
|
}
|
|
781
949
|
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
782
950
|
const [a, b, c, d, e, f] = image.transform;
|
|
783
|
-
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(
|
|
784
|
-
const opacity =
|
|
951
|
+
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number3).join(" ");
|
|
952
|
+
const opacity = isUnitInterval2(image.opacity) ? ` opacity="${number3(image.opacity)}"` : "";
|
|
785
953
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
786
954
|
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
787
955
|
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
@@ -795,25 +963,13 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
|
795
963
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
796
964
|
if (!isSvgPath(clip.d)) return "";
|
|
797
965
|
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
798
|
-
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${
|
|
966
|
+
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number3(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
|
|
799
967
|
})
|
|
800
968
|
).join("");
|
|
801
969
|
}
|
|
802
970
|
function imageClipId(pageNumber, imageIndex, clipIndex) {
|
|
803
971
|
return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
|
|
804
972
|
}
|
|
805
|
-
function pathClipDefinitions(paths, pageNumber) {
|
|
806
|
-
return paths.flatMap(
|
|
807
|
-
(path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
|
|
808
|
-
if (!isSvgPath(clip.d)) return "";
|
|
809
|
-
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
810
|
-
return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
|
|
811
|
-
})
|
|
812
|
-
).join("");
|
|
813
|
-
}
|
|
814
|
-
function pathClipId(pageNumber, pathIndex, clipIndex) {
|
|
815
|
-
return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
|
|
816
|
-
}
|
|
817
973
|
function rgbBmp2(image) {
|
|
818
974
|
const stride = Math.ceil(image.width * 3 / 4) * 4;
|
|
819
975
|
const output = new Uint8Array(54 + stride * image.height);
|
|
@@ -842,11 +998,11 @@ function rgbBmp2(image) {
|
|
|
842
998
|
function rotationTransform(page) {
|
|
843
999
|
switch (page.rotate) {
|
|
844
1000
|
case 90:
|
|
845
|
-
return `;transform:translate(${
|
|
1001
|
+
return `;transform:translate(${number3(page.height)}pt,0) rotate(90deg)`;
|
|
846
1002
|
case 180:
|
|
847
|
-
return `;transform:translate(${
|
|
1003
|
+
return `;transform:translate(${number3(page.width)}pt,${number3(page.height)}pt) rotate(180deg)`;
|
|
848
1004
|
case 270:
|
|
849
|
-
return `;transform:translate(0,${
|
|
1005
|
+
return `;transform:translate(0,${number3(page.width)}pt) rotate(270deg)`;
|
|
850
1006
|
default:
|
|
851
1007
|
return "";
|
|
852
1008
|
}
|
|
@@ -854,13 +1010,13 @@ function rotationTransform(page) {
|
|
|
854
1010
|
function positionedSpan(span, fontAliases) {
|
|
855
1011
|
const direction = directionAttribute([span]);
|
|
856
1012
|
const style = [
|
|
857
|
-
`left:${
|
|
858
|
-
`bottom:${
|
|
859
|
-
`width:${
|
|
860
|
-
`height:${
|
|
861
|
-
`font-size:${
|
|
862
|
-
...
|
|
863
|
-
...
|
|
1013
|
+
`left:${number3(span.bounds.x)}pt`,
|
|
1014
|
+
`bottom:${number3(span.bounds.y)}pt`,
|
|
1015
|
+
`width:${number3(span.bounds.width)}pt`,
|
|
1016
|
+
`height:${number3(span.bounds.height)}pt`,
|
|
1017
|
+
`font-size:${number3(span.fontSize)}pt`,
|
|
1018
|
+
...isCssHexColor2(span.color) ? [`color:${span.color}`] : [],
|
|
1019
|
+
...isUnitInterval2(span.fillOpacity) ? [`opacity:${number3(span.fillOpacity)}`] : [],
|
|
864
1020
|
...visualFontStyles(
|
|
865
1021
|
span.fontFamily,
|
|
866
1022
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -873,19 +1029,39 @@ async function writeFlowPage(page, write) {
|
|
|
873
1029
|
const structured = (0, import_structure2.structurePage)(withoutSemanticMediaSpans(page, media));
|
|
874
1030
|
const defaultColor = dominantTextColor(structured.lines);
|
|
875
1031
|
let mediaIndex = 0;
|
|
1032
|
+
const captions = clearMediaCaptionAssociations(
|
|
1033
|
+
media,
|
|
1034
|
+
structured.blocks,
|
|
1035
|
+
page.width,
|
|
1036
|
+
page.height,
|
|
1037
|
+
structured.lines
|
|
1038
|
+
);
|
|
1039
|
+
const captionedMedia = new Set(captions.values());
|
|
876
1040
|
await write(
|
|
877
1041
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
878
1042
|
);
|
|
879
1043
|
for (const block of structured.blocks) {
|
|
880
1044
|
const blockY = semanticBlockY2(block);
|
|
1045
|
+
let emittedAsCaption = false;
|
|
881
1046
|
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
882
|
-
|
|
1047
|
+
const item = media[mediaIndex];
|
|
1048
|
+
if (item && captions.get(block) === item && block.type === "paragraph") {
|
|
1049
|
+
await write(
|
|
1050
|
+
`<figure class="pdf-semantic-figure">${item.html}<figcaption>${semanticTextHtml(block.text, block.lines, defaultColor)}</figcaption></figure>`
|
|
1051
|
+
);
|
|
1052
|
+
mediaIndex += 1;
|
|
1053
|
+
emittedAsCaption = true;
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
if (item && captionedMedia.has(item)) break;
|
|
1057
|
+
await write(`<div class="pdf-semantic-visual">${item?.html}</div>`);
|
|
883
1058
|
mediaIndex += 1;
|
|
884
1059
|
}
|
|
1060
|
+
if (emittedAsCaption) continue;
|
|
885
1061
|
if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
|
|
886
1062
|
else if (block.type === "heading") {
|
|
887
1063
|
await write(
|
|
888
|
-
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor)}</h${block.level}>`
|
|
1064
|
+
`<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`
|
|
889
1065
|
);
|
|
890
1066
|
} else if (block.type === "paragraph") {
|
|
891
1067
|
await write(
|
|
@@ -921,10 +1097,16 @@ async function writeFlowPage(page, write) {
|
|
|
921
1097
|
await write(
|
|
922
1098
|
`<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`
|
|
923
1099
|
);
|
|
1100
|
+
} else if (block.type === "insetGroup") {
|
|
1101
|
+
await write(
|
|
1102
|
+
`<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`
|
|
1103
|
+
);
|
|
924
1104
|
} else {
|
|
925
1105
|
const tag = block.ordered ? "ol" : "ul";
|
|
926
1106
|
await write(`<${tag}>`);
|
|
927
|
-
for (const item of block.items)
|
|
1107
|
+
for (const item of block.items) {
|
|
1108
|
+
await write(`<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`);
|
|
1109
|
+
}
|
|
928
1110
|
await write(`</${tag}>`);
|
|
929
1111
|
}
|
|
930
1112
|
}
|
|
@@ -934,6 +1116,33 @@ async function writeFlowPage(page, write) {
|
|
|
934
1116
|
}
|
|
935
1117
|
await write("</section>");
|
|
936
1118
|
}
|
|
1119
|
+
function nestedSemanticBlockHtml(block, defaultColor) {
|
|
1120
|
+
if (block.type === "insetGroup") {
|
|
1121
|
+
return `<div class="pdf-semantic-inset" style="margin-inline-start:${number3(block.indentEm)}em">${block.blocks.map((item) => nestedSemanticBlockHtml(item, defaultColor)).join("")}</div>`;
|
|
1122
|
+
}
|
|
1123
|
+
if (block.type === "table") return (0, import_structure2.tableToHtml)(block.table);
|
|
1124
|
+
if (block.type === "heading") {
|
|
1125
|
+
return `<h${block.level}>${semanticTextHtml(block.text, block.lines, defaultColor, false)}</h${block.level}>`;
|
|
1126
|
+
}
|
|
1127
|
+
if (block.type === "paragraph") {
|
|
1128
|
+
return `<p>${semanticTextHtml(block.text, block.lines, defaultColor)}</p>`;
|
|
1129
|
+
}
|
|
1130
|
+
if (block.type === "preformatted") return `<pre>${escapeHtml4(block.text)}</pre>`;
|
|
1131
|
+
if (block.type === "definitionList") {
|
|
1132
|
+
return `<dl>${block.entries.map((entry) => `<div><dt>${escapeHtml4(entry.term)}</dt><dd>${escapeHtml4(entry.description)}</dd></div>`).join("")}</dl>`;
|
|
1133
|
+
}
|
|
1134
|
+
if (block.type === "cardList") {
|
|
1135
|
+
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>`;
|
|
1136
|
+
}
|
|
1137
|
+
if (block.type === "sectionGroup") {
|
|
1138
|
+
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>`;
|
|
1139
|
+
}
|
|
1140
|
+
if (block.type === "employment") {
|
|
1141
|
+
return `<section><h3>${escapeHtml4(block.role)}</h3><p>${escapeHtml4(block.organization)}</p><p>${escapeHtml4(block.date)}</p></section>`;
|
|
1142
|
+
}
|
|
1143
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
1144
|
+
return `<${tag}>${block.items.map((item) => `<li>${semanticTextHtml(item.text, item.lines, defaultColor)}</li>`).join("")}</${tag}>`;
|
|
1145
|
+
}
|
|
937
1146
|
function semanticBlockY2(block) {
|
|
938
1147
|
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
939
1148
|
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
@@ -946,15 +1155,15 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
946
1155
|
span.fontFamily,
|
|
947
1156
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
948
1157
|
).join(";");
|
|
949
|
-
const stroke =
|
|
950
|
-
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${
|
|
1158
|
+
const stroke = isCssHexColor2(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
|
|
1159
|
+
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number3(span.strokeWidth ?? 0)}` : "";
|
|
951
1160
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
952
|
-
const fillOpacity =
|
|
953
|
-
const strokeOpacity =
|
|
1161
|
+
const fillOpacity = isUnitInterval2(span.fillOpacity) ? `fill-opacity:${number3(span.fillOpacity)}` : "";
|
|
1162
|
+
const strokeOpacity = isUnitInterval2(span.strokeOpacity) ? `stroke-opacity:${number3(span.strokeOpacity)}` : "";
|
|
954
1163
|
const style = [
|
|
955
1164
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
956
1165
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
957
|
-
strokeOnly ? "fill:none" :
|
|
1166
|
+
strokeOnly ? "fill:none" : isCssHexColor2(span.color) ? `fill:${span.color}` : "",
|
|
958
1167
|
stroke,
|
|
959
1168
|
strokeWidth,
|
|
960
1169
|
fillOpacity,
|
|
@@ -962,7 +1171,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
962
1171
|
font
|
|
963
1172
|
].filter(Boolean).join(";");
|
|
964
1173
|
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
965
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${
|
|
1174
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number3(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
966
1175
|
const transform = counterRotateReflectedText && span.transform ? [
|
|
967
1176
|
span.transform[0],
|
|
968
1177
|
span.transform[1],
|
|
@@ -975,8 +1184,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
975
1184
|
const basisY = transform?.[1] ?? 0;
|
|
976
1185
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
977
1186
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
978
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(
|
|
979
|
-
return `<text${direction}${position} font-size="${
|
|
1187
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number3).join(" ")} ${number3(anchorX)} ${number3(anchorY)})"` : ` x="${number3(anchorX)}" y="${number3(anchorY)}"`;
|
|
1188
|
+
return `<text${direction}${position} font-size="${number3(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml4(span.text)}</text>`;
|
|
980
1189
|
}
|
|
981
1190
|
function isAdobeCjkFont(fontFamily) {
|
|
982
1191
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -988,16 +1197,16 @@ function visualType3Text(span, font, pageHeight) {
|
|
|
988
1197
|
const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
|
|
989
1198
|
if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
|
|
990
1199
|
const transform = span.transform ?? [1, 0, 0, 1];
|
|
991
|
-
const outer = `matrix(${transform.map(
|
|
1200
|
+
const outer = `matrix(${transform.map(number3).join(" ")} ${number3(span.bounds.x)} ${number3(pageHeight - span.bounds.y)})`;
|
|
992
1201
|
const xScale = span.bounds.width / totalAdvance;
|
|
993
1202
|
let offset = 0;
|
|
994
1203
|
let content = "";
|
|
995
1204
|
for (const glyph of sequence) {
|
|
996
1205
|
if (!glyph) continue;
|
|
997
|
-
content += `<g transform="translate(${
|
|
1206
|
+
content += `<g transform="translate(${number3(offset)} 0)">${type3Glyph(glyph, span.color)}</g>`;
|
|
998
1207
|
offset += glyph.advance;
|
|
999
1208
|
}
|
|
1000
|
-
return `<g transform="${outer}"><g transform="scale(${
|
|
1209
|
+
return `<g transform="${outer}"><g transform="scale(${number3(xScale)} ${number3(-span.fontSize)})">${content}</g></g>`;
|
|
1001
1210
|
}
|
|
1002
1211
|
function isHebrewPaintOrder(span) {
|
|
1003
1212
|
return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
|
|
@@ -1008,30 +1217,27 @@ function usesSpacingAdjustment(span) {
|
|
|
1008
1217
|
function type3Glyph(glyph, textColor) {
|
|
1009
1218
|
let output = "";
|
|
1010
1219
|
for (const fill of glyph.fills ?? []) {
|
|
1011
|
-
const color = glyph.usesTextColor &&
|
|
1012
|
-
if (!
|
|
1013
|
-
const points = fill.points.map(([x, y]) => `${
|
|
1014
|
-
const opacity =
|
|
1220
|
+
const color = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : fill.color;
|
|
1221
|
+
if (!isCssHexColor2(color)) continue;
|
|
1222
|
+
const points = fill.points.map(([x, y]) => `${number3(x)},${number3(y)}`).join(" ");
|
|
1223
|
+
const opacity = isUnitInterval2(fill.opacity) ? ` fill-opacity="${number3(fill.opacity)}"` : "";
|
|
1015
1224
|
output += `<polygon points="${points}" fill="${color}"${opacity}/>`;
|
|
1016
1225
|
}
|
|
1017
1226
|
for (const path of glyph.paths ?? []) {
|
|
1018
1227
|
if (!isSvgPath(path.d)) continue;
|
|
1019
|
-
const fill = glyph.usesTextColor &&
|
|
1020
|
-
const stroke = glyph.usesTextColor &&
|
|
1021
|
-
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${
|
|
1228
|
+
const fill = glyph.usesTextColor && isCssHexColor2(textColor) ? textColor : isCssHexColor2(path.fill) ? path.fill : "none";
|
|
1229
|
+
const stroke = glyph.usesTextColor && isCssHexColor2(textColor) && path.stroke ? textColor : isCssHexColor2(path.stroke) ? path.stroke : "none";
|
|
1230
|
+
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number3(path.strokeWidth)}"` : "";
|
|
1022
1231
|
output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
|
|
1023
1232
|
}
|
|
1024
1233
|
return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
|
|
1025
1234
|
}
|
|
1026
|
-
function
|
|
1235
|
+
function isCssHexColor2(value) {
|
|
1027
1236
|
return /^#[\da-f]{6}$/i.test(value ?? "");
|
|
1028
1237
|
}
|
|
1029
|
-
function
|
|
1238
|
+
function isUnitInterval2(value) {
|
|
1030
1239
|
return Number.isFinite(value) && (value ?? -1) >= 0 && (value ?? 2) <= 1;
|
|
1031
1240
|
}
|
|
1032
|
-
function isSvgPath(value) {
|
|
1033
|
-
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
1034
|
-
}
|
|
1035
1241
|
function isMonospace(fontFamily) {
|
|
1036
1242
|
return /courier|mono/i.test(fontFamily ?? "");
|
|
1037
1243
|
}
|
|
@@ -1049,7 +1255,7 @@ function directionAttribute(spans) {
|
|
|
1049
1255
|
if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
|
|
1050
1256
|
return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
|
|
1051
1257
|
}
|
|
1052
|
-
function
|
|
1258
|
+
function number3(value) {
|
|
1053
1259
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
1054
1260
|
}
|
|
1055
1261
|
function escapeAttribute(value) {
|