@boxpdf/html-writer 0.1.12 → 0.1.14
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 +225 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +225 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -81,6 +81,152 @@ function escapeHtml(value) {
|
|
|
81
81
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// src/semantic-media.ts
|
|
85
|
+
function semanticMedia(page) {
|
|
86
|
+
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
87
|
+
const vector = vectorMedia(page);
|
|
88
|
+
if (vector) output.push(vector);
|
|
89
|
+
return output.sort((left, right) => right.bounds.y - left.bounds.y);
|
|
90
|
+
}
|
|
91
|
+
function rasterMedia(image) {
|
|
92
|
+
const bounds = transformedUnitBounds(image.transform);
|
|
93
|
+
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
94
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
95
|
+
const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
|
|
96
|
+
return {
|
|
97
|
+
bounds,
|
|
98
|
+
html: `<img class="pdf-semantic-media" src="data:${mime};base64,${base64(data)}" width="${number(bounds.width)}" height="${number(bounds.height)}" alt="" style="max-width:100%;height:auto${opacity}">`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function vectorMedia(page) {
|
|
102
|
+
const paths = (page.paths ?? []).filter((path) => safePath(path.d));
|
|
103
|
+
const fills = page.fills ?? [];
|
|
104
|
+
const bounds = unionBounds([
|
|
105
|
+
...paths.map((path) => pathBounds(path.d)),
|
|
106
|
+
...fills.map(fillBounds)
|
|
107
|
+
]);
|
|
108
|
+
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
|
|
109
|
+
const content = fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("");
|
|
110
|
+
return {
|
|
111
|
+
bounds,
|
|
112
|
+
html: `<svg class="pdf-semantic-media" xmlns="http://www.w3.org/2000/svg" viewBox="${number(bounds.x)} ${number(page.height - bounds.y - bounds.height)} ${number(bounds.width)} ${number(bounds.height)}" style="display:block;max-width:100%;height:auto" aria-hidden="true"><g transform="translate(0 ${number(page.height)}) scale(1 -1)">${content}</g></svg>`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function vectorFill(fill) {
|
|
116
|
+
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
117
|
+
const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
118
|
+
return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
|
|
119
|
+
}
|
|
120
|
+
function vectorPath(path) {
|
|
121
|
+
const fill = cssColor(path.fill) ? path.fill : "none";
|
|
122
|
+
const stroke = cssColor(path.stroke) ? path.stroke : "none";
|
|
123
|
+
const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
|
|
124
|
+
const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
|
|
125
|
+
const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
|
|
126
|
+
const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
|
|
127
|
+
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
128
|
+
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
129
|
+
const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
130
|
+
return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
|
|
131
|
+
}
|
|
132
|
+
function transformedUnitBounds([a, b, c, d, e, f]) {
|
|
133
|
+
const points = [
|
|
134
|
+
[e, f],
|
|
135
|
+
[a + e, b + f],
|
|
136
|
+
[c + e, d + f],
|
|
137
|
+
[a + c + e, b + d + f]
|
|
138
|
+
];
|
|
139
|
+
const xs = points.map(([x]) => x ?? 0);
|
|
140
|
+
const ys = points.map(([, y]) => y ?? 0);
|
|
141
|
+
const minX = Math.min(...xs);
|
|
142
|
+
const minY = Math.min(...ys);
|
|
143
|
+
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
144
|
+
}
|
|
145
|
+
function pathBounds(path) {
|
|
146
|
+
const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
|
|
147
|
+
(match) => Number(match[0])
|
|
148
|
+
);
|
|
149
|
+
if (values.length < 2) return void 0;
|
|
150
|
+
const xs = [];
|
|
151
|
+
const ys = [];
|
|
152
|
+
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
153
|
+
xs.push(values[index] ?? 0);
|
|
154
|
+
ys.push(values[index + 1] ?? 0);
|
|
155
|
+
}
|
|
156
|
+
const minX = Math.min(...xs);
|
|
157
|
+
const minY = Math.min(...ys);
|
|
158
|
+
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
159
|
+
}
|
|
160
|
+
function fillBounds(fill) {
|
|
161
|
+
if (fill.points.length === 0) return void 0;
|
|
162
|
+
const xs = fill.points.map(([x]) => x);
|
|
163
|
+
const ys = fill.points.map(([, y]) => y);
|
|
164
|
+
const minX = Math.min(...xs);
|
|
165
|
+
const minY = Math.min(...ys);
|
|
166
|
+
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
167
|
+
}
|
|
168
|
+
function unionBounds(bounds) {
|
|
169
|
+
const values = bounds.filter((value) => Boolean(value));
|
|
170
|
+
if (values.length === 0) return void 0;
|
|
171
|
+
const x = Math.min(...values.map((value) => value.x));
|
|
172
|
+
const y = Math.min(...values.map((value) => value.y));
|
|
173
|
+
const right = Math.max(...values.map((value) => value.x + value.width));
|
|
174
|
+
const top = Math.max(...values.map((value) => value.y + value.height));
|
|
175
|
+
return { x, y, width: right - x, height: top - y };
|
|
176
|
+
}
|
|
177
|
+
function rgbBmp(image) {
|
|
178
|
+
const stride = Math.ceil(image.width * 3 / 4) * 4;
|
|
179
|
+
const output = new Uint8Array(54 + stride * image.height);
|
|
180
|
+
const view = new DataView(output.buffer);
|
|
181
|
+
output.set([66, 77]);
|
|
182
|
+
view.setUint32(2, output.length, true);
|
|
183
|
+
view.setUint32(10, 54, true);
|
|
184
|
+
view.setUint32(14, 40, true);
|
|
185
|
+
view.setInt32(18, image.width, true);
|
|
186
|
+
view.setInt32(22, -image.height, true);
|
|
187
|
+
view.setUint16(26, 1, true);
|
|
188
|
+
view.setUint16(28, 24, true);
|
|
189
|
+
for (let row = 0; row < image.height; row += 1) {
|
|
190
|
+
for (let column = 0; column < image.width; column += 1) {
|
|
191
|
+
const source = (row * image.width + column) * 3;
|
|
192
|
+
const target = 54 + row * stride + column * 3;
|
|
193
|
+
output[target] = image.data[source + 2] ?? 0;
|
|
194
|
+
output[target + 1] = image.data[source + 1] ?? 0;
|
|
195
|
+
output[target + 2] = image.data[source] ?? 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return output;
|
|
199
|
+
}
|
|
200
|
+
function base64(bytes) {
|
|
201
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
202
|
+
let output = "";
|
|
203
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
204
|
+
const first = bytes[index] ?? 0;
|
|
205
|
+
const second = bytes[index + 1] ?? 0;
|
|
206
|
+
const third = bytes[index + 2] ?? 0;
|
|
207
|
+
output += alphabet[first >> 2];
|
|
208
|
+
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
209
|
+
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
210
|
+
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
211
|
+
}
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
function safePath(value) {
|
|
215
|
+
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
216
|
+
}
|
|
217
|
+
function cssColor(value) {
|
|
218
|
+
return /^#[\da-f]{6}$/i.test(value ?? "");
|
|
219
|
+
}
|
|
220
|
+
function finiteNonnegative(value) {
|
|
221
|
+
return Number.isFinite(value) && (value ?? -1) >= 0;
|
|
222
|
+
}
|
|
223
|
+
function unitInterval(value) {
|
|
224
|
+
return finiteNonnegative(value) && value <= 1;
|
|
225
|
+
}
|
|
226
|
+
function number(value) {
|
|
227
|
+
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
228
|
+
}
|
|
229
|
+
|
|
84
230
|
// src/semantic-document.ts
|
|
85
231
|
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
86
232
|
const stats = {
|
|
@@ -94,6 +240,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
94
240
|
const seenFurniture = /* @__PURE__ */ new Set();
|
|
95
241
|
const sectionLevels = [];
|
|
96
242
|
let activeTable;
|
|
243
|
+
const pendingMedia = [];
|
|
97
244
|
let headerOpen = false;
|
|
98
245
|
let headerHasParagraph = false;
|
|
99
246
|
let contentStarted = false;
|
|
@@ -104,6 +251,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
104
251
|
if (!activeTable) return;
|
|
105
252
|
await write("</table>");
|
|
106
253
|
activeTable = void 0;
|
|
254
|
+
while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
|
|
107
255
|
};
|
|
108
256
|
const closeSections = async (minimumLevel = 0) => {
|
|
109
257
|
while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
|
|
@@ -123,10 +271,19 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
123
271
|
};
|
|
124
272
|
const emitPage = async (page, future) => {
|
|
125
273
|
const defaultColor = dominantTextColor(page.structured.lines);
|
|
274
|
+
let mediaIndex = 0;
|
|
126
275
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
127
276
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
128
277
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
129
278
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
279
|
+
const blockY = semanticBlockY(block);
|
|
280
|
+
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
281
|
+
await flushPendingParagraph();
|
|
282
|
+
const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
|
|
283
|
+
if (activeTable) pendingMedia.push(html);
|
|
284
|
+
else await write(html);
|
|
285
|
+
mediaIndex += 1;
|
|
286
|
+
}
|
|
130
287
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
131
288
|
stats.suppressedFurniture += 1;
|
|
132
289
|
continue;
|
|
@@ -217,11 +374,18 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
217
374
|
}
|
|
218
375
|
await write(semanticBlockHtml(block, defaultColor));
|
|
219
376
|
}
|
|
377
|
+
while (mediaIndex < page.media.length) {
|
|
378
|
+
await flushPendingParagraph();
|
|
379
|
+
const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
|
|
380
|
+
if (activeTable) pendingMedia.push(html);
|
|
381
|
+
else await write(html);
|
|
382
|
+
mediaIndex += 1;
|
|
383
|
+
}
|
|
220
384
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
221
385
|
};
|
|
222
386
|
for await (const page of pages) {
|
|
223
387
|
const structured = (0, import_structure.structurePage)(page);
|
|
224
|
-
buffer.push({ width: page.width, height: page.height, structured });
|
|
388
|
+
buffer.push({ width: page.width, height: page.height, structured, media: semanticMedia(page) });
|
|
225
389
|
stats.pagesProcessed += 1;
|
|
226
390
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
227
391
|
stats.peakBufferedLines = Math.max(
|
|
@@ -375,6 +539,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
375
539
|
const tag = block.ordered ? "ol" : "ul";
|
|
376
540
|
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
|
|
377
541
|
}
|
|
542
|
+
function semanticBlockY(block) {
|
|
543
|
+
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
544
|
+
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
545
|
+
}
|
|
378
546
|
function cardTableRow(item) {
|
|
379
547
|
const trailing = item.details.at(-1) ?? "";
|
|
380
548
|
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
@@ -458,7 +626,7 @@ async function writePositionedPage(page, write, options) {
|
|
|
458
626
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
459
627
|
const displayHeight = quarterTurn ? page.width : page.height;
|
|
460
628
|
await write(
|
|
461
|
-
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${
|
|
629
|
+
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number2(displayWidth)}pt;height:${number2(displayHeight)}pt">`
|
|
462
630
|
);
|
|
463
631
|
const fontAliases = new Map(
|
|
464
632
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
|
|
@@ -470,10 +638,10 @@ async function writePositionedPage(page, write, options) {
|
|
|
470
638
|
await write(`<style>${page.fonts.map((font) => fontFace(font, fontAliases)).join("")}</style>`);
|
|
471
639
|
}
|
|
472
640
|
await write(
|
|
473
|
-
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${
|
|
641
|
+
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
|
|
474
642
|
);
|
|
475
643
|
await write(
|
|
476
|
-
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${
|
|
644
|
+
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number2(page.width)}pt" height="${number2(page.height)}pt" viewBox="0 0 ${number2(page.width)} ${number2(page.height)}">`
|
|
477
645
|
);
|
|
478
646
|
const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
|
|
479
647
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
@@ -483,24 +651,24 @@ async function writePositionedPage(page, write, options) {
|
|
|
483
651
|
}
|
|
484
652
|
}
|
|
485
653
|
for (const fill of page.fills ?? []) {
|
|
486
|
-
const points = fill.points.map(([x, y]) => `${
|
|
654
|
+
const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
|
|
487
655
|
if (isCssHexColor(fill.color)) {
|
|
488
|
-
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${
|
|
656
|
+
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
|
|
489
657
|
await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
|
|
490
658
|
}
|
|
491
659
|
}
|
|
492
660
|
if (page.paths?.length) {
|
|
493
|
-
await write(`<g transform="translate(0 ${
|
|
661
|
+
await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
|
|
494
662
|
for (const [pathIndex, path] of page.paths.entries()) {
|
|
495
663
|
if (!isSvgPath(path.d)) continue;
|
|
496
664
|
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
497
665
|
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
498
|
-
const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${
|
|
666
|
+
const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
|
|
499
667
|
const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
500
|
-
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${
|
|
501
|
-
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${
|
|
502
|
-
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(
|
|
503
|
-
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${
|
|
668
|
+
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
|
|
669
|
+
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
|
|
670
|
+
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
|
|
671
|
+
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
|
|
504
672
|
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
505
673
|
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
506
674
|
let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
|
|
@@ -537,11 +705,11 @@ function usesReflectedVisualOverlay(page, spans) {
|
|
|
537
705
|
}
|
|
538
706
|
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
539
707
|
const [a, b, c, d, e, f] = image.transform;
|
|
540
|
-
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(
|
|
541
|
-
const opacity = isUnitInterval(image.opacity) ? ` opacity="${
|
|
708
|
+
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
|
|
709
|
+
const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
|
|
542
710
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
543
|
-
const data = image.format === "jpeg" ? image.data :
|
|
544
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${
|
|
711
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
712
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base642(data)}"${opacity}/>`;
|
|
545
713
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
546
714
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
547
715
|
}
|
|
@@ -552,7 +720,7 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
|
552
720
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
553
721
|
if (!isSvgPath(clip.d)) return "";
|
|
554
722
|
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
555
|
-
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${
|
|
723
|
+
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number2(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
|
|
556
724
|
})
|
|
557
725
|
).join("");
|
|
558
726
|
}
|
|
@@ -571,7 +739,7 @@ function pathClipDefinitions(paths, pageNumber) {
|
|
|
571
739
|
function pathClipId(pageNumber, pathIndex, clipIndex) {
|
|
572
740
|
return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
|
|
573
741
|
}
|
|
574
|
-
function
|
|
742
|
+
function rgbBmp2(image) {
|
|
575
743
|
const stride = Math.ceil(image.width * 3 / 4) * 4;
|
|
576
744
|
const output = new Uint8Array(54 + stride * image.height);
|
|
577
745
|
const view = new DataView(output.buffer);
|
|
@@ -599,11 +767,11 @@ function rgbBmp(image) {
|
|
|
599
767
|
function rotationTransform(page) {
|
|
600
768
|
switch (page.rotate) {
|
|
601
769
|
case 90:
|
|
602
|
-
return `;transform:translate(${
|
|
770
|
+
return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
|
|
603
771
|
case 180:
|
|
604
|
-
return `;transform:translate(${
|
|
772
|
+
return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
|
|
605
773
|
case 270:
|
|
606
|
-
return `;transform:translate(0,${
|
|
774
|
+
return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
|
|
607
775
|
default:
|
|
608
776
|
return "";
|
|
609
777
|
}
|
|
@@ -611,13 +779,13 @@ function rotationTransform(page) {
|
|
|
611
779
|
function positionedSpan(span, fontAliases) {
|
|
612
780
|
const direction = directionAttribute([span]);
|
|
613
781
|
const style = [
|
|
614
|
-
`left:${
|
|
615
|
-
`bottom:${
|
|
616
|
-
`width:${
|
|
617
|
-
`height:${
|
|
618
|
-
`font-size:${
|
|
782
|
+
`left:${number2(span.bounds.x)}pt`,
|
|
783
|
+
`bottom:${number2(span.bounds.y)}pt`,
|
|
784
|
+
`width:${number2(span.bounds.width)}pt`,
|
|
785
|
+
`height:${number2(span.bounds.height)}pt`,
|
|
786
|
+
`font-size:${number2(span.fontSize)}pt`,
|
|
619
787
|
...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
|
|
620
|
-
...isUnitInterval(span.fillOpacity) ? [`opacity:${
|
|
788
|
+
...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
|
|
621
789
|
...fontStyles(
|
|
622
790
|
span.fontFamily,
|
|
623
791
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -628,10 +796,17 @@ function positionedSpan(span, fontAliases) {
|
|
|
628
796
|
async function writeFlowPage(page, write) {
|
|
629
797
|
const structured = (0, import_structure2.structurePage)(page);
|
|
630
798
|
const defaultColor = dominantTextColor(structured.lines);
|
|
799
|
+
const media = semanticMedia(page);
|
|
800
|
+
let mediaIndex = 0;
|
|
631
801
|
await write(
|
|
632
802
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
633
803
|
);
|
|
634
804
|
for (const block of structured.blocks) {
|
|
805
|
+
const blockY = semanticBlockY2(block);
|
|
806
|
+
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
807
|
+
await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
|
|
808
|
+
mediaIndex += 1;
|
|
809
|
+
}
|
|
635
810
|
if (block.type === "table") await write((0, import_structure2.tableToHtml)(block.table));
|
|
636
811
|
else if (block.type === "heading") {
|
|
637
812
|
await write(
|
|
@@ -678,8 +853,16 @@ async function writeFlowPage(page, write) {
|
|
|
678
853
|
await write(`</${tag}>`);
|
|
679
854
|
}
|
|
680
855
|
}
|
|
856
|
+
while (mediaIndex < media.length) {
|
|
857
|
+
await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
|
|
858
|
+
mediaIndex += 1;
|
|
859
|
+
}
|
|
681
860
|
await write("</section>");
|
|
682
861
|
}
|
|
862
|
+
function semanticBlockY2(block) {
|
|
863
|
+
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
864
|
+
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
865
|
+
}
|
|
683
866
|
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
|
|
684
867
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
685
868
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
@@ -689,10 +872,10 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
689
872
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
690
873
|
).join(";");
|
|
691
874
|
const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
|
|
692
|
-
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${
|
|
875
|
+
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number2(span.strokeWidth ?? 0)}` : "";
|
|
693
876
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
694
|
-
const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${
|
|
695
|
-
const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${
|
|
877
|
+
const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
|
|
878
|
+
const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number2(span.strokeOpacity)}` : "";
|
|
696
879
|
const style = [
|
|
697
880
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
698
881
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
@@ -704,7 +887,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
704
887
|
font
|
|
705
888
|
].filter(Boolean).join(";");
|
|
706
889
|
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
707
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${
|
|
890
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number2(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
708
891
|
const transform = counterRotateReflectedText && span.transform ? [
|
|
709
892
|
span.transform[0],
|
|
710
893
|
span.transform[1],
|
|
@@ -717,8 +900,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
717
900
|
const basisY = transform?.[1] ?? 0;
|
|
718
901
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
719
902
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
720
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(
|
|
721
|
-
return `<text${direction}${position} font-size="${
|
|
903
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
|
|
904
|
+
return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
|
|
722
905
|
}
|
|
723
906
|
function isAdobeCjkFont(fontFamily) {
|
|
724
907
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -730,16 +913,16 @@ function visualType3Text(span, font, pageHeight) {
|
|
|
730
913
|
const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
|
|
731
914
|
if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
|
|
732
915
|
const transform = span.transform ?? [1, 0, 0, 1];
|
|
733
|
-
const outer = `matrix(${transform.map(
|
|
916
|
+
const outer = `matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(pageHeight - span.bounds.y)})`;
|
|
734
917
|
const xScale = span.bounds.width / totalAdvance;
|
|
735
918
|
let offset = 0;
|
|
736
919
|
let content = "";
|
|
737
920
|
for (const glyph of sequence) {
|
|
738
921
|
if (!glyph) continue;
|
|
739
|
-
content += `<g transform="translate(${
|
|
922
|
+
content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph)}</g>`;
|
|
740
923
|
offset += glyph.advance;
|
|
741
924
|
}
|
|
742
|
-
return `<g transform="${outer}"><g transform="scale(${
|
|
925
|
+
return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
|
|
743
926
|
}
|
|
744
927
|
function isHebrewPaintOrder(span) {
|
|
745
928
|
return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
|
|
@@ -751,15 +934,15 @@ function type3Glyph(glyph) {
|
|
|
751
934
|
let output = "";
|
|
752
935
|
for (const fill of glyph.fills ?? []) {
|
|
753
936
|
if (!isCssHexColor(fill.color)) continue;
|
|
754
|
-
const points = fill.points.map(([x, y]) => `${
|
|
755
|
-
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${
|
|
937
|
+
const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
|
|
938
|
+
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
|
|
756
939
|
output += `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
|
|
757
940
|
}
|
|
758
941
|
for (const path of glyph.paths ?? []) {
|
|
759
942
|
if (!isSvgPath(path.d)) continue;
|
|
760
943
|
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
761
944
|
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
762
|
-
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${
|
|
945
|
+
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
|
|
763
946
|
output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
|
|
764
947
|
}
|
|
765
948
|
return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
|
|
@@ -809,9 +992,9 @@ function fontFace(font, aliases) {
|
|
|
809
992
|
const styles2 = fontStyles(font.family, alias).filter(
|
|
810
993
|
(style) => !style.startsWith("font-family:")
|
|
811
994
|
);
|
|
812
|
-
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${
|
|
995
|
+
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
813
996
|
}
|
|
814
|
-
function
|
|
997
|
+
function base642(bytes) {
|
|
815
998
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
816
999
|
let output = "";
|
|
817
1000
|
for (let index = 0; index < bytes.length; index += 3) {
|
|
@@ -831,7 +1014,7 @@ function directionAttribute(spans) {
|
|
|
831
1014
|
if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
|
|
832
1015
|
return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
|
|
833
1016
|
}
|
|
834
|
-
function
|
|
1017
|
+
function number2(value) {
|
|
835
1018
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
836
1019
|
}
|
|
837
1020
|
function escapeAttribute(value) {
|