@boxpdf/html-writer 0.1.12 → 0.1.13
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.js
CHANGED
|
@@ -58,6 +58,152 @@ function escapeHtml(value) {
|
|
|
58
58
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// src/semantic-media.ts
|
|
62
|
+
function semanticMedia(page) {
|
|
63
|
+
const output = (page.images ?? []).map((image) => rasterMedia(image));
|
|
64
|
+
const vector = vectorMedia(page);
|
|
65
|
+
if (vector) output.push(vector);
|
|
66
|
+
return output.sort((left, right) => right.bounds.y - left.bounds.y);
|
|
67
|
+
}
|
|
68
|
+
function rasterMedia(image) {
|
|
69
|
+
const bounds = transformedUnitBounds(image.transform);
|
|
70
|
+
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
71
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
72
|
+
const opacity = unitInterval(image.opacity) ? `;opacity:${number(image.opacity)}` : "";
|
|
73
|
+
return {
|
|
74
|
+
bounds,
|
|
75
|
+
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}">`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function vectorMedia(page) {
|
|
79
|
+
const paths = (page.paths ?? []).filter((path) => safePath(path.d));
|
|
80
|
+
const fills = page.fills ?? [];
|
|
81
|
+
const bounds = unionBounds([
|
|
82
|
+
...paths.map((path) => pathBounds(path.d)),
|
|
83
|
+
...fills.map(fillBounds)
|
|
84
|
+
]);
|
|
85
|
+
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return void 0;
|
|
86
|
+
const content = fills.map(vectorFill).join("") + paths.map((path) => vectorPath(path)).join("");
|
|
87
|
+
return {
|
|
88
|
+
bounds,
|
|
89
|
+
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>`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function vectorFill(fill) {
|
|
93
|
+
const points = fill.points.map(([x, y]) => `${number(x)},${number(y)}`).join(" ");
|
|
94
|
+
const opacity = unitInterval(fill.opacity) ? ` fill-opacity="${number(fill.opacity)}"` : "";
|
|
95
|
+
return cssColor(fill.color) ? `<polygon points="${points}" fill="${fill.color}"${opacity}/>` : "";
|
|
96
|
+
}
|
|
97
|
+
function vectorPath(path) {
|
|
98
|
+
const fill = cssColor(path.fill) ? path.fill : "none";
|
|
99
|
+
const stroke = cssColor(path.stroke) ? path.stroke : "none";
|
|
100
|
+
const width = finiteNonnegative(path.strokeWidth) ? ` stroke-width="${number(path.strokeWidth)}"` : "";
|
|
101
|
+
const fillOpacity = unitInterval(path.fillOpacity) ? ` fill-opacity="${number(path.fillOpacity)}"` : "";
|
|
102
|
+
const strokeOpacity = unitInterval(path.strokeOpacity) ? ` stroke-opacity="${number(path.strokeOpacity)}"` : "";
|
|
103
|
+
const dash = path.strokeDasharray?.every(finiteNonnegative) ? ` stroke-dasharray="${path.strokeDasharray.map(number).join(" ")}"` : "";
|
|
104
|
+
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
105
|
+
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
106
|
+
const rule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
107
|
+
return `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}${fillOpacity}${strokeOpacity}${dash}${linecap}${linejoin}${rule}/>`;
|
|
108
|
+
}
|
|
109
|
+
function transformedUnitBounds([a, b, c, d, e, f]) {
|
|
110
|
+
const points = [
|
|
111
|
+
[e, f],
|
|
112
|
+
[a + e, b + f],
|
|
113
|
+
[c + e, d + f],
|
|
114
|
+
[a + c + e, b + d + f]
|
|
115
|
+
];
|
|
116
|
+
const xs = points.map(([x]) => x ?? 0);
|
|
117
|
+
const ys = points.map(([, y]) => y ?? 0);
|
|
118
|
+
const minX = Math.min(...xs);
|
|
119
|
+
const minY = Math.min(...ys);
|
|
120
|
+
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
121
|
+
}
|
|
122
|
+
function pathBounds(path) {
|
|
123
|
+
const values = [...path.matchAll(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi)].map(
|
|
124
|
+
(match) => Number(match[0])
|
|
125
|
+
);
|
|
126
|
+
if (values.length < 2) return void 0;
|
|
127
|
+
const xs = [];
|
|
128
|
+
const ys = [];
|
|
129
|
+
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
130
|
+
xs.push(values[index] ?? 0);
|
|
131
|
+
ys.push(values[index + 1] ?? 0);
|
|
132
|
+
}
|
|
133
|
+
const minX = Math.min(...xs);
|
|
134
|
+
const minY = Math.min(...ys);
|
|
135
|
+
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
|
|
136
|
+
}
|
|
137
|
+
function fillBounds(fill) {
|
|
138
|
+
if (fill.points.length === 0) return void 0;
|
|
139
|
+
const xs = fill.points.map(([x]) => x);
|
|
140
|
+
const ys = fill.points.map(([, y]) => y);
|
|
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 unionBounds(bounds) {
|
|
146
|
+
const values = bounds.filter((value) => Boolean(value));
|
|
147
|
+
if (values.length === 0) return void 0;
|
|
148
|
+
const x = Math.min(...values.map((value) => value.x));
|
|
149
|
+
const y = Math.min(...values.map((value) => value.y));
|
|
150
|
+
const right = Math.max(...values.map((value) => value.x + value.width));
|
|
151
|
+
const top = Math.max(...values.map((value) => value.y + value.height));
|
|
152
|
+
return { x, y, width: right - x, height: top - y };
|
|
153
|
+
}
|
|
154
|
+
function rgbBmp(image) {
|
|
155
|
+
const stride = Math.ceil(image.width * 3 / 4) * 4;
|
|
156
|
+
const output = new Uint8Array(54 + stride * image.height);
|
|
157
|
+
const view = new DataView(output.buffer);
|
|
158
|
+
output.set([66, 77]);
|
|
159
|
+
view.setUint32(2, output.length, true);
|
|
160
|
+
view.setUint32(10, 54, true);
|
|
161
|
+
view.setUint32(14, 40, true);
|
|
162
|
+
view.setInt32(18, image.width, true);
|
|
163
|
+
view.setInt32(22, -image.height, true);
|
|
164
|
+
view.setUint16(26, 1, true);
|
|
165
|
+
view.setUint16(28, 24, true);
|
|
166
|
+
for (let row = 0; row < image.height; row += 1) {
|
|
167
|
+
for (let column = 0; column < image.width; column += 1) {
|
|
168
|
+
const source = (row * image.width + column) * 3;
|
|
169
|
+
const target = 54 + row * stride + column * 3;
|
|
170
|
+
output[target] = image.data[source + 2] ?? 0;
|
|
171
|
+
output[target + 1] = image.data[source + 1] ?? 0;
|
|
172
|
+
output[target + 2] = image.data[source] ?? 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return output;
|
|
176
|
+
}
|
|
177
|
+
function base64(bytes) {
|
|
178
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
179
|
+
let output = "";
|
|
180
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
181
|
+
const first = bytes[index] ?? 0;
|
|
182
|
+
const second = bytes[index + 1] ?? 0;
|
|
183
|
+
const third = bytes[index + 2] ?? 0;
|
|
184
|
+
output += alphabet[first >> 2];
|
|
185
|
+
output += alphabet[(first & 3) << 4 | second >> 4];
|
|
186
|
+
output += index + 1 < bytes.length ? alphabet[(second & 15) << 2 | third >> 6] : "=";
|
|
187
|
+
output += index + 2 < bytes.length ? alphabet[third & 63] : "=";
|
|
188
|
+
}
|
|
189
|
+
return output;
|
|
190
|
+
}
|
|
191
|
+
function safePath(value) {
|
|
192
|
+
return value.length <= 1e6 && /^[\d\s.,+\-eEMmLlCcZz]+$/.test(value);
|
|
193
|
+
}
|
|
194
|
+
function cssColor(value) {
|
|
195
|
+
return /^#[\da-f]{6}$/i.test(value ?? "");
|
|
196
|
+
}
|
|
197
|
+
function finiteNonnegative(value) {
|
|
198
|
+
return Number.isFinite(value) && (value ?? -1) >= 0;
|
|
199
|
+
}
|
|
200
|
+
function unitInterval(value) {
|
|
201
|
+
return finiteNonnegative(value) && value <= 1;
|
|
202
|
+
}
|
|
203
|
+
function number(value) {
|
|
204
|
+
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
205
|
+
}
|
|
206
|
+
|
|
61
207
|
// src/semantic-document.ts
|
|
62
208
|
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
63
209
|
const stats = {
|
|
@@ -71,6 +217,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
71
217
|
const seenFurniture = /* @__PURE__ */ new Set();
|
|
72
218
|
const sectionLevels = [];
|
|
73
219
|
let activeTable;
|
|
220
|
+
const pendingMedia = [];
|
|
74
221
|
let headerOpen = false;
|
|
75
222
|
let headerHasParagraph = false;
|
|
76
223
|
let contentStarted = false;
|
|
@@ -81,6 +228,7 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
81
228
|
if (!activeTable) return;
|
|
82
229
|
await write("</table>");
|
|
83
230
|
activeTable = void 0;
|
|
231
|
+
while (pendingMedia.length > 0) await write(pendingMedia.shift() ?? "");
|
|
84
232
|
};
|
|
85
233
|
const closeSections = async (minimumLevel = 0) => {
|
|
86
234
|
while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
|
|
@@ -100,10 +248,19 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
100
248
|
};
|
|
101
249
|
const emitPage = async (page, future) => {
|
|
102
250
|
const defaultColor = dominantTextColor(page.structured.lines);
|
|
251
|
+
let mediaIndex = 0;
|
|
103
252
|
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
104
253
|
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
105
254
|
for (const [blockIndex, block] of page.structured.blocks.entries()) {
|
|
106
255
|
const nextBlock = page.structured.blocks[blockIndex + 1];
|
|
256
|
+
const blockY = semanticBlockY(block);
|
|
257
|
+
while ((page.media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
258
|
+
await flushPendingParagraph();
|
|
259
|
+
const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
|
|
260
|
+
if (activeTable) pendingMedia.push(html);
|
|
261
|
+
else await write(html);
|
|
262
|
+
mediaIndex += 1;
|
|
263
|
+
}
|
|
107
264
|
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
108
265
|
stats.suppressedFurniture += 1;
|
|
109
266
|
continue;
|
|
@@ -194,11 +351,18 @@ async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
|
194
351
|
}
|
|
195
352
|
await write(semanticBlockHtml(block, defaultColor));
|
|
196
353
|
}
|
|
354
|
+
while (mediaIndex < page.media.length) {
|
|
355
|
+
await flushPendingParagraph();
|
|
356
|
+
const html = `<div class="pdf-semantic-visual">${page.media[mediaIndex]?.html}</div>`;
|
|
357
|
+
if (activeTable) pendingMedia.push(html);
|
|
358
|
+
else await write(html);
|
|
359
|
+
mediaIndex += 1;
|
|
360
|
+
}
|
|
197
361
|
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
198
362
|
};
|
|
199
363
|
for await (const page of pages) {
|
|
200
364
|
const structured = structurePage(page);
|
|
201
|
-
buffer.push({ width: page.width, height: page.height, structured });
|
|
365
|
+
buffer.push({ width: page.width, height: page.height, structured, media: semanticMedia(page) });
|
|
202
366
|
stats.pagesProcessed += 1;
|
|
203
367
|
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
204
368
|
stats.peakBufferedLines = Math.max(
|
|
@@ -352,6 +516,10 @@ function semanticBlockHtml(block, defaultColor = "#000000") {
|
|
|
352
516
|
const tag = block.ordered ? "ol" : "ul";
|
|
353
517
|
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml2(item.text)}</li>`).join("")}</${tag}>`;
|
|
354
518
|
}
|
|
519
|
+
function semanticBlockY(block) {
|
|
520
|
+
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
521
|
+
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
522
|
+
}
|
|
355
523
|
function cardTableRow(item) {
|
|
356
524
|
const trailing = item.details.at(-1) ?? "";
|
|
357
525
|
const match = /^\s*[×x]\s*(\d+)\s+(.+)$/u.exec(trailing);
|
|
@@ -435,7 +603,7 @@ async function writePositionedPage(page, write, options) {
|
|
|
435
603
|
const displayWidth = quarterTurn ? page.height : page.width;
|
|
436
604
|
const displayHeight = quarterTurn ? page.width : page.height;
|
|
437
605
|
await write(
|
|
438
|
-
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${
|
|
606
|
+
`<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">`
|
|
439
607
|
);
|
|
440
608
|
const fontAliases = new Map(
|
|
441
609
|
(page.fonts ?? []).filter((font) => font.format === "truetype" && !/(?:courier|^TTE)/i.test(font.family ?? "")).map((font) => [font.id, `boxpdf-${page.number}-${font.id}`])
|
|
@@ -447,10 +615,10 @@ async function writePositionedPage(page, write, options) {
|
|
|
447
615
|
await write(`<style>${page.fonts.map((font) => fontFace(font, fontAliases)).join("")}</style>`);
|
|
448
616
|
}
|
|
449
617
|
await write(
|
|
450
|
-
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${
|
|
618
|
+
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number2(page.width)}pt;height:${number2(page.height)}pt${rotationTransform(page)}">`
|
|
451
619
|
);
|
|
452
620
|
await write(
|
|
453
|
-
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${
|
|
621
|
+
`<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)}">`
|
|
454
622
|
);
|
|
455
623
|
const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
|
|
456
624
|
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
@@ -460,24 +628,24 @@ async function writePositionedPage(page, write, options) {
|
|
|
460
628
|
}
|
|
461
629
|
}
|
|
462
630
|
for (const fill of page.fills ?? []) {
|
|
463
|
-
const points = fill.points.map(([x, y]) => `${
|
|
631
|
+
const points = fill.points.map(([x, y]) => `${number2(x)},${number2(page.height - y)}`).join(" ");
|
|
464
632
|
if (isCssHexColor(fill.color)) {
|
|
465
|
-
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${
|
|
633
|
+
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
|
|
466
634
|
await write(`<polygon points="${points}" fill="${fill.color}"${opacity}/>`);
|
|
467
635
|
}
|
|
468
636
|
}
|
|
469
637
|
if (page.paths?.length) {
|
|
470
|
-
await write(`<g transform="translate(0 ${
|
|
638
|
+
await write(`<g transform="translate(0 ${number2(page.height)}) scale(1 -1)">`);
|
|
471
639
|
for (const [pathIndex, path] of page.paths.entries()) {
|
|
472
640
|
if (!isSvgPath(path.d)) continue;
|
|
473
641
|
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
474
642
|
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
475
|
-
const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${
|
|
643
|
+
const strokeWidth = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
|
|
476
644
|
const fillRule = path.fillRule ? ` fill-rule="${path.fillRule}"` : "";
|
|
477
|
-
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${
|
|
478
|
-
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${
|
|
479
|
-
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(
|
|
480
|
-
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${
|
|
645
|
+
const fillOpacity = isUnitInterval(path.fillOpacity) ? ` fill-opacity="${number2(path.fillOpacity)}"` : "";
|
|
646
|
+
const strokeOpacity = isUnitInterval(path.strokeOpacity) ? ` stroke-opacity="${number2(path.strokeOpacity)}"` : "";
|
|
647
|
+
const dasharray = path.strokeDasharray?.every((value) => Number.isFinite(value) && value >= 0) ? ` stroke-dasharray="${path.strokeDasharray.map(number2).join(" ")}"` : "";
|
|
648
|
+
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number2(path.strokeDashoffset ?? 0)}"` : "";
|
|
481
649
|
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
482
650
|
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
483
651
|
let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
|
|
@@ -514,11 +682,11 @@ function usesReflectedVisualOverlay(page, spans) {
|
|
|
514
682
|
}
|
|
515
683
|
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
516
684
|
const [a, b, c, d, e, f] = image.transform;
|
|
517
|
-
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(
|
|
518
|
-
const opacity = isUnitInterval(image.opacity) ? ` opacity="${
|
|
685
|
+
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number2).join(" ");
|
|
686
|
+
const opacity = isUnitInterval(image.opacity) ? ` opacity="${number2(image.opacity)}"` : "";
|
|
519
687
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
520
|
-
const data = image.format === "jpeg" ? image.data :
|
|
521
|
-
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${
|
|
688
|
+
const data = image.format === "jpeg" ? image.data : rgbBmp2(image);
|
|
689
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base642(data)}"${opacity}/>`;
|
|
522
690
|
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
523
691
|
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
524
692
|
}
|
|
@@ -529,7 +697,7 @@ function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
|
529
697
|
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
530
698
|
if (!isSvgPath(clip.d)) return "";
|
|
531
699
|
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
532
|
-
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${
|
|
700
|
+
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number2(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
|
|
533
701
|
})
|
|
534
702
|
).join("");
|
|
535
703
|
}
|
|
@@ -548,7 +716,7 @@ function pathClipDefinitions(paths, pageNumber) {
|
|
|
548
716
|
function pathClipId(pageNumber, pathIndex, clipIndex) {
|
|
549
717
|
return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
|
|
550
718
|
}
|
|
551
|
-
function
|
|
719
|
+
function rgbBmp2(image) {
|
|
552
720
|
const stride = Math.ceil(image.width * 3 / 4) * 4;
|
|
553
721
|
const output = new Uint8Array(54 + stride * image.height);
|
|
554
722
|
const view = new DataView(output.buffer);
|
|
@@ -576,11 +744,11 @@ function rgbBmp(image) {
|
|
|
576
744
|
function rotationTransform(page) {
|
|
577
745
|
switch (page.rotate) {
|
|
578
746
|
case 90:
|
|
579
|
-
return `;transform:translate(${
|
|
747
|
+
return `;transform:translate(${number2(page.height)}pt,0) rotate(90deg)`;
|
|
580
748
|
case 180:
|
|
581
|
-
return `;transform:translate(${
|
|
749
|
+
return `;transform:translate(${number2(page.width)}pt,${number2(page.height)}pt) rotate(180deg)`;
|
|
582
750
|
case 270:
|
|
583
|
-
return `;transform:translate(0,${
|
|
751
|
+
return `;transform:translate(0,${number2(page.width)}pt) rotate(270deg)`;
|
|
584
752
|
default:
|
|
585
753
|
return "";
|
|
586
754
|
}
|
|
@@ -588,13 +756,13 @@ function rotationTransform(page) {
|
|
|
588
756
|
function positionedSpan(span, fontAliases) {
|
|
589
757
|
const direction = directionAttribute([span]);
|
|
590
758
|
const style = [
|
|
591
|
-
`left:${
|
|
592
|
-
`bottom:${
|
|
593
|
-
`width:${
|
|
594
|
-
`height:${
|
|
595
|
-
`font-size:${
|
|
759
|
+
`left:${number2(span.bounds.x)}pt`,
|
|
760
|
+
`bottom:${number2(span.bounds.y)}pt`,
|
|
761
|
+
`width:${number2(span.bounds.width)}pt`,
|
|
762
|
+
`height:${number2(span.bounds.height)}pt`,
|
|
763
|
+
`font-size:${number2(span.fontSize)}pt`,
|
|
596
764
|
...isCssHexColor(span.color) ? [`color:${span.color}`] : [],
|
|
597
|
-
...isUnitInterval(span.fillOpacity) ? [`opacity:${
|
|
765
|
+
...isUnitInterval(span.fillOpacity) ? [`opacity:${number2(span.fillOpacity)}`] : [],
|
|
598
766
|
...fontStyles(
|
|
599
767
|
span.fontFamily,
|
|
600
768
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
@@ -605,10 +773,17 @@ function positionedSpan(span, fontAliases) {
|
|
|
605
773
|
async function writeFlowPage(page, write) {
|
|
606
774
|
const structured = structurePage2(page);
|
|
607
775
|
const defaultColor = dominantTextColor(structured.lines);
|
|
776
|
+
const media = semanticMedia(page);
|
|
777
|
+
let mediaIndex = 0;
|
|
608
778
|
await write(
|
|
609
779
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
610
780
|
);
|
|
611
781
|
for (const block of structured.blocks) {
|
|
782
|
+
const blockY = semanticBlockY2(block);
|
|
783
|
+
while ((media[mediaIndex]?.bounds.y ?? -Infinity) >= blockY) {
|
|
784
|
+
await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
|
|
785
|
+
mediaIndex += 1;
|
|
786
|
+
}
|
|
612
787
|
if (block.type === "table") await write(tableToHtml(block.table));
|
|
613
788
|
else if (block.type === "heading") {
|
|
614
789
|
await write(
|
|
@@ -655,8 +830,16 @@ async function writeFlowPage(page, write) {
|
|
|
655
830
|
await write(`</${tag}>`);
|
|
656
831
|
}
|
|
657
832
|
}
|
|
833
|
+
while (mediaIndex < media.length) {
|
|
834
|
+
await write(`<div class="pdf-semantic-visual">${media[mediaIndex]?.html}</div>`);
|
|
835
|
+
mediaIndex += 1;
|
|
836
|
+
}
|
|
658
837
|
await write("</section>");
|
|
659
838
|
}
|
|
839
|
+
function semanticBlockY2(block) {
|
|
840
|
+
const lines = block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
841
|
+
return Math.max(...lines.map((line) => line.bounds.y + line.bounds.height));
|
|
842
|
+
}
|
|
660
843
|
function visualText(span, pageHeight, fontAliases, counterRotateReflectedText = false) {
|
|
661
844
|
if (span.renderingMode === 3 || span.renderingMode === 7) return "";
|
|
662
845
|
if (!span.fontAssetId && isAdobeCjkFont(span.fontFamily)) return "";
|
|
@@ -666,10 +849,10 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
666
849
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
667
850
|
).join(";");
|
|
668
851
|
const stroke = isCssHexColor(span.strokeColor) ? `stroke:${span.strokeColor}` : "";
|
|
669
|
-
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${
|
|
852
|
+
const strokeWidth = stroke && Number.isFinite(span.strokeWidth) && (span.strokeWidth ?? -1) >= 0 ? `stroke-width:${number2(span.strokeWidth ?? 0)}` : "";
|
|
670
853
|
const strokeOnly = span.renderingMode === 1 || span.renderingMode === 5;
|
|
671
|
-
const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${
|
|
672
|
-
const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${
|
|
854
|
+
const fillOpacity = isUnitInterval(span.fillOpacity) ? `fill-opacity:${number2(span.fillOpacity)}` : "";
|
|
855
|
+
const strokeOpacity = isUnitInterval(span.strokeOpacity) ? `stroke-opacity:${number2(span.strokeOpacity)}` : "";
|
|
673
856
|
const style = [
|
|
674
857
|
isHebrewPaintOrder(span) ? "unicode-bidi:bidi-override;direction:ltr" : "",
|
|
675
858
|
span.direction === "ttb" ? "writing-mode:vertical-rl" : "",
|
|
@@ -681,7 +864,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
681
864
|
font
|
|
682
865
|
].filter(Boolean).join(";");
|
|
683
866
|
const textExtent = span.direction === "ttb" ? span.bounds.height : span.bounds.width;
|
|
684
|
-
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${
|
|
867
|
+
const textLength = textExtent > 0 && !isHebrewPaintOrder(span) ? ` textLength="${number2(textExtent)}" lengthAdjust="${span.direction === "ttb" || usesSpacingAdjustment(span) ? "spacing" : "spacingAndGlyphs"}"` : "";
|
|
685
868
|
const transform = counterRotateReflectedText && span.transform ? [
|
|
686
869
|
span.transform[0],
|
|
687
870
|
span.transform[1],
|
|
@@ -694,8 +877,8 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
694
877
|
const basisY = transform?.[1] ?? 0;
|
|
695
878
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
696
879
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
697
|
-
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(
|
|
698
|
-
return `<text${direction}${position} font-size="${
|
|
880
|
+
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number2).join(" ")} ${number2(anchorX)} ${number2(anchorY)})"` : ` x="${number2(anchorX)}" y="${number2(anchorY)}"`;
|
|
881
|
+
return `<text${direction}${position} font-size="${number2(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml3(span.text)}</text>`;
|
|
699
882
|
}
|
|
700
883
|
function isAdobeCjkFont(fontFamily) {
|
|
701
884
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -707,16 +890,16 @@ function visualType3Text(span, font, pageHeight) {
|
|
|
707
890
|
const totalAdvance = sequence.reduce((total, glyph) => total + (glyph?.advance ?? 0), 0);
|
|
708
891
|
if (totalAdvance <= 0 || span.bounds.width <= 0 || span.fontSize <= 0) return "";
|
|
709
892
|
const transform = span.transform ?? [1, 0, 0, 1];
|
|
710
|
-
const outer = `matrix(${transform.map(
|
|
893
|
+
const outer = `matrix(${transform.map(number2).join(" ")} ${number2(span.bounds.x)} ${number2(pageHeight - span.bounds.y)})`;
|
|
711
894
|
const xScale = span.bounds.width / totalAdvance;
|
|
712
895
|
let offset = 0;
|
|
713
896
|
let content = "";
|
|
714
897
|
for (const glyph of sequence) {
|
|
715
898
|
if (!glyph) continue;
|
|
716
|
-
content += `<g transform="translate(${
|
|
899
|
+
content += `<g transform="translate(${number2(offset)} 0)">${type3Glyph(glyph)}</g>`;
|
|
717
900
|
offset += glyph.advance;
|
|
718
901
|
}
|
|
719
|
-
return `<g transform="${outer}"><g transform="scale(${
|
|
902
|
+
return `<g transform="${outer}"><g transform="scale(${number2(xScale)} ${number2(-span.fontSize)})">${content}</g></g>`;
|
|
720
903
|
}
|
|
721
904
|
function isHebrewPaintOrder(span) {
|
|
722
905
|
return span.direction === "ltr" && /[\u0590-\u05ff]/u.test(span.text);
|
|
@@ -728,15 +911,15 @@ function type3Glyph(glyph) {
|
|
|
728
911
|
let output = "";
|
|
729
912
|
for (const fill of glyph.fills ?? []) {
|
|
730
913
|
if (!isCssHexColor(fill.color)) continue;
|
|
731
|
-
const points = fill.points.map(([x, y]) => `${
|
|
732
|
-
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${
|
|
914
|
+
const points = fill.points.map(([x, y]) => `${number2(x)},${number2(y)}`).join(" ");
|
|
915
|
+
const opacity = isUnitInterval(fill.opacity) ? ` fill-opacity="${number2(fill.opacity)}"` : "";
|
|
733
916
|
output += `<polygon points="${points}" fill="${fill.color}"${opacity}/>`;
|
|
734
917
|
}
|
|
735
918
|
for (const path of glyph.paths ?? []) {
|
|
736
919
|
if (!isSvgPath(path.d)) continue;
|
|
737
920
|
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
738
921
|
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
739
|
-
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${
|
|
922
|
+
const width = path.strokeWidth !== void 0 && Number.isFinite(path.strokeWidth) && path.strokeWidth >= 0 ? ` stroke-width="${number2(path.strokeWidth)}"` : "";
|
|
740
923
|
output += `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${width}/>`;
|
|
741
924
|
}
|
|
742
925
|
return (glyph.fills?.length ?? 0) > 64 && glyph.advance > 2 ? `<g shape-rendering="crispEdges">${output}</g>` : output;
|
|
@@ -786,9 +969,9 @@ function fontFace(font, aliases) {
|
|
|
786
969
|
const styles2 = fontStyles(font.family, alias).filter(
|
|
787
970
|
(style) => !style.startsWith("font-family:")
|
|
788
971
|
);
|
|
789
|
-
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${
|
|
972
|
+
return `@font-face{font-family:${alias};src:url(data:font/ttf;base64,${base642(font.data)}) format("truetype");${styles2.join(";")}}`;
|
|
790
973
|
}
|
|
791
|
-
function
|
|
974
|
+
function base642(bytes) {
|
|
792
975
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
793
976
|
let output = "";
|
|
794
977
|
for (let index = 0; index < bytes.length; index += 3) {
|
|
@@ -808,7 +991,7 @@ function directionAttribute(spans) {
|
|
|
808
991
|
if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
|
|
809
992
|
return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
|
|
810
993
|
}
|
|
811
|
-
function
|
|
994
|
+
function number2(value) {
|
|
812
995
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
813
996
|
}
|
|
814
997
|
function escapeAttribute(value) {
|