@boxpdf/html-writer 0.1.0
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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/index.cjs +138 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +111 -0
- package/dist/index.js.map +1 -0
- package/examples/file.ts +25 -0
- package/examples/http.ts +25 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erik Aronesty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# `@boxpdf/html-writer`
|
|
2
|
+
|
|
3
|
+
Streams text-oriented HTML from pages produced by `@boxpdf/reader`. This is the
|
|
4
|
+
PDF-to-HTML package; it is intentionally named differently from `boxpdf-html`,
|
|
5
|
+
which converts HTML in the opposite direction.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { open } from "node:fs/promises";
|
|
9
|
+
import { openPdf } from "@boxpdf/reader";
|
|
10
|
+
import { fileSource } from "@boxpdf/reader/node";
|
|
11
|
+
import { writeHtmlDocument } from "@boxpdf/html-writer";
|
|
12
|
+
|
|
13
|
+
const source = await fileSource("input.pdf");
|
|
14
|
+
const pdf = await openPdf(source);
|
|
15
|
+
const output = await open("output.html", "w");
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await writeHtmlDocument(pdf.pages(), async (chunk) => {
|
|
19
|
+
await output.write(chunk);
|
|
20
|
+
});
|
|
21
|
+
} finally {
|
|
22
|
+
await output.close();
|
|
23
|
+
pdf.close();
|
|
24
|
+
await source.close();
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The default `positioned` layout preserves text coordinates. The optional
|
|
29
|
+
`flow` layout uses the reader's inferred lines and tables. Images, vector
|
|
30
|
+
graphics, and exact font reproduction are not yet rendered.
|
|
31
|
+
|
|
32
|
+
The callback is awaited for every chunk, so a file stream, HTTP response, or
|
|
33
|
+
Web `WritableStream` can apply backpressure. The caller owns and closes the PDF
|
|
34
|
+
source, reader, and destination; the writer owns only HTML serialization. See
|
|
35
|
+
[`examples/file.ts`](examples/file.ts) for Node file-to-file conversion and
|
|
36
|
+
[`examples/http.ts`](examples/http.ts) for a streaming Web `Response`. Validate
|
|
37
|
+
or allowlist user-provided PDF URLs before passing them to the HTTP example.
|
|
38
|
+
|
|
39
|
+
## Compatibility oracle
|
|
40
|
+
|
|
41
|
+
Tests compare normalized page geometry, text, and anchor positions with
|
|
42
|
+
Poppler's `pdftohtml -c -hidden -noframes -zoom 1` output. Poppler runs only as
|
|
43
|
+
an independent test oracle; its resource use is not part of the writer's memory
|
|
44
|
+
contract.
|
|
45
|
+
|
|
46
|
+
`pnpm poppler:report` runs the positioned writer over all 62 text fixtures in
|
|
47
|
+
the pinned PDF.js corpus. The checked-in baseline currently records exact text
|
|
48
|
+
and geometry agreement on 51 fixtures. The remaining cases are retained in the
|
|
49
|
+
denominator; most exercise intentional PDF.js/Poppler differences in RTL text,
|
|
50
|
+
font encodings, or malformed Unicode maps. `pnpm poppler:gate` rejects any loss
|
|
51
|
+
from the known-good pass set and runs in CI.
|
|
52
|
+
|
|
53
|
+
The writer retains the reader's logical Unicode order. RTL spans and flow lines
|
|
54
|
+
receive `dir="rtl"` plus isolated bidirectional CSS so browsers perform visual
|
|
55
|
+
ordering without changing extracted text. Vertical spans use CSS writing mode.
|
|
56
|
+
Poppler emits visual-order text for several RTL corpus files, so those source
|
|
57
|
+
strings are expected to differ.
|
|
58
|
+
|
|
59
|
+
`issue16224` is the geometry exception: its 531 × 666 point MediaBox contains an
|
|
60
|
+
182.77 × 32.539 point CropBox. PDF.js, `pdfinfo`, and the reader expose the
|
|
61
|
+
CropBox as page size; Poppler 22.02 `pdftohtml` emits the MediaBox. The writer
|
|
62
|
+
keeps the PDF.js-compatible CropBox dimensions.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
pageToHtml: () => pageToHtml,
|
|
24
|
+
writeHtmlDocument: () => writeHtmlDocument,
|
|
25
|
+
writePage: () => writePage
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_structure = require("@boxpdf/reader/structure");
|
|
29
|
+
var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-page-content--90{transform:translateX(100%) rotate(90deg)}.pdf-page-content--180{transform:translate(100%,100%) rotate(180deg)}.pdf-page-content--270{transform:translateY(100%) rotate(270deg)}.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--flow{max-width:60rem;padding:1rem}.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}`;
|
|
30
|
+
async function writeHtmlDocument(pages, write, options = {}) {
|
|
31
|
+
const includeDocument = options.includeDocument ?? true;
|
|
32
|
+
if (includeDocument) {
|
|
33
|
+
await write("<!doctype html><html");
|
|
34
|
+
await write(
|
|
35
|
+
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
36
|
+
);
|
|
37
|
+
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
38
|
+
await write(`<title>${escapeHtml(options.title ?? "PDF document")}</title>`);
|
|
39
|
+
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
40
|
+
await write("</head><body>");
|
|
41
|
+
}
|
|
42
|
+
await write('<main class="pdf-document">');
|
|
43
|
+
for await (const page of pages) await writePage(page, write, options);
|
|
44
|
+
await write("</main>");
|
|
45
|
+
if (includeDocument) await write("</body></html>");
|
|
46
|
+
}
|
|
47
|
+
async function writePage(page, write, options = {}) {
|
|
48
|
+
if ((options.layout ?? "positioned") === "flow") await writeFlowPage(page, write);
|
|
49
|
+
else await writePositionedPage(page, write);
|
|
50
|
+
}
|
|
51
|
+
async function pageToHtml(page, options = {}) {
|
|
52
|
+
let output = "";
|
|
53
|
+
await writePage(
|
|
54
|
+
page,
|
|
55
|
+
(chunk) => {
|
|
56
|
+
output += chunk;
|
|
57
|
+
},
|
|
58
|
+
options
|
|
59
|
+
);
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
async function writePositionedPage(page, write) {
|
|
63
|
+
const quarterTurn = page.rotate === 90 || page.rotate === 270;
|
|
64
|
+
const displayWidth = quarterTurn ? page.height : page.width;
|
|
65
|
+
const displayHeight = quarterTurn ? page.width : page.height;
|
|
66
|
+
await write(
|
|
67
|
+
`<section class="pdf-page pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number(displayWidth)}pt;height:${number(displayHeight)}pt">`
|
|
68
|
+
);
|
|
69
|
+
await write(
|
|
70
|
+
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number(page.width)}pt;height:${number(page.height)}pt">`
|
|
71
|
+
);
|
|
72
|
+
for (const span of page.spans) await write(positionedSpan(span));
|
|
73
|
+
await write("</div></section>");
|
|
74
|
+
}
|
|
75
|
+
async function writeFlowPage(page, write) {
|
|
76
|
+
const structured = (0, import_structure.structurePage)(page);
|
|
77
|
+
const tables = [...structured.tables].sort((left, right) => right.bounds.y - left.bounds.y);
|
|
78
|
+
const emittedTables = /* @__PURE__ */ new Set();
|
|
79
|
+
await write(`<section class="pdf-page pdf-page--flow" data-page="${page.number}">`);
|
|
80
|
+
for (const line of structured.lines) {
|
|
81
|
+
const table = tables.find((candidate) => containsY(candidate, line.bounds.y));
|
|
82
|
+
if (table) {
|
|
83
|
+
if (!emittedTables.has(table)) {
|
|
84
|
+
await write((0, import_structure.tableToHtml)(table));
|
|
85
|
+
emittedTables.add(table);
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
await write(`<p${directionAttribute(line.spans)}>${escapeHtml(line.text)}</p>`);
|
|
90
|
+
}
|
|
91
|
+
for (const table of tables) {
|
|
92
|
+
if (!emittedTables.has(table)) await write((0, import_structure.tableToHtml)(table));
|
|
93
|
+
}
|
|
94
|
+
await write("</section>");
|
|
95
|
+
}
|
|
96
|
+
function positionedSpan(span) {
|
|
97
|
+
const direction = directionAttribute([span]);
|
|
98
|
+
const style = [
|
|
99
|
+
`left:${number(span.bounds.x)}pt`,
|
|
100
|
+
`bottom:${number(span.bounds.y)}pt`,
|
|
101
|
+
`width:${number(span.bounds.width)}pt`,
|
|
102
|
+
`height:${number(span.bounds.height)}pt`,
|
|
103
|
+
`font-size:${number(span.fontSize)}pt`
|
|
104
|
+
].join(";");
|
|
105
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml(span.text)}</span>`;
|
|
106
|
+
}
|
|
107
|
+
function directionAttribute(spans) {
|
|
108
|
+
const rtl = spans.filter((span) => span.direction === "rtl").length;
|
|
109
|
+
const vertical = spans.filter((span) => span.direction === "ttb").length;
|
|
110
|
+
if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
|
|
111
|
+
return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
|
|
112
|
+
}
|
|
113
|
+
function containsY(table, y) {
|
|
114
|
+
return y >= table.bounds.y && y <= table.bounds.y + table.bounds.height;
|
|
115
|
+
}
|
|
116
|
+
function number(value) {
|
|
117
|
+
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
118
|
+
}
|
|
119
|
+
function escapeAttribute(value) {
|
|
120
|
+
return escapeHtml(value).replaceAll("`", "`");
|
|
121
|
+
}
|
|
122
|
+
function escapeHtml(value) {
|
|
123
|
+
return [...value].map((character) => {
|
|
124
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
125
|
+
if (codePoint === 13) return "\n";
|
|
126
|
+
return isForbiddenControl(codePoint) ? "\uFFFD" : character;
|
|
127
|
+
}).join("").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
128
|
+
}
|
|
129
|
+
function isForbiddenControl(codePoint) {
|
|
130
|
+
return codePoint <= 8 || codePoint === 11 || codePoint === 12 || codePoint >= 14 && codePoint <= 31 || codePoint === 127;
|
|
131
|
+
}
|
|
132
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
133
|
+
0 && (module.exports = {
|
|
134
|
+
pageToHtml,
|
|
135
|
+
writeHtmlDocument,
|
|
136
|
+
writePage
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { ExtractedPage, TextSpan } from \"@boxpdf/reader\";\nimport { structurePage, type Table, tableToHtml } from \"@boxpdf/reader/structure\";\n\nexport type HtmlLayout = \"positioned\" | \"flow\";\nexport type HtmlWrite = (chunk: string) => void | Promise<void>;\n\nexport interface HtmlWriterOptions {\n layout?: HtmlLayout;\n title?: string;\n language?: string;\n includeDocument?: boolean;\n includeStyles?: boolean;\n}\n\nconst styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-page-content--90{transform:translateX(100%) rotate(90deg)}.pdf-page-content--180{transform:translate(100%,100%) rotate(180deg)}.pdf-page-content--270{transform:translateY(100%) rotate(270deg)}.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--flow{max-width:60rem;padding:1rem}.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}`;\n\nexport async function writeHtmlDocument(\n pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>,\n write: HtmlWrite,\n options: HtmlWriterOptions = {},\n): Promise<void> {\n const includeDocument = options.includeDocument ?? true;\n if (includeDocument) {\n await write(\"<!doctype html><html\");\n await write(\n ` lang=\"${escapeAttribute(options.language ?? \"en\")}\"><head><meta charset=\"utf-8\">`,\n );\n await write('<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">');\n await write(`<title>${escapeHtml(options.title ?? \"PDF document\")}</title>`);\n if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);\n await write(\"</head><body>\");\n }\n await write('<main class=\"pdf-document\">');\n for await (const page of pages) await writePage(page, write, options);\n await write(\"</main>\");\n if (includeDocument) await write(\"</body></html>\");\n}\n\nexport async function writePage(\n page: ExtractedPage,\n write: HtmlWrite,\n options: HtmlWriterOptions = {},\n): Promise<void> {\n if ((options.layout ?? \"positioned\") === \"flow\") await writeFlowPage(page, write);\n else await writePositionedPage(page, write);\n}\n\nexport async function pageToHtml(\n page: ExtractedPage,\n options: HtmlWriterOptions = {},\n): Promise<string> {\n let output = \"\";\n await writePage(\n page,\n (chunk) => {\n output += chunk;\n },\n options,\n );\n return output;\n}\n\nasync function writePositionedPage(page: ExtractedPage, write: HtmlWrite): Promise<void> {\n const quarterTurn = page.rotate === 90 || page.rotate === 270;\n const displayWidth = quarterTurn ? page.height : page.width;\n const displayHeight = quarterTurn ? page.width : page.height;\n await write(\n `<section class=\"pdf-page pdf-page--positioned\" data-page=\"${page.number}\" data-rotate=\"${page.rotate}\" style=\"width:${number(displayWidth)}pt;height:${number(displayHeight)}pt\">`,\n );\n await write(\n `<div class=\"pdf-page-content pdf-page-content--${page.rotate}\" style=\"width:${number(page.width)}pt;height:${number(page.height)}pt\">`,\n );\n for (const span of page.spans) await write(positionedSpan(span));\n await write(\"</div></section>\");\n}\n\nasync function writeFlowPage(page: ExtractedPage, write: HtmlWrite): Promise<void> {\n const structured = structurePage(page);\n const tables = [...structured.tables].sort((left, right) => right.bounds.y - left.bounds.y);\n const emittedTables = new Set<Table>();\n await write(`<section class=\"pdf-page pdf-page--flow\" data-page=\"${page.number}\">`);\n for (const line of structured.lines) {\n const table = tables.find((candidate) => containsY(candidate, line.bounds.y));\n if (table) {\n if (!emittedTables.has(table)) {\n await write(tableToHtml(table));\n emittedTables.add(table);\n }\n continue;\n }\n await write(`<p${directionAttribute(line.spans)}>${escapeHtml(line.text)}</p>`);\n }\n for (const table of tables) {\n if (!emittedTables.has(table)) await write(tableToHtml(table));\n }\n await write(\"</section>\");\n}\n\nfunction positionedSpan(span: TextSpan): string {\n const direction = directionAttribute([span]);\n const style = [\n `left:${number(span.bounds.x)}pt`,\n `bottom:${number(span.bounds.y)}pt`,\n `width:${number(span.bounds.width)}pt`,\n `height:${number(span.bounds.height)}pt`,\n `font-size:${number(span.fontSize)}pt`,\n ].join(\";\");\n return `<span class=\"pdf-span\"${direction} style=\"${style}\">${escapeHtml(span.text)}</span>`;\n}\n\nfunction directionAttribute(spans: TextSpan[]): string {\n const rtl = spans.filter((span) => span.direction === \"rtl\").length;\n const vertical = spans.filter((span) => span.direction === \"ttb\").length;\n if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction=\"ttb\"';\n return rtl * 2 >= spans.length && spans.length > 0 ? ' dir=\"rtl\"' : \"\";\n}\n\nfunction containsY(table: Table, y: number): boolean {\n return y >= table.bounds.y && y <= table.bounds.y + table.bounds.height;\n}\n\nfunction number(value: number): string {\n return Number.isFinite(value) ? String(Math.round(value * 1000) / 1000) : \"0\";\n}\n\nfunction escapeAttribute(value: string): string {\n return escapeHtml(value).replaceAll(\"`\", \"`\");\n}\n\nfunction escapeHtml(value: string): string {\n return [...value]\n .map((character) => {\n const codePoint = character.codePointAt(0) ?? 0;\n if (codePoint === 13) return \"\\n\";\n return isForbiddenControl(codePoint) ? \"�\" : character;\n })\n .join(\"\")\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\nfunction isForbiddenControl(codePoint: number): boolean {\n return (\n codePoint <= 8 ||\n codePoint === 11 ||\n codePoint === 12 ||\n (codePoint >= 14 && codePoint <= 31) ||\n codePoint === 127\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ExtractedPage } from '@boxpdf/reader';
|
|
2
|
+
|
|
3
|
+
type HtmlLayout = "positioned" | "flow";
|
|
4
|
+
type HtmlWrite = (chunk: string) => void | Promise<void>;
|
|
5
|
+
interface HtmlWriterOptions {
|
|
6
|
+
layout?: HtmlLayout;
|
|
7
|
+
title?: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
includeDocument?: boolean;
|
|
10
|
+
includeStyles?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare function writeHtmlDocument(pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
13
|
+
declare function writePage(page: ExtractedPage, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
14
|
+
declare function pageToHtml(page: ExtractedPage, options?: HtmlWriterOptions): Promise<string>;
|
|
15
|
+
|
|
16
|
+
export { type HtmlLayout, type HtmlWrite, type HtmlWriterOptions, pageToHtml, writeHtmlDocument, writePage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ExtractedPage } from '@boxpdf/reader';
|
|
2
|
+
|
|
3
|
+
type HtmlLayout = "positioned" | "flow";
|
|
4
|
+
type HtmlWrite = (chunk: string) => void | Promise<void>;
|
|
5
|
+
interface HtmlWriterOptions {
|
|
6
|
+
layout?: HtmlLayout;
|
|
7
|
+
title?: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
includeDocument?: boolean;
|
|
10
|
+
includeStyles?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare function writeHtmlDocument(pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
13
|
+
declare function writePage(page: ExtractedPage, write: HtmlWrite, options?: HtmlWriterOptions): Promise<void>;
|
|
14
|
+
declare function pageToHtml(page: ExtractedPage, options?: HtmlWriterOptions): Promise<string>;
|
|
15
|
+
|
|
16
|
+
export { type HtmlLayout, type HtmlWrite, type HtmlWriterOptions, pageToHtml, writeHtmlDocument, writePage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { structurePage, tableToHtml } from "@boxpdf/reader/structure";
|
|
3
|
+
var styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-page-content--90{transform:translateX(100%) rotate(90deg)}.pdf-page-content--180{transform:translate(100%,100%) rotate(180deg)}.pdf-page-content--270{transform:translateY(100%) rotate(270deg)}.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--flow{max-width:60rem;padding:1rem}.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
|
+
async function writeHtmlDocument(pages, write, options = {}) {
|
|
5
|
+
const includeDocument = options.includeDocument ?? true;
|
|
6
|
+
if (includeDocument) {
|
|
7
|
+
await write("<!doctype html><html");
|
|
8
|
+
await write(
|
|
9
|
+
` lang="${escapeAttribute(options.language ?? "en")}"><head><meta charset="utf-8">`
|
|
10
|
+
);
|
|
11
|
+
await write('<meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
12
|
+
await write(`<title>${escapeHtml(options.title ?? "PDF document")}</title>`);
|
|
13
|
+
if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);
|
|
14
|
+
await write("</head><body>");
|
|
15
|
+
}
|
|
16
|
+
await write('<main class="pdf-document">');
|
|
17
|
+
for await (const page of pages) await writePage(page, write, options);
|
|
18
|
+
await write("</main>");
|
|
19
|
+
if (includeDocument) await write("</body></html>");
|
|
20
|
+
}
|
|
21
|
+
async function writePage(page, write, options = {}) {
|
|
22
|
+
if ((options.layout ?? "positioned") === "flow") await writeFlowPage(page, write);
|
|
23
|
+
else await writePositionedPage(page, write);
|
|
24
|
+
}
|
|
25
|
+
async function pageToHtml(page, options = {}) {
|
|
26
|
+
let output = "";
|
|
27
|
+
await writePage(
|
|
28
|
+
page,
|
|
29
|
+
(chunk) => {
|
|
30
|
+
output += chunk;
|
|
31
|
+
},
|
|
32
|
+
options
|
|
33
|
+
);
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
async function writePositionedPage(page, write) {
|
|
37
|
+
const quarterTurn = page.rotate === 90 || page.rotate === 270;
|
|
38
|
+
const displayWidth = quarterTurn ? page.height : page.width;
|
|
39
|
+
const displayHeight = quarterTurn ? page.width : page.height;
|
|
40
|
+
await write(
|
|
41
|
+
`<section class="pdf-page pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number(displayWidth)}pt;height:${number(displayHeight)}pt">`
|
|
42
|
+
);
|
|
43
|
+
await write(
|
|
44
|
+
`<div class="pdf-page-content pdf-page-content--${page.rotate}" style="width:${number(page.width)}pt;height:${number(page.height)}pt">`
|
|
45
|
+
);
|
|
46
|
+
for (const span of page.spans) await write(positionedSpan(span));
|
|
47
|
+
await write("</div></section>");
|
|
48
|
+
}
|
|
49
|
+
async function writeFlowPage(page, write) {
|
|
50
|
+
const structured = structurePage(page);
|
|
51
|
+
const tables = [...structured.tables].sort((left, right) => right.bounds.y - left.bounds.y);
|
|
52
|
+
const emittedTables = /* @__PURE__ */ new Set();
|
|
53
|
+
await write(`<section class="pdf-page pdf-page--flow" data-page="${page.number}">`);
|
|
54
|
+
for (const line of structured.lines) {
|
|
55
|
+
const table = tables.find((candidate) => containsY(candidate, line.bounds.y));
|
|
56
|
+
if (table) {
|
|
57
|
+
if (!emittedTables.has(table)) {
|
|
58
|
+
await write(tableToHtml(table));
|
|
59
|
+
emittedTables.add(table);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
await write(`<p${directionAttribute(line.spans)}>${escapeHtml(line.text)}</p>`);
|
|
64
|
+
}
|
|
65
|
+
for (const table of tables) {
|
|
66
|
+
if (!emittedTables.has(table)) await write(tableToHtml(table));
|
|
67
|
+
}
|
|
68
|
+
await write("</section>");
|
|
69
|
+
}
|
|
70
|
+
function positionedSpan(span) {
|
|
71
|
+
const direction = directionAttribute([span]);
|
|
72
|
+
const style = [
|
|
73
|
+
`left:${number(span.bounds.x)}pt`,
|
|
74
|
+
`bottom:${number(span.bounds.y)}pt`,
|
|
75
|
+
`width:${number(span.bounds.width)}pt`,
|
|
76
|
+
`height:${number(span.bounds.height)}pt`,
|
|
77
|
+
`font-size:${number(span.fontSize)}pt`
|
|
78
|
+
].join(";");
|
|
79
|
+
return `<span class="pdf-span"${direction} style="${style}">${escapeHtml(span.text)}</span>`;
|
|
80
|
+
}
|
|
81
|
+
function directionAttribute(spans) {
|
|
82
|
+
const rtl = spans.filter((span) => span.direction === "rtl").length;
|
|
83
|
+
const vertical = spans.filter((span) => span.direction === "ttb").length;
|
|
84
|
+
if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction="ttb"';
|
|
85
|
+
return rtl * 2 >= spans.length && spans.length > 0 ? ' dir="rtl"' : "";
|
|
86
|
+
}
|
|
87
|
+
function containsY(table, y) {
|
|
88
|
+
return y >= table.bounds.y && y <= table.bounds.y + table.bounds.height;
|
|
89
|
+
}
|
|
90
|
+
function number(value) {
|
|
91
|
+
return Number.isFinite(value) ? String(Math.round(value * 1e3) / 1e3) : "0";
|
|
92
|
+
}
|
|
93
|
+
function escapeAttribute(value) {
|
|
94
|
+
return escapeHtml(value).replaceAll("`", "`");
|
|
95
|
+
}
|
|
96
|
+
function escapeHtml(value) {
|
|
97
|
+
return [...value].map((character) => {
|
|
98
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
99
|
+
if (codePoint === 13) return "\n";
|
|
100
|
+
return isForbiddenControl(codePoint) ? "\uFFFD" : character;
|
|
101
|
+
}).join("").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
102
|
+
}
|
|
103
|
+
function isForbiddenControl(codePoint) {
|
|
104
|
+
return codePoint <= 8 || codePoint === 11 || codePoint === 12 || codePoint >= 14 && codePoint <= 31 || codePoint === 127;
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
pageToHtml,
|
|
108
|
+
writeHtmlDocument,
|
|
109
|
+
writePage
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { ExtractedPage, TextSpan } from \"@boxpdf/reader\";\nimport { structurePage, type Table, tableToHtml } from \"@boxpdf/reader/structure\";\n\nexport type HtmlLayout = \"positioned\" | \"flow\";\nexport type HtmlWrite = (chunk: string) => void | Promise<void>;\n\nexport interface HtmlWriterOptions {\n layout?: HtmlLayout;\n title?: string;\n language?: string;\n includeDocument?: boolean;\n includeStyles?: boolean;\n}\n\nconst styles = `.pdf-document{margin:0 auto}.pdf-page{box-sizing:border-box;margin:1rem auto;background:#fff;color:#000}.pdf-page--positioned{position:relative;overflow:hidden}.pdf-page-content{position:absolute;transform-origin:0 0}.pdf-page-content--90{transform:translateX(100%) rotate(90deg)}.pdf-page-content--180{transform:translate(100%,100%) rotate(180deg)}.pdf-page-content--270{transform:translateY(100%) rotate(270deg)}.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--flow{max-width:60rem;padding:1rem}.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}`;\n\nexport async function writeHtmlDocument(\n pages: AsyncIterable<ExtractedPage> | Iterable<ExtractedPage>,\n write: HtmlWrite,\n options: HtmlWriterOptions = {},\n): Promise<void> {\n const includeDocument = options.includeDocument ?? true;\n if (includeDocument) {\n await write(\"<!doctype html><html\");\n await write(\n ` lang=\"${escapeAttribute(options.language ?? \"en\")}\"><head><meta charset=\"utf-8\">`,\n );\n await write('<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">');\n await write(`<title>${escapeHtml(options.title ?? \"PDF document\")}</title>`);\n if (options.includeStyles ?? true) await write(`<style>${styles}</style>`);\n await write(\"</head><body>\");\n }\n await write('<main class=\"pdf-document\">');\n for await (const page of pages) await writePage(page, write, options);\n await write(\"</main>\");\n if (includeDocument) await write(\"</body></html>\");\n}\n\nexport async function writePage(\n page: ExtractedPage,\n write: HtmlWrite,\n options: HtmlWriterOptions = {},\n): Promise<void> {\n if ((options.layout ?? \"positioned\") === \"flow\") await writeFlowPage(page, write);\n else await writePositionedPage(page, write);\n}\n\nexport async function pageToHtml(\n page: ExtractedPage,\n options: HtmlWriterOptions = {},\n): Promise<string> {\n let output = \"\";\n await writePage(\n page,\n (chunk) => {\n output += chunk;\n },\n options,\n );\n return output;\n}\n\nasync function writePositionedPage(page: ExtractedPage, write: HtmlWrite): Promise<void> {\n const quarterTurn = page.rotate === 90 || page.rotate === 270;\n const displayWidth = quarterTurn ? page.height : page.width;\n const displayHeight = quarterTurn ? page.width : page.height;\n await write(\n `<section class=\"pdf-page pdf-page--positioned\" data-page=\"${page.number}\" data-rotate=\"${page.rotate}\" style=\"width:${number(displayWidth)}pt;height:${number(displayHeight)}pt\">`,\n );\n await write(\n `<div class=\"pdf-page-content pdf-page-content--${page.rotate}\" style=\"width:${number(page.width)}pt;height:${number(page.height)}pt\">`,\n );\n for (const span of page.spans) await write(positionedSpan(span));\n await write(\"</div></section>\");\n}\n\nasync function writeFlowPage(page: ExtractedPage, write: HtmlWrite): Promise<void> {\n const structured = structurePage(page);\n const tables = [...structured.tables].sort((left, right) => right.bounds.y - left.bounds.y);\n const emittedTables = new Set<Table>();\n await write(`<section class=\"pdf-page pdf-page--flow\" data-page=\"${page.number}\">`);\n for (const line of structured.lines) {\n const table = tables.find((candidate) => containsY(candidate, line.bounds.y));\n if (table) {\n if (!emittedTables.has(table)) {\n await write(tableToHtml(table));\n emittedTables.add(table);\n }\n continue;\n }\n await write(`<p${directionAttribute(line.spans)}>${escapeHtml(line.text)}</p>`);\n }\n for (const table of tables) {\n if (!emittedTables.has(table)) await write(tableToHtml(table));\n }\n await write(\"</section>\");\n}\n\nfunction positionedSpan(span: TextSpan): string {\n const direction = directionAttribute([span]);\n const style = [\n `left:${number(span.bounds.x)}pt`,\n `bottom:${number(span.bounds.y)}pt`,\n `width:${number(span.bounds.width)}pt`,\n `height:${number(span.bounds.height)}pt`,\n `font-size:${number(span.fontSize)}pt`,\n ].join(\";\");\n return `<span class=\"pdf-span\"${direction} style=\"${style}\">${escapeHtml(span.text)}</span>`;\n}\n\nfunction directionAttribute(spans: TextSpan[]): string {\n const rtl = spans.filter((span) => span.direction === \"rtl\").length;\n const vertical = spans.filter((span) => span.direction === \"ttb\").length;\n if (vertical > rtl && vertical * 2 >= spans.length) return ' data-direction=\"ttb\"';\n return rtl * 2 >= spans.length && spans.length > 0 ? ' dir=\"rtl\"' : \"\";\n}\n\nfunction containsY(table: Table, y: number): boolean {\n return y >= table.bounds.y && y <= table.bounds.y + table.bounds.height;\n}\n\nfunction number(value: number): string {\n return Number.isFinite(value) ? String(Math.round(value * 1000) / 1000) : \"0\";\n}\n\nfunction escapeAttribute(value: string): string {\n return escapeHtml(value).replaceAll(\"`\", \"`\");\n}\n\nfunction escapeHtml(value: string): string {\n return [...value]\n .map((character) => {\n const codePoint = character.codePointAt(0) ?? 0;\n if (codePoint === 13) return \"\\n\";\n return isForbiddenControl(codePoint) ? \"�\" : character;\n })\n .join(\"\")\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\nfunction isForbiddenControl(codePoint: number): boolean {\n return (\n codePoint <= 8 ||\n codePoint === 11 ||\n codePoint === 12 ||\n (codePoint >= 14 && codePoint <= 31) ||\n codePoint === 127\n );\n}\n"],"mappings":"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","names":[]}
|
package/examples/file.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { finished } from "node:stream/promises";
|
|
4
|
+
import { writeHtmlDocument } from "@boxpdf/html-writer";
|
|
5
|
+
import { openPdf } from "@boxpdf/reader";
|
|
6
|
+
import { fileSource } from "@boxpdf/reader/node";
|
|
7
|
+
|
|
8
|
+
const [inputPath, outputPath] = process.argv.slice(2);
|
|
9
|
+
if (!inputPath || !outputPath) throw new Error("usage: file.ts input.pdf output.html");
|
|
10
|
+
|
|
11
|
+
const source = await fileSource(inputPath);
|
|
12
|
+
const reader = await openPdf(source);
|
|
13
|
+
const output = createWriteStream(outputPath, { encoding: "utf8" });
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await writeHtmlDocument(reader.pages(), async (chunk) => {
|
|
17
|
+
if (!output.write(chunk)) await once(output, "drain");
|
|
18
|
+
});
|
|
19
|
+
output.end();
|
|
20
|
+
await finished(output);
|
|
21
|
+
} finally {
|
|
22
|
+
output.destroy();
|
|
23
|
+
reader.close();
|
|
24
|
+
await source.close();
|
|
25
|
+
}
|
package/examples/http.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { writeHtmlDocument } from "@boxpdf/html-writer";
|
|
2
|
+
import { httpSource, openPdf } from "@boxpdf/reader";
|
|
3
|
+
|
|
4
|
+
export async function pdfUrlToHtmlResponse(pdfUrl: URL): Promise<Response> {
|
|
5
|
+
const source = await httpSource(pdfUrl);
|
|
6
|
+
const reader = await openPdf(source);
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const output = new TransformStream<Uint8Array, Uint8Array>();
|
|
9
|
+
const writer = output.writable.getWriter();
|
|
10
|
+
|
|
11
|
+
void (async () => {
|
|
12
|
+
try {
|
|
13
|
+
await writeHtmlDocument(reader.pages(), (chunk) => writer.write(encoder.encode(chunk)));
|
|
14
|
+
await writer.close();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
await writer.abort(error);
|
|
17
|
+
} finally {
|
|
18
|
+
reader.close();
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
21
|
+
|
|
22
|
+
return new Response(output.readable, {
|
|
23
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
24
|
+
});
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boxpdf/html-writer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stream PDF pages from @boxpdf/reader to accessible or positioned HTML.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pdf",
|
|
7
|
+
"pdf-to-html",
|
|
8
|
+
"html-writer",
|
|
9
|
+
"streaming-pdf",
|
|
10
|
+
"text-extraction"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Erik Aronesty",
|
|
15
|
+
"email": "erik@q32.com"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/earonesty/streaming-pdf-reader#html-writer",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/earonesty/streaming-pdf-reader.git",
|
|
21
|
+
"directory": "packages/html-writer"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/index.d.cts",
|
|
36
|
+
"default": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"examples",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean",
|
|
47
|
+
"check": "biome check --error-on-warnings . && tsc --noEmit",
|
|
48
|
+
"test": "vitest run --coverage",
|
|
49
|
+
"package:check": "publint && pnpm pack --dry-run",
|
|
50
|
+
"quality": "pnpm check && pnpm test && pnpm build && pnpm package:check",
|
|
51
|
+
"prepublishOnly": "pnpm quality"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@boxpdf/reader": "workspace:^"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@biomejs/biome": "^2.5.10",
|
|
58
|
+
"@boxpdf/reader": "workspace:*",
|
|
59
|
+
"@types/node": "^24.3.0",
|
|
60
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
61
|
+
"publint": "^0.3.24",
|
|
62
|
+
"tsup": "^8.5.0",
|
|
63
|
+
"typescript": "^5.9.2",
|
|
64
|
+
"vitest": "^4.0.0"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=20"
|
|
71
|
+
}
|
|
72
|
+
}
|