@telorun/pdf 0.3.0 → 0.8.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/README.md +5 -3
- package/dist/text-controller.d.ts +39 -0
- package/dist/text-controller.js +86 -0
- package/package.json +8 -3
- package/src/text-controller.ts +117 -0
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ Built to be language-agnostic and infinitely extensible.
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# Reconcile your manifest into a running backend
|
|
26
|
-
$ telo ./examples/
|
|
26
|
+
$ telo ./examples/todo-app
|
|
27
27
|
|
|
28
|
-
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:
|
|
28
|
+
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8077"}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Why use Telo?
|
|
@@ -48,7 +48,9 @@ See [examples/](./examples/) for a list of working applications.
|
|
|
48
48
|
|
|
49
49
|
## Status
|
|
50
50
|
|
|
51
|
-
Telo is under
|
|
51
|
+
Telo is under heavy development. While it is pre-1.0, breaking changes ship in **minor** releases — manifest shapes, kind schemas, and APIs can change between versions. Pin your imports and expect to update manifests when you upgrade. The core runtime, module system, and standard library are functional, but Telo is not yet recommended for production use.
|
|
52
|
+
|
|
53
|
+
**1.0 lands when the Rust kernel reaches feature parity with the Node.js implementation** — that is the milestone that freezes the manifest contract across runtimes.
|
|
52
54
|
|
|
53
55
|
## The Meaning of Telo
|
|
54
56
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
+
interface TextResource {
|
|
3
|
+
metadata: {
|
|
4
|
+
name: string;
|
|
5
|
+
module?: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
interface TextInputs {
|
|
9
|
+
data: Uint8Array;
|
|
10
|
+
page?: number;
|
|
11
|
+
}
|
|
12
|
+
interface TextOutputs {
|
|
13
|
+
text: string;
|
|
14
|
+
pages: string[];
|
|
15
|
+
pageCount: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads the text a PDF actually renders.
|
|
19
|
+
*
|
|
20
|
+
* The counterpart to `Pdf.Rasterizer`: rasterizing answers *what does this page
|
|
21
|
+
* look like*, which is a question about pixels and moves with the platform's
|
|
22
|
+
* font rasterization. This answers *what does it say*, which is what an
|
|
23
|
+
* assertion about a generated document needs — a rendered page image cannot
|
|
24
|
+
* tell a correct table from an empty one.
|
|
25
|
+
*
|
|
26
|
+
* Items are joined in the order pdf.js reports them, with a line break at each
|
|
27
|
+
* end-of-line marker. That is reading order for ordinary flowed content; it is
|
|
28
|
+
* not a layout reconstruction, and a multi-column page reads column by column
|
|
29
|
+
* the way the producer wrote it.
|
|
30
|
+
*/
|
|
31
|
+
declare class PdfText implements ResourceInstance<TextInputs, TextOutputs> {
|
|
32
|
+
private readonly resource;
|
|
33
|
+
constructor(resource: TextResource);
|
|
34
|
+
invoke(inputs: TextInputs): Promise<TextOutputs>;
|
|
35
|
+
snapshot(): Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
export declare function register(): void;
|
|
38
|
+
export declare function create(resource: TextResource, _ctx: ResourceContext): Promise<PdfText>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
// The legacy build is pdf.js's Node target — the same one the rasterizer loads.
|
|
4
|
+
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
5
|
+
import { InvokeError } from "@telorun/sdk";
|
|
6
|
+
const PDFJS_ROOT = dirname(createRequire(import.meta.url).resolve("pdfjs-dist/package.json"));
|
|
7
|
+
const ASSET_OPTIONS = {
|
|
8
|
+
standardFontDataUrl: join(PDFJS_ROOT, "standard_fonts") + "/",
|
|
9
|
+
cMapUrl: join(PDFJS_ROOT, "cmaps") + "/",
|
|
10
|
+
cMapPacked: true,
|
|
11
|
+
wasmUrl: join(PDFJS_ROOT, "wasm") + "/",
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Reads the text a PDF actually renders.
|
|
15
|
+
*
|
|
16
|
+
* The counterpart to `Pdf.Rasterizer`: rasterizing answers *what does this page
|
|
17
|
+
* look like*, which is a question about pixels and moves with the platform's
|
|
18
|
+
* font rasterization. This answers *what does it say*, which is what an
|
|
19
|
+
* assertion about a generated document needs — a rendered page image cannot
|
|
20
|
+
* tell a correct table from an empty one.
|
|
21
|
+
*
|
|
22
|
+
* Items are joined in the order pdf.js reports them, with a line break at each
|
|
23
|
+
* end-of-line marker. That is reading order for ordinary flowed content; it is
|
|
24
|
+
* not a layout reconstruction, and a multi-column page reads column by column
|
|
25
|
+
* the way the producer wrote it.
|
|
26
|
+
*/
|
|
27
|
+
class PdfText {
|
|
28
|
+
resource;
|
|
29
|
+
constructor(resource) {
|
|
30
|
+
this.resource = resource;
|
|
31
|
+
}
|
|
32
|
+
async invoke(inputs) {
|
|
33
|
+
const name = this.resource.metadata.name;
|
|
34
|
+
const data = inputs?.data;
|
|
35
|
+
if (!(data instanceof Uint8Array)) {
|
|
36
|
+
throw new InvokeError("ERR_INVALID_INPUT", `Pdf.Text "${name}": 'data' must be a Uint8Array of PDF bytes; got ${typeof data}.`);
|
|
37
|
+
}
|
|
38
|
+
// pdf.js transfers the buffer it is given — hand it a copy so the caller's
|
|
39
|
+
// bytes survive a second read.
|
|
40
|
+
const task = getDocument({ data: new Uint8Array(data), ...ASSET_OPTIONS });
|
|
41
|
+
try {
|
|
42
|
+
const doc = await task.promise.catch((err) => {
|
|
43
|
+
throw new InvokeError("ERR_INVALID_INPUT", `Pdf.Text "${name}": failed to parse PDF — ${err instanceof Error ? err.message : String(err)}`);
|
|
44
|
+
});
|
|
45
|
+
const requested = inputs.page;
|
|
46
|
+
if (requested !== undefined && requested > doc.numPages) {
|
|
47
|
+
throw new InvokeError("ERR_INVALID_INPUT", `Pdf.Text "${name}": page ${requested} is out of range; document has ${doc.numPages} page(s).`);
|
|
48
|
+
}
|
|
49
|
+
const first = requested ?? 1;
|
|
50
|
+
const last = requested ?? doc.numPages;
|
|
51
|
+
const pages = [];
|
|
52
|
+
for (let n = first; n <= last; n++) {
|
|
53
|
+
const page = await doc.getPage(n);
|
|
54
|
+
try {
|
|
55
|
+
pages.push(renderTextContent(await page.getTextContent()));
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
page.cleanup();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { text: pages.join("\n"), pages, pageCount: doc.numPages };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await task.destroy();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
snapshot() {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function renderTextContent(content) {
|
|
72
|
+
let out = "";
|
|
73
|
+
for (const raw of content.items) {
|
|
74
|
+
const item = raw;
|
|
75
|
+
if (typeof item.str !== "string")
|
|
76
|
+
continue;
|
|
77
|
+
out += item.str;
|
|
78
|
+
if (item.hasEOL)
|
|
79
|
+
out += "\n";
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
export function register() { }
|
|
84
|
+
export async function create(resource, _ctx) {
|
|
85
|
+
return new PdfText(resource);
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/pdf",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "PDF primitives — rasterize pages to PNG images and author editable AcroForm fields.",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "PDF primitives — rasterize pages to PNG images, read the text a document renders, and author editable AcroForm fields.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
7
7
|
"pdf",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"types": "./dist/form-fields-controller.d.ts",
|
|
39
39
|
"bun": "./src/form-fields-controller.ts",
|
|
40
40
|
"import": "./dist/form-fields-controller.js"
|
|
41
|
+
},
|
|
42
|
+
"./text": {
|
|
43
|
+
"types": "./dist/text-controller.d.ts",
|
|
44
|
+
"bun": "./src/text-controller.ts",
|
|
45
|
+
"import": "./dist/text-controller.js"
|
|
41
46
|
}
|
|
42
47
|
},
|
|
43
48
|
"files": [
|
|
@@ -47,7 +52,7 @@
|
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"@types/node": "^20.0.0",
|
|
49
54
|
"typescript": "^5.0.0",
|
|
50
|
-
"@telorun/sdk": "0.
|
|
55
|
+
"@telorun/sdk": "0.82.0"
|
|
51
56
|
},
|
|
52
57
|
"peerDependencies": {
|
|
53
58
|
"@telorun/sdk": "*"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
// The legacy build is pdf.js's Node target — the same one the rasterizer loads.
|
|
4
|
+
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
5
|
+
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
6
|
+
import { InvokeError } from "@telorun/sdk";
|
|
7
|
+
|
|
8
|
+
const PDFJS_ROOT = dirname(createRequire(import.meta.url).resolve("pdfjs-dist/package.json"));
|
|
9
|
+
const ASSET_OPTIONS = {
|
|
10
|
+
standardFontDataUrl: join(PDFJS_ROOT, "standard_fonts") + "/",
|
|
11
|
+
cMapUrl: join(PDFJS_ROOT, "cmaps") + "/",
|
|
12
|
+
cMapPacked: true,
|
|
13
|
+
wasmUrl: join(PDFJS_ROOT, "wasm") + "/",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
interface TextResource {
|
|
17
|
+
metadata: { name: string; module?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TextInputs {
|
|
21
|
+
data: Uint8Array;
|
|
22
|
+
page?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TextOutputs {
|
|
26
|
+
text: string;
|
|
27
|
+
pages: string[];
|
|
28
|
+
pageCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reads the text a PDF actually renders.
|
|
33
|
+
*
|
|
34
|
+
* The counterpart to `Pdf.Rasterizer`: rasterizing answers *what does this page
|
|
35
|
+
* look like*, which is a question about pixels and moves with the platform's
|
|
36
|
+
* font rasterization. This answers *what does it say*, which is what an
|
|
37
|
+
* assertion about a generated document needs — a rendered page image cannot
|
|
38
|
+
* tell a correct table from an empty one.
|
|
39
|
+
*
|
|
40
|
+
* Items are joined in the order pdf.js reports them, with a line break at each
|
|
41
|
+
* end-of-line marker. That is reading order for ordinary flowed content; it is
|
|
42
|
+
* not a layout reconstruction, and a multi-column page reads column by column
|
|
43
|
+
* the way the producer wrote it.
|
|
44
|
+
*/
|
|
45
|
+
class PdfText implements ResourceInstance<TextInputs, TextOutputs> {
|
|
46
|
+
constructor(private readonly resource: TextResource) {}
|
|
47
|
+
|
|
48
|
+
async invoke(inputs: TextInputs): Promise<TextOutputs> {
|
|
49
|
+
const name = this.resource.metadata.name;
|
|
50
|
+
const data = inputs?.data;
|
|
51
|
+
if (!(data instanceof Uint8Array)) {
|
|
52
|
+
throw new InvokeError(
|
|
53
|
+
"ERR_INVALID_INPUT",
|
|
54
|
+
`Pdf.Text "${name}": 'data' must be a Uint8Array of PDF bytes; got ${typeof data}.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// pdf.js transfers the buffer it is given — hand it a copy so the caller's
|
|
59
|
+
// bytes survive a second read.
|
|
60
|
+
const task = getDocument({ data: new Uint8Array(data), ...ASSET_OPTIONS });
|
|
61
|
+
try {
|
|
62
|
+
const doc = await task.promise.catch((err: unknown) => {
|
|
63
|
+
throw new InvokeError(
|
|
64
|
+
"ERR_INVALID_INPUT",
|
|
65
|
+
`Pdf.Text "${name}": failed to parse PDF — ${err instanceof Error ? err.message : String(err)}`,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
const requested = inputs.page;
|
|
69
|
+
if (requested !== undefined && requested > doc.numPages) {
|
|
70
|
+
throw new InvokeError(
|
|
71
|
+
"ERR_INVALID_INPUT",
|
|
72
|
+
`Pdf.Text "${name}": page ${requested} is out of range; document has ${doc.numPages} page(s).`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const first = requested ?? 1;
|
|
76
|
+
const last = requested ?? doc.numPages;
|
|
77
|
+
const pages: string[] = [];
|
|
78
|
+
for (let n = first; n <= last; n++) {
|
|
79
|
+
const page = await doc.getPage(n);
|
|
80
|
+
try {
|
|
81
|
+
pages.push(renderTextContent(await page.getTextContent()));
|
|
82
|
+
} finally {
|
|
83
|
+
page.cleanup();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { text: pages.join("\n"), pages, pageCount: doc.numPages };
|
|
87
|
+
} finally {
|
|
88
|
+
await task.destroy();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
snapshot(): Record<string, unknown> {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface TextItem {
|
|
98
|
+
str?: string;
|
|
99
|
+
hasEOL?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderTextContent(content: { items: unknown[] }): string {
|
|
103
|
+
let out = "";
|
|
104
|
+
for (const raw of content.items) {
|
|
105
|
+
const item = raw as TextItem;
|
|
106
|
+
if (typeof item.str !== "string") continue;
|
|
107
|
+
out += item.str;
|
|
108
|
+
if (item.hasEOL) out += "\n";
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function register(): void {}
|
|
114
|
+
|
|
115
|
+
export async function create(resource: TextResource, _ctx: ResourceContext): Promise<PdfText> {
|
|
116
|
+
return new PdfText(resource);
|
|
117
|
+
}
|