@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/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
- import { LiteParse } from "./lib.js";
4
- import { parseResultToCliJson } from "./cli-json.js";
5
- import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
6
- import { join, relative, parse as parsePath } from "node:path";
7
- program
8
- .name("liteparse")
9
- .description("Fast, lightweight PDF and document parsing")
10
- .version("2.0.0");
11
- /**
12
- * Resolve a CLI `<file>` argument into a parser input. `-` means read the
13
- * document from stdin (e.g. `curl -sL … | liteparse parse -`); anything else is
14
- * passed through as a path. Streaming stdin (rather than `readFileSync(0)`)
15
- * avoids EAGAIN on non-blocking pipes.
16
- */
17
- async function resolveInput(file) {
18
- if (file !== "-")
19
- return file;
20
- const chunks = [];
21
- for await (const chunk of process.stdin) {
22
- chunks.push(chunk);
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
- const bytes = Buffer.concat(chunks);
25
- if (bytes.length === 0) {
26
- throw new Error("no data on stdin (input `-` expects a document piped in, e.g. `curl … | liteparse parse -`)");
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
- return bytes;
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
- /** Collect repeated `--ocr-server-header "Name: Value"` flags into an object. */
31
- function collectHeader(value, previous = {}) {
32
- const idx = value.indexOf(":");
33
- if (idx === -1) {
34
- throw new Error(`invalid header '${value}', expected 'Name: Value'`);
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
- const name = value.slice(0, idx).trim();
37
- if (name === "") {
38
- throw new Error(`invalid header '${value}', empty header name`);
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
- previous[name] = value.slice(idx + 1).trim();
41
- return previous;
93
+ }
94
+ return result;
42
95
  }
43
- program
44
- .command("parse")
45
- .description("Parse a document and extract text")
46
- .argument("<file>", "Path to the document file")
47
- .option("-o, --output <file>", "Output file path")
48
- .option("--format <format>", 'Output format: json|text|markdown (default: "text")')
49
- .option("--image-mode <mode>", "How to surface raster images in markdown: off|placeholder|embed (default: placeholder)")
50
- .option("--image-output-dir <dir>", "Directory to write embedded images to when --image-mode embed is set")
51
- .option("--extract-images", "Extract embedded image bytes and metadata")
52
- .option("--no-links", "Disable hyperlink extraction (emit plain anchor text)")
53
- .option("--keep-headers-footers", "Keep running headers/footers in markdown output instead of stripping them")
54
- .option("--extract-annotations", "Include all PDF annotations in page output")
55
- .option("--extract-form-fields", "Include AcroForm widget fields and values")
56
- .option("--extract-structure-tree", "Include the tagged-PDF logical structure tree")
57
- .option("--extract-blocks", "Include each page's classified layout blocks with bounding boxes")
58
- .option("--extract-xfa-packets", "Include raw XFA packets (name + XML content) in JSON output")
59
- .option("--extract-content-bounds", "Include each page's content_bounds in JSON output")
60
- .option("--ocr-server-url <url>", "HTTP OCR server URL")
61
- .option("--ocr-server-header <header>", 'Extra header for OCR server requests, "Name: Value" (repeatable)', collectHeader)
62
- .option("--no-ocr", "Disable OCR")
63
- .option("--ocr-language <lang>", "OCR language (default: eng)")
64
- .option("--max-pages <n>", "Max pages to parse", parseInt)
65
- .option("--target-pages <pages>", 'Pages to parse (e.g., "1-5,10,15-20")')
66
- .option("--continue-on-page-error", "Continue after page-level extraction errors and report them in JSON")
67
- .option("--dpi <dpi>", "Rendering DPI", parseFloat)
68
- .option("--preserve-small-text", "Keep very small text")
69
- .option("--extract-text-metadata", "Include rich PDF text metadata in text items and JSON output")
70
- .option("--password <password>", "Password for encrypted documents")
71
- .option("--config <file>", "JSON config file path")
72
- .option("-q, --quiet", "Suppress progress output")
73
- .option("--num-workers <n>", "Number of concurrent OCR workers", parseInt)
74
- .option("--complexity", "Include per-page complexity signals in JSON output")
75
- .option("--extract-vector-graphics", "Include page-scoped vector shapes and merged horizontal/vertical lines")
76
- .action(async (file, opts) => {
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
- const config = {};
79
- // Load config file if provided
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
- catch (err) {
162
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
163
- process.exit(1);
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
- program
167
- .command("is-complex")
168
- .description("Check if a document is 'complex' enough to require OCR or other advanced parsing")
169
- .argument("<file>", "Path to the document file")
170
- .option("--compact", "Emit dense, whitespace-free JSON instead of pretty")
171
- .option("--max-pages <n>", "Max pages to parse", parseInt)
172
- .option("--target-pages <pages>", 'Pages to check (e.g., "1-5,10,15-20")')
173
- .option("--password <password>", "Password for encrypted documents")
174
- .option("-q, --quiet", "Suppress progress output")
175
- .action(async (file, opts) => {
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
- const config = {};
178
- if (opts.maxPages)
179
- config.maxPages = opts.maxPages;
180
- if (opts.targetPages)
181
- config.targetPages = opts.targetPages;
182
- if (opts.password)
183
- config.password = opts.password;
184
- if (opts.quiet)
185
- config.quiet = true;
186
- const parser = new LiteParse(config);
187
- const stats = await parser.isComplex(await resolveInput(file));
188
- const complexPages = stats.filter((s) => s.needsOcr).length;
189
- // Always emit JSON on stdout so the command composes with `jq` without a
190
- // flag. Pretty by default; `--compact` drops the whitespace. Both parse
191
- // identically for any reader.
192
- process.stdout.write(opts.compact
193
- ? JSON.stringify(stats)
194
- : JSON.stringify(stats, null, 2));
195
- process.stdout.write("\n");
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
- catch (err) {
212
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
213
- process.exit(1);
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
- program
217
- .command("screenshot")
218
- .description("Generate screenshots of document pages")
219
- .argument("<file>", "Path to the document file")
220
- .option("-o, --output-dir <dir>", "Output directory for screenshots", "./screenshots")
221
- .option("--target-pages <pages>", 'Pages to screenshot (e.g., "1,3,5" or "1-5")')
222
- .option("--dpi <dpi>", "Rendering DPI", parseFloat)
223
- .option("--password <password>", "Password for encrypted documents")
224
- .option("-q, --quiet", "Suppress progress output")
225
- .action(async (file, opts) => {
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
- const config = {};
228
- if (opts.dpi)
229
- config.dpi = opts.dpi;
230
- if (opts.password)
231
- config.password = opts.password;
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
- catch (err) {
265
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
266
- process.exit(1);
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
- program
270
- .command("batch-parse")
271
- .description("Parse multiple documents in batch mode")
272
- .argument("<input-dir>", "Input directory")
273
- .argument("<output-dir>", "Output directory")
274
- .option("--format <format>", 'Output format: json|text|markdown (default: "text")')
275
- .option("--no-ocr", "Disable OCR")
276
- .option("--ocr-language <lang>", "OCR language (default: eng)")
277
- .option("--ocr-server-url <url>", "HTTP OCR server URL")
278
- .option("--ocr-server-header <header>", 'Extra header for OCR server requests, "Name: Value" (repeatable)', collectHeader)
279
- .option("--max-pages <n>", "Max pages to parse per file", parseInt)
280
- .option("--dpi <dpi>", "Rendering DPI", parseFloat)
281
- .option("--recursive", "Recursively search input directory")
282
- .option("--extension <ext>", "Only process files with this extension")
283
- .option("--password <password>", "Password for encrypted documents")
284
- .option("-q, --quiet", "Suppress progress output")
285
- .option("--num-workers <n>", "Number of concurrent OCR workers", parseInt)
286
- .option("--extract-text-metadata", "Include rich PDF text metadata in text items and JSON output")
287
- .option("--extract-images", "Extract embedded image bytes and metadata")
288
- .option("--extract-annotations", "Include all PDF annotations in page output")
289
- .option("--extract-form-fields", "Include AcroForm widget fields and values")
290
- .option("--extract-structure-tree", "Include the tagged-PDF logical structure tree")
291
- .option("--extract-blocks", "Include each page's classified layout blocks with bounding boxes")
292
- .option("--extract-xfa-packets", "Include raw XFA packets (name + XML content) in JSON output")
293
- .option("--extract-content-bounds", "Include each page's content_bounds in JSON output")
294
- .option("--extract-vector-graphics", "Include page-scoped vector shapes and merged horizontal/vertical lines")
295
- .action(async (inputDir, outputDir, opts) => {
296
- try {
297
- const config = {};
298
- const format = opts.format ?? "text";
299
- config.outputFormat = format;
300
- if (opts.ocr === false)
301
- config.ocrEnabled = false;
302
- if (opts.ocrLanguage)
303
- config.ocrLanguage = opts.ocrLanguage;
304
- if (opts.ocrServerUrl)
305
- config.ocrServerUrl = opts.ocrServerUrl;
306
- if (opts.ocrServerHeader)
307
- config.ocrServerHeaders = opts.ocrServerHeader;
308
- if (opts.maxPages)
309
- config.maxPages = opts.maxPages;
310
- if (opts.dpi)
311
- config.dpi = opts.dpi;
312
- if (opts.password)
313
- config.password = opts.password;
314
- if (opts.quiet)
315
- config.quiet = true;
316
- if (opts.numWorkers)
317
- config.numWorkers = opts.numWorkers;
318
- if (opts.extractTextMetadata)
319
- config.extractTextMetadata = true;
320
- if (opts.extractImages)
321
- config.extractImages = true;
322
- if (opts.extractAnnotations)
323
- config.extractAnnotations = true;
324
- if (opts.extractFormFields)
325
- config.extractFormFields = true;
326
- if (opts.extractStructureTree)
327
- config.extractStructureTree = true;
328
- if (opts.extractBlocks)
329
- config.extractBlocks = true;
330
- if (opts.extractXfaPackets)
331
- config.extractXfaPackets = true;
332
- if (opts.extractContentBounds)
333
- config.extractContentBounds = true;
334
- if (opts.extractContentBounds)
335
- config.extractContentBounds = true;
336
- if (opts.extractXfaPackets)
337
- config.extractXfaPackets = true;
338
- if (opts.extractContentBounds)
339
- config.extractContentBounds = true;
340
- if (opts.extractVectorGraphics)
341
- config.extractVectorGraphics = true;
342
- const parser = new LiteParse(config);
343
- const outExt = format === "json" ? ".json" : format === "markdown" ? ".md" : ".txt";
344
- mkdirSync(outputDir, { recursive: true });
345
- const extFilter = opts.extension
346
- ? opts.extension.startsWith(".")
347
- ? opts.extension.toLowerCase()
348
- : `.${opts.extension.toLowerCase()}`
349
- : undefined;
350
- const files = collectFiles(inputDir, !!opts.recursive, extFilter);
351
- if (files.length === 0) {
352
- console.error(`[liteparse] no matching files found in ${inputDir}`);
353
- return;
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
- if (!opts.quiet) {
356
- console.error(`[liteparse] found ${files.length} files to process`);
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
- let success = 0;
359
- let errors = 0;
360
- for (const filePath of files) {
361
- const t0 = Date.now();
362
- const rel = relative(inputDir, filePath);
363
- const parsed = parsePath(rel);
364
- const outPath = join(outputDir, parsed.dir, parsed.name + outExt);
365
- mkdirSync(join(outputDir, parsed.dir), { recursive: true });
366
- try {
367
- const result = await parser.parse(filePath);
368
- const output = format === "json"
369
- ? JSON.stringify(parseResultToCliJson(result, {
370
- extractTextMetadata: config.extractTextMetadata,
371
- }), null, 2)
372
- : result.text;
373
- writeFileSync(outPath, output, "utf-8");
374
- success++;
375
- if (!opts.quiet) {
376
- const elapsed = Date.now() - t0;
377
- console.error(`[liteparse] ${filePath} ${outPath} (${elapsed}ms)`);
378
- }
379
- }
380
- catch (err) {
381
- console.error(`[liteparse] error parsing ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
382
- errors++;
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
- console.error(`[liteparse] batch complete: ${success} succeeded, ${errors} failed`);
386
- if (errors > 0)
387
- process.exit(1);
1063
+ }
388
1064
  }
389
- catch (err) {
390
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
391
- process.exit(1);
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
- const SUPPORTED_EXTENSIONS = new Set([
395
- ".pdf",
396
- ".doc", ".docx", ".docm", ".dot", ".dotm", ".dotx", ".odt", ".ott", ".rtf", ".pages",
397
- ".ppt", ".pptx", ".pptm", ".pot", ".potm", ".potx", ".odp", ".otp", ".key",
398
- ".xls", ".xlsx", ".xlsm", ".xlsb", ".ods", ".ots", ".csv", ".tsv", ".numbers",
399
- ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp", ".svg",
400
- ".txt", ".md", ".markdown", ".log",
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
- const files = [];
404
- collectFilesInner(dir, recursive, extFilter, files);
405
- files.sort();
406
- return files;
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
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
410
- const fullPath = join(dir, entry.name);
411
- if (entry.isDirectory()) {
412
- if (recursive)
413
- collectFilesInner(fullPath, recursive, extFilter, files);
414
- continue;
415
- }
416
- const lower = entry.name.toLowerCase();
417
- if (extFilter) {
418
- if (!lower.endsWith(extFilter))
419
- continue;
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