pdf-fill 0.6.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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/dist/index.js +899 -0
  3. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # pdffill
2
+
3
+ CLI for listing, diagnosing, diffing, and filling PDF AcroForms.
4
+
5
+ ```bash
6
+ npm install -g pdffill
7
+ pdffill doctor template.pdf
8
+ pdffill run template.pdf data.json -o filled.pdf
9
+ ```
10
+
11
+ Requires Node.js 18+. Library: [`@lzlz94/pdffill-core`](https://www.npmjs.com/package/@lzlz94/pdffill-core).
12
+
13
+ Documentation: [README](https://github.com/lz1834career/pdf-fill/blob/main/README.md).
package/dist/index.js ADDED
@@ -0,0 +1,899 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/list.ts
7
+ import { listFields } from "@lzlz94/pdffill-core";
8
+
9
+ // src/util/read-file.ts
10
+ import { readFile } from "fs/promises";
11
+ async function readBinary(path) {
12
+ const buf = await readFile(path);
13
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
14
+ }
15
+ async function readJson(path) {
16
+ const text = await readFile(path, "utf8");
17
+ return JSON.parse(text);
18
+ }
19
+ async function readJsonSchema(path) {
20
+ return await readJson(path);
21
+ }
22
+
23
+ // src/commands/list.ts
24
+ function printTable(fields) {
25
+ if (fields.length === 0) {
26
+ console.log("No AcroForm fields found.");
27
+ return;
28
+ }
29
+ for (const f of fields) {
30
+ const opts = f.options?.length ? ` options=[${f.options.join(", ")}]` : "";
31
+ const ro = f.readOnly ? " readonly" : "";
32
+ console.log(`${f.name} ${f.type}${ro}${opts}`);
33
+ }
34
+ }
35
+ function registerListCommand(program2) {
36
+ program2.command("list").description("List AcroForm fields in a PDF").argument("<pdf>", "Path to PDF template").option("--json", "Output as JSON array").action(async (pdfPath, opts) => {
37
+ const bytes = await readBinary(pdfPath);
38
+ const fields = await listFields(bytes);
39
+ if (opts.json) {
40
+ console.log(JSON.stringify(fields, null, 2));
41
+ } else {
42
+ printTable(fields);
43
+ }
44
+ });
45
+ }
46
+
47
+ // src/commands/doctor.ts
48
+ import { doctor } from "@lzlz94/pdffill-core";
49
+ function registerDoctorCommand(program2) {
50
+ program2.command("doctor").description("Diagnose whether a PDF can be filled with pdffill").argument("<pdf>", "Path to PDF template").option("--json", "Output report as JSON").action(async (pdfPath, opts) => {
51
+ const bytes = await readBinary(pdfPath);
52
+ const report = await doctor(bytes);
53
+ if (opts.json) {
54
+ console.log(JSON.stringify(report, null, 2));
55
+ } else {
56
+ console.log(report.ok ? "OK" : "NOT OK");
57
+ console.log(`encrypted: ${report.encrypted}`);
58
+ console.log(`hasXFA: ${report.hasXFA}`);
59
+ console.log(`fieldCount: ${report.fieldCount}`);
60
+ for (const issue of report.issues) {
61
+ console.log(`[${issue.level}] ${issue.code}: ${issue.message}`);
62
+ }
63
+ }
64
+ if (!report.ok) process.exitCode = 1;
65
+ });
66
+ }
67
+
68
+ // src/commands/fill.ts
69
+ import { writeFile as writeFile2 } from "fs/promises";
70
+ import {
71
+ fillForm,
72
+ doctor as doctor2,
73
+ assertDoctorReady,
74
+ buildFillReport,
75
+ SchemaValidationError,
76
+ PdfFillError as PdfFillError2,
77
+ parseFillDataInput as parseFillDataInput2,
78
+ parseMappingFile as parseMappingFile2,
79
+ applyFieldMapping as applyFieldMapping2
80
+ } from "@lzlz94/pdffill-core";
81
+
82
+ // src/util/config.ts
83
+ import {
84
+ mergePdffillConfig,
85
+ parsePdffillConfig
86
+ } from "@lzlz94/pdffill-core";
87
+ async function loadPdffillConfig(path) {
88
+ if (!path) return void 0;
89
+ const raw = await readJson(path);
90
+ return parsePdffillConfig(raw);
91
+ }
92
+ function mergeConfig(config, cli) {
93
+ if (!config) return cli;
94
+ return mergePdffillConfig(config, cli);
95
+ }
96
+ function resolveIgnoreFields(config, cliList) {
97
+ if (cliList) {
98
+ return cliList.split(",").map((s) => s.trim()).filter(Boolean);
99
+ }
100
+ return config?.ignoreFields ?? [];
101
+ }
102
+ function resolveMappingPath(config, cliPath) {
103
+ return cliPath ?? config?.mapping;
104
+ }
105
+
106
+ // src/util/print-diff.ts
107
+ function printFieldDiff(result) {
108
+ console.log(
109
+ `Fields: ${result.pdfFieldCount} in PDF, ${result.dataKeyCount} in data \u2014 ${result.ok ? "OK" : "issues found"}`
110
+ );
111
+ if (result.missingInData.length) {
112
+ console.log(`Missing in data (${result.missingInData.length}):`);
113
+ for (const f of result.missingInData) console.log(` - ${f}`);
114
+ }
115
+ if (result.unknownInData.length) {
116
+ console.log(`Unknown in data (${result.unknownInData.length}):`);
117
+ for (const f of result.unknownInData) console.log(` - ${f}`);
118
+ }
119
+ if (result.missingInBusinessData?.length) {
120
+ console.log(`Missing in business JSON (${result.missingInBusinessData.length}):`);
121
+ for (const g of result.missingInBusinessData) {
122
+ console.log(` - ${g.sourcePath} \u2192 ${g.pdfField}`);
123
+ }
124
+ }
125
+ if (result.unmappedPdfFields?.length) {
126
+ console.log(`Unmapped PDF fields (${result.unmappedPdfFields.length}):`);
127
+ for (const f of result.unmappedPdfFields) console.log(` - ${f}`);
128
+ }
129
+ if (result.typeMismatches?.length) {
130
+ console.log(`Type mismatches (${result.typeMismatches.length}):`);
131
+ for (const m of result.typeMismatches) {
132
+ console.log(` - ${m.field}: ${m.message}`);
133
+ }
134
+ }
135
+ if (result.unchangedFromTemplate?.length) {
136
+ console.log(
137
+ `Unchanged from template (${result.unchangedFromTemplate.length}):`
138
+ );
139
+ for (const u of result.unchangedFromTemplate) {
140
+ console.log(` - ${u.field}`);
141
+ }
142
+ }
143
+ }
144
+
145
+ // src/util/resolve-data-diff.ts
146
+ import {
147
+ applyFieldMapping,
148
+ diffFields,
149
+ diffFieldsWithMapping,
150
+ listFields as listFields2,
151
+ parseFillDataInput,
152
+ parseMappingFile,
153
+ PdfFillError
154
+ } from "@lzlz94/pdffill-core";
155
+ async function resolveDataAndDiff(opts) {
156
+ const diffOpts = {
157
+ ignoreFields: opts.ignoreFields ?? [],
158
+ includeReadOnly: opts.includeReadOnly ?? false,
159
+ compareTemplate: opts.compareTemplate ?? false
160
+ };
161
+ if (opts.mappingPath) {
162
+ const mapping = parseMappingFile(await readJson(opts.mappingPath));
163
+ if (typeof opts.raw !== "object" || opts.raw === null || Array.isArray(opts.raw)) {
164
+ throw new PdfFillError(
165
+ "With --mapping, data must be a JSON object",
166
+ "INVALID_FILL_DATA_SHAPE"
167
+ );
168
+ }
169
+ const business = opts.raw;
170
+ const fields = await listFields2(opts.template);
171
+ const diff2 = diffFieldsWithMapping(fields, business, mapping, diffOpts);
172
+ const data2 = applyFieldMapping(business, mapping);
173
+ return { data: data2, diff: diff2 };
174
+ }
175
+ const data = parseFillDataInput(opts.raw);
176
+ const diff = await diffFields(opts.template, data, diffOpts);
177
+ return { data, diff };
178
+ }
179
+ function assertDiffOk(diff) {
180
+ if (diff.ok) return;
181
+ const parts = [];
182
+ if (diff.missingInData.length) {
183
+ parts.push(`${diff.missingInData.length} missing in data`);
184
+ }
185
+ if (diff.unknownInData.length) {
186
+ parts.push(`${diff.unknownInData.length} unknown in data`);
187
+ }
188
+ if (diff.typeMismatches?.length) {
189
+ parts.push(`${diff.typeMismatches.length} type mismatch(es)`);
190
+ }
191
+ if (diff.missingInBusinessData?.length) {
192
+ parts.push(`${diff.missingInBusinessData.length} missing in business data`);
193
+ }
194
+ if (diff.unmappedPdfFields?.length) {
195
+ parts.push(`${diff.unmappedPdfFields.length} unmapped PDF field(s)`);
196
+ }
197
+ throw new PdfFillError(
198
+ `Field diff failed: ${parts.join(", ")}`,
199
+ "DIFF_FAILED"
200
+ );
201
+ }
202
+
203
+ // src/util/resolve-fill-cli.ts
204
+ import {
205
+ parseFailOnWarningsSpec
206
+ } from "@lzlz94/pdffill-core";
207
+ function resolveFillCli(opts, fileConfig) {
208
+ const missingRaw = opts.missing ?? fileConfig?.missing ?? "skip";
209
+ return {
210
+ mappingPath: resolveMappingPath(fileConfig, opts.mapping),
211
+ ignoreFields: resolveIgnoreFields(fileConfig, opts.ignoreFields),
212
+ flatten: opts.flatten ?? fileConfig?.flatten ?? false,
213
+ strict: opts.strict ?? fileConfig?.strict ?? false,
214
+ missing: missingRaw === "error" ? "error" : "skip",
215
+ updateAppearances: opts.updateAppearances === false ? false : fileConfig?.updateAppearances === false ? false : true,
216
+ fontPath: opts.font ?? fileConfig?.fontPath,
217
+ requireDoctor: opts.requireDoctor ?? fileConfig?.requireDoctor ?? false,
218
+ skipDoctor: opts.skipDoctor ?? false,
219
+ failOnWarnings: parseFailOnWarningsSpec(opts.failOnWarnings) ?? fileConfig?.failOnWarnings
220
+ };
221
+ }
222
+
223
+ // src/util/write-report.ts
224
+ import { writeFile } from "fs/promises";
225
+ import {
226
+ serializeBatchManifest
227
+ } from "@lzlz94/pdffill-core";
228
+ async function writeJsonReport(path, data) {
229
+ await writeFile(path, `${JSON.stringify(data, null, 2)}
230
+ `, "utf8");
231
+ console.log(`Wrote report ${path}`);
232
+ }
233
+ async function writeBatchManifest(path, entries, format = "json") {
234
+ await writeFile(path, serializeBatchManifest(entries, format), "utf8");
235
+ console.log(`Wrote manifest ${path} (${format})`);
236
+ }
237
+
238
+ // src/commands/fill.ts
239
+ function registerFillCommand(program2) {
240
+ program2.command("fill").description("Fill PDF form fields from JSON (editable by default)").argument("<pdf>", "Path to PDF template").argument("<data.json>", "JSON object: field name \u2192 value").requiredOption("-o, --output <path>", "Output PDF path").option("--flatten", "Flatten form (read-only output)").option("--strict", "Fail on unknown fields in JSON").option("--schema <path>", "JSON Schema file to validate data").option(
241
+ "--missing <mode>",
242
+ "When template field missing in JSON: skip or error",
243
+ "skip"
244
+ ).option(
245
+ "--no-update-appearances",
246
+ "Skip appearance regeneration (Unicode may use NeedAppearances only)"
247
+ ).option(
248
+ "--font <path>",
249
+ "Font file (.ttf/.otf/.ttc) for Chinese/Unicode field appearances"
250
+ ).option(
251
+ "--mapping <path>",
252
+ "Map business JSON paths to PDF field names (see mapping.json)"
253
+ ).option(
254
+ "--ignore-fields <names>",
255
+ "Comma-separated PDF field names to skip MISSING_FIELD warnings"
256
+ ).option("--require-doctor", "Run doctor before fill").option("--report <path>", "Write operation report JSON").option("--config <path>", "pdffill.json (ignoreFields, fontPath, mapping, \u2026)").option("--diff", "Run field diff before fill; fail on mismatch").option(
257
+ "--compare-template",
258
+ "With --diff: flag values unchanged from template"
259
+ ).option(
260
+ "--fail-on-warnings <codes>",
261
+ "Fail if warnings match codes (comma-separated, or * for any)"
262
+ ).action(
263
+ async (pdfPath, dataPath, opts) => {
264
+ const startedAt = /* @__PURE__ */ new Date();
265
+ let fieldDiff;
266
+ try {
267
+ const fileConfig = await loadPdffillConfig(opts.config);
268
+ const resolved = resolveFillCli(opts, fileConfig);
269
+ const template = await readBinary(pdfPath);
270
+ const raw = await readJson(dataPath);
271
+ let data;
272
+ if (opts.diff) {
273
+ const resolvedDiff = await resolveDataAndDiff({
274
+ template,
275
+ raw,
276
+ mappingPath: resolved.mappingPath,
277
+ ignoreFields: resolved.ignoreFields,
278
+ compareTemplate: opts.compareTemplate ?? false
279
+ });
280
+ fieldDiff = resolvedDiff.diff;
281
+ if (!fieldDiff.ok) printFieldDiff(fieldDiff);
282
+ assertDiffOk(fieldDiff);
283
+ data = resolvedDiff.data;
284
+ } else if (resolved.mappingPath) {
285
+ const map = parseMappingFile2(await readJson(resolved.mappingPath));
286
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
287
+ throw new PdfFillError2(
288
+ "With --mapping, data file must be a JSON object (business fields)",
289
+ "INVALID_FILL_DATA_SHAPE"
290
+ );
291
+ }
292
+ data = applyFieldMapping2(
293
+ raw,
294
+ map
295
+ );
296
+ } else {
297
+ data = parseFillDataInput2(raw);
298
+ }
299
+ const schema = opts.schema ? await readJsonSchema(opts.schema) : void 0;
300
+ const doctorReport = resolved.requireDoctor || opts.report ? resolved.requireDoctor ? await assertDoctorReady(template) : await doctor2(template) : void 0;
301
+ const result = await fillForm(template, data, {
302
+ flatten: resolved.flatten,
303
+ strict: resolved.strict,
304
+ missing: resolved.missing,
305
+ updateAppearances: resolved.updateAppearances,
306
+ fontPath: resolved.fontPath,
307
+ schema,
308
+ ignoreFields: resolved.ignoreFields,
309
+ failOnWarnings: resolved.failOnWarnings
310
+ });
311
+ await writeFile2(opts.output, result.pdf);
312
+ for (const w of result.warnings) {
313
+ console.warn(`[${w.code}] ${w.message}`);
314
+ }
315
+ console.log(
316
+ `Wrote ${opts.output} (${result.fieldsFilled.length} field(s) filled)`
317
+ );
318
+ if (opts.report) {
319
+ await writeJsonReport(
320
+ opts.report,
321
+ buildFillReport({
322
+ ok: true,
323
+ startedAt,
324
+ durationMs: Date.now() - startedAt.getTime(),
325
+ template: pdfPath,
326
+ output: opts.output,
327
+ fill: result,
328
+ doctor: doctorReport,
329
+ diff: fieldDiff
330
+ })
331
+ );
332
+ }
333
+ } catch (err) {
334
+ if (opts.report) {
335
+ const error = err instanceof PdfFillError2 ? { code: err.code, message: err.message } : err instanceof SchemaValidationError ? { code: "SCHEMA_VALIDATION", message: err.message } : {
336
+ code: "UNKNOWN",
337
+ message: err instanceof Error ? err.message : String(err)
338
+ };
339
+ await writeJsonReport(
340
+ opts.report,
341
+ buildFillReport({
342
+ ok: false,
343
+ startedAt,
344
+ durationMs: Date.now() - startedAt.getTime(),
345
+ template: pdfPath,
346
+ output: opts.output,
347
+ fill: { pdf: new Uint8Array(0), fieldsFilled: [], warnings: [] },
348
+ diff: fieldDiff,
349
+ error
350
+ })
351
+ );
352
+ }
353
+ if (err instanceof SchemaValidationError) {
354
+ console.error(err.message, err.errors);
355
+ process.exitCode = 2;
356
+ return;
357
+ }
358
+ if (err instanceof PdfFillError2) {
359
+ console.error(`${err.code}: ${err.message}`);
360
+ process.exitCode = 1;
361
+ return;
362
+ }
363
+ throw err;
364
+ }
365
+ }
366
+ );
367
+ }
368
+
369
+ // src/commands/scaffold.ts
370
+ import { writeFile as writeFile3 } from "fs/promises";
371
+ import {
372
+ listFields as listFields3,
373
+ scaffoldAll,
374
+ scaffoldFromTemplate,
375
+ scaffoldJsonSchema
376
+ } from "@lzlz94/pdffill-core";
377
+ function registerScaffoldCommand(program2) {
378
+ program2.command("scaffold").description(
379
+ "Generate fill-ready data.json (and optional fields.json / schema.json) from a template"
380
+ ).argument("<pdf>", "Path to PDF template").option("-o, --output <path>", "PDF field names \u2192 values (fill direct)", "data.json").option(
381
+ "--fields-output <path>",
382
+ "Also write field metadata (same as list --json)"
383
+ ).option("--schema-output <path>", "Also write JSON Schema for fill data").option("--include-readonly", "Include read-only fields").option(
384
+ "--with-mapping",
385
+ "Also write business data + mapping.json for --mapping fill/batch"
386
+ ).option(
387
+ "--business-output <path>",
388
+ "Business JSON path (with --with-mapping)",
389
+ "data.business.json"
390
+ ).option(
391
+ "--mapping-output <path>",
392
+ "Mapping JSON path (with --with-mapping)",
393
+ "mapping.json"
394
+ ).option(
395
+ "--business-style <style>",
396
+ "nested (applicant.name \u2192 tree) or flat (applicant_name keys)",
397
+ "nested"
398
+ ).option(
399
+ "--prefill-from-template",
400
+ "Use non-empty values already in the PDF as defaults"
401
+ ).action(
402
+ async (pdfPath, opts) => {
403
+ const bytes = await readBinary(pdfPath);
404
+ const fields = await listFields3(bytes);
405
+ if (fields.length === 0) {
406
+ console.error("No AcroForm fields found. Run pdffill doctor first.");
407
+ process.exitCode = 1;
408
+ return;
409
+ }
410
+ const style = opts.businessStyle === "flat" ? "flat" : "nested";
411
+ const scaffoldOpts = {
412
+ includeReadOnly: opts.includeReadonly ?? false,
413
+ withMapping: opts.withMapping ?? false,
414
+ businessStyle: style,
415
+ prefillFromTemplate: opts.prefillFromTemplate ?? false
416
+ };
417
+ const bundle = opts.prefillFromTemplate ? await scaffoldFromTemplate(bytes, scaffoldOpts) : scaffoldAll(fields, scaffoldOpts);
418
+ await writeFile3(
419
+ opts.output,
420
+ `${JSON.stringify(bundle.pdfData, null, 2)}
421
+ `,
422
+ "utf8"
423
+ );
424
+ console.log(
425
+ `Wrote ${opts.output} (${Object.keys(bundle.pdfData).length} PDF field(s))`
426
+ );
427
+ if (bundle.business) {
428
+ await writeFile3(
429
+ opts.businessOutput,
430
+ `${JSON.stringify(bundle.business.businessData, null, 2)}
431
+ `,
432
+ "utf8"
433
+ );
434
+ const mappingPayload = { mapping: bundle.business.mapping };
435
+ await writeFile3(
436
+ opts.mappingOutput,
437
+ `${JSON.stringify(mappingPayload, null, 2)}
438
+ `,
439
+ "utf8"
440
+ );
441
+ console.log(
442
+ `Wrote ${opts.businessOutput} + ${opts.mappingOutput} (${Object.keys(bundle.business.mapping).length} mapped field(s), ${style})`
443
+ );
444
+ console.log(
445
+ "Fill with: pdffill fill <pdf> " + opts.businessOutput + " -o out.pdf --mapping " + opts.mappingOutput
446
+ );
447
+ }
448
+ if (opts.fieldsOutput) {
449
+ await writeFile3(
450
+ opts.fieldsOutput,
451
+ `${JSON.stringify(fields, null, 2)}
452
+ `,
453
+ "utf8"
454
+ );
455
+ console.log(`Wrote ${opts.fieldsOutput} (field metadata)`);
456
+ }
457
+ if (opts.schemaOutput) {
458
+ const schema = scaffoldJsonSchema(fields, {
459
+ includeReadOnly: opts.includeReadonly ?? false
460
+ });
461
+ await writeFile3(
462
+ opts.schemaOutput,
463
+ `${JSON.stringify(schema, null, 2)}
464
+ `,
465
+ "utf8"
466
+ );
467
+ console.log(`Wrote ${opts.schemaOutput} (JSON Schema)`);
468
+ }
469
+ }
470
+ );
471
+ }
472
+
473
+ // src/commands/verify.ts
474
+ import {
475
+ parseFillDataInput as parseFillDataInput3,
476
+ verifyFilledPdf,
477
+ PdfFillError as PdfFillError3
478
+ } from "@lzlz94/pdffill-core";
479
+ function registerVerifyCommand(program2) {
480
+ program2.command("verify").description("Verify filled PDF field values match expected data.json").argument("<pdf>", "Path to filled PDF").argument("<data.json>", "Expected field values (same shape as fill)").option("--json", "Output result as JSON").action(async (pdfPath, dataPath, opts) => {
481
+ try {
482
+ const pdf = await readBinary(pdfPath);
483
+ const raw = await readJson(dataPath);
484
+ const expected = parseFillDataInput3(raw);
485
+ const result = await verifyFilledPdf(pdf, expected);
486
+ if (opts.json) {
487
+ console.log(JSON.stringify(result, null, 2));
488
+ } else {
489
+ console.log(result.ok ? "OK" : "FAILED");
490
+ console.log(`checked: ${result.checked.length}`);
491
+ for (const m of result.mismatches) {
492
+ console.log(
493
+ ` mismatch ${m.field}: expected ${JSON.stringify(m.expected)}, got ${JSON.stringify(m.actual)}`
494
+ );
495
+ }
496
+ for (const name of result.missingInPdf) {
497
+ console.log(` missing in PDF: ${name}`);
498
+ }
499
+ }
500
+ if (!result.ok) process.exitCode = 1;
501
+ } catch (err) {
502
+ if (err instanceof PdfFillError3) {
503
+ console.error(`${err.code}: ${err.message}`);
504
+ process.exitCode = 1;
505
+ return;
506
+ }
507
+ throw err;
508
+ }
509
+ });
510
+ }
511
+
512
+ // src/commands/batch.ts
513
+ import { mkdir, readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
514
+ import { dirname, join } from "path";
515
+ import {
516
+ batchFill,
517
+ diffFields as diffFields2,
518
+ verifyFilledPdf as verifyFilledPdf2,
519
+ PdfFillError as PdfFillError4,
520
+ SchemaValidationError as SchemaValidationError2,
521
+ parseRowsJson,
522
+ parseRowsCsv,
523
+ rowsToFillData,
524
+ csvRowsToFillData,
525
+ parseMappingFile as parseMappingFile3
526
+ } from "@lzlz94/pdffill-core";
527
+
528
+ // src/util/format-output.ts
529
+ function formatOutputPath(pattern, ctx, index) {
530
+ let out = pattern.replace(/\{index\}/g, String(index));
531
+ for (const [key, value] of Object.entries(ctx)) {
532
+ if (value === void 0) continue;
533
+ out = out.replaceAll(`{${key}}`, String(value));
534
+ }
535
+ return out;
536
+ }
537
+
538
+ // src/commands/batch.ts
539
+ function registerBatchCommand(program2) {
540
+ program2.command("batch").description("Fill one template with many data rows (JSON array or CSV)").argument("<pdf>", "PDF template path").argument("<rows>", "JSON array file or .csv file").requiredOption("-o, --output-dir <dir>", "Output directory").option("--pattern <tpl>", "Output filename pattern", "{index}.pdf").option("--mapping <path>", "Field mapping JSON (source path \u2192 PDF field)").option("--flatten", "Flatten each output PDF").option("--strict", "Fail on unknown fields").option("--schema <path>", "JSON Schema applied to each row").option("--missing <mode>", "skip or error", "skip").option("--no-update-appearances", "Skip appearance regeneration").option("--font <path>", "CJK/Unicode font path").option("--fail-fast", "Stop on first row error").option("--verify", "Verify each output PDF against its row data").option(
541
+ "--ignore-fields <names>",
542
+ "Comma-separated PDF field names to skip MISSING_FIELD warnings"
543
+ ).option("--manifest <path>", "Write batch manifest (per-row results)").option(
544
+ "--manifest-format <fmt>",
545
+ "Manifest format: json (array) or jsonl (one object per line)",
546
+ "json"
547
+ ).option("--config <path>", "pdffill.json config file").option("--diff", "Run field diff on each row before fill").option(
548
+ "--fail-on-warnings <codes>",
549
+ "Fail if warnings match codes (comma-separated, or * for any)"
550
+ ).action(
551
+ async (pdfPath, rowsPath, opts) => {
552
+ try {
553
+ const fileConfig = await loadPdffillConfig(opts.config);
554
+ const resolved = resolveFillCli(opts, fileConfig);
555
+ const template = await readBinary(pdfPath);
556
+ const isCsv = rowsPath.toLowerCase().endsWith(".csv");
557
+ const mapping = resolved.mappingPath ? parseMappingFile3(await readJson(resolved.mappingPath)) : void 0;
558
+ const schema = opts.schema ? await readJsonSchema(opts.schema) : void 0;
559
+ let fillRows;
560
+ let rawRowsForNames = [];
561
+ if (isCsv) {
562
+ const text = await readFile2(rowsPath, "utf8");
563
+ const csvRows = parseRowsCsv(text);
564
+ fillRows = csvRowsToFillData(csvRows, mapping);
565
+ rawRowsForNames = csvRows.map((r) => ({ ...r }));
566
+ } else {
567
+ const raw = await readJson(rowsPath);
568
+ const jsonRows = parseRowsJson(raw);
569
+ fillRows = rowsToFillData(jsonRows, mapping);
570
+ rawRowsForNames = jsonRows.map((r) => {
571
+ const flat = {};
572
+ for (const [k, v] of Object.entries(r)) {
573
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
574
+ flat[k] = v;
575
+ }
576
+ }
577
+ return flat;
578
+ });
579
+ }
580
+ await mkdir(opts.outputDir, { recursive: true });
581
+ const rowDiffs = [];
582
+ if (opts.diff) {
583
+ for (let i = 0; i < fillRows.length; i++) {
584
+ const diff = await diffFields2(template, fillRows[i], {
585
+ ignoreFields: resolved.ignoreFields
586
+ });
587
+ rowDiffs[i] = diff;
588
+ if (!diff.ok) {
589
+ console.error(`Row ${i}: field diff failed`);
590
+ printFieldDiff(diff);
591
+ assertDiffOk(diff);
592
+ }
593
+ }
594
+ }
595
+ const results = await batchFill(template, fillRows, {
596
+ flatten: resolved.flatten,
597
+ strict: resolved.strict,
598
+ missing: resolved.missing,
599
+ updateAppearances: resolved.updateAppearances,
600
+ fontPath: resolved.fontPath,
601
+ schema,
602
+ failFast: opts.failFast ?? false,
603
+ ignoreFields: resolved.ignoreFields,
604
+ failOnWarnings: resolved.failOnWarnings
605
+ });
606
+ let failCount = 0;
607
+ let verifyFailCount = 0;
608
+ const manifestEntries = [];
609
+ for (const row of results) {
610
+ const rowStarted = Date.now();
611
+ if (!row.ok) {
612
+ failCount++;
613
+ console.error(`Row ${row.index}: ${row.error}`);
614
+ manifestEntries.push({
615
+ index: row.index,
616
+ ok: false,
617
+ warnings: row.warnings,
618
+ error: row.error,
619
+ durationMs: Date.now() - rowStarted
620
+ });
621
+ continue;
622
+ }
623
+ const ctx = { ...rawRowsForNames[row.index], index: row.index };
624
+ const name = formatOutputPath(opts.pattern, ctx, row.index);
625
+ const outPath = join(opts.outputDir, name);
626
+ await mkdir(dirname(outPath), { recursive: true });
627
+ await writeFile4(outPath, row.pdf);
628
+ for (const w of row.warnings) {
629
+ console.warn(`Row ${row.index} [${w.code}] ${w.message}`);
630
+ }
631
+ let verifyResult;
632
+ if (opts.verify) {
633
+ verifyResult = await verifyFilledPdf2(
634
+ row.pdf,
635
+ fillRows[row.index]
636
+ );
637
+ if (!verifyResult.ok) {
638
+ verifyFailCount++;
639
+ console.error(
640
+ `Row ${row.index}: verify failed (${verifyResult.mismatches.length} mismatch, ${verifyResult.missingInPdf.length} missing)`
641
+ );
642
+ }
643
+ }
644
+ console.log(`Row ${row.index}: ${outPath}`);
645
+ manifestEntries.push({
646
+ index: row.index,
647
+ ok: verifyResult ? verifyResult.ok : true,
648
+ output: outPath,
649
+ fieldsFilled: row.fieldsFilled,
650
+ warnings: row.warnings,
651
+ verify: verifyResult,
652
+ diff: rowDiffs[row.index],
653
+ durationMs: Date.now() - rowStarted
654
+ });
655
+ }
656
+ const ok = results.filter((r) => r.ok).length;
657
+ console.log(`Done: ${ok}/${results.length} \u2192 ${opts.outputDir}`);
658
+ if (opts.manifest) {
659
+ const manifestFormat = opts.manifestFormat === "jsonl" ? "jsonl" : "json";
660
+ await writeBatchManifest(
661
+ opts.manifest,
662
+ manifestEntries,
663
+ manifestFormat
664
+ );
665
+ }
666
+ if (failCount > 0 || verifyFailCount > 0) process.exitCode = 1;
667
+ } catch (err) {
668
+ if (err instanceof SchemaValidationError2) {
669
+ console.error(err.message, err.errors);
670
+ process.exitCode = 2;
671
+ return;
672
+ }
673
+ if (err instanceof PdfFillError4) {
674
+ console.error(`${err.code}: ${err.message}`);
675
+ process.exitCode = 1;
676
+ return;
677
+ }
678
+ throw err;
679
+ }
680
+ }
681
+ );
682
+ }
683
+
684
+ // src/commands/run.ts
685
+ import { writeFile as writeFile5 } from "fs/promises";
686
+ import {
687
+ runPipeline,
688
+ buildRunReport,
689
+ parseFillDataInput as parseFillDataInput4,
690
+ parseMappingFile as parseMappingFile4,
691
+ applyFieldMapping as applyFieldMapping3,
692
+ PdfFillError as PdfFillError5,
693
+ SchemaValidationError as SchemaValidationError3
694
+ } from "@lzlz94/pdffill-core";
695
+ function registerRunCommand(program2) {
696
+ program2.command("run").description("One-shot: doctor \u2192 fill \u2192 verify").argument("<pdf>", "PDF template path").argument("<data.json>", "Fill data (object) or business JSON with --mapping").requiredOption("-o, --output <path>", "Output PDF path").option("--mapping <path>", "Business JSON \u2192 PDF field mapping").option("--flatten", "Flatten form").option("--strict", "Fail on unknown fields in data").option("--schema <path>", "JSON Schema for data").option("--missing <mode>", "skip or error", "skip").option("--no-update-appearances", "Skip appearance regeneration").option("--font <path>", "CJK/Unicode font").option("--skip-doctor", "Skip template doctor check").option("--no-verify", "Skip post-fill verify").option(
697
+ "--ignore-fields <names>",
698
+ "Comma-separated PDF field names to skip MISSING_FIELD warnings"
699
+ ).option("--report <path>", "Write operation report JSON").option("--config <path>", "pdffill.json config file").option("--diff", "Run field diff before pipeline; fail on mismatch").option(
700
+ "--compare-template",
701
+ "With --diff: flag values unchanged from template"
702
+ ).option(
703
+ "--fail-on-warnings <codes>",
704
+ "Fail if warnings match codes (comma-separated, or * for any)"
705
+ ).action(
706
+ async (pdfPath, dataPath, opts) => {
707
+ const startedAt = /* @__PURE__ */ new Date();
708
+ let fieldDiff;
709
+ try {
710
+ const fileConfig = await loadPdffillConfig(opts.config);
711
+ const resolved = resolveFillCli(opts, fileConfig);
712
+ const template = await readBinary(pdfPath);
713
+ const raw = await readJson(dataPath);
714
+ let data;
715
+ if (opts.diff) {
716
+ const resolvedDiff = await resolveDataAndDiff({
717
+ template,
718
+ raw,
719
+ mappingPath: resolved.mappingPath,
720
+ ignoreFields: resolved.ignoreFields,
721
+ compareTemplate: opts.compareTemplate ?? false
722
+ });
723
+ fieldDiff = resolvedDiff.diff;
724
+ if (!fieldDiff.ok) printFieldDiff(fieldDiff);
725
+ assertDiffOk(fieldDiff);
726
+ data = resolvedDiff.data;
727
+ } else if (resolved.mappingPath) {
728
+ const map = parseMappingFile4(await readJson(resolved.mappingPath));
729
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
730
+ throw new PdfFillError5(
731
+ "With --mapping, data must be a JSON object",
732
+ "INVALID_FILL_DATA_SHAPE"
733
+ );
734
+ }
735
+ data = applyFieldMapping3(raw, map);
736
+ } else {
737
+ data = parseFillDataInput4(raw);
738
+ }
739
+ const schema = opts.schema ? await readJsonSchema(opts.schema) : void 0;
740
+ const result = await runPipeline(template, data, {
741
+ flatten: resolved.flatten,
742
+ strict: resolved.strict,
743
+ missing: resolved.missing,
744
+ updateAppearances: resolved.updateAppearances,
745
+ fontPath: resolved.fontPath,
746
+ schema,
747
+ skipDoctor: resolved.skipDoctor,
748
+ verify: opts.noVerify !== true,
749
+ ignoreFields: resolved.ignoreFields,
750
+ failOnWarnings: resolved.failOnWarnings
751
+ });
752
+ await writeFile5(opts.output, result.fill.pdf);
753
+ for (const w of result.fill.warnings) {
754
+ console.warn(`[${w.code}] ${w.message}`);
755
+ }
756
+ console.log(`OK: ${opts.output} (${result.fill.fieldsFilled.length} field(s))`);
757
+ if (result.verify) {
758
+ console.log(`Verified ${result.verify.checked.length} field(s)`);
759
+ }
760
+ if (opts.report) {
761
+ await writeJsonReport(
762
+ opts.report,
763
+ buildRunReport({
764
+ ok: true,
765
+ startedAt,
766
+ durationMs: Date.now() - startedAt.getTime(),
767
+ template: pdfPath,
768
+ output: opts.output,
769
+ fill: result.fill,
770
+ doctor: result.doctor,
771
+ verify: result.verify,
772
+ diff: fieldDiff
773
+ })
774
+ );
775
+ }
776
+ } catch (err) {
777
+ if (opts.report) {
778
+ const error = err instanceof PdfFillError5 ? { code: err.code, message: err.message } : err instanceof SchemaValidationError3 ? { code: "SCHEMA_VALIDATION", message: err.message } : {
779
+ code: "UNKNOWN",
780
+ message: err instanceof Error ? err.message : String(err)
781
+ };
782
+ await writeJsonReport(
783
+ opts.report,
784
+ buildRunReport({
785
+ ok: false,
786
+ startedAt,
787
+ durationMs: Date.now() - startedAt.getTime(),
788
+ template: pdfPath,
789
+ output: opts.output,
790
+ fill: { pdf: new Uint8Array(0), fieldsFilled: [], warnings: [] },
791
+ diff: fieldDiff,
792
+ error
793
+ })
794
+ );
795
+ }
796
+ if (err instanceof SchemaValidationError3) {
797
+ console.error(err.message, err.errors);
798
+ process.exitCode = 2;
799
+ return;
800
+ }
801
+ if (err instanceof PdfFillError5) {
802
+ console.error(`${err.code}: ${err.message}`);
803
+ process.exitCode = 1;
804
+ return;
805
+ }
806
+ throw err;
807
+ }
808
+ }
809
+ );
810
+ }
811
+
812
+ // src/commands/diff.ts
813
+ import { writeFile as writeFile6 } from "fs/promises";
814
+ import {
815
+ diffFields as diffFields3,
816
+ diffFieldsWithMapping as diffFieldsWithMapping2,
817
+ listFields as listFields4,
818
+ parseFillDataInput as parseFillDataInput5,
819
+ parseMappingFile as parseMappingFile5,
820
+ PdfFillError as PdfFillError6
821
+ } from "@lzlz94/pdffill-core";
822
+ function registerDiffCommand(program2) {
823
+ program2.command("diff").description("Compare template fields to fill data (catch missing/unknown keys)").argument("<pdf>", "PDF template path").argument("<data.json>", "Fill data or business JSON (with --mapping)").option("--config <path>", "pdffill.json config file").option("--mapping <path>", "Business field mapping (overrides config.mapping)").option(
824
+ "--ignore-fields <names>",
825
+ "Comma-separated PDF fields to exclude from missing checks"
826
+ ).option("--include-readonly", "Include read-only PDF fields in diff").option(
827
+ "--compare-template",
828
+ "Flag values identical to current template field values"
829
+ ).option("--json", "Write diff result as JSON to stdout or --output").option("-o, --output <path>", "Write JSON diff result to file").action(
830
+ async (pdfPath, dataPath, opts) => {
831
+ try {
832
+ const fileConfig = await loadPdffillConfig(opts.config);
833
+ const merged = mergeConfig(fileConfig, opts);
834
+ const template = await readBinary(pdfPath);
835
+ const raw = await readJson(dataPath);
836
+ const mappingPath = resolveMappingPath(fileConfig, opts.mapping);
837
+ const ignoreFields = resolveIgnoreFields(fileConfig, opts.ignoreFields);
838
+ const diffOpts = {
839
+ ignoreFields,
840
+ includeReadOnly: opts.includeReadonly ?? false,
841
+ compareTemplate: opts.compareTemplate ?? false
842
+ };
843
+ let result;
844
+ if (mappingPath) {
845
+ const mapping = parseMappingFile5(await readJson(mappingPath));
846
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
847
+ throw new PdfFillError6(
848
+ "With --mapping, data must be a JSON object",
849
+ "INVALID_FILL_DATA_SHAPE"
850
+ );
851
+ }
852
+ const fields = await listFields4(template);
853
+ result = diffFieldsWithMapping2(
854
+ fields,
855
+ raw,
856
+ mapping,
857
+ diffOpts
858
+ );
859
+ } else {
860
+ const data = parseFillDataInput5(raw);
861
+ result = await diffFields3(template, data, diffOpts);
862
+ }
863
+ if (opts.json || opts.output) {
864
+ const text = `${JSON.stringify(result, null, 2)}
865
+ `;
866
+ if (opts.output) {
867
+ await writeFile6(opts.output, text, "utf8");
868
+ console.log(`Wrote ${opts.output}`);
869
+ } else {
870
+ console.log(text.trimEnd());
871
+ }
872
+ } else {
873
+ printFieldDiff(result);
874
+ }
875
+ if (!result.ok) process.exitCode = 1;
876
+ } catch (err) {
877
+ if (err instanceof PdfFillError6) {
878
+ console.error(`${err.code}: ${err.message}`);
879
+ process.exitCode = 1;
880
+ return;
881
+ }
882
+ throw err;
883
+ }
884
+ }
885
+ );
886
+ }
887
+
888
+ // src/index.ts
889
+ var program = new Command();
890
+ program.name("pdffill").description("List, diagnose, and fill PDF AcroForms (pdf-lib, no pdftk)").version("0.6.0");
891
+ registerListCommand(program);
892
+ registerDoctorCommand(program);
893
+ registerFillCommand(program);
894
+ registerScaffoldCommand(program);
895
+ registerVerifyCommand(program);
896
+ registerBatchCommand(program);
897
+ registerRunCommand(program);
898
+ registerDiffCommand(program);
899
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "pdf-fill",
3
+ "version": "0.6.0",
4
+ "description": "CLI for listing, diagnosing, and filling PDF AcroForms",
5
+ "type": "module",
6
+ "bin": {
7
+ "pdffill": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --clean --external @lzlz94/pdffill-core",
15
+ "typecheck": "tsc -p tsconfig.json --noEmit",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "@lzlz94/pdffill-core": "0.6.0",
20
+ "commander": "^12.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.3.5",
24
+ "typescript": "^5.7.2"
25
+ },
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "keywords": [
31
+ "pdf",
32
+ "acroform",
33
+ "cli",
34
+ "pdf-form",
35
+ "pdffill"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/lz1834career/pdf-fill.git",
40
+ "directory": "packages/cli"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/lz1834career/pdf-fill/issues"
44
+ },
45
+ "homepage": "https://github.com/lz1834career/pdf-fill/tree/main/packages/cli#readme"
46
+ }