@boxpdf/html-writer 0.1.9 → 0.1.10
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/README.md +21 -0
- package/dist/index.cjs +255 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +259 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { ExtractedPage } from '@boxpdf/reader';
|
|
2
2
|
|
|
3
|
+
interface SemanticDocumentStats {
|
|
4
|
+
pagesProcessed: number;
|
|
5
|
+
peakBufferedPages: number;
|
|
6
|
+
peakBufferedLines: number;
|
|
7
|
+
mergedTables: number;
|
|
8
|
+
suppressedFurniture: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
type HtmlLayout = "positioned" | "flow";
|
|
4
12
|
type HtmlProfile = "visual" | "semantic";
|
|
5
13
|
type HtmlWrite = (chunk: string) => void | Promise<void>;
|
|
@@ -12,9 +20,13 @@ interface HtmlWriterOptions {
|
|
|
12
20
|
language?: string;
|
|
13
21
|
includeDocument?: boolean;
|
|
14
22
|
includeStyles?: boolean;
|
|
23
|
+
/** Maximum extracted page models retained for document-level semantic decisions. */
|
|
24
|
+
semanticLookaheadPages?: number;
|
|
25
|
+
/** Receives bounded-buffer and document-inference statistics after semantic output completes. */
|
|
26
|
+
onSemanticStats?: (stats: Readonly<SemanticDocumentStats>) => void;
|
|
15
27
|
}
|
|
16
28
|
declare function writeHtmlDocument(pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
17
29
|
declare function writePage(page: ExtractedPage, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
18
30
|
declare function pageToHtml(page: ExtractedPage, options?: HtmlWriterOptions): Promise<string>;
|
|
19
31
|
|
|
20
|
-
export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, pageToHtml, writeHtmlDocument, writePage };
|
|
32
|
+
export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, type SemanticDocumentStats, pageToHtml, writeHtmlDocument, writePage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { ExtractedPage } from '@boxpdf/reader';
|
|
2
2
|
|
|
3
|
+
interface SemanticDocumentStats {
|
|
4
|
+
pagesProcessed: number;
|
|
5
|
+
peakBufferedPages: number;
|
|
6
|
+
peakBufferedLines: number;
|
|
7
|
+
mergedTables: number;
|
|
8
|
+
suppressedFurniture: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
type HtmlLayout = "positioned" | "flow";
|
|
4
12
|
type HtmlProfile = "visual" | "semantic";
|
|
5
13
|
type HtmlWrite = (chunk: string) => void | Promise<void>;
|
|
@@ -12,9 +20,13 @@ interface HtmlWriterOptions {
|
|
|
12
20
|
language?: string;
|
|
13
21
|
includeDocument?: boolean;
|
|
14
22
|
includeStyles?: boolean;
|
|
23
|
+
/** Maximum extracted page models retained for document-level semantic decisions. */
|
|
24
|
+
semanticLookaheadPages?: number;
|
|
25
|
+
/** Receives bounded-buffer and document-inference statistics after semantic output completes. */
|
|
26
|
+
onSemanticStats?: (stats: Readonly<SemanticDocumentStats>) => void;
|
|
15
27
|
}
|
|
16
28
|
declare function writeHtmlDocument(pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
17
29
|
declare function writePage(page: ExtractedPage, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
18
30
|
declare function pageToHtml(page: ExtractedPage, options?: HtmlWriterOptions): Promise<string>;
|
|
19
31
|
|
|
20
|
-
export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, pageToHtml, writeHtmlDocument, writePage };
|
|
32
|
+
export { type HtmlLayout, type HtmlProfile, type HtmlWrite, type HtmlWriterOptions, type SemanticDocumentStats, pageToHtml, writeHtmlDocument, writePage };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,163 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { structurePage, tableToHtml } from "@boxpdf/reader/structure";
|
|
2
|
+
import { structurePage as structurePage2, tableToHtml } from "@boxpdf/reader/structure";
|
|
3
|
+
|
|
4
|
+
// src/semantic-document.ts
|
|
5
|
+
import {
|
|
6
|
+
structurePage,
|
|
7
|
+
tableToRows
|
|
8
|
+
} from "@boxpdf/reader/structure";
|
|
9
|
+
async function writeSemanticDocument(pages, write, lookaheadPages) {
|
|
10
|
+
const stats = {
|
|
11
|
+
pagesProcessed: 0,
|
|
12
|
+
peakBufferedPages: 0,
|
|
13
|
+
peakBufferedLines: 0,
|
|
14
|
+
mergedTables: 0,
|
|
15
|
+
suppressedFurniture: 0
|
|
16
|
+
};
|
|
17
|
+
const buffer = [];
|
|
18
|
+
const seenFurniture = /* @__PURE__ */ new Set();
|
|
19
|
+
const sectionLevels = [];
|
|
20
|
+
let activeTable;
|
|
21
|
+
await write('<article class="pdf-semantic-document">');
|
|
22
|
+
const closeTable = async () => {
|
|
23
|
+
if (!activeTable) return;
|
|
24
|
+
await write("</table>");
|
|
25
|
+
activeTable = void 0;
|
|
26
|
+
};
|
|
27
|
+
const closeSections = async (minimumLevel = 0) => {
|
|
28
|
+
while ((sectionLevels.at(-1) ?? -1) >= minimumLevel && sectionLevels.length > 0) {
|
|
29
|
+
await write("</section>");
|
|
30
|
+
sectionLevels.pop();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const emitPage = async (page, future) => {
|
|
34
|
+
const futureFurniture = new Set(future.flatMap((candidate) => marginSignatures(candidate)));
|
|
35
|
+
const repeatedFurniture = /* @__PURE__ */ new Set([...seenFurniture, ...futureFurniture]);
|
|
36
|
+
for (const block of page.structured.blocks) {
|
|
37
|
+
if (isRepeatedFurniture(block, page, repeatedFurniture)) {
|
|
38
|
+
stats.suppressedFurniture += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (block.type === "table") {
|
|
42
|
+
const rows = tableToRows(block.table);
|
|
43
|
+
if (activeTable && tablesContinue(activeTable.table, block.table, page.width)) {
|
|
44
|
+
const continuationRows = sameRow(activeTable.header, rows[0]) ? rows.slice(1) : rows;
|
|
45
|
+
for (const row of continuationRows) await write(tableRow(row, false));
|
|
46
|
+
activeTable.table = block.table;
|
|
47
|
+
stats.mergedTables += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
await closeTable();
|
|
51
|
+
const header = tableHeader(rows);
|
|
52
|
+
await write("<table>");
|
|
53
|
+
for (const [index, row] of rows.entries())
|
|
54
|
+
await write(tableRow(row, Boolean(header && index === 0)));
|
|
55
|
+
activeTable = { table: block.table, header };
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
await closeTable();
|
|
59
|
+
if (block.type === "heading") {
|
|
60
|
+
await closeSections(block.level);
|
|
61
|
+
await write(
|
|
62
|
+
`<section data-level="${block.level}"><h${block.level}>${escapeHtml(block.text)}</h${block.level}>`
|
|
63
|
+
);
|
|
64
|
+
sectionLevels.push(block.level);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
await write(semanticBlockHtml(block));
|
|
68
|
+
}
|
|
69
|
+
for (const signature of marginSignatures(page)) seenFurniture.add(signature);
|
|
70
|
+
};
|
|
71
|
+
for await (const page of pages) {
|
|
72
|
+
const structured = structurePage(page);
|
|
73
|
+
buffer.push({ width: page.width, height: page.height, structured });
|
|
74
|
+
stats.pagesProcessed += 1;
|
|
75
|
+
stats.peakBufferedPages = Math.max(stats.peakBufferedPages, buffer.length);
|
|
76
|
+
stats.peakBufferedLines = Math.max(
|
|
77
|
+
stats.peakBufferedLines,
|
|
78
|
+
buffer.reduce((total, item) => total + item.structured.lines.length, 0)
|
|
79
|
+
);
|
|
80
|
+
if (buffer.length >= lookaheadPages) {
|
|
81
|
+
const ready = buffer.shift();
|
|
82
|
+
if (ready) await emitPage(ready, buffer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
while (buffer.length > 0) {
|
|
86
|
+
const ready = buffer.shift();
|
|
87
|
+
if (ready) await emitPage(ready, buffer);
|
|
88
|
+
}
|
|
89
|
+
await closeTable();
|
|
90
|
+
await closeSections();
|
|
91
|
+
await write("</article>");
|
|
92
|
+
return stats;
|
|
93
|
+
}
|
|
94
|
+
function marginSignatures(page) {
|
|
95
|
+
return page.structured.blocks.flatMap(
|
|
96
|
+
(block) => blockLines(block).filter((line) => isMarginLine(line.bounds.y, line.bounds.height, page.height)).map((line) => furnitureSignature(line.text))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
function isRepeatedFurniture(block, page, futureFurniture) {
|
|
100
|
+
const lines = blockLines(block);
|
|
101
|
+
return lines.length > 0 && lines.every(
|
|
102
|
+
(line) => isMarginLine(line.bounds.y, line.bounds.height, page.height) && futureFurniture.has(furnitureSignature(line.text))
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function blockLines(block) {
|
|
106
|
+
return block.type === "list" ? block.items.flatMap((item) => item.lines) : block.lines;
|
|
107
|
+
}
|
|
108
|
+
function isMarginLine(y, height, pageHeight) {
|
|
109
|
+
return y + height <= pageHeight * 0.12 || y >= pageHeight * 0.88;
|
|
110
|
+
}
|
|
111
|
+
function furnitureSignature(value) {
|
|
112
|
+
return value.toLocaleLowerCase("en").replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
113
|
+
}
|
|
114
|
+
function tablesContinue(previous, next, pageWidth) {
|
|
115
|
+
if (previous.columns.length !== next.columns.length || previous.columns.length < 2) return false;
|
|
116
|
+
return previous.columns.every(
|
|
117
|
+
(column, index) => Math.abs(column - (next.columns[index] ?? Number.POSITIVE_INFINITY)) / pageWidth <= 0.04
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function tableHeader(rows) {
|
|
121
|
+
const first = rows[0];
|
|
122
|
+
if (!first) return void 0;
|
|
123
|
+
return first.some((value) => /^(?:item|description|feature|qty|unit|amount|total)$/i.test(value)) ? first : void 0;
|
|
124
|
+
}
|
|
125
|
+
function sameRow(left, right) {
|
|
126
|
+
return Boolean(
|
|
127
|
+
left && right && left.length === right.length && left.every((value, index) => value === right[index])
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
function tableRow(row, header) {
|
|
131
|
+
const cell = header ? "th" : "td";
|
|
132
|
+
return `<tr>${row.map((value) => `<${cell}>${escapeHtml(value)}</${cell}>`).join("")}</tr>`;
|
|
133
|
+
}
|
|
134
|
+
function semanticBlockHtml(block) {
|
|
135
|
+
if (block.type === "heading")
|
|
136
|
+
return `<h${block.level}>${escapeHtml(block.text)}</h${block.level}>`;
|
|
137
|
+
if (block.type === "paragraph") return `<p>${escapeHtml(block.text)}</p>`;
|
|
138
|
+
if (block.type === "definitionList") {
|
|
139
|
+
return `<dl>${block.entries.map(
|
|
140
|
+
(entry) => `<div><dt>${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.description)}</dd></div>`
|
|
141
|
+
).join("")}</dl>`;
|
|
142
|
+
}
|
|
143
|
+
if (block.type === "cardList") {
|
|
144
|
+
return `<div class="pdf-semantic-cards">${block.items.map(
|
|
145
|
+
(item) => `<article><h3>${escapeHtml(item.title)}</h3>${item.details.map((detail) => `<p>${escapeHtml(detail)}</p>`).join("")}</article>`
|
|
146
|
+
).join("")}</div>`;
|
|
147
|
+
}
|
|
148
|
+
if (block.type === "sectionGroup") {
|
|
149
|
+
return `<div class="pdf-semantic-sections">${block.items.map(
|
|
150
|
+
(item) => `<section><h3>${escapeHtml(item.label)}</h3>${item.content.map((content) => `<p>${escapeHtml(content)}</p>`).join("")}</section>`
|
|
151
|
+
).join("")}</div>`;
|
|
152
|
+
}
|
|
153
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
154
|
+
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml(item.text)}</li>`).join("")}</${tag}>`;
|
|
155
|
+
}
|
|
156
|
+
function escapeHtml(value) {
|
|
157
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/index.ts
|
|
3
161
|
var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--visual,.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-span{position:absolute;white-space:pre;transform-origin:left bottom;unicode-bidi:isolate}.pdf-span[data-direction=ttb]{writing-mode:vertical-rl}.pdf-page--semantic,.pdf-page--flow{max-width:60rem;padding:1rem}.pdf-page--semantic p,.pdf-page--flow p{white-space:pre-wrap;unicode-bidi:plaintext}.pdf-page table{border-collapse:collapse}.pdf-page td{padding:.15rem .4rem;vertical-align:top}`;
|
|
4
162
|
async function writeHtmlDocument(pages, write, options = {}) {
|
|
5
163
|
const includeDocument = options.includeDocument ?? true;
|
|
@@ -9,15 +167,28 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
9
167
|
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
10
168
|
);
|
|
11
169
|
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
12
|
-
await write(`<title>${
|
|
170
|
+
await write(`<title>${escapeHtml2(options.title ?? "PDF document")}</title>`);
|
|
13
171
|
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
14
172
|
await write("</head><body>");
|
|
15
173
|
}
|
|
16
174
|
await write('<main class="pdf-document">');
|
|
17
|
-
|
|
175
|
+
if (resolveProfile(options) === "semantic") {
|
|
176
|
+
const lookahead = semanticLookahead(options.semanticLookaheadPages);
|
|
177
|
+
const stats = await writeSemanticDocument(pages, write, lookahead);
|
|
178
|
+
options.onSemanticStats?.(stats);
|
|
179
|
+
} else {
|
|
180
|
+
for await (const page of pages) await writePage(page, write, options);
|
|
181
|
+
}
|
|
18
182
|
await write("</main>");
|
|
19
183
|
if (includeDocument) await write("</body></html>");
|
|
20
184
|
}
|
|
185
|
+
function semanticLookahead(value) {
|
|
186
|
+
const lookahead = value ?? 4;
|
|
187
|
+
if (!Number.isSafeInteger(lookahead) || lookahead < 1 || lookahead > 16) {
|
|
188
|
+
throw new RangeError("semanticLookaheadPages must be an integer between 1 and 16");
|
|
189
|
+
}
|
|
190
|
+
return lookahead;
|
|
191
|
+
}
|
|
21
192
|
async function writePage(page, write, options = {}) {
|
|
22
193
|
if (resolveProfile(options) === "semantic") await writeFlowPage(page, write);
|
|
23
194
|
else await writePositionedPage(page, write, options);
|
|
@@ -57,8 +228,12 @@ async function writePositionedPage(page, write, options) {
|
|
|
57
228
|
await write(
|
|
58
229
|
`<svg class="pdf-visual-text" xmlns="http://www.w3.org/2000/svg" width="${number(page.width)}pt" height="${number(page.height)}pt" viewBox="0 0 ${number(page.width)} ${number(page.height)}">`
|
|
59
230
|
);
|
|
231
|
+
const clipDefinitions = imageClipDefinitions(page.images ?? [], page.number, page.height) + pathClipDefinitions(page.paths ?? [], page.number);
|
|
232
|
+
if (clipDefinitions) await write(`<defs>${clipDefinitions}</defs>`);
|
|
60
233
|
if (reflectedOverlay) {
|
|
61
|
-
for (const image of page.images ?? [])
|
|
234
|
+
for (const [index, image] of (page.images ?? []).entries()) {
|
|
235
|
+
await write(visualImage(image, page.height, page.number, index));
|
|
236
|
+
}
|
|
62
237
|
}
|
|
63
238
|
for (const fill of page.fills ?? []) {
|
|
64
239
|
const points = fill.points.map(([x, y]) => `${number(x)},${number(page.height - y)}`).join(" ");
|
|
@@ -69,7 +244,7 @@ async function writePositionedPage(page, write, options) {
|
|
|
69
244
|
}
|
|
70
245
|
if (page.paths?.length) {
|
|
71
246
|
await write(`<g transform="translate(0 ${number(page.height)}) scale(1 -1)">`);
|
|
72
|
-
for (const path of page.paths) {
|
|
247
|
+
for (const [pathIndex, path] of page.paths.entries()) {
|
|
73
248
|
if (!isSvgPath(path.d)) continue;
|
|
74
249
|
const fill = isCssHexColor(path.fill) ? path.fill : "none";
|
|
75
250
|
const stroke = isCssHexColor(path.stroke) ? path.stroke : "none";
|
|
@@ -81,14 +256,18 @@ async function writePositionedPage(page, write, options) {
|
|
|
81
256
|
const dashoffset = Number.isFinite(path.strokeDashoffset) ? ` stroke-dashoffset="${number(path.strokeDashoffset ?? 0)}"` : "";
|
|
82
257
|
const linecap = path.strokeLinecap ? ` stroke-linecap="${path.strokeLinecap}"` : "";
|
|
83
258
|
const linejoin = path.strokeLinejoin ? ` stroke-linejoin="${path.strokeLinejoin}"` : "";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
259
|
+
let output = `<path d="${path.d}" fill="${fill}" stroke="${stroke}"${strokeWidth}${fillOpacity}${strokeOpacity}${dasharray}${dashoffset}${linecap}${linejoin}${fillRule}/>`;
|
|
260
|
+
for (let index = (path.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
261
|
+
output = `<g clip-path="url(#${pathClipId(page.number, pathIndex, index)})">${output}</g>`;
|
|
262
|
+
}
|
|
263
|
+
await write(output);
|
|
87
264
|
}
|
|
88
265
|
await write("</g>");
|
|
89
266
|
}
|
|
90
267
|
if (!reflectedOverlay) {
|
|
91
|
-
for (const image of page.images ?? [])
|
|
268
|
+
for (const [index, image] of (page.images ?? []).entries()) {
|
|
269
|
+
await write(visualImage(image, page.height, page.number, index));
|
|
270
|
+
}
|
|
92
271
|
}
|
|
93
272
|
for (const span of visualSpans) {
|
|
94
273
|
if (!usesPositionedSpan(span)) {
|
|
@@ -109,13 +288,41 @@ function usesReflectedVisualOverlay(page, spans) {
|
|
|
109
288
|
(span) => span.transform !== void 0 && Math.abs(span.transform[0] + 1) < 1e-6 && Math.abs(span.transform[1]) < 1e-6 && Math.abs(span.transform[2]) < 1e-6 && Math.abs(span.transform[3] - 1) < 1e-6
|
|
110
289
|
);
|
|
111
290
|
}
|
|
112
|
-
function visualImage(image, pageHeight) {
|
|
291
|
+
function visualImage(image, pageHeight, pageNumber, imageIndex) {
|
|
113
292
|
const [a, b, c, d, e, f] = image.transform;
|
|
114
293
|
const transform = [a, -b, -c, d, c + e, pageHeight - d - f].map(number).join(" ");
|
|
115
294
|
const opacity = isUnitInterval(image.opacity) ? ` opacity="${number(image.opacity)}"` : "";
|
|
116
295
|
const mime = image.format === "jpeg" ? "image/jpeg" : "image/bmp";
|
|
117
296
|
const data = image.format === "jpeg" ? image.data : rgbBmp(image);
|
|
118
|
-
|
|
297
|
+
let output = `<image width="1" height="1" preserveAspectRatio="none" transform="matrix(${transform})" href="data:${mime};base64,${base64(data)}"${opacity}/>`;
|
|
298
|
+
for (let index = (image.clips?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
299
|
+
output = `<g clip-path="url(#${imageClipId(pageNumber, imageIndex, index)})">${output}</g>`;
|
|
300
|
+
}
|
|
301
|
+
return output;
|
|
302
|
+
}
|
|
303
|
+
function imageClipDefinitions(images, pageNumber, pageHeight) {
|
|
304
|
+
return images.flatMap(
|
|
305
|
+
(image, imageIndex) => (image.clips ?? []).map((clip, clipIndex) => {
|
|
306
|
+
if (!isSvgPath(clip.d)) return "";
|
|
307
|
+
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
308
|
+
return `<clipPath id="${imageClipId(pageNumber, imageIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}" transform="translate(0 ${number(pageHeight)}) scale(1 -1)"${fillRule}/></clipPath>`;
|
|
309
|
+
})
|
|
310
|
+
).join("");
|
|
311
|
+
}
|
|
312
|
+
function imageClipId(pageNumber, imageIndex, clipIndex) {
|
|
313
|
+
return `boxpdf-clip-${pageNumber}-${imageIndex}-${clipIndex}`;
|
|
314
|
+
}
|
|
315
|
+
function pathClipDefinitions(paths, pageNumber) {
|
|
316
|
+
return paths.flatMap(
|
|
317
|
+
(path, pathIndex) => (path.clips ?? []).map((clip, clipIndex) => {
|
|
318
|
+
if (!isSvgPath(clip.d)) return "";
|
|
319
|
+
const fillRule = clip.fillRule ? ` clip-rule="${clip.fillRule}"` : "";
|
|
320
|
+
return `<clipPath id="${pathClipId(pageNumber, pathIndex, clipIndex)}" clipPathUnits="userSpaceOnUse"><path d="${clip.d}"${fillRule}/></clipPath>`;
|
|
321
|
+
})
|
|
322
|
+
).join("");
|
|
323
|
+
}
|
|
324
|
+
function pathClipId(pageNumber, pathIndex, clipIndex) {
|
|
325
|
+
return `boxpdf-path-clip-${pageNumber}-${pathIndex}-${clipIndex}`;
|
|
119
326
|
}
|
|
120
327
|
function rgbBmp(image) {
|
|
121
328
|
const stride = Math.ceil(image.width * 3 / 4) * 4;
|
|
@@ -169,28 +376,51 @@ function positionedSpan(span, fontAliases) {
|
|
|
169
376
|
span.fontAssetId ? fontAliases.get(span.fontAssetId) : void 0
|
|
170
377
|
)
|
|
171
378
|
].join(";");
|
|
172
|
-
return `<span class="pdf-span"${direction} style="${style}">${
|
|
379
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml2(span.text)}</span>`;
|
|
173
380
|
}
|
|
174
381
|
async function writeFlowPage(page, write) {
|
|
175
|
-
const structured =
|
|
176
|
-
const tables = [...structured.tables].sort((left, right) => right.bounds.y - left.bounds.y);
|
|
177
|
-
const emittedTables = /* @__PURE__ */ new Set();
|
|
382
|
+
const structured = structurePage2(page);
|
|
178
383
|
await write(
|
|
179
384
|
`<section class="pdf-page pdf-page--semantic pdf-page--flow" data-page="${page.number}">`
|
|
180
385
|
);
|
|
181
|
-
for (const
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
386
|
+
for (const block of structured.blocks) {
|
|
387
|
+
if (block.type === "table") await write(tableToHtml(block.table));
|
|
388
|
+
else if (block.type === "heading") {
|
|
389
|
+
await write(`<h${block.level}>${escapeHtml2(block.text)}</h${block.level}>`);
|
|
390
|
+
} else if (block.type === "paragraph") {
|
|
391
|
+
await write(
|
|
392
|
+
`<p${directionAttribute(block.lines.flatMap((line) => line.spans))}>${escapeHtml2(block.text)}</p>`
|
|
393
|
+
);
|
|
394
|
+
} else if (block.type === "definitionList") {
|
|
395
|
+
await write("<dl>");
|
|
396
|
+
for (const entry of block.entries) {
|
|
397
|
+
await write(
|
|
398
|
+
`<div><dt>${escapeHtml2(entry.term)}</dt><dd>${escapeHtml2(entry.description)}</dd></div>`
|
|
399
|
+
);
|
|
187
400
|
}
|
|
188
|
-
|
|
401
|
+
await write("</dl>");
|
|
402
|
+
} else if (block.type === "cardList") {
|
|
403
|
+
await write('<div class="pdf-semantic-cards">');
|
|
404
|
+
for (const item of block.items) {
|
|
405
|
+
await write(`<article><h3>${escapeHtml2(item.title)}</h3>`);
|
|
406
|
+
for (const detail of item.details) await write(`<p>${escapeHtml2(detail)}</p>`);
|
|
407
|
+
await write("</article>");
|
|
408
|
+
}
|
|
409
|
+
await write("</div>");
|
|
410
|
+
} else if (block.type === "sectionGroup") {
|
|
411
|
+
await write('<div class="pdf-semantic-sections">');
|
|
412
|
+
for (const item of block.items) {
|
|
413
|
+
await write(`<section><h3>${escapeHtml2(item.label)}</h3>`);
|
|
414
|
+
for (const content of item.content) await write(`<p>${escapeHtml2(content)}</p>`);
|
|
415
|
+
await write("</section>");
|
|
416
|
+
}
|
|
417
|
+
await write("</div>");
|
|
418
|
+
} else {
|
|
419
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
420
|
+
await write(`<${tag}>`);
|
|
421
|
+
for (const item of block.items) await write(`<li>${escapeHtml2(item.text)}</li>`);
|
|
422
|
+
await write(`</${tag}>`);
|
|
189
423
|
}
|
|
190
|
-
await write(`<p${directionAttribute(line.spans)}>${escapeHtml(line.text)}</p>`);
|
|
191
|
-
}
|
|
192
|
-
for (const table of tables) {
|
|
193
|
-
if (!emittedTables.has(table)) await write(tableToHtml(table));
|
|
194
424
|
}
|
|
195
425
|
await write("</section>");
|
|
196
426
|
}
|
|
@@ -232,7 +462,7 @@ function visualText(span, pageHeight, fontAliases, counterRotateReflectedText =
|
|
|
232
462
|
const anchorX = span.bounds.x + basisX * rtlOffset;
|
|
233
463
|
const anchorY = pageHeight - span.bounds.y + basisY * rtlOffset;
|
|
234
464
|
const position = transformed ? ` x="0" y="0" transform="matrix(${transform?.map(number).join(" ")} ${number(anchorX)} ${number(anchorY)})"` : ` x="${number(anchorX)}" y="${number(anchorY)}"`;
|
|
235
|
-
return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${
|
|
465
|
+
return `<text${direction}${position} font-size="${number(span.fontSize)}"${textLength}${style ? ` style="${style}"` : ""}>${escapeHtml2(span.text)}</text>`;
|
|
236
466
|
}
|
|
237
467
|
function isAdobeCjkFont(fontFamily) {
|
|
238
468
|
return /^Adobe(?:Heiti|Song|Kaiti|Ming|Gothic|Mincho)Std-/i.test(fontFamily ?? "");
|
|
@@ -345,16 +575,13 @@ function directionAttribute(spans) {
|
|
|
345
575
|
if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
|
|
346
576
|
return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
|
|
347
577
|
}
|
|
348
|
-
function containsY(table, y) {
|
|
349
|
-
return y >= table.bounds.y && y <= table.bounds.y + table.bounds.height;
|
|
350
|
-
}
|
|
351
578
|
function number(value) {
|
|
352
579
|
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
353
580
|
}
|
|
354
581
|
function escapeAttribute(value) {
|
|
355
|
-
return
|
|
582
|
+
return escapeHtml2(value).replaceAll("`", "`");
|
|
356
583
|
}
|
|
357
|
-
function
|
|
584
|
+
function escapeHtml2(value) {
|
|
358
585
|
return [...value].map((character) => {
|
|
359
586
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
360
587
|
if (codePoint === 13) return "\n";
|