@llamaindex/liteparse 2.13.1 → 2.14.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 +38 -0
- package/dist/cli.js +1236 -398
- package/dist/cli.js.map +1 -1
- package/dist/lib.cjs +712 -0
- package/dist/lib.cjs.map +1 -0
- package/dist/lib.d.cts +911 -0
- package/dist/lib.d.ts +339 -38
- package/dist/lib.js +654 -323
- package/dist/lib.js.map +1 -1
- package/dist/pool-worker.js +253 -0
- package/dist/pool-worker.js.map +1 -0
- package/liteparse.linux-x64-gnu.node +0 -0
- package/package.json +22 -12
- package/dist/cli-json.d.ts +0 -154
- package/dist/cli-json.d.ts.map +0 -1
- package/dist/cli-json.js +0 -241
- package/dist/cli-json.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/lib.d.ts.map +0 -1
- package/dist/native.d.ts +0 -345
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -71
- package/dist/native.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,430 +1,1268 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
2
4
|
import { program } from "commander";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import { join,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
|
|
6
|
+
// src/native.ts
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var require2 = createRequire(import.meta.url);
|
|
11
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
function loadNative() {
|
|
13
|
+
const triples = {
|
|
14
|
+
"darwin-x64": "@llamaindex/liteparse-darwin-x64",
|
|
15
|
+
"darwin-arm64": "@llamaindex/liteparse-darwin-arm64",
|
|
16
|
+
"linux-x64-gnu": "@llamaindex/liteparse-linux-x64-gnu",
|
|
17
|
+
"linux-x64-musl": "@llamaindex/liteparse-linux-x64-musl",
|
|
18
|
+
"linux-arm64-gnu": "@llamaindex/liteparse-linux-arm64-gnu",
|
|
19
|
+
"linux-arm64-musl": "@llamaindex/liteparse-linux-arm64-musl",
|
|
20
|
+
"win32-x64-msvc": "@llamaindex/liteparse-win32-x64-msvc",
|
|
21
|
+
"win32-arm64-msvc": "@llamaindex/liteparse-win32-arm64-msvc"
|
|
22
|
+
};
|
|
23
|
+
const platform = process.platform;
|
|
24
|
+
const arch = process.arch;
|
|
25
|
+
const candidates = [];
|
|
26
|
+
if (platform === "linux") {
|
|
27
|
+
candidates.push(`${platform}-${arch}-gnu`);
|
|
28
|
+
candidates.push(`${platform}-${arch}-musl`);
|
|
29
|
+
} else if (platform === "win32") {
|
|
30
|
+
candidates.push(`${platform}-${arch}-msvc`);
|
|
31
|
+
} else {
|
|
32
|
+
candidates.push(`${platform}-${arch}`);
|
|
33
|
+
}
|
|
34
|
+
for (const key of candidates) {
|
|
35
|
+
const pkg = triples[key];
|
|
36
|
+
if (pkg) {
|
|
37
|
+
try {
|
|
38
|
+
return require2(pkg);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
23
41
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
}
|
|
43
|
+
const searchDirs = [__dirname, join(__dirname, ".."), join(__dirname, "..", "..")];
|
|
44
|
+
const fileNames = [
|
|
45
|
+
...candidates.map((c) => `liteparse.${c}.node`),
|
|
46
|
+
`liteparse.${platform}-${arch}.node`,
|
|
47
|
+
"liteparse.node"
|
|
48
|
+
];
|
|
49
|
+
for (const dir of searchDirs) {
|
|
50
|
+
for (const fileName of fileNames) {
|
|
51
|
+
try {
|
|
52
|
+
return require2(join(dir, fileName));
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
27
55
|
}
|
|
28
|
-
|
|
56
|
+
}
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to load native module for ${platform}-${arch}. Ensure the correct optional dependency is installed.`
|
|
59
|
+
);
|
|
29
60
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
var native = loadNative();
|
|
62
|
+
|
|
63
|
+
// src/pool.ts
|
|
64
|
+
import { fork } from "child_process";
|
|
65
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
66
|
+
var ParseTimeoutError = class extends Error {
|
|
67
|
+
source;
|
|
68
|
+
timeoutMs;
|
|
69
|
+
constructor(message, source, timeoutMs) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "ParseTimeoutError";
|
|
72
|
+
this.source = source;
|
|
73
|
+
this.timeoutMs = timeoutMs;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var WorkerTimeout = class extends Error {
|
|
77
|
+
};
|
|
78
|
+
var WorkerCrashed = class extends Error {
|
|
79
|
+
};
|
|
80
|
+
var WORKER_PATH = fileURLToPath2(new URL("./pool-worker.js", import.meta.url));
|
|
81
|
+
function reviveBuffers(result) {
|
|
82
|
+
for (const image of result.images ?? []) {
|
|
83
|
+
if (image.bytes && !Buffer.isBuffer(image.bytes)) {
|
|
84
|
+
const b = image.bytes;
|
|
85
|
+
image.bytes = Buffer.from(b.buffer, b.byteOffset, b.byteLength);
|
|
35
86
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
}
|
|
88
|
+
for (const shot of result.screenshots ?? []) {
|
|
89
|
+
if (shot.imageBuffer && !Buffer.isBuffer(shot.imageBuffer)) {
|
|
90
|
+
const b = shot.imageBuffer;
|
|
91
|
+
shot.imageBuffer = Buffer.from(b.buffer, b.byteOffset, b.byteLength);
|
|
39
92
|
}
|
|
40
|
-
|
|
41
|
-
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
42
95
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
var WorkerHandle = class {
|
|
97
|
+
child;
|
|
98
|
+
readyPromise;
|
|
99
|
+
pending = null;
|
|
100
|
+
dead = false;
|
|
101
|
+
constructor(config) {
|
|
102
|
+
this.child = fork(WORKER_PATH, [], {
|
|
103
|
+
serialization: "advanced",
|
|
104
|
+
// stdout/stderr inherited: parse logs and crash traces stay visible.
|
|
105
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"]
|
|
106
|
+
});
|
|
107
|
+
let readyResolve;
|
|
108
|
+
let readyReject;
|
|
109
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
110
|
+
readyResolve = resolve;
|
|
111
|
+
readyReject = reject;
|
|
112
|
+
});
|
|
113
|
+
this.readyPromise.catch(() => {
|
|
114
|
+
});
|
|
115
|
+
this.child.on("message", (msg) => {
|
|
116
|
+
if (msg.type === "ready") {
|
|
117
|
+
readyResolve();
|
|
118
|
+
if (this.pending === null) this.idle();
|
|
119
|
+
} else if (msg.type === "initError") {
|
|
120
|
+
readyReject(new Error(msg.message));
|
|
121
|
+
} else if (this.pending) {
|
|
122
|
+
const { resolve, reject } = this.pending;
|
|
123
|
+
this.pending = null;
|
|
124
|
+
this.idle();
|
|
125
|
+
if (msg.type === "ok") resolve(reviveBuffers(msg.result));
|
|
126
|
+
else reject(new WorkerCrashed(msg.message));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const onGone = (cause) => {
|
|
130
|
+
this.dead = true;
|
|
131
|
+
readyReject(new WorkerCrashed(cause));
|
|
132
|
+
if (this.pending) {
|
|
133
|
+
const { reject } = this.pending;
|
|
134
|
+
this.pending = null;
|
|
135
|
+
reject(new WorkerCrashed(cause));
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
this.child.on("error", (e) => onGone(e.message));
|
|
139
|
+
this.child.on(
|
|
140
|
+
"exit",
|
|
141
|
+
(code, signal) => onGone(`worker exited (code=${code}, signal=${signal})`)
|
|
142
|
+
);
|
|
143
|
+
this.child.send({ type: "init", config });
|
|
144
|
+
}
|
|
145
|
+
/** Resolves when the worker's native parser is constructed. Init time
|
|
146
|
+
* never counts toward the parse deadline — the deadline is a promise about
|
|
147
|
+
* parsing, not about process startup. */
|
|
148
|
+
ready() {
|
|
149
|
+
return this.readyPromise;
|
|
150
|
+
}
|
|
151
|
+
/** An idle pool must not hold the parent's event loop open. */
|
|
152
|
+
idle() {
|
|
153
|
+
this.child.unref();
|
|
154
|
+
this.child.channel?.unref();
|
|
155
|
+
}
|
|
156
|
+
request(payload, timeoutMs) {
|
|
157
|
+
if (this.dead) {
|
|
158
|
+
return Promise.reject(new WorkerCrashed("worker already exited"));
|
|
159
|
+
}
|
|
160
|
+
this.child.ref();
|
|
161
|
+
this.child.channel?.ref();
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
let timer;
|
|
164
|
+
const settle = (fn) => (value) => {
|
|
165
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
166
|
+
fn(value);
|
|
167
|
+
};
|
|
168
|
+
this.pending = {
|
|
169
|
+
resolve: settle(resolve),
|
|
170
|
+
reject: settle(reject)
|
|
171
|
+
};
|
|
172
|
+
if (timeoutMs !== void 0) {
|
|
173
|
+
timer = setTimeout(() => {
|
|
174
|
+
if (this.pending) {
|
|
175
|
+
const { reject: rejectPending } = this.pending;
|
|
176
|
+
this.pending = null;
|
|
177
|
+
rejectPending(new WorkerTimeout());
|
|
178
|
+
}
|
|
179
|
+
}, timeoutMs);
|
|
180
|
+
}
|
|
181
|
+
this.child.send({ type: "parse", payload });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
kill() {
|
|
185
|
+
this.dead = true;
|
|
186
|
+
this.child.kill("SIGKILL");
|
|
187
|
+
}
|
|
188
|
+
/** Graceful shutdown; escalates to SIGKILL if the worker doesn't exit. */
|
|
189
|
+
stop() {
|
|
190
|
+
if (this.dead) return;
|
|
191
|
+
this.dead = true;
|
|
77
192
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (opts.config) {
|
|
81
|
-
const fileConfig = JSON.parse(readFileSync(opts.config, "utf-8"));
|
|
82
|
-
Object.assign(config, fileConfig);
|
|
83
|
-
}
|
|
84
|
-
// CLI options override config file
|
|
85
|
-
if (opts.format)
|
|
86
|
-
config.outputFormat = opts.format;
|
|
87
|
-
if (opts.imageMode)
|
|
88
|
-
config.imageMode = opts.imageMode;
|
|
89
|
-
if (opts.imageOutputDir)
|
|
90
|
-
config.imageOutputDir = opts.imageOutputDir;
|
|
91
|
-
if (opts.extractImages)
|
|
92
|
-
config.extractImages = true;
|
|
93
|
-
if (opts.links === false)
|
|
94
|
-
config.extractLinks = false;
|
|
95
|
-
if (opts.keepHeadersFooters)
|
|
96
|
-
config.keepHeadersFooters = true;
|
|
97
|
-
if (opts.extractAnnotations)
|
|
98
|
-
config.extractAnnotations = true;
|
|
99
|
-
if (opts.extractFormFields)
|
|
100
|
-
config.extractFormFields = true;
|
|
101
|
-
if (opts.extractStructureTree)
|
|
102
|
-
config.extractStructureTree = true;
|
|
103
|
-
if (opts.extractBlocks)
|
|
104
|
-
config.extractBlocks = true;
|
|
105
|
-
if (opts.extractXfaPackets)
|
|
106
|
-
config.extractXfaPackets = true;
|
|
107
|
-
if (opts.extractContentBounds)
|
|
108
|
-
config.extractContentBounds = true;
|
|
109
|
-
if (opts.ocrServerUrl)
|
|
110
|
-
config.ocrServerUrl = opts.ocrServerUrl;
|
|
111
|
-
if (opts.ocrServerHeader)
|
|
112
|
-
config.ocrServerHeaders = opts.ocrServerHeader;
|
|
113
|
-
if (opts.ocr === false)
|
|
114
|
-
config.ocrEnabled = false;
|
|
115
|
-
if (opts.ocrLanguage)
|
|
116
|
-
config.ocrLanguage = opts.ocrLanguage;
|
|
117
|
-
if (opts.maxPages)
|
|
118
|
-
config.maxPages = opts.maxPages;
|
|
119
|
-
if (opts.targetPages)
|
|
120
|
-
config.targetPages = opts.targetPages;
|
|
121
|
-
if (opts.continueOnPageError)
|
|
122
|
-
config.continueOnPageError = true;
|
|
123
|
-
if (opts.dpi)
|
|
124
|
-
config.dpi = opts.dpi;
|
|
125
|
-
if (opts.preserveSmallText)
|
|
126
|
-
config.preserveVerySmallText = true;
|
|
127
|
-
if (opts.extractTextMetadata)
|
|
128
|
-
config.extractTextMetadata = true;
|
|
129
|
-
if (opts.password)
|
|
130
|
-
config.password = opts.password;
|
|
131
|
-
if (opts.quiet)
|
|
132
|
-
config.quiet = true;
|
|
133
|
-
if (opts.numWorkers)
|
|
134
|
-
config.numWorkers = opts.numWorkers;
|
|
135
|
-
if (opts.complexity)
|
|
136
|
-
config.includeComplexity = true;
|
|
137
|
-
if (opts.extractVectorGraphics)
|
|
138
|
-
config.extractVectorGraphics = true;
|
|
139
|
-
// Default CLI output to text (library defaults to json)
|
|
140
|
-
if (!config.outputFormat)
|
|
141
|
-
config.outputFormat = "text";
|
|
142
|
-
const parser = new LiteParse(config);
|
|
143
|
-
const result = await parser.parse(await resolveInput(file));
|
|
144
|
-
// JSON output carries pageErrors itself; text/markdown would silently
|
|
145
|
-
// omit the failed pages, so always surface them on stderr.
|
|
146
|
-
for (const error of result.pageErrors) {
|
|
147
|
-
console.error(`[liteparse] page ${error.pageNum} failed to extract and was skipped: ${error.message}`);
|
|
148
|
-
}
|
|
149
|
-
const output = config.outputFormat === "json"
|
|
150
|
-
? JSON.stringify(parseResultToCliJson(result, {
|
|
151
|
-
extractTextMetadata: config.extractTextMetadata,
|
|
152
|
-
}), null, 2)
|
|
153
|
-
: result.text;
|
|
154
|
-
if (opts.output) {
|
|
155
|
-
writeFileSync(opts.output, output, "utf-8");
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
process.stdout.write(output);
|
|
159
|
-
}
|
|
193
|
+
this.child.send({ type: "stop" });
|
|
194
|
+
} catch {
|
|
160
195
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
196
|
+
const escalate = setTimeout(() => this.child.kill("SIGKILL"), 5e3);
|
|
197
|
+
escalate.unref();
|
|
198
|
+
this.child.once("exit", () => clearTimeout(escalate));
|
|
199
|
+
this.idle();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
var WorkerPool = class {
|
|
203
|
+
config;
|
|
204
|
+
timeoutMs;
|
|
205
|
+
workers = /* @__PURE__ */ new Set();
|
|
206
|
+
idle = [];
|
|
207
|
+
waiters = [];
|
|
208
|
+
closed = false;
|
|
209
|
+
constructor(config, poolSize, parseTimeoutMs) {
|
|
210
|
+
if (!Number.isInteger(poolSize) || poolSize < 1) {
|
|
211
|
+
throw new Error("poolSize must be an integer >= 1");
|
|
164
212
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
213
|
+
if (parseTimeoutMs !== void 0 && !(parseTimeoutMs > 0)) {
|
|
214
|
+
throw new Error("parseTimeoutMs must be > 0");
|
|
215
|
+
}
|
|
216
|
+
this.config = config;
|
|
217
|
+
this.timeoutMs = parseTimeoutMs;
|
|
218
|
+
for (let i = 0; i < poolSize; i++) {
|
|
219
|
+
this.spawnWorker();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
spawnWorker() {
|
|
223
|
+
const worker = new WorkerHandle(this.config);
|
|
224
|
+
this.workers.add(worker);
|
|
225
|
+
this.release(worker);
|
|
226
|
+
}
|
|
227
|
+
acquire() {
|
|
228
|
+
const worker = this.idle.pop();
|
|
229
|
+
if (worker !== void 0) return Promise.resolve(worker);
|
|
230
|
+
return new Promise(
|
|
231
|
+
(resolve, reject) => this.waiters.push({ resolve, reject })
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
release(worker) {
|
|
235
|
+
if (this.closed) {
|
|
236
|
+
this.workers.delete(worker);
|
|
237
|
+
worker.stop();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const waiter = this.waiters.shift();
|
|
241
|
+
if (waiter !== void 0) waiter.resolve(worker);
|
|
242
|
+
else this.idle.push(worker);
|
|
243
|
+
}
|
|
244
|
+
retire(worker) {
|
|
245
|
+
worker.kill();
|
|
246
|
+
this.workers.delete(worker);
|
|
247
|
+
if (!this.closed) this.spawnWorker();
|
|
248
|
+
}
|
|
249
|
+
/** Run one parse on an idle worker.
|
|
250
|
+
*
|
|
251
|
+
* Waits for a free worker first; `parseTimeoutMs` bounds the parse itself,
|
|
252
|
+
* not the wait. */
|
|
253
|
+
async parse(payload, source) {
|
|
254
|
+
if (this.closed) throw new Error("parser pool is closed");
|
|
255
|
+
const worker = await this.acquire();
|
|
176
256
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// Human verdict goes to stderr so it never pollutes the JSON on stdout;
|
|
197
|
-
// the exit code below carries the same signal for scripts.
|
|
198
|
-
if (!opts.quiet) {
|
|
199
|
-
const verdict = complexPages > 0 ? "COMPLEX" : "SIMPLE";
|
|
200
|
-
const layoutCount = (reason) => stats.filter((s) => s.layout?.reasons.includes(reason)).length;
|
|
201
|
-
console.error(`${verdict} — ${complexPages}/${stats.length} page(s) need OCR; ` +
|
|
202
|
-
`layout: ${layoutCount("multi-column")} multi-column, ` +
|
|
203
|
-
`${layoutCount("table-likely")} table, ` +
|
|
204
|
-
`${layoutCount("dense-graphics")} graphics-dense`);
|
|
205
|
-
}
|
|
206
|
-
// Exit non-zero when any page needs OCR, so the command is usable as a
|
|
207
|
-
// shell predicate (e.g. `is-complex doc.pdf && parse --no-ocr`).
|
|
208
|
-
if (complexPages > 0)
|
|
209
|
-
process.exit(1);
|
|
257
|
+
await worker.ready();
|
|
258
|
+
const result = await worker.request(payload, this.timeoutMs);
|
|
259
|
+
this.release(worker);
|
|
260
|
+
return result;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
this.retire(worker);
|
|
263
|
+
if (e instanceof WorkerTimeout) {
|
|
264
|
+
throw new ParseTimeoutError(
|
|
265
|
+
`parse of ${source} exceeded ${this.timeoutMs}ms; the worker process was killed`,
|
|
266
|
+
source,
|
|
267
|
+
this.timeoutMs
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (e instanceof WorkerCrashed) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`liteparse worker process died while parsing ${source}: ${e.message}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
throw e;
|
|
210
276
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
277
|
+
}
|
|
278
|
+
/** Resolves when every worker is initialized. Optional — the first parse
|
|
279
|
+
* per worker waits for init anyway. */
|
|
280
|
+
async warmUp() {
|
|
281
|
+
await Promise.all([...this.workers].map((w) => w.ready()));
|
|
282
|
+
}
|
|
283
|
+
/** Shut down all workers. Idempotent. Busy workers are stopped as their
|
|
284
|
+
* in-flight parses finish. */
|
|
285
|
+
close() {
|
|
286
|
+
if (this.closed) return;
|
|
287
|
+
this.closed = true;
|
|
288
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
289
|
+
waiter.reject(new Error("parser pool is closed"));
|
|
214
290
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
291
|
+
for (const worker of this.idle.splice(0)) {
|
|
292
|
+
this.workers.delete(worker);
|
|
293
|
+
worker.stop();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/lib.ts
|
|
299
|
+
var LiteParse = class {
|
|
300
|
+
_native;
|
|
301
|
+
_config;
|
|
302
|
+
_pool = null;
|
|
303
|
+
constructor(userConfig = {}) {
|
|
304
|
+
const nativeConfig = {
|
|
305
|
+
ocrLanguage: userConfig.ocrLanguage,
|
|
306
|
+
ocrEnabled: userConfig.ocrEnabled,
|
|
307
|
+
ocrServerUrl: userConfig.ocrServerUrl,
|
|
308
|
+
ocrServerHeaders: userConfig.ocrServerHeaders,
|
|
309
|
+
tessdataPath: userConfig.tessdataPath,
|
|
310
|
+
maxPages: userConfig.maxPages,
|
|
311
|
+
targetPages: userConfig.targetPages,
|
|
312
|
+
extractScreenshots: userConfig.extractScreenshots,
|
|
313
|
+
continueOnPageError: userConfig.continueOnPageError,
|
|
314
|
+
dpi: userConfig.dpi,
|
|
315
|
+
outputFormat: userConfig.outputFormat,
|
|
316
|
+
imageMode: userConfig.imageMode,
|
|
317
|
+
extractImages: userConfig.extractImages,
|
|
318
|
+
imageOutputDir: userConfig.imageOutputDir,
|
|
319
|
+
extractLinks: userConfig.extractLinks,
|
|
320
|
+
keepHeadersFooters: userConfig.keepHeadersFooters,
|
|
321
|
+
extractAnnotations: userConfig.extractAnnotations,
|
|
322
|
+
extractFormFields: userConfig.extractFormFields,
|
|
323
|
+
extractStructureTree: userConfig.extractStructureTree,
|
|
324
|
+
extractBlocks: userConfig.extractBlocks,
|
|
325
|
+
extractXfaPackets: userConfig.extractXfaPackets,
|
|
326
|
+
extractDocumentMetadata: userConfig.extractDocumentMetadata,
|
|
327
|
+
extractContentBounds: userConfig.extractContentBounds,
|
|
328
|
+
detectScreenshotRects: userConfig.detectScreenshotRects,
|
|
329
|
+
renderFormFields: userConfig.renderFormFields,
|
|
330
|
+
preserveVerySmallText: userConfig.preserveVerySmallText,
|
|
331
|
+
password: userConfig.password,
|
|
332
|
+
quiet: userConfig.quiet,
|
|
333
|
+
numWorkers: userConfig.numWorkers,
|
|
334
|
+
ocrFailureFatal: userConfig.ocrFailureFatal,
|
|
335
|
+
ocrHedgeDelaysMs: userConfig.ocrHedgeDelaysMs,
|
|
336
|
+
emitWordBoxes: userConfig.emitWordBoxes,
|
|
337
|
+
extractTextMetadata: userConfig.extractTextMetadata,
|
|
338
|
+
cropBox: userConfig.cropBox,
|
|
339
|
+
skipDiagonalText: userConfig.skipDiagonalText,
|
|
340
|
+
includeComplexity: userConfig.includeComplexity,
|
|
341
|
+
extractVectorGraphics: userConfig.extractVectorGraphics
|
|
342
|
+
};
|
|
343
|
+
this._native = new native.LiteParse(nativeConfig);
|
|
344
|
+
if (userConfig.parseTimeoutMs !== void 0 && userConfig.poolSize === void 0) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
"parseTimeoutMs requires poolSize"
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (userConfig.poolSize !== void 0) {
|
|
350
|
+
this._pool = new WorkerPool(
|
|
351
|
+
nativeConfig,
|
|
352
|
+
userConfig.poolSize,
|
|
353
|
+
userConfig.parseTimeoutMs
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const resolved = this._native.config;
|
|
357
|
+
this._config = {
|
|
358
|
+
ocrLanguage: resolved.ocrLanguage ?? "eng",
|
|
359
|
+
ocrEnabled: resolved.ocrEnabled ?? true,
|
|
360
|
+
ocrServerUrl: resolved.ocrServerUrl ?? void 0,
|
|
361
|
+
ocrServerHeaders: resolved.ocrServerHeaders ?? void 0,
|
|
362
|
+
tessdataPath: resolved.tessdataPath ?? void 0,
|
|
363
|
+
maxPages: resolved.maxPages ?? 1e3,
|
|
364
|
+
targetPages: resolved.targetPages ?? void 0,
|
|
365
|
+
extractScreenshots: resolved.extractScreenshots ?? false,
|
|
366
|
+
continueOnPageError: resolved.continueOnPageError ?? false,
|
|
367
|
+
dpi: resolved.dpi ?? 150,
|
|
368
|
+
outputFormat: resolved.outputFormat ?? "json",
|
|
369
|
+
imageMode: resolved.imageMode ?? "placeholder",
|
|
370
|
+
extractImages: resolved.extractImages ?? false,
|
|
371
|
+
imageOutputDir: resolved.imageOutputDir ?? void 0,
|
|
372
|
+
extractLinks: resolved.extractLinks ?? true,
|
|
373
|
+
keepHeadersFooters: resolved.keepHeadersFooters ?? false,
|
|
374
|
+
extractAnnotations: resolved.extractAnnotations ?? false,
|
|
375
|
+
extractFormFields: resolved.extractFormFields ?? false,
|
|
376
|
+
extractStructureTree: resolved.extractStructureTree ?? false,
|
|
377
|
+
extractBlocks: resolved.extractBlocks ?? false,
|
|
378
|
+
extractXfaPackets: resolved.extractXfaPackets ?? false,
|
|
379
|
+
extractDocumentMetadata: resolved.extractDocumentMetadata ?? false,
|
|
380
|
+
extractContentBounds: resolved.extractContentBounds ?? false,
|
|
381
|
+
detectScreenshotRects: resolved.detectScreenshotRects ?? false,
|
|
382
|
+
renderFormFields: resolved.renderFormFields ?? false,
|
|
383
|
+
preserveVerySmallText: resolved.preserveVerySmallText ?? false,
|
|
384
|
+
password: resolved.password ?? void 0,
|
|
385
|
+
quiet: resolved.quiet ?? false,
|
|
386
|
+
numWorkers: resolved.numWorkers ?? 1,
|
|
387
|
+
ocrFailureFatal: resolved.ocrFailureFatal ?? true,
|
|
388
|
+
ocrHedgeDelaysMs: resolved.ocrHedgeDelaysMs ?? [],
|
|
389
|
+
emitWordBoxes: resolved.emitWordBoxes ?? false,
|
|
390
|
+
extractTextMetadata: resolved.extractTextMetadata ?? false,
|
|
391
|
+
cropBox: resolved.cropBox ?? void 0,
|
|
392
|
+
skipDiagonalText: resolved.skipDiagonalText ?? false,
|
|
393
|
+
includeComplexity: resolved.includeComplexity ?? false,
|
|
394
|
+
extractVectorGraphics: resolved.extractVectorGraphics ?? false
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async parse(input) {
|
|
398
|
+
const nativeInput = typeof input === "string" ? input : Buffer.from(input);
|
|
399
|
+
if (this._pool !== null) {
|
|
400
|
+
const source = typeof nativeInput === "string" ? nativeInput : `<${nativeInput.byteLength} bytes>`;
|
|
401
|
+
return this._pool.parse(nativeInput, source);
|
|
402
|
+
}
|
|
403
|
+
const result = await this._native.parse(nativeInput);
|
|
404
|
+
return toParseResult(result);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Resolves once every pool worker is initialized. No-op without `poolSize`.
|
|
408
|
+
*
|
|
409
|
+
* Optional: the first parse on each worker waits for its init anyway. Call
|
|
410
|
+
* this before latency-sensitive traffic to avoid paying worker startup on
|
|
411
|
+
* the first request.
|
|
412
|
+
*/
|
|
413
|
+
async warmUp() {
|
|
414
|
+
if (this._pool !== null) await this._pool.warmUp();
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Shut down pool workers, if pool mode is enabled. Idempotent.
|
|
418
|
+
*
|
|
419
|
+
* Without `poolSize` this is a no-op. An idle pool never keeps the event
|
|
420
|
+
* loop alive and workers exit when the parent does, so forgetting to call
|
|
421
|
+
* this leaks nothing past process exit.
|
|
422
|
+
*/
|
|
423
|
+
close() {
|
|
424
|
+
if (this._pool !== null) this._pool.close();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Parse a document in bounded-memory page batches of `batchSize` pages.
|
|
428
|
+
*
|
|
429
|
+
* Each yielded result is independent and becomes collectible once the caller
|
|
430
|
+
* advances the iterator, so a consumer that does not retain batches never
|
|
431
|
+
* holds more than one batch of pages in memory. A non-PDF source is
|
|
432
|
+
* converted once when the iterator starts, not once per batch; its temporary
|
|
433
|
+
* file is released when iteration ends — including an early `break` or
|
|
434
|
+
* `throw`, which run the generator's cleanup.
|
|
435
|
+
*
|
|
436
|
+
* Cross-page passes see only the pages in their own batch, so repeated
|
|
437
|
+
* header/footer removal and image deduplication are batch-local and the
|
|
438
|
+
* output can differ from `parse()`. Prefer `parse()` unless the size of the
|
|
439
|
+
* materialized result is the problem.
|
|
440
|
+
*
|
|
441
|
+
* As with any async generator, work starts on the first `next()` call, so
|
|
442
|
+
* errors (an unreadable file, or a parser configured with `targetPages` —
|
|
443
|
+
* ambiguous with generated batch ranges) surface on the first iteration
|
|
444
|
+
* rather than when `parseBatches()` itself is called.
|
|
445
|
+
*/
|
|
446
|
+
async *parseBatches(input, options = {}) {
|
|
447
|
+
const nativeInput = typeof input === "string" ? input : Buffer.from(input);
|
|
448
|
+
const session = await this._native.openBatchSession(
|
|
449
|
+
nativeInput,
|
|
450
|
+
options.batchSize
|
|
451
|
+
);
|
|
226
452
|
try {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
if (opts.quiet)
|
|
233
|
-
config.quiet = true;
|
|
234
|
-
if (opts.targetPages)
|
|
235
|
-
config.targetPages = opts.targetPages;
|
|
236
|
-
const parser = new LiteParse(config);
|
|
237
|
-
// Parse target pages into number array
|
|
238
|
-
let pageNumbers;
|
|
239
|
-
if (opts.targetPages) {
|
|
240
|
-
pageNumbers = [];
|
|
241
|
-
for (const part of opts.targetPages.split(",")) {
|
|
242
|
-
const trimmed = part.trim();
|
|
243
|
-
if (trimmed.includes("-")) {
|
|
244
|
-
const [start, end] = trimmed.split("-").map(Number);
|
|
245
|
-
for (let i = start; i <= end; i++)
|
|
246
|
-
pageNumbers.push(i);
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
pageNumbers.push(Number(trimmed));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const outputDir = opts.outputDir;
|
|
254
|
-
mkdirSync(outputDir, { recursive: true });
|
|
255
|
-
const results = await parser.screenshot(file, pageNumbers);
|
|
256
|
-
for (const result of results) {
|
|
257
|
-
const outputPath = join(outputDir, `page_${result.pageNum}.png`);
|
|
258
|
-
writeFileSync(outputPath, result.imageBuffer);
|
|
259
|
-
if (!opts.quiet) {
|
|
260
|
-
console.error(`[liteparse] screenshot page ${result.pageNum} → ${outputPath}`);
|
|
261
|
-
}
|
|
453
|
+
const totalPages = session.totalPages;
|
|
454
|
+
for (; ; ) {
|
|
455
|
+
const batch = await session.nextBatch();
|
|
456
|
+
if (batch == null) {
|
|
457
|
+
return;
|
|
262
458
|
}
|
|
459
|
+
yield {
|
|
460
|
+
startPage: batch.startPage,
|
|
461
|
+
endPage: batch.endPage,
|
|
462
|
+
totalPages,
|
|
463
|
+
result: toParseResult(batch.result)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
} finally {
|
|
467
|
+
await session.close();
|
|
263
468
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Parse from pre-extracted pages, skipping PDFium text extraction. Runs only
|
|
472
|
+
* grid projection + the configured output formatter, so the caller's own
|
|
473
|
+
* text-extraction / font-recovery owns the text content. Synchronous: no
|
|
474
|
+
* PDFium load and no OCR on this path.
|
|
475
|
+
*/
|
|
476
|
+
parsePages(pages) {
|
|
477
|
+
const nativePages = pages.map((p) => ({
|
|
478
|
+
pageNumber: p.pageNumber,
|
|
479
|
+
pageWidth: p.pageWidth,
|
|
480
|
+
pageHeight: p.pageHeight,
|
|
481
|
+
textItems: p.textItems,
|
|
482
|
+
graphics: p.graphics
|
|
483
|
+
}));
|
|
484
|
+
const result = this._native.parsePages(nativePages);
|
|
485
|
+
return toParseResult(result);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Determine per-page complexity without running a full parse. Returns one
|
|
489
|
+
* entry per page with signals and a `needsOcr` verdict — a cheap pre-OCR
|
|
490
|
+
* check to decide whether a document needs advanced parsing.
|
|
491
|
+
*/
|
|
492
|
+
async isComplex(input) {
|
|
493
|
+
const nativeInput = typeof input === "string" ? input : Buffer.from(input);
|
|
494
|
+
const stats = await this._native.isComplex(nativeInput);
|
|
495
|
+
return stats.map(toComplexity);
|
|
496
|
+
}
|
|
497
|
+
async screenshot(input, pageNumbers) {
|
|
498
|
+
const nativeInput = typeof input === "string" ? input : Buffer.from(input);
|
|
499
|
+
const results = await this._native.screenshot(
|
|
500
|
+
nativeInput,
|
|
501
|
+
pageNumbers ?? null
|
|
502
|
+
);
|
|
503
|
+
return results.map((r) => ({
|
|
504
|
+
pageNum: r.pageNum,
|
|
505
|
+
width: r.width,
|
|
506
|
+
height: r.height,
|
|
507
|
+
imageBuffer: r.imageBuffer,
|
|
508
|
+
isSolidFill: r.isSolidFill,
|
|
509
|
+
rects: r.rects
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
getConfig() {
|
|
513
|
+
return { ...this._config };
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
function toComplexity(s) {
|
|
517
|
+
return {
|
|
518
|
+
pageNumber: s.pageNumber,
|
|
519
|
+
textLength: s.textLength,
|
|
520
|
+
textCoverage: s.textCoverage,
|
|
521
|
+
hasSubstantialImages: s.hasSubstantialImages,
|
|
522
|
+
imageBlockCount: s.imageBlockCount,
|
|
523
|
+
imageCoverage: s.imageCoverage,
|
|
524
|
+
largestImageCoverage: s.largestImageCoverage,
|
|
525
|
+
fullPageImage: s.fullPageImage,
|
|
526
|
+
uncoveredVectorArea: s.uncoveredVectorArea ?? void 0,
|
|
527
|
+
isGarbled: s.isGarbled,
|
|
528
|
+
pageArea: s.pageArea,
|
|
529
|
+
needsOcr: s.needsOcr,
|
|
530
|
+
reasons: s.reasons,
|
|
531
|
+
layout: s.layout ? {
|
|
532
|
+
columnCount: s.layout.columnCount,
|
|
533
|
+
ruledTableCount: s.layout.ruledTableCount,
|
|
534
|
+
ruledTableCoverage: s.layout.ruledTableCoverage,
|
|
535
|
+
textTableRunCount: s.layout.textTableRunCount,
|
|
536
|
+
figureCount: s.layout.figureCount,
|
|
537
|
+
figureCoverage: s.layout.figureCoverage,
|
|
538
|
+
isComplex: s.layout.isComplex,
|
|
539
|
+
reasons: s.layout.reasons
|
|
540
|
+
} : void 0
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function toParseResult(result) {
|
|
544
|
+
return {
|
|
545
|
+
totalPages: result.totalPages,
|
|
546
|
+
pages: result.pages.map(toPage),
|
|
547
|
+
pageErrors: result.pageErrors ?? [],
|
|
548
|
+
text: result.text,
|
|
549
|
+
images: (result.images ?? []).map(toImage),
|
|
550
|
+
screenshots: (result.screenshots ?? []).map(toScreenshot),
|
|
551
|
+
imageErrorCount: result.imageErrorCount ?? 0,
|
|
552
|
+
formType: result.formType,
|
|
553
|
+
creator: result.creator,
|
|
554
|
+
producer: result.producer,
|
|
555
|
+
docMeta: result.docMeta,
|
|
556
|
+
xfaPackets: result.xfaPackets
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function toPage(p) {
|
|
560
|
+
return {
|
|
561
|
+
pageNum: p.pageNum,
|
|
562
|
+
width: p.width,
|
|
563
|
+
height: p.height,
|
|
564
|
+
contentBounds: p.contentBounds,
|
|
565
|
+
text: p.text,
|
|
566
|
+
markdown: p.markdown,
|
|
567
|
+
textItems: p.textItems.map(toTextItem),
|
|
568
|
+
complexity: p.complexity ? toComplexity(p.complexity) : void 0,
|
|
569
|
+
vectorGraphics: p.vectorGraphics ?? void 0,
|
|
570
|
+
annotations: p.annotations,
|
|
571
|
+
formFields: p.formFields?.map((field) => ({
|
|
572
|
+
id: field.id,
|
|
573
|
+
type: field.fieldType,
|
|
574
|
+
page: field.page,
|
|
575
|
+
annotationIndex: field.annotationIndex,
|
|
576
|
+
widgetIndex: field.widgetIndex,
|
|
577
|
+
objectNumber: field.objectNumber,
|
|
578
|
+
name: field.name,
|
|
579
|
+
alternateName: field.alternateName,
|
|
580
|
+
value: field.value,
|
|
581
|
+
exportValue: field.exportValue,
|
|
582
|
+
fieldFlags: field.fieldFlags,
|
|
583
|
+
controlCount: field.controlCount,
|
|
584
|
+
controlIndex: field.controlIndex,
|
|
585
|
+
checked: field.checked,
|
|
586
|
+
rect: field.rect,
|
|
587
|
+
options: field.options,
|
|
588
|
+
selectedOptions: field.selectedOptions
|
|
589
|
+
})),
|
|
590
|
+
structureTree: p.structureTree ? { roots: p.structureTree.roots.map(toStructureTreeElement) } : void 0,
|
|
591
|
+
blocks: p.blocks
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function toStructureTreeElement(element) {
|
|
595
|
+
const attributes = {};
|
|
596
|
+
for (const attribute of element.attributes) {
|
|
597
|
+
if (attribute.booleanValue !== void 0) {
|
|
598
|
+
attributes[attribute.name] = attribute.booleanValue;
|
|
599
|
+
} else if (attribute.numberValue !== void 0) {
|
|
600
|
+
attributes[attribute.name] = attribute.numberValue;
|
|
601
|
+
} else if (attribute.stringValue !== void 0) {
|
|
602
|
+
attributes[attribute.name] = attribute.stringValue;
|
|
267
603
|
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.
|
|
271
|
-
.
|
|
272
|
-
.
|
|
273
|
-
.
|
|
274
|
-
.
|
|
275
|
-
|
|
276
|
-
.
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
.
|
|
284
|
-
.
|
|
285
|
-
.
|
|
286
|
-
.
|
|
287
|
-
.
|
|
288
|
-
.
|
|
289
|
-
.
|
|
290
|
-
.
|
|
291
|
-
.
|
|
292
|
-
.
|
|
293
|
-
.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
type: element.elementType,
|
|
607
|
+
id: element.id,
|
|
608
|
+
actualText: element.actualText,
|
|
609
|
+
altText: element.altText,
|
|
610
|
+
title: element.title,
|
|
611
|
+
attributes,
|
|
612
|
+
markedContentIds: element.markedContentIds,
|
|
613
|
+
children: element.children.map(toStructureTreeElement),
|
|
614
|
+
annotations: element.annotations
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function toImage(img) {
|
|
618
|
+
return {
|
|
619
|
+
id: img.id,
|
|
620
|
+
name: img.name,
|
|
621
|
+
path: img.path,
|
|
622
|
+
page: img.page,
|
|
623
|
+
bbox: img.bbox,
|
|
624
|
+
width: img.width,
|
|
625
|
+
height: img.height,
|
|
626
|
+
rotation: img.rotation,
|
|
627
|
+
format: img.format,
|
|
628
|
+
duplicateOf: img.duplicateOf,
|
|
629
|
+
bytes: img.bytes
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function toScreenshot(result) {
|
|
633
|
+
return {
|
|
634
|
+
pageNum: result.pageNum,
|
|
635
|
+
width: result.width,
|
|
636
|
+
height: result.height,
|
|
637
|
+
imageBuffer: result.imageBuffer,
|
|
638
|
+
isSolidFill: result.isSolidFill,
|
|
639
|
+
rects: result.rects
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function toTextItem(item) {
|
|
643
|
+
return {
|
|
644
|
+
text: item.text,
|
|
645
|
+
x: item.x,
|
|
646
|
+
y: item.y,
|
|
647
|
+
width: item.width,
|
|
648
|
+
height: item.height,
|
|
649
|
+
fontName: item.fontName,
|
|
650
|
+
fontSize: item.fontSize,
|
|
651
|
+
fontHeight: item.fontHeight,
|
|
652
|
+
fontAscent: item.fontAscent,
|
|
653
|
+
fontDescent: item.fontDescent,
|
|
654
|
+
fontWeight: item.fontWeight,
|
|
655
|
+
textWidth: item.textWidth,
|
|
656
|
+
fontIsBuggy: item.fontIsBuggy,
|
|
657
|
+
mcid: item.mcid,
|
|
658
|
+
fillColor: item.fillColor,
|
|
659
|
+
strokeColor: item.strokeColor,
|
|
660
|
+
charCodes: item.charCodes,
|
|
661
|
+
trailingSpaceGenerated: item.trailingSpaceGenerated,
|
|
662
|
+
confidence: item.confidence,
|
|
663
|
+
rotation: item.rotation,
|
|
664
|
+
words: item.words
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/cli-json.ts
|
|
669
|
+
function textItemToCliJson(item, extractTextMetadata) {
|
|
670
|
+
return {
|
|
671
|
+
text: item.text,
|
|
672
|
+
x: item.x,
|
|
673
|
+
y: item.y,
|
|
674
|
+
width: item.width,
|
|
675
|
+
height: item.height,
|
|
676
|
+
...extractTextMetadata && item.rotation !== void 0 ? { rotation: item.rotation } : {},
|
|
677
|
+
...item.fontName !== void 0 ? { font_name: item.fontName } : {},
|
|
678
|
+
...item.fontSize !== void 0 ? { font_size: item.fontSize } : {},
|
|
679
|
+
...item.fontHeight !== void 0 ? { font_height: item.fontHeight } : {},
|
|
680
|
+
...item.fontAscent !== void 0 ? { font_ascent: item.fontAscent } : {},
|
|
681
|
+
...item.fontDescent !== void 0 ? { font_descent: item.fontDescent } : {},
|
|
682
|
+
...item.fontWeight !== void 0 ? { font_weight: item.fontWeight } : {},
|
|
683
|
+
...item.textWidth !== void 0 ? { text_width: item.textWidth } : {},
|
|
684
|
+
...item.fontIsBuggy !== void 0 ? { font_is_buggy: item.fontIsBuggy } : {},
|
|
685
|
+
...item.mcid !== void 0 ? { mcid: item.mcid } : {},
|
|
686
|
+
...item.fillColor !== void 0 ? { fill_color: item.fillColor } : {},
|
|
687
|
+
...item.strokeColor !== void 0 ? { stroke_color: item.strokeColor } : {},
|
|
688
|
+
...item.charCodes?.length ? { char_codes: item.charCodes } : {},
|
|
689
|
+
...item.trailingSpaceGenerated ? { trailing_space_generated: true } : {},
|
|
690
|
+
// The CLI JSON format reports native text as fully confident, matching the
|
|
691
|
+
// Rust CLI. The `confidence` field on TextItem itself stays undefined for
|
|
692
|
+
// native text so it can discriminate OCR-derived items.
|
|
693
|
+
confidence: item.confidence ?? 1
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function structureTreeElementToCliJson(element) {
|
|
697
|
+
return {
|
|
698
|
+
type: element.type,
|
|
699
|
+
...element.id !== void 0 ? { id: element.id } : {},
|
|
700
|
+
...element.actualText !== void 0 ? { actual_text: element.actualText } : {},
|
|
701
|
+
...element.altText !== void 0 ? { alt_text: element.altText } : {},
|
|
702
|
+
...element.title !== void 0 ? { title: element.title } : {},
|
|
703
|
+
...Object.keys(element.attributes).length ? { attributes: element.attributes } : {},
|
|
704
|
+
marked_content_ids: element.markedContentIds,
|
|
705
|
+
children: element.children.map(structureTreeElementToCliJson),
|
|
706
|
+
annotations: element.annotations.map(annotationToCliJson)
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function complexityToCliJson(complexity) {
|
|
710
|
+
return {
|
|
711
|
+
page_number: complexity.pageNumber,
|
|
712
|
+
text_length: complexity.textLength,
|
|
713
|
+
text_coverage: complexity.textCoverage,
|
|
714
|
+
has_substantial_images: complexity.hasSubstantialImages,
|
|
715
|
+
image_block_count: complexity.imageBlockCount,
|
|
716
|
+
image_coverage: complexity.imageCoverage,
|
|
717
|
+
largest_image_coverage: complexity.largestImageCoverage,
|
|
718
|
+
full_page_image: complexity.fullPageImage,
|
|
719
|
+
...complexity.uncoveredVectorArea !== void 0 ? { uncovered_vector_area: complexity.uncoveredVectorArea } : {},
|
|
720
|
+
is_garbled: complexity.isGarbled,
|
|
721
|
+
page_area: complexity.pageArea,
|
|
722
|
+
needs_ocr: complexity.needsOcr,
|
|
723
|
+
reasons: complexity.reasons,
|
|
724
|
+
...complexity.layout ? {
|
|
725
|
+
layout: {
|
|
726
|
+
column_count: complexity.layout.columnCount,
|
|
727
|
+
ruled_table_count: complexity.layout.ruledTableCount,
|
|
728
|
+
ruled_table_coverage: complexity.layout.ruledTableCoverage,
|
|
729
|
+
text_table_run_count: complexity.layout.textTableRunCount,
|
|
730
|
+
figure_count: complexity.layout.figureCount,
|
|
731
|
+
figure_coverage: complexity.layout.figureCoverage,
|
|
732
|
+
is_complex: complexity.layout.isComplex,
|
|
733
|
+
reasons: complexity.layout.reasons
|
|
734
|
+
}
|
|
735
|
+
} : {}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function vectorGraphicsToCliJson(vectorGraphics) {
|
|
739
|
+
return {
|
|
740
|
+
shapes: vectorGraphics.shapes.map((shape) => ({
|
|
741
|
+
bbox: {
|
|
742
|
+
x: shape.bbox.x,
|
|
743
|
+
y: shape.bbox.y,
|
|
744
|
+
width: shape.bbox.width,
|
|
745
|
+
height: shape.bbox.height
|
|
746
|
+
},
|
|
747
|
+
stroke: shape.stroke,
|
|
748
|
+
...shape.strokeColor !== void 0 ? { stroke_color: shape.strokeColor } : {},
|
|
749
|
+
fill: shape.fill,
|
|
750
|
+
...shape.fillColor !== void 0 ? { fill_color: shape.fillColor } : {},
|
|
751
|
+
has_curve: shape.hasCurve
|
|
752
|
+
})),
|
|
753
|
+
lines: vectorGraphics.lines.map((line) => ({
|
|
754
|
+
x1: line.x1,
|
|
755
|
+
y1: line.y1,
|
|
756
|
+
x2: line.x2,
|
|
757
|
+
y2: line.y2,
|
|
758
|
+
stroke: line.stroke,
|
|
759
|
+
...line.strokeWidth !== void 0 ? { stroke_width: line.strokeWidth } : {},
|
|
760
|
+
...line.strokeColor !== void 0 ? { stroke_color: line.strokeColor } : {},
|
|
761
|
+
fill: line.fill,
|
|
762
|
+
...line.fillColor !== void 0 ? { fill_color: line.fillColor } : {}
|
|
763
|
+
}))
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function annotationToCliJson(annotation) {
|
|
767
|
+
return {
|
|
768
|
+
subtype: annotation.subtype,
|
|
769
|
+
...annotation.contents !== void 0 ? { contents: annotation.contents } : {},
|
|
770
|
+
...annotation.created !== void 0 ? { created: annotation.created } : {},
|
|
771
|
+
...annotation.modified !== void 0 ? { modified: annotation.modified } : {},
|
|
772
|
+
...annotation.title !== void 0 ? { title: annotation.title } : {},
|
|
773
|
+
...annotation.rect !== void 0 ? { rect: annotation.rect } : {},
|
|
774
|
+
...annotation.quadpointRects.length ? {
|
|
775
|
+
quadpoint_rects: annotation.quadpointRects.map((rect) => ({
|
|
776
|
+
x: rect.x,
|
|
777
|
+
y: rect.y,
|
|
778
|
+
width: rect.width,
|
|
779
|
+
height: rect.height
|
|
780
|
+
}))
|
|
781
|
+
} : {},
|
|
782
|
+
...annotation.uri !== void 0 ? { uri: annotation.uri } : {}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function formFieldToCliJson(field) {
|
|
786
|
+
return {
|
|
787
|
+
id: field.id,
|
|
788
|
+
type: field.type,
|
|
789
|
+
page: field.page,
|
|
790
|
+
annotation_index: field.annotationIndex,
|
|
791
|
+
widget_index: field.widgetIndex,
|
|
792
|
+
...field.objectNumber !== void 0 ? { object_number: field.objectNumber } : {},
|
|
793
|
+
...field.name !== void 0 ? { name: field.name } : {},
|
|
794
|
+
...field.alternateName !== void 0 ? { alternate_name: field.alternateName } : {},
|
|
795
|
+
...field.value !== void 0 ? { value: field.value } : {},
|
|
796
|
+
...field.exportValue !== void 0 ? { export_value: field.exportValue } : {},
|
|
797
|
+
field_flags: field.fieldFlags,
|
|
798
|
+
...field.controlCount !== void 0 ? { control_count: field.controlCount } : {},
|
|
799
|
+
...field.controlIndex !== void 0 ? { control_index: field.controlIndex } : {},
|
|
800
|
+
...field.checked !== void 0 ? { checked: field.checked } : {},
|
|
801
|
+
...field.rect !== void 0 ? { rect: field.rect } : {},
|
|
802
|
+
...field.options.length ? { options: field.options } : {},
|
|
803
|
+
...field.selectedOptions.length ? { selected_options: field.selectedOptions } : {}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function xfaPacketToCliJson(packet) {
|
|
807
|
+
return {
|
|
808
|
+
index: packet.index,
|
|
809
|
+
...packet.name !== void 0 ? { name: packet.name } : {},
|
|
810
|
+
content_length: packet.contentLength,
|
|
811
|
+
...packet.content !== void 0 ? { content: packet.content } : {}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function imageToCliJson(image) {
|
|
815
|
+
return {
|
|
816
|
+
id: image.id,
|
|
817
|
+
name: image.name,
|
|
818
|
+
...image.path !== void 0 ? { path: image.path } : {},
|
|
819
|
+
page: image.page,
|
|
820
|
+
bbox: {
|
|
821
|
+
x: image.bbox.x,
|
|
822
|
+
y: image.bbox.y,
|
|
823
|
+
width: image.bbox.width,
|
|
824
|
+
height: image.bbox.height
|
|
825
|
+
},
|
|
826
|
+
width: image.width,
|
|
827
|
+
height: image.height,
|
|
828
|
+
rotation: image.rotation,
|
|
829
|
+
format: image.format,
|
|
830
|
+
...image.duplicateOf !== void 0 ? { duplicate_of: image.duplicateOf } : {}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function parseResultToCliJson(result, options = {}) {
|
|
834
|
+
const extractTextMetadata = options.extractTextMetadata ?? false;
|
|
835
|
+
return {
|
|
836
|
+
pages: result.pages.map((page) => ({
|
|
837
|
+
page: page.pageNum,
|
|
838
|
+
width: page.width,
|
|
839
|
+
height: page.height,
|
|
840
|
+
...page.contentBounds ? {
|
|
841
|
+
content_bounds: {
|
|
842
|
+
x: page.contentBounds.x,
|
|
843
|
+
y: page.contentBounds.y,
|
|
844
|
+
width: page.contentBounds.width,
|
|
845
|
+
height: page.contentBounds.height
|
|
354
846
|
}
|
|
355
|
-
|
|
356
|
-
|
|
847
|
+
} : {},
|
|
848
|
+
text: page.text,
|
|
849
|
+
text_items: page.textItems.map(
|
|
850
|
+
(item) => textItemToCliJson(item, extractTextMetadata)
|
|
851
|
+
),
|
|
852
|
+
...page.complexity ? { complexity: complexityToCliJson(page.complexity) } : {},
|
|
853
|
+
...page.vectorGraphics ? { vector_graphics: vectorGraphicsToCliJson(page.vectorGraphics) } : {},
|
|
854
|
+
...page.annotations !== void 0 ? { annotations: page.annotations.map(annotationToCliJson) } : {},
|
|
855
|
+
...page.formFields !== void 0 ? { form_fields: page.formFields.map(formFieldToCliJson) } : {},
|
|
856
|
+
...page.structureTree !== void 0 ? {
|
|
857
|
+
structure_tree: {
|
|
858
|
+
roots: page.structureTree.roots.map(structureTreeElementToCliJson)
|
|
357
859
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
860
|
+
} : {}
|
|
861
|
+
})),
|
|
862
|
+
...result.images.length ? { images: result.images.map(imageToCliJson) } : {},
|
|
863
|
+
...result.imageErrorCount ? { image_error_count: result.imageErrorCount } : {},
|
|
864
|
+
...result.formType !== void 0 ? { form_type: result.formType } : {},
|
|
865
|
+
...result.xfaPackets !== void 0 ? { xfa_packets: result.xfaPackets.map(xfaPacketToCliJson) } : {}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/cli.ts
|
|
870
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
871
|
+
import { join as join2, relative, parse as parsePath } from "path";
|
|
872
|
+
program.name("liteparse").description("Fast, lightweight PDF and document parsing").version("2.0.0");
|
|
873
|
+
async function resolveInput(file) {
|
|
874
|
+
if (file !== "-") return file;
|
|
875
|
+
const chunks = [];
|
|
876
|
+
for await (const chunk of process.stdin) {
|
|
877
|
+
chunks.push(chunk);
|
|
878
|
+
}
|
|
879
|
+
const bytes = Buffer.concat(chunks);
|
|
880
|
+
if (bytes.length === 0) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
"no data on stdin (input `-` expects a document piped in, e.g. `curl \u2026 | liteparse parse -`)"
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return bytes;
|
|
886
|
+
}
|
|
887
|
+
function collectHeader(value, previous = {}) {
|
|
888
|
+
const idx = value.indexOf(":");
|
|
889
|
+
if (idx === -1) {
|
|
890
|
+
throw new Error(`invalid header '${value}', expected 'Name: Value'`);
|
|
891
|
+
}
|
|
892
|
+
const name = value.slice(0, idx).trim();
|
|
893
|
+
if (name === "") {
|
|
894
|
+
throw new Error(`invalid header '${value}', empty header name`);
|
|
895
|
+
}
|
|
896
|
+
previous[name] = value.slice(idx + 1).trim();
|
|
897
|
+
return previous;
|
|
898
|
+
}
|
|
899
|
+
program.command("parse").description("Parse a document and extract text").argument("<file>", "Path to the document file").option("-o, --output <file>", "Output file path").option("--format <format>", 'Output format: json|text|markdown (default: "text")').option(
|
|
900
|
+
"--image-mode <mode>",
|
|
901
|
+
"How to surface raster images in markdown: off|placeholder|embed (default: placeholder)"
|
|
902
|
+
).option(
|
|
903
|
+
"--image-output-dir <dir>",
|
|
904
|
+
"Directory to write embedded images to when --image-mode embed is set"
|
|
905
|
+
).option("--extract-images", "Extract embedded image bytes and metadata").option("--no-links", "Disable hyperlink extraction (emit plain anchor text)").option(
|
|
906
|
+
"--keep-headers-footers",
|
|
907
|
+
"Keep running headers/footers in markdown output instead of stripping them"
|
|
908
|
+
).option("--extract-annotations", "Include all PDF annotations in page output").option("--extract-form-fields", "Include AcroForm widget fields and values").option("--extract-structure-tree", "Include the tagged-PDF logical structure tree").option(
|
|
909
|
+
"--extract-blocks",
|
|
910
|
+
"Include each page's classified layout blocks with bounding boxes"
|
|
911
|
+
).option(
|
|
912
|
+
"--extract-xfa-packets",
|
|
913
|
+
"Include raw XFA packets (name + XML content) in JSON output"
|
|
914
|
+
).option(
|
|
915
|
+
"--extract-content-bounds",
|
|
916
|
+
"Include each page's content_bounds in JSON output"
|
|
917
|
+
).option("--ocr-server-url <url>", "HTTP OCR server URL").option(
|
|
918
|
+
"--ocr-server-header <header>",
|
|
919
|
+
'Extra header for OCR server requests, "Name: Value" (repeatable)',
|
|
920
|
+
collectHeader
|
|
921
|
+
).option("--no-ocr", "Disable OCR").option("--ocr-language <lang>", "OCR language (default: eng)").option("--max-pages <n>", "Max pages to parse", parseInt).option(
|
|
922
|
+
"--target-pages <pages>",
|
|
923
|
+
'Pages to parse (e.g., "1-5,10,15-20")'
|
|
924
|
+
).option(
|
|
925
|
+
"--continue-on-page-error",
|
|
926
|
+
"Continue after page-level extraction errors and report them in JSON"
|
|
927
|
+
).option("--dpi <dpi>", "Rendering DPI", parseFloat).option("--preserve-small-text", "Keep very small text").option(
|
|
928
|
+
"--extract-text-metadata",
|
|
929
|
+
"Include rich PDF text metadata in text items and JSON output"
|
|
930
|
+
).option("--password <password>", "Password for encrypted documents").option("--config <file>", "JSON config file path").option("-q, --quiet", "Suppress progress output").option("--num-workers <n>", "Number of concurrent OCR workers", parseInt).option(
|
|
931
|
+
"--complexity",
|
|
932
|
+
"Include per-page complexity signals in JSON output"
|
|
933
|
+
).option(
|
|
934
|
+
"--extract-vector-graphics",
|
|
935
|
+
"Include page-scoped vector shapes and merged horizontal/vertical lines"
|
|
936
|
+
).action(async (file, opts) => {
|
|
937
|
+
try {
|
|
938
|
+
const config = {};
|
|
939
|
+
if (opts.config) {
|
|
940
|
+
const fileConfig = JSON.parse(
|
|
941
|
+
readFileSync(opts.config, "utf-8")
|
|
942
|
+
);
|
|
943
|
+
Object.assign(config, fileConfig);
|
|
944
|
+
}
|
|
945
|
+
if (opts.format) config.outputFormat = opts.format;
|
|
946
|
+
if (opts.imageMode)
|
|
947
|
+
config.imageMode = opts.imageMode;
|
|
948
|
+
if (opts.imageOutputDir)
|
|
949
|
+
config.imageOutputDir = opts.imageOutputDir;
|
|
950
|
+
if (opts.extractImages) config.extractImages = true;
|
|
951
|
+
if (opts.links === false) config.extractLinks = false;
|
|
952
|
+
if (opts.keepHeadersFooters) config.keepHeadersFooters = true;
|
|
953
|
+
if (opts.extractAnnotations) config.extractAnnotations = true;
|
|
954
|
+
if (opts.extractFormFields) config.extractFormFields = true;
|
|
955
|
+
if (opts.extractStructureTree) config.extractStructureTree = true;
|
|
956
|
+
if (opts.extractBlocks) config.extractBlocks = true;
|
|
957
|
+
if (opts.extractXfaPackets) config.extractXfaPackets = true;
|
|
958
|
+
if (opts.extractContentBounds) config.extractContentBounds = true;
|
|
959
|
+
if (opts.ocrServerUrl)
|
|
960
|
+
config.ocrServerUrl = opts.ocrServerUrl;
|
|
961
|
+
if (opts.ocrServerHeader)
|
|
962
|
+
config.ocrServerHeaders = opts.ocrServerHeader;
|
|
963
|
+
if (opts.ocr === false) config.ocrEnabled = false;
|
|
964
|
+
if (opts.ocrLanguage) config.ocrLanguage = opts.ocrLanguage;
|
|
965
|
+
if (opts.maxPages) config.maxPages = opts.maxPages;
|
|
966
|
+
if (opts.targetPages) config.targetPages = opts.targetPages;
|
|
967
|
+
if (opts.continueOnPageError) config.continueOnPageError = true;
|
|
968
|
+
if (opts.dpi) config.dpi = opts.dpi;
|
|
969
|
+
if (opts.preserveSmallText) config.preserveVerySmallText = true;
|
|
970
|
+
if (opts.extractTextMetadata) config.extractTextMetadata = true;
|
|
971
|
+
if (opts.password) config.password = opts.password;
|
|
972
|
+
if (opts.quiet) config.quiet = true;
|
|
973
|
+
if (opts.numWorkers) config.numWorkers = opts.numWorkers;
|
|
974
|
+
if (opts.complexity) config.includeComplexity = true;
|
|
975
|
+
if (opts.extractVectorGraphics) config.extractVectorGraphics = true;
|
|
976
|
+
if (!config.outputFormat) config.outputFormat = "text";
|
|
977
|
+
const parser = new LiteParse(config);
|
|
978
|
+
const result = await parser.parse(await resolveInput(file));
|
|
979
|
+
for (const error of result.pageErrors) {
|
|
980
|
+
console.error(
|
|
981
|
+
`[liteparse] page ${error.pageNum} failed to extract and was skipped: ${error.message}`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
const output = config.outputFormat === "json" ? JSON.stringify(
|
|
985
|
+
parseResultToCliJson(result, {
|
|
986
|
+
extractTextMetadata: config.extractTextMetadata
|
|
987
|
+
}),
|
|
988
|
+
null,
|
|
989
|
+
2
|
|
990
|
+
) : result.text;
|
|
991
|
+
if (opts.output) {
|
|
992
|
+
writeFileSync(opts.output, output, "utf-8");
|
|
993
|
+
} else {
|
|
994
|
+
process.stdout.write(output);
|
|
995
|
+
}
|
|
996
|
+
} catch (err) {
|
|
997
|
+
console.error(
|
|
998
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`
|
|
999
|
+
);
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
program.command("is-complex").description(
|
|
1004
|
+
"Check if a document is 'complex' enough to require OCR or other advanced parsing"
|
|
1005
|
+
).argument("<file>", "Path to the document file").option("--compact", "Emit dense, whitespace-free JSON instead of pretty").option("--max-pages <n>", "Max pages to parse", parseInt).option(
|
|
1006
|
+
"--target-pages <pages>",
|
|
1007
|
+
'Pages to check (e.g., "1-5,10,15-20")'
|
|
1008
|
+
).option("--password <password>", "Password for encrypted documents").option("-q, --quiet", "Suppress progress output").action(async (file, opts) => {
|
|
1009
|
+
try {
|
|
1010
|
+
const config = {};
|
|
1011
|
+
if (opts.maxPages) config.maxPages = opts.maxPages;
|
|
1012
|
+
if (opts.targetPages) config.targetPages = opts.targetPages;
|
|
1013
|
+
if (opts.password) config.password = opts.password;
|
|
1014
|
+
if (opts.quiet) config.quiet = true;
|
|
1015
|
+
const parser = new LiteParse(config);
|
|
1016
|
+
const stats = await parser.isComplex(await resolveInput(file));
|
|
1017
|
+
const complexPages = stats.filter((s) => s.needsOcr).length;
|
|
1018
|
+
process.stdout.write(
|
|
1019
|
+
opts.compact ? JSON.stringify(stats) : JSON.stringify(stats, null, 2)
|
|
1020
|
+
);
|
|
1021
|
+
process.stdout.write("\n");
|
|
1022
|
+
if (!opts.quiet) {
|
|
1023
|
+
const verdict = complexPages > 0 ? "COMPLEX" : "SIMPLE";
|
|
1024
|
+
const layoutCount = (reason) => stats.filter((s) => s.layout?.reasons.includes(reason)).length;
|
|
1025
|
+
console.error(
|
|
1026
|
+
`${verdict} \u2014 ${complexPages}/${stats.length} page(s) need OCR; layout: ${layoutCount("multi-column")} multi-column, ${layoutCount("table-likely")} table, ${layoutCount("dense-graphics")} graphics-dense`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
if (complexPages > 0) process.exit(1);
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
console.error(
|
|
1032
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`
|
|
1033
|
+
);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
program.command("screenshot").description("Generate screenshots of document pages").argument("<file>", "Path to the document file").option(
|
|
1038
|
+
"-o, --output-dir <dir>",
|
|
1039
|
+
"Output directory for screenshots",
|
|
1040
|
+
"./screenshots"
|
|
1041
|
+
).option(
|
|
1042
|
+
"--target-pages <pages>",
|
|
1043
|
+
'Pages to screenshot (e.g., "1,3,5" or "1-5")'
|
|
1044
|
+
).option("--dpi <dpi>", "Rendering DPI", parseFloat).option("--password <password>", "Password for encrypted documents").option("-q, --quiet", "Suppress progress output").action(async (file, opts) => {
|
|
1045
|
+
try {
|
|
1046
|
+
const config = {};
|
|
1047
|
+
if (opts.dpi) config.dpi = opts.dpi;
|
|
1048
|
+
if (opts.password) config.password = opts.password;
|
|
1049
|
+
if (opts.quiet) config.quiet = true;
|
|
1050
|
+
if (opts.targetPages) config.targetPages = opts.targetPages;
|
|
1051
|
+
const parser = new LiteParse(config);
|
|
1052
|
+
let pageNumbers;
|
|
1053
|
+
if (opts.targetPages) {
|
|
1054
|
+
pageNumbers = [];
|
|
1055
|
+
for (const part of opts.targetPages.split(",")) {
|
|
1056
|
+
const trimmed = part.trim();
|
|
1057
|
+
if (trimmed.includes("-")) {
|
|
1058
|
+
const [start, end] = trimmed.split("-").map(Number);
|
|
1059
|
+
for (let i = start; i <= end; i++) pageNumbers.push(i);
|
|
1060
|
+
} else {
|
|
1061
|
+
pageNumbers.push(Number(trimmed));
|
|
384
1062
|
}
|
|
385
|
-
|
|
386
|
-
if (errors > 0)
|
|
387
|
-
process.exit(1);
|
|
1063
|
+
}
|
|
388
1064
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1065
|
+
const outputDir = opts.outputDir;
|
|
1066
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1067
|
+
const results = await parser.screenshot(file, pageNumbers);
|
|
1068
|
+
for (const result of results) {
|
|
1069
|
+
const outputPath = join2(outputDir, `page_${result.pageNum}.png`);
|
|
1070
|
+
writeFileSync(outputPath, result.imageBuffer);
|
|
1071
|
+
if (!opts.quiet) {
|
|
1072
|
+
console.error(
|
|
1073
|
+
`[liteparse] screenshot page ${result.pageNum} \u2192 ${outputPath}`
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
392
1076
|
}
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
console.error(
|
|
1079
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`
|
|
1080
|
+
);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
393
1083
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1084
|
+
program.command("batch-parse").description("Parse multiple documents in batch mode").argument("<input-dir>", "Input directory").argument("<output-dir>", "Output directory").option("--format <format>", 'Output format: json|text|markdown (default: "text")').option("--no-ocr", "Disable OCR").option("--ocr-language <lang>", "OCR language (default: eng)").option("--ocr-server-url <url>", "HTTP OCR server URL").option(
|
|
1085
|
+
"--ocr-server-header <header>",
|
|
1086
|
+
'Extra header for OCR server requests, "Name: Value" (repeatable)',
|
|
1087
|
+
collectHeader
|
|
1088
|
+
).option("--max-pages <n>", "Max pages to parse per file", parseInt).option("--dpi <dpi>", "Rendering DPI", parseFloat).option("--recursive", "Recursively search input directory").option("--extension <ext>", "Only process files with this extension").option("--password <password>", "Password for encrypted documents").option("-q, --quiet", "Suppress progress output").option("--num-workers <n>", "Number of concurrent OCR workers", parseInt).option(
|
|
1089
|
+
"--extract-text-metadata",
|
|
1090
|
+
"Include rich PDF text metadata in text items and JSON output"
|
|
1091
|
+
).option("--extract-images", "Extract embedded image bytes and metadata").option("--extract-annotations", "Include all PDF annotations in page output").option("--extract-form-fields", "Include AcroForm widget fields and values").option("--extract-structure-tree", "Include the tagged-PDF logical structure tree").option(
|
|
1092
|
+
"--extract-blocks",
|
|
1093
|
+
"Include each page's classified layout blocks with bounding boxes"
|
|
1094
|
+
).option(
|
|
1095
|
+
"--extract-xfa-packets",
|
|
1096
|
+
"Include raw XFA packets (name + XML content) in JSON output"
|
|
1097
|
+
).option(
|
|
1098
|
+
"--extract-content-bounds",
|
|
1099
|
+
"Include each page's content_bounds in JSON output"
|
|
1100
|
+
).option(
|
|
1101
|
+
"--extract-vector-graphics",
|
|
1102
|
+
"Include page-scoped vector shapes and merged horizontal/vertical lines"
|
|
1103
|
+
).action(
|
|
1104
|
+
async (inputDir, outputDir, opts) => {
|
|
1105
|
+
try {
|
|
1106
|
+
const config = {};
|
|
1107
|
+
const format = opts.format ?? "text";
|
|
1108
|
+
config.outputFormat = format;
|
|
1109
|
+
if (opts.ocr === false) config.ocrEnabled = false;
|
|
1110
|
+
if (opts.ocrLanguage) config.ocrLanguage = opts.ocrLanguage;
|
|
1111
|
+
if (opts.ocrServerUrl)
|
|
1112
|
+
config.ocrServerUrl = opts.ocrServerUrl;
|
|
1113
|
+
if (opts.ocrServerHeader)
|
|
1114
|
+
config.ocrServerHeaders = opts.ocrServerHeader;
|
|
1115
|
+
if (opts.maxPages) config.maxPages = opts.maxPages;
|
|
1116
|
+
if (opts.dpi) config.dpi = opts.dpi;
|
|
1117
|
+
if (opts.password) config.password = opts.password;
|
|
1118
|
+
if (opts.quiet) config.quiet = true;
|
|
1119
|
+
if (opts.numWorkers) config.numWorkers = opts.numWorkers;
|
|
1120
|
+
if (opts.extractTextMetadata) config.extractTextMetadata = true;
|
|
1121
|
+
if (opts.extractImages) config.extractImages = true;
|
|
1122
|
+
if (opts.extractAnnotations) config.extractAnnotations = true;
|
|
1123
|
+
if (opts.extractFormFields) config.extractFormFields = true;
|
|
1124
|
+
if (opts.extractStructureTree) config.extractStructureTree = true;
|
|
1125
|
+
if (opts.extractBlocks) config.extractBlocks = true;
|
|
1126
|
+
if (opts.extractXfaPackets) config.extractXfaPackets = true;
|
|
1127
|
+
if (opts.extractContentBounds) config.extractContentBounds = true;
|
|
1128
|
+
if (opts.extractContentBounds) config.extractContentBounds = true;
|
|
1129
|
+
if (opts.extractXfaPackets) config.extractXfaPackets = true;
|
|
1130
|
+
if (opts.extractContentBounds) config.extractContentBounds = true;
|
|
1131
|
+
if (opts.extractVectorGraphics) config.extractVectorGraphics = true;
|
|
1132
|
+
const parser = new LiteParse(config);
|
|
1133
|
+
const outExt = format === "json" ? ".json" : format === "markdown" ? ".md" : ".txt";
|
|
1134
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1135
|
+
const extFilter = opts.extension ? opts.extension.startsWith(".") ? opts.extension.toLowerCase() : `.${opts.extension.toLowerCase()}` : void 0;
|
|
1136
|
+
const files = collectFiles(
|
|
1137
|
+
inputDir,
|
|
1138
|
+
!!opts.recursive,
|
|
1139
|
+
extFilter
|
|
1140
|
+
);
|
|
1141
|
+
if (files.length === 0) {
|
|
1142
|
+
console.error(
|
|
1143
|
+
`[liteparse] no matching files found in ${inputDir}`
|
|
1144
|
+
);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (!opts.quiet) {
|
|
1148
|
+
console.error(
|
|
1149
|
+
`[liteparse] found ${files.length} files to process`
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
let success = 0;
|
|
1153
|
+
let errors = 0;
|
|
1154
|
+
for (const filePath of files) {
|
|
1155
|
+
const t0 = Date.now();
|
|
1156
|
+
const rel = relative(inputDir, filePath);
|
|
1157
|
+
const parsed = parsePath(rel);
|
|
1158
|
+
const outPath = join2(
|
|
1159
|
+
outputDir,
|
|
1160
|
+
parsed.dir,
|
|
1161
|
+
parsed.name + outExt
|
|
1162
|
+
);
|
|
1163
|
+
mkdirSync(join2(outputDir, parsed.dir), { recursive: true });
|
|
1164
|
+
try {
|
|
1165
|
+
const result = await parser.parse(filePath);
|
|
1166
|
+
const output = format === "json" ? JSON.stringify(
|
|
1167
|
+
parseResultToCliJson(result, {
|
|
1168
|
+
extractTextMetadata: config.extractTextMetadata
|
|
1169
|
+
}),
|
|
1170
|
+
null,
|
|
1171
|
+
2
|
|
1172
|
+
) : result.text;
|
|
1173
|
+
writeFileSync(outPath, output, "utf-8");
|
|
1174
|
+
success++;
|
|
1175
|
+
if (!opts.quiet) {
|
|
1176
|
+
const elapsed = Date.now() - t0;
|
|
1177
|
+
console.error(
|
|
1178
|
+
`[liteparse] ${filePath} \u2192 ${outPath} (${elapsed}ms)`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
console.error(
|
|
1183
|
+
`[liteparse] error parsing ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1184
|
+
);
|
|
1185
|
+
errors++;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
console.error(
|
|
1189
|
+
`[liteparse] batch complete: ${success} succeeded, ${errors} failed`
|
|
1190
|
+
);
|
|
1191
|
+
if (errors > 0) process.exit(1);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
console.error(
|
|
1194
|
+
`Error: ${err instanceof Error ? err.message : String(err)}`
|
|
1195
|
+
);
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
);
|
|
1200
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1201
|
+
".pdf",
|
|
1202
|
+
".doc",
|
|
1203
|
+
".docx",
|
|
1204
|
+
".docm",
|
|
1205
|
+
".dot",
|
|
1206
|
+
".dotm",
|
|
1207
|
+
".dotx",
|
|
1208
|
+
".odt",
|
|
1209
|
+
".ott",
|
|
1210
|
+
".rtf",
|
|
1211
|
+
".pages",
|
|
1212
|
+
".ppt",
|
|
1213
|
+
".pptx",
|
|
1214
|
+
".pptm",
|
|
1215
|
+
".pot",
|
|
1216
|
+
".potm",
|
|
1217
|
+
".potx",
|
|
1218
|
+
".odp",
|
|
1219
|
+
".otp",
|
|
1220
|
+
".key",
|
|
1221
|
+
".xls",
|
|
1222
|
+
".xlsx",
|
|
1223
|
+
".xlsm",
|
|
1224
|
+
".xlsb",
|
|
1225
|
+
".ods",
|
|
1226
|
+
".ots",
|
|
1227
|
+
".csv",
|
|
1228
|
+
".tsv",
|
|
1229
|
+
".numbers",
|
|
1230
|
+
".jpg",
|
|
1231
|
+
".jpeg",
|
|
1232
|
+
".png",
|
|
1233
|
+
".gif",
|
|
1234
|
+
".bmp",
|
|
1235
|
+
".tiff",
|
|
1236
|
+
".tif",
|
|
1237
|
+
".webp",
|
|
1238
|
+
".svg",
|
|
1239
|
+
".txt",
|
|
1240
|
+
".md",
|
|
1241
|
+
".markdown",
|
|
1242
|
+
".log"
|
|
401
1243
|
]);
|
|
402
1244
|
function collectFiles(dir, recursive, extFilter) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1245
|
+
const files = [];
|
|
1246
|
+
collectFilesInner(dir, recursive, extFilter, files);
|
|
1247
|
+
files.sort();
|
|
1248
|
+
return files;
|
|
407
1249
|
}
|
|
408
1250
|
function collectFilesInner(dir, recursive, extFilter, files) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
else {
|
|
422
|
-
const ext = lower.lastIndexOf(".") >= 0 ? lower.slice(lower.lastIndexOf(".")) : "";
|
|
423
|
-
if (!SUPPORTED_EXTENSIONS.has(ext))
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
files.push(fullPath);
|
|
1251
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1252
|
+
const fullPath = join2(dir, entry.name);
|
|
1253
|
+
if (entry.isDirectory()) {
|
|
1254
|
+
if (recursive) collectFilesInner(fullPath, recursive, extFilter, files);
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const lower = entry.name.toLowerCase();
|
|
1258
|
+
if (extFilter) {
|
|
1259
|
+
if (!lower.endsWith(extFilter)) continue;
|
|
1260
|
+
} else {
|
|
1261
|
+
const ext = lower.lastIndexOf(".") >= 0 ? lower.slice(lower.lastIndexOf(".")) : "";
|
|
1262
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
|
|
427
1263
|
}
|
|
1264
|
+
files.push(fullPath);
|
|
1265
|
+
}
|
|
428
1266
|
}
|
|
429
1267
|
program.parse(process.argv);
|
|
430
1268
|
//# sourceMappingURL=cli.js.map
|