@pdfme/cli 5.5.10-dev.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +870 -0
- package/dist/cjk-detect.d.ts +5 -0
- package/dist/commands/doctor.d.ts +50 -0
- package/dist/commands/examples.d.ts +35 -0
- package/dist/commands/generate.d.ts +78 -0
- package/dist/commands/pdf2img.d.ts +48 -0
- package/dist/commands/pdf2size.d.ts +19 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/contract.d.ts +30 -0
- package/dist/diagnostics.d.ts +62 -0
- package/dist/example-fonts.d.ts +8 -0
- package/dist/example-templates.d.ts +36 -0
- package/dist/fonts.d.ts +35 -0
- package/dist/grid.d.ts +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3107 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-plugins.d.ts +3 -0
- package/dist/utils.d.ts +53 -0
- package/dist/version.d.ts +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { PDFDocument } from "@pdfme/pdf-lib";
|
|
4
|
+
import { generate } from "@pdfme/generator";
|
|
5
|
+
import { pdf2img, pdf2size } from "@pdfme/converter";
|
|
6
|
+
import { DEFAULT_FONT_NAME, checkGenerateProps, checkTemplate, getDefaultFont, isUrlSafeToFetch } from "@pdfme/common";
|
|
7
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
8
|
+
import * as schemas from "@pdfme/schemas";
|
|
9
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
|
+
//#region src/contract.ts
|
|
12
|
+
var CliError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
exitCode;
|
|
15
|
+
details;
|
|
16
|
+
constructor(message, options = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CliError";
|
|
19
|
+
this.code = options.code ?? "ECLI";
|
|
20
|
+
this.exitCode = options.exitCode ?? 1;
|
|
21
|
+
this.details = options.details;
|
|
22
|
+
if (options.cause !== void 0) this.cause = options.cause;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function fail(message, options = {}) {
|
|
26
|
+
throw new CliError(message, options);
|
|
27
|
+
}
|
|
28
|
+
function printJson(value) {
|
|
29
|
+
console.log(JSON.stringify(value, null, 2));
|
|
30
|
+
}
|
|
31
|
+
async function runWithContract(options, task) {
|
|
32
|
+
try {
|
|
33
|
+
return await task();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return handleCommandError(error, options.json);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function handleCommandError(error, json) {
|
|
39
|
+
const normalized = normalizeCliError(error);
|
|
40
|
+
if (json) {
|
|
41
|
+
const payload = {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: {
|
|
44
|
+
code: normalized.code,
|
|
45
|
+
message: normalized.message
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
if (normalized.details !== void 0) payload.error.details = normalized.details;
|
|
49
|
+
printJson(payload);
|
|
50
|
+
} else console.error(`Error: ${normalized.message}`);
|
|
51
|
+
process.exit(normalized.exitCode);
|
|
52
|
+
}
|
|
53
|
+
function assertNoUnknownFlags(rawArgs, argsDefinition) {
|
|
54
|
+
const booleanFlags = /* @__PURE__ */ new Set();
|
|
55
|
+
const valueFlags = /* @__PURE__ */ new Set();
|
|
56
|
+
const negatedBooleanFlags = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const [name, definition] of Object.entries(argsDefinition)) {
|
|
58
|
+
if (definition.type === "positional") continue;
|
|
59
|
+
for (const flag of getFlagVariants(name, definition.alias)) if (definition.type === "boolean") {
|
|
60
|
+
booleanFlags.add(flag);
|
|
61
|
+
if (flag.startsWith("--") && !name.startsWith("no")) negatedBooleanFlags.add(`--no-${flag.slice(2)}`);
|
|
62
|
+
} else valueFlags.add(flag);
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
65
|
+
const token = rawArgs[i];
|
|
66
|
+
if (token === "--") break;
|
|
67
|
+
if (!token.startsWith("-") || token === "-") continue;
|
|
68
|
+
const [flag, inlineValue] = splitFlagToken(token);
|
|
69
|
+
if (booleanFlags.has(flag) || negatedBooleanFlags.has(flag)) {
|
|
70
|
+
if (inlineValue !== void 0) fail(`Boolean flag ${flag} does not take a value.`, {
|
|
71
|
+
code: "EARG",
|
|
72
|
+
exitCode: 1
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (valueFlags.has(flag)) {
|
|
77
|
+
if (inlineValue !== void 0) {
|
|
78
|
+
if (inlineValue.length === 0) fail(`Missing value for argument ${flag}.`, {
|
|
79
|
+
code: "EARG",
|
|
80
|
+
exitCode: 1
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
const next = rawArgs[i + 1];
|
|
84
|
+
if (!next || next === "--" || next.startsWith("-")) fail(`Missing value for argument ${flag}.`, {
|
|
85
|
+
code: "EARG",
|
|
86
|
+
exitCode: 1
|
|
87
|
+
});
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
fail(`Unknown argument: ${flag}`, {
|
|
93
|
+
code: "EARG",
|
|
94
|
+
exitCode: 1
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isOptionProvided(rawArgs, name, alias) {
|
|
99
|
+
const allowedFlags = new Set(getFlagVariants(name, alias));
|
|
100
|
+
return rawArgs.some((token) => {
|
|
101
|
+
if (!token.startsWith("-") || token === "-") return false;
|
|
102
|
+
const [flag] = splitFlagToken(token);
|
|
103
|
+
return allowedFlags.has(flag);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function parseEnumArg(optionName, value, allowedValues) {
|
|
107
|
+
if (typeof value !== "string" || !allowedValues.includes(value)) fail(`Invalid value for --${optionName}: expected one of ${allowedValues.join(", ")}, received ${formatValue(value)}.`, {
|
|
108
|
+
code: "EARG",
|
|
109
|
+
exitCode: 1
|
|
110
|
+
});
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function parsePositiveNumberArg(optionName, value) {
|
|
114
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
115
|
+
if (!Number.isFinite(parsed) || parsed <= 0) fail(`Invalid value for --${optionName}: expected a positive number, received ${formatValue(value)}.`, {
|
|
116
|
+
code: "EARG",
|
|
117
|
+
exitCode: 1
|
|
118
|
+
});
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
function normalizeCliError(error) {
|
|
122
|
+
if (error instanceof CliError) return error;
|
|
123
|
+
if (error instanceof Error) {
|
|
124
|
+
const errorCode = typeof error.code === "string" ? error.code : void 0;
|
|
125
|
+
if (errorCode && errorCode.startsWith("E")) return new CliError(error.message, {
|
|
126
|
+
code: "EIO",
|
|
127
|
+
exitCode: 3,
|
|
128
|
+
details: { errno: errorCode },
|
|
129
|
+
cause: error
|
|
130
|
+
});
|
|
131
|
+
return new CliError(error.message, {
|
|
132
|
+
code: "ERUNTIME",
|
|
133
|
+
exitCode: 2,
|
|
134
|
+
cause: error
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return new CliError(String(error), {
|
|
138
|
+
code: "ERUNTIME",
|
|
139
|
+
exitCode: 2
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function splitFlagToken(token) {
|
|
143
|
+
const eqIndex = token.indexOf("=");
|
|
144
|
+
if (eqIndex === -1) return [token, void 0];
|
|
145
|
+
return [token.slice(0, eqIndex), token.slice(eqIndex + 1)];
|
|
146
|
+
}
|
|
147
|
+
function getFlagVariants(name, alias) {
|
|
148
|
+
const flags = /* @__PURE__ */ new Set();
|
|
149
|
+
addFlagVariants(flags, name);
|
|
150
|
+
for (const item of toArray(alias)) addFlagVariants(flags, item);
|
|
151
|
+
return [...flags];
|
|
152
|
+
}
|
|
153
|
+
function addFlagVariants(flags, value) {
|
|
154
|
+
if (value.length === 1) {
|
|
155
|
+
flags.add(`-${value}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
flags.add(`--${value}`);
|
|
159
|
+
flags.add(`--${toKebabCase(value)}`);
|
|
160
|
+
flags.add(`--${toCamelCase(value)}`);
|
|
161
|
+
}
|
|
162
|
+
function toArray(value) {
|
|
163
|
+
if (!value) return [];
|
|
164
|
+
return Array.isArray(value) ? value : [value];
|
|
165
|
+
}
|
|
166
|
+
function toKebabCase(value) {
|
|
167
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
function toCamelCase(value) {
|
|
170
|
+
return value.replace(/[-_ ]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
|
|
171
|
+
}
|
|
172
|
+
function formatValue(value) {
|
|
173
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
174
|
+
return String(value);
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/schema-plugins.ts
|
|
178
|
+
function isPlugin(value) {
|
|
179
|
+
if (!value || typeof value !== "object") return false;
|
|
180
|
+
const plugin = value;
|
|
181
|
+
return typeof plugin.pdf === "function" && typeof plugin.ui === "function" && typeof plugin.propPanel === "object" && plugin.propPanel !== null && typeof plugin.propPanel.defaultSchema === "object" && plugin.propPanel.defaultSchema !== null && typeof plugin.propPanel.defaultSchema.type === "string";
|
|
182
|
+
}
|
|
183
|
+
function collectPluginsByType(value, plugins, seen) {
|
|
184
|
+
if (!value || typeof value !== "object") return;
|
|
185
|
+
if (seen.has(value)) return;
|
|
186
|
+
seen.add(value);
|
|
187
|
+
if (isPlugin(value)) {
|
|
188
|
+
const type = value.propPanel.defaultSchema.type;
|
|
189
|
+
if (!(type in plugins)) plugins[type] = value;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const child of Object.values(value)) collectPluginsByType(child, plugins, seen);
|
|
193
|
+
}
|
|
194
|
+
function buildSchemaPlugins() {
|
|
195
|
+
const plugins = {};
|
|
196
|
+
collectPluginsByType(schemas, plugins, /* @__PURE__ */ new WeakSet());
|
|
197
|
+
return plugins;
|
|
198
|
+
}
|
|
199
|
+
var schemaPlugins = buildSchemaPlugins();
|
|
200
|
+
var schemaTypes = new Set(Object.keys(schemaPlugins));
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/utils.ts
|
|
203
|
+
function readJsonFile(filePath) {
|
|
204
|
+
const resolvedPath = resolve(filePath);
|
|
205
|
+
if (!existsSync(resolvedPath)) fail(`File not found: ${resolvedPath}`, {
|
|
206
|
+
code: "EIO",
|
|
207
|
+
exitCode: 3
|
|
208
|
+
});
|
|
209
|
+
try {
|
|
210
|
+
const content = readFileSync(resolvedPath, "utf8");
|
|
211
|
+
return JSON.parse(content);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
fail(`Failed to parse JSON file: ${resolvedPath}. ${error instanceof Error ? error.message : String(error)}`, {
|
|
214
|
+
code: "EIO",
|
|
215
|
+
exitCode: 3,
|
|
216
|
+
cause: error
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function loadInput(args) {
|
|
221
|
+
const positionalFile = args._[0];
|
|
222
|
+
if (positionalFile && !args.template && !args.inputs) {
|
|
223
|
+
const jobFilePath = resolve(positionalFile);
|
|
224
|
+
const data = readJsonFile(jobFilePath);
|
|
225
|
+
if ("template" in data && "inputs" in data) return {
|
|
226
|
+
template: data.template,
|
|
227
|
+
inputs: data.inputs,
|
|
228
|
+
options: data.options,
|
|
229
|
+
templateDir: dirname(jobFilePath)
|
|
230
|
+
};
|
|
231
|
+
fail("Positional file must be a unified format with \"template\" and \"inputs\" keys. Use -t and -i for separate files.", {
|
|
232
|
+
code: "EARG",
|
|
233
|
+
exitCode: 1
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (args.template) {
|
|
237
|
+
if (!args.inputs) fail("--inputs (-i) is required when using --template (-t).", {
|
|
238
|
+
code: "EARG",
|
|
239
|
+
exitCode: 1
|
|
240
|
+
});
|
|
241
|
+
const templatePath = resolve(args.template);
|
|
242
|
+
return {
|
|
243
|
+
template: readJsonFile(templatePath),
|
|
244
|
+
inputs: readJsonFile(resolve(args.inputs)),
|
|
245
|
+
templateDir: dirname(templatePath)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
fail("No input provided. Use a unified job file or pass --template/-t with --inputs/-i.", {
|
|
249
|
+
code: "EARG",
|
|
250
|
+
exitCode: 1
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function resolveBasePdf(template, basePdfArg, templateDir) {
|
|
254
|
+
if (basePdfArg) {
|
|
255
|
+
const resolvedBasePdf = resolve(basePdfArg);
|
|
256
|
+
if (!existsSync(resolvedBasePdf)) fail(`Base PDF file not found: ${resolvedBasePdf}`, {
|
|
257
|
+
code: "EIO",
|
|
258
|
+
exitCode: 3
|
|
259
|
+
});
|
|
260
|
+
const pdfData = new Uint8Array(readFileSync(resolvedBasePdf));
|
|
261
|
+
return {
|
|
262
|
+
...template,
|
|
263
|
+
basePdf: pdfData
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const basePdf = template.basePdf;
|
|
267
|
+
if (typeof basePdf === "string" && basePdf.endsWith(".pdf") && !basePdf.startsWith("data:")) {
|
|
268
|
+
const resolvedBasePdf = templateDir ? resolve(templateDir, basePdf) : resolve(basePdf);
|
|
269
|
+
if (!existsSync(resolvedBasePdf)) fail(`Base PDF file not found: ${resolvedBasePdf}`, {
|
|
270
|
+
code: "EIO",
|
|
271
|
+
exitCode: 3
|
|
272
|
+
});
|
|
273
|
+
const pdfData = new Uint8Array(readFileSync(resolvedBasePdf));
|
|
274
|
+
return {
|
|
275
|
+
...template,
|
|
276
|
+
basePdf: pdfData
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return template;
|
|
280
|
+
}
|
|
281
|
+
function getImageOutputPaths(pdfOutputPath, pageCount, imageFormat) {
|
|
282
|
+
const dir = dirname(pdfOutputPath);
|
|
283
|
+
const base = basename(pdfOutputPath, extname(pdfOutputPath));
|
|
284
|
+
const ext = imageFormat === "jpeg" ? "jpg" : "png";
|
|
285
|
+
return Array.from({ length: pageCount }, (_, i) => join(dir, `${base}-${i + 1}.${ext}`));
|
|
286
|
+
}
|
|
287
|
+
function writeOutput(filePath, data) {
|
|
288
|
+
try {
|
|
289
|
+
const dir = dirname(filePath);
|
|
290
|
+
if (dir && dir !== "." && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
291
|
+
writeFileSync(filePath, data instanceof ArrayBuffer ? new Uint8Array(data) : data);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
fail(`Failed to write file: ${filePath}. ${error instanceof Error ? error.message : String(error)}`, {
|
|
294
|
+
code: "EIO",
|
|
295
|
+
exitCode: 3,
|
|
296
|
+
cause: error
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function readPdfFile(filePath) {
|
|
301
|
+
const resolvedPath = resolve(filePath);
|
|
302
|
+
if (!existsSync(resolvedPath)) fail(`PDF file not found: ${resolvedPath}`, {
|
|
303
|
+
code: "EIO",
|
|
304
|
+
exitCode: 3
|
|
305
|
+
});
|
|
306
|
+
try {
|
|
307
|
+
return new Uint8Array(readFileSync(resolvedPath));
|
|
308
|
+
} catch (error) {
|
|
309
|
+
fail(`Failed to read PDF file: ${resolvedPath}. ${error instanceof Error ? error.message : String(error)}`, {
|
|
310
|
+
code: "EIO",
|
|
311
|
+
exitCode: 3,
|
|
312
|
+
cause: error
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
var PAPER_SIZES = {
|
|
317
|
+
A3: [297, 420],
|
|
318
|
+
A4: [210, 297],
|
|
319
|
+
A5: [148, 210],
|
|
320
|
+
A6: [105, 148],
|
|
321
|
+
B4: [250, 353],
|
|
322
|
+
B5: [176, 250],
|
|
323
|
+
Letter: [216, 279],
|
|
324
|
+
Legal: [216, 356],
|
|
325
|
+
Tabloid: [279, 432]
|
|
326
|
+
};
|
|
327
|
+
function detectPaperSize(width, height) {
|
|
328
|
+
const tolerance = 2;
|
|
329
|
+
for (const [name, [w, h]] of Object.entries(PAPER_SIZES)) if (Math.abs(width - w) <= tolerance && Math.abs(height - h) <= tolerance || Math.abs(width - h) <= tolerance && Math.abs(height - w) <= tolerance) return `${name} ${width < height ? "portrait" : "landscape"}`;
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
function parsePageRange(rangeStr, totalPages) {
|
|
333
|
+
const pages = /* @__PURE__ */ new Set();
|
|
334
|
+
for (const part of rangeStr.split(",")) {
|
|
335
|
+
const trimmed = part.trim();
|
|
336
|
+
if (!trimmed) fail(`Invalid page range: ${JSON.stringify(rangeStr)}. Empty segments are not allowed.`, {
|
|
337
|
+
code: "EARG",
|
|
338
|
+
exitCode: 1
|
|
339
|
+
});
|
|
340
|
+
if (trimmed.includes("-")) {
|
|
341
|
+
const [startStr, endStr] = trimmed.split("-");
|
|
342
|
+
if (!startStr || !endStr || !/^\d+$/.test(startStr) || !/^\d+$/.test(endStr)) fail(`Invalid page range segment: ${JSON.stringify(trimmed)}. Use formats like "1-3" or "2".`, {
|
|
343
|
+
code: "EARG",
|
|
344
|
+
exitCode: 1
|
|
345
|
+
});
|
|
346
|
+
const start = Number.parseInt(startStr, 10);
|
|
347
|
+
const end = Number.parseInt(endStr, 10);
|
|
348
|
+
if (start < 1 || end < 1 || start > end || end > totalPages) fail(`Invalid page range segment: ${JSON.stringify(trimmed)}. Pages must be between 1 and ${totalPages}.`, {
|
|
349
|
+
code: "EARG",
|
|
350
|
+
exitCode: 1
|
|
351
|
+
});
|
|
352
|
+
for (let i = start; i <= end; i++) pages.add(i);
|
|
353
|
+
} else {
|
|
354
|
+
if (!/^\d+$/.test(trimmed)) fail(`Invalid page range segment: ${JSON.stringify(trimmed)}. Use formats like "1-3" or "2".`, {
|
|
355
|
+
code: "EARG",
|
|
356
|
+
exitCode: 1
|
|
357
|
+
});
|
|
358
|
+
const p = Number.parseInt(trimmed, 10);
|
|
359
|
+
if (p < 1 || p > totalPages) fail(`Invalid page number: ${p}. Pages must be between 1 and ${totalPages}.`, {
|
|
360
|
+
code: "EARG",
|
|
361
|
+
exitCode: 1
|
|
362
|
+
});
|
|
363
|
+
pages.add(p);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return [...pages].sort((a, b) => a - b);
|
|
367
|
+
}
|
|
368
|
+
async function readJsonFromStdin() {
|
|
369
|
+
const chunks = [];
|
|
370
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
371
|
+
const content = Buffer.concat(chunks).toString("utf8").trim();
|
|
372
|
+
if (!content) fail("No JSON input received on stdin.", {
|
|
373
|
+
code: "EARG",
|
|
374
|
+
exitCode: 1
|
|
375
|
+
});
|
|
376
|
+
try {
|
|
377
|
+
return JSON.parse(content);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
fail(`Failed to parse JSON from stdin. ${error instanceof Error ? error.message : String(error)}`, {
|
|
380
|
+
code: "EIO",
|
|
381
|
+
exitCode: 3,
|
|
382
|
+
cause: error
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function ensureSafeDefaultOutputPath(options) {
|
|
387
|
+
const issue = getSafeDefaultOutputPathIssue(options);
|
|
388
|
+
if (issue) fail(issue, {
|
|
389
|
+
code: "EARG",
|
|
390
|
+
exitCode: 1
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function getSafeDefaultOutputPathIssue(options) {
|
|
394
|
+
const { filePath, rawArgs, optionName, optionAlias, defaultValue, force = false } = options;
|
|
395
|
+
if (force || isOptionProvided(rawArgs, optionName, optionAlias) || filePath !== defaultValue) return;
|
|
396
|
+
const resolvedPath = resolve(filePath);
|
|
397
|
+
if (!existsSync(resolvedPath)) return;
|
|
398
|
+
return `Refusing to overwrite implicit default output file: ${resolvedPath}. Use -o to choose an explicit path or --force to overwrite.`;
|
|
399
|
+
}
|
|
400
|
+
function inspectWriteTarget(filePath) {
|
|
401
|
+
const resolvedPath = resolve(filePath);
|
|
402
|
+
const parentDir = dirname(resolvedPath);
|
|
403
|
+
const exists = existsSync(resolvedPath);
|
|
404
|
+
let existingType;
|
|
405
|
+
if (exists) {
|
|
406
|
+
const stat = statSync(resolvedPath);
|
|
407
|
+
if (stat.isFile()) existingType = "file";
|
|
408
|
+
else if (stat.isDirectory()) existingType = "directory";
|
|
409
|
+
else existingType = "other";
|
|
410
|
+
}
|
|
411
|
+
const checkedPath = exists && existingType === "file" ? resolvedPath : findExistingParent$1(parentDir);
|
|
412
|
+
const checkedType = getFsEntryType(checkedPath);
|
|
413
|
+
try {
|
|
414
|
+
accessSync(checkedPath, constants.W_OK);
|
|
415
|
+
return {
|
|
416
|
+
path: filePath,
|
|
417
|
+
resolvedPath,
|
|
418
|
+
parentDir,
|
|
419
|
+
exists,
|
|
420
|
+
existingType,
|
|
421
|
+
writable: true,
|
|
422
|
+
checkedPath: checkedPath !== resolvedPath ? checkedPath : void 0,
|
|
423
|
+
checkedType
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
path: filePath,
|
|
428
|
+
resolvedPath,
|
|
429
|
+
parentDir,
|
|
430
|
+
exists,
|
|
431
|
+
existingType,
|
|
432
|
+
writable: false,
|
|
433
|
+
checkedPath: checkedPath !== resolvedPath ? checkedPath : void 0,
|
|
434
|
+
checkedType,
|
|
435
|
+
error: error instanceof Error ? error.message : String(error)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function findExistingParent$1(path) {
|
|
440
|
+
let current = path;
|
|
441
|
+
while (!existsSync(current)) {
|
|
442
|
+
const parent = dirname(current);
|
|
443
|
+
if (parent === current) break;
|
|
444
|
+
current = parent;
|
|
445
|
+
}
|
|
446
|
+
return current;
|
|
447
|
+
}
|
|
448
|
+
function getFsEntryType(path) {
|
|
449
|
+
const stat = statSync(path);
|
|
450
|
+
if (stat.isFile()) return "file";
|
|
451
|
+
if (stat.isDirectory()) return "directory";
|
|
452
|
+
return "other";
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/diagnostics.ts
|
|
456
|
+
var KNOWN_TEMPLATE_KEYS = new Set([
|
|
457
|
+
"author",
|
|
458
|
+
"basePdf",
|
|
459
|
+
"columns",
|
|
460
|
+
"pdfmeVersion",
|
|
461
|
+
"schemas"
|
|
462
|
+
]);
|
|
463
|
+
var KNOWN_JOB_KEYS = new Set([
|
|
464
|
+
"template",
|
|
465
|
+
"inputs",
|
|
466
|
+
"options"
|
|
467
|
+
]);
|
|
468
|
+
function findClosestType(type) {
|
|
469
|
+
let bestMatch = null;
|
|
470
|
+
let bestDist = Infinity;
|
|
471
|
+
for (const known of schemaTypes) {
|
|
472
|
+
const dist = levenshtein(type.toLowerCase(), known.toLowerCase());
|
|
473
|
+
if (dist < bestDist && dist <= 3) {
|
|
474
|
+
bestDist = dist;
|
|
475
|
+
bestMatch = known;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return bestMatch;
|
|
479
|
+
}
|
|
480
|
+
function levenshtein(a, b) {
|
|
481
|
+
const m = a.length;
|
|
482
|
+
const n = b.length;
|
|
483
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
484
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
485
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
486
|
+
for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
487
|
+
return dp[m][n];
|
|
488
|
+
}
|
|
489
|
+
function validateTemplate(template) {
|
|
490
|
+
const errors = [];
|
|
491
|
+
const warnings = [];
|
|
492
|
+
try {
|
|
493
|
+
checkTemplate(template);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
496
|
+
}
|
|
497
|
+
const schemaPages = normalizeSchemaPages$2(template.schemas);
|
|
498
|
+
if (schemaPages.length === 0) return {
|
|
499
|
+
errors,
|
|
500
|
+
warnings,
|
|
501
|
+
pages: 0,
|
|
502
|
+
fields: 0
|
|
503
|
+
};
|
|
504
|
+
const totalFields = schemaPages.reduce((sum, page) => sum + page.length, 0);
|
|
505
|
+
let pageWidth = 210;
|
|
506
|
+
let pageHeight = 297;
|
|
507
|
+
if (template.basePdf && typeof template.basePdf === "object" && "width" in template.basePdf) {
|
|
508
|
+
pageWidth = template.basePdf.width;
|
|
509
|
+
pageHeight = template.basePdf.height;
|
|
510
|
+
}
|
|
511
|
+
const allNames = /* @__PURE__ */ new Map();
|
|
512
|
+
for (let pageIdx = 0; pageIdx < schemaPages.length; pageIdx++) {
|
|
513
|
+
const page = schemaPages[pageIdx];
|
|
514
|
+
if (!Array.isArray(page)) continue;
|
|
515
|
+
const pageNames = /* @__PURE__ */ new Set();
|
|
516
|
+
for (const schema of page) {
|
|
517
|
+
if (typeof schema !== "object" || schema === null) continue;
|
|
518
|
+
const name = schema.name;
|
|
519
|
+
const type = schema.type;
|
|
520
|
+
const position = schema.position;
|
|
521
|
+
const width = schema.width;
|
|
522
|
+
const height = schema.height;
|
|
523
|
+
if (type && !schemaTypes.has(type)) {
|
|
524
|
+
const suggestion = findClosestType(type);
|
|
525
|
+
const hint = suggestion ? ` Did you mean: ${suggestion}?` : "";
|
|
526
|
+
errors.push(`Field "${name}" has unknown type "${type}".${hint} Available types: ${[...schemaTypes].join(", ")}`);
|
|
527
|
+
}
|
|
528
|
+
if (name && pageNames.has(name)) errors.push(`Duplicate field name "${name}" on page ${pageIdx + 1}`);
|
|
529
|
+
if (name) {
|
|
530
|
+
pageNames.add(name);
|
|
531
|
+
if (!allNames.has(name)) allNames.set(name, []);
|
|
532
|
+
allNames.get(name).push(pageIdx + 1);
|
|
533
|
+
}
|
|
534
|
+
if (position && width !== void 0 && height !== void 0) {
|
|
535
|
+
if (position.x + width > pageWidth + 1) warnings.push(`Field "${name}" at (${position.x},${position.y}) extends beyond page width (${pageWidth}mm)`);
|
|
536
|
+
if (position.y + height > pageHeight + 1) warnings.push(`Field "${name}" at (${position.x},${position.y}) extends beyond page height (${pageHeight}mm)`);
|
|
537
|
+
if (position.x < 0 || position.y < 0) warnings.push(`Field "${name}" has negative position (${position.x},${position.y})`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const [name, pages] of allNames) if (pages.length > 1) warnings.push(`Field name "${name}" appears on multiple pages: ${pages.join(", ")}`);
|
|
542
|
+
return {
|
|
543
|
+
errors,
|
|
544
|
+
warnings,
|
|
545
|
+
pages: schemaPages.length,
|
|
546
|
+
fields: totalFields
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function inspectTemplate(template, templateDir) {
|
|
550
|
+
const flattenedSchemas = normalizeSchemaPages$2(template.schemas).flat();
|
|
551
|
+
const collectedSchemaTypes = getUniqueStringValues(flattenedSchemas.map((schema) => schema.type));
|
|
552
|
+
const requiredFonts = getUniqueStringValues(flattenedSchemas.map((schema) => schema.fontName));
|
|
553
|
+
return {
|
|
554
|
+
schemaTypes: collectedSchemaTypes,
|
|
555
|
+
requiredPlugins: collectedSchemaTypes.filter((type) => schemaTypes.has(type)),
|
|
556
|
+
requiredFonts,
|
|
557
|
+
basePdf: summarizeBasePdf(template.basePdf, templateDir)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function collectInputHints(template) {
|
|
561
|
+
const hintMap = /* @__PURE__ */ new Map();
|
|
562
|
+
const schemaPages = normalizeSchemaPages$2(template.schemas);
|
|
563
|
+
const radioGroupMembers = collectRadioGroupMembers(schemaPages);
|
|
564
|
+
for (let pageIdx = 0; pageIdx < schemaPages.length; pageIdx++) for (const schema of schemaPages[pageIdx]) {
|
|
565
|
+
const name = typeof schema.name === "string" ? schema.name : "";
|
|
566
|
+
const type = typeof schema.type === "string" ? schema.type : "";
|
|
567
|
+
const readOnly = schema.readOnly === true;
|
|
568
|
+
if (!name || !type || readOnly) continue;
|
|
569
|
+
const hint = buildFieldInputHint(schema, pageIdx + 1, radioGroupMembers);
|
|
570
|
+
const key = [
|
|
571
|
+
hint.name,
|
|
572
|
+
hint.type,
|
|
573
|
+
hint.expectedInput.kind,
|
|
574
|
+
JSON.stringify(hint.expectedInput.example ?? null),
|
|
575
|
+
hint.expectedInput.format ?? "",
|
|
576
|
+
hint.expectedInput.canonicalFormat ?? "",
|
|
577
|
+
hint.expectedInput.contentKind ?? "",
|
|
578
|
+
hint.expectedInput.rule ?? "",
|
|
579
|
+
(hint.expectedInput.variableNames ?? []).join("\0"),
|
|
580
|
+
(hint.expectedInput.allowedValues ?? []).join("\0"),
|
|
581
|
+
hint.expectedInput.groupName ?? "",
|
|
582
|
+
(hint.expectedInput.groupMemberNames ?? []).join("\0"),
|
|
583
|
+
String(hint.expectedInput.columnCount ?? ""),
|
|
584
|
+
(hint.expectedInput.columnHeaders ?? []).join("\0"),
|
|
585
|
+
hint.expectedInput.acceptsJsonString === true ? "1" : "0"
|
|
586
|
+
].join("");
|
|
587
|
+
const existing = hintMap.get(key);
|
|
588
|
+
if (existing) {
|
|
589
|
+
existing.pages = [...new Set([...existing.pages, pageIdx + 1])].sort((a, b) => a - b);
|
|
590
|
+
existing.required = existing.required || hint.required;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
hintMap.set(key, hint);
|
|
594
|
+
}
|
|
595
|
+
return [...hintMap.values()].sort((a, b) => a.name.localeCompare(b.name) || a.type.localeCompare(b.type));
|
|
596
|
+
}
|
|
597
|
+
function validateInputContracts(template, inputs) {
|
|
598
|
+
const issues = getInputContractIssues(template, inputs);
|
|
599
|
+
if (issues.length > 0) fail(issues[0], {
|
|
600
|
+
code: "EVALIDATE",
|
|
601
|
+
exitCode: 1
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function getInputContractIssues(template, inputs) {
|
|
605
|
+
const hints = collectInputHints(template);
|
|
606
|
+
const issues = [];
|
|
607
|
+
for (let inputIndex = 0; inputIndex < inputs.length; inputIndex++) {
|
|
608
|
+
const input = inputs[inputIndex] ?? {};
|
|
609
|
+
for (const hint of hints) {
|
|
610
|
+
const issue = getInputContractIssue(hint, input, inputIndex);
|
|
611
|
+
if (issue) issues.push(issue);
|
|
612
|
+
}
|
|
613
|
+
issues.push(...getRadioGroupSelectionIssues(hints, input, inputIndex));
|
|
614
|
+
}
|
|
615
|
+
return issues;
|
|
616
|
+
}
|
|
617
|
+
async function loadValidationSource(file, options) {
|
|
618
|
+
const data = await loadValidationInput(file, options.noInputMessage);
|
|
619
|
+
const record = assertRecordObject(data.json, "Validation input");
|
|
620
|
+
const hasTemplate = "template" in record;
|
|
621
|
+
const hasInputs = "inputs" in record;
|
|
622
|
+
if (hasTemplate || hasInputs) {
|
|
623
|
+
if (!hasTemplate || !hasInputs) fail("Unified job validation requires both \"template\" and \"inputs\" keys.", {
|
|
624
|
+
code: "EARG",
|
|
625
|
+
exitCode: 1
|
|
626
|
+
});
|
|
627
|
+
return {
|
|
628
|
+
mode: "job",
|
|
629
|
+
template: assertRecordObject(record.template, "Unified job template"),
|
|
630
|
+
inputs: record.inputs,
|
|
631
|
+
options: record.options,
|
|
632
|
+
templateDir: data.templateDir,
|
|
633
|
+
jobWarnings: Object.keys(record).filter((key) => !KNOWN_JOB_KEYS.has(key)).sort().map((key) => `Unknown unified job field: ${key}`)
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
mode: "template",
|
|
638
|
+
template: assertRecordObject(record, "Template"),
|
|
639
|
+
templateDir: data.templateDir,
|
|
640
|
+
jobWarnings: []
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function loadValidationInput(file, noInputMessage) {
|
|
644
|
+
if (!file || file === "-") {
|
|
645
|
+
if (file === "-" || !process.stdin.isTTY) return { json: await readJsonFromStdin() };
|
|
646
|
+
fail(noInputMessage, {
|
|
647
|
+
code: "EARG",
|
|
648
|
+
exitCode: 1
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
const resolvedFile = resolve(file);
|
|
652
|
+
return {
|
|
653
|
+
json: readJsonFile(resolvedFile),
|
|
654
|
+
templateDir: dirname(resolvedFile)
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function assertRecordObject(value, label) {
|
|
658
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) fail(`${label} must be a JSON object.`, {
|
|
659
|
+
code: "EARG",
|
|
660
|
+
exitCode: 1
|
|
661
|
+
});
|
|
662
|
+
return value;
|
|
663
|
+
}
|
|
664
|
+
function normalizeSchemaPages$2(rawSchemas) {
|
|
665
|
+
if (!Array.isArray(rawSchemas)) return [];
|
|
666
|
+
return rawSchemas.map((page) => {
|
|
667
|
+
if (Array.isArray(page)) return page.filter((schema) => typeof schema === "object" && schema !== null);
|
|
668
|
+
if (typeof page === "object" && page !== null) return Object.values(page).filter((schema) => typeof schema === "object" && schema !== null);
|
|
669
|
+
return [];
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function buildFieldInputHint(schema, page, radioGroupMembers) {
|
|
673
|
+
const type = schema.type;
|
|
674
|
+
if (type === "multiVariableText") {
|
|
675
|
+
const variableNames = getUniqueStringValues(Array.isArray(schema.variables) ? schema.variables : []);
|
|
676
|
+
return {
|
|
677
|
+
name: schema.name,
|
|
678
|
+
type,
|
|
679
|
+
pages: [page],
|
|
680
|
+
required: schema.required === true,
|
|
681
|
+
expectedInput: {
|
|
682
|
+
kind: "jsonStringObject",
|
|
683
|
+
variableNames,
|
|
684
|
+
example: buildMultiVariableTextExample(variableNames)
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (type === "checkbox") return {
|
|
689
|
+
name: schema.name,
|
|
690
|
+
type,
|
|
691
|
+
pages: [page],
|
|
692
|
+
required: schema.required === true,
|
|
693
|
+
expectedInput: {
|
|
694
|
+
kind: "enumString",
|
|
695
|
+
allowedValues: ["false", "true"],
|
|
696
|
+
example: "true"
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
if (type === "radioGroup") {
|
|
700
|
+
const groupName = typeof schema.group === "string" ? schema.group : "";
|
|
701
|
+
const groupMemberNames = groupName ? radioGroupMembers.get(groupName) ?? [] : [];
|
|
702
|
+
return {
|
|
703
|
+
name: schema.name,
|
|
704
|
+
type,
|
|
705
|
+
pages: [page],
|
|
706
|
+
required: schema.required === true,
|
|
707
|
+
expectedInput: {
|
|
708
|
+
kind: "enumString",
|
|
709
|
+
allowedValues: ["false", "true"],
|
|
710
|
+
example: "true",
|
|
711
|
+
...groupName ? { groupName } : {},
|
|
712
|
+
...groupMemberNames.length > 0 ? { groupMemberNames } : {}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (type === "select") {
|
|
717
|
+
const allowedValues = getUniqueOrderedStringValues(Array.isArray(schema.options) ? schema.options : []);
|
|
718
|
+
if (allowedValues.length > 0) return {
|
|
719
|
+
name: schema.name,
|
|
720
|
+
type,
|
|
721
|
+
pages: [page],
|
|
722
|
+
required: schema.required === true,
|
|
723
|
+
expectedInput: {
|
|
724
|
+
kind: "enumString",
|
|
725
|
+
allowedValues,
|
|
726
|
+
example: allowedValues[0]
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const barcodeRule = getBarcodeRule(type);
|
|
731
|
+
if (barcodeRule) return {
|
|
732
|
+
name: schema.name,
|
|
733
|
+
type,
|
|
734
|
+
pages: [page],
|
|
735
|
+
required: schema.required === true,
|
|
736
|
+
expectedInput: {
|
|
737
|
+
kind: "string",
|
|
738
|
+
contentKind: "barcodeText",
|
|
739
|
+
rule: barcodeRule
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
const assetContentKind = getAssetContentKind(type);
|
|
743
|
+
if (assetContentKind) return {
|
|
744
|
+
name: schema.name,
|
|
745
|
+
type,
|
|
746
|
+
pages: [page],
|
|
747
|
+
required: schema.required === true,
|
|
748
|
+
expectedInput: {
|
|
749
|
+
kind: "string",
|
|
750
|
+
contentKind: assetContentKind
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
if (type === "table") {
|
|
754
|
+
const columnHeaders = getOrderedStringValues(Array.isArray(schema.head) ? schema.head : []);
|
|
755
|
+
const columnCount = getTableColumnCount(schema, columnHeaders);
|
|
756
|
+
return {
|
|
757
|
+
name: schema.name,
|
|
758
|
+
type,
|
|
759
|
+
pages: [page],
|
|
760
|
+
required: schema.required === true,
|
|
761
|
+
expectedInput: {
|
|
762
|
+
kind: "stringMatrix",
|
|
763
|
+
...columnCount > 0 ? { columnCount } : {},
|
|
764
|
+
...columnHeaders.length > 0 ? { columnHeaders } : {},
|
|
765
|
+
example: buildTableInputExample(columnHeaders, columnCount),
|
|
766
|
+
acceptsJsonString: true
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (type === "date" || type === "time" || type === "dateTime") {
|
|
771
|
+
const canonicalFormat = getCanonicalDateStoredFormat(type);
|
|
772
|
+
return {
|
|
773
|
+
name: schema.name,
|
|
774
|
+
type,
|
|
775
|
+
pages: [page],
|
|
776
|
+
required: schema.required === true,
|
|
777
|
+
expectedInput: {
|
|
778
|
+
kind: "string",
|
|
779
|
+
format: getDateHintFormat(schema, canonicalFormat),
|
|
780
|
+
canonicalFormat,
|
|
781
|
+
example: getDateInputExample(type)
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
name: schema.name,
|
|
787
|
+
type,
|
|
788
|
+
pages: [page],
|
|
789
|
+
required: schema.required === true,
|
|
790
|
+
expectedInput: { kind: "string" }
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function buildMultiVariableTextExample(variableNames) {
|
|
794
|
+
return JSON.stringify(Object.fromEntries(variableNames.map((variableName) => [variableName, variableName.toUpperCase()])));
|
|
795
|
+
}
|
|
796
|
+
function buildTableInputExample(columnHeaders, columnCount) {
|
|
797
|
+
if (columnCount <= 0) return [];
|
|
798
|
+
return [Array.from({ length: columnCount }, (_, index) => {
|
|
799
|
+
const header = columnHeaders[index];
|
|
800
|
+
return header ? `${header} value` : `cell-${index + 1}`;
|
|
801
|
+
})];
|
|
802
|
+
}
|
|
803
|
+
function getTableColumnCount(schema, columnHeaders) {
|
|
804
|
+
if (columnHeaders.length > 0) return columnHeaders.length;
|
|
805
|
+
if (Array.isArray(schema.headWidthPercentages) && schema.headWidthPercentages.length > 0) return schema.headWidthPercentages.length;
|
|
806
|
+
return parseTableStringMatrix(schema.content)?.[0]?.length ?? 0;
|
|
807
|
+
}
|
|
808
|
+
function getAssetContentKind(type) {
|
|
809
|
+
switch (type) {
|
|
810
|
+
case "image": return "imageDataUrl";
|
|
811
|
+
case "signature": return "signatureImageDataUrl";
|
|
812
|
+
case "svg": return "svgMarkup";
|
|
813
|
+
default: return null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function getBarcodeRule(type) {
|
|
817
|
+
switch (type) {
|
|
818
|
+
case "qrcode": return "Any non-empty string up to 499 characters.";
|
|
819
|
+
case "japanpost": return "Start with 7 digits, then continue with digits, A-Z, or hyphen (-).";
|
|
820
|
+
case "ean13": return "12 or 13 digits; if 13 digits are provided, the final check digit must be valid.";
|
|
821
|
+
case "ean8": return "7 or 8 digits; if 8 digits are provided, the final check digit must be valid.";
|
|
822
|
+
case "code39": return "Use uppercase A-Z, digits, spaces, and symbols - . $ / + %.";
|
|
823
|
+
case "code128": return "Text must not contain Kanji, Hiragana, Katakana, or full-width ASCII characters.";
|
|
824
|
+
case "nw7": return "Start and end with A-D; inner characters may be digits or - . $ : / +.";
|
|
825
|
+
case "itf14": return "13 or 14 digits; if 14 digits are provided, the final check digit must be valid.";
|
|
826
|
+
case "upca": return "11 or 12 digits; if 12 digits are provided, the final check digit must be valid.";
|
|
827
|
+
case "upce": return "Must start with 0 and be 7 or 8 digits total; if 8 digits are provided, the final check digit must be valid.";
|
|
828
|
+
case "gs1datamatrix": return "Include (01) followed by a GTIN of 8, 12, 13, or 14 digits with a valid check digit; total length must be 52 characters or fewer.";
|
|
829
|
+
case "pdf417": return "Any non-empty string up to 1000 characters.";
|
|
830
|
+
default: return null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function getCanonicalDateStoredFormat(type) {
|
|
834
|
+
switch (type) {
|
|
835
|
+
case "date": return "yyyy/MM/dd";
|
|
836
|
+
case "time": return "HH:mm";
|
|
837
|
+
case "dateTime": return "yyyy/MM/dd HH:mm";
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function getDateHintFormat(schema, canonicalFormat) {
|
|
841
|
+
const formatValue = typeof schema.format === "string" ? schema.format.trim() : "";
|
|
842
|
+
if (!formatValue || formatValue === "undefined") return canonicalFormat;
|
|
843
|
+
return formatValue;
|
|
844
|
+
}
|
|
845
|
+
function getDateInputExample(type) {
|
|
846
|
+
switch (type) {
|
|
847
|
+
case "date": return "2026/03/28";
|
|
848
|
+
case "time": return "14:30";
|
|
849
|
+
case "dateTime": return "2026/03/28 14:30";
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function getInputContractIssue(hint, input, inputIndex) {
|
|
853
|
+
if (hint.expectedInput.kind === "jsonStringObject") return getMultiVariableTextInputIssue(hint, input, inputIndex);
|
|
854
|
+
if (hint.expectedInput.kind === "enumString") return getEnumStringInputIssue(hint, input, inputIndex);
|
|
855
|
+
if (hint.expectedInput.kind === "stringMatrix") return getStringMatrixInputIssue(hint, input, inputIndex);
|
|
856
|
+
if (isCanonicalDateHint(hint)) return getCanonicalDateInputIssue(hint, input, inputIndex);
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
function getMultiVariableTextInputIssue(hint, input, inputIndex) {
|
|
860
|
+
const rawValue = input[hint.name];
|
|
861
|
+
const variableNames = hint.expectedInput.variableNames ?? [];
|
|
862
|
+
const example = hint.expectedInput.example ?? "{}";
|
|
863
|
+
if (rawValue === void 0 || rawValue === "") {
|
|
864
|
+
if (!hint.required || variableNames.length === 0) return null;
|
|
865
|
+
return buildMultiVariableTextErrorMessage({
|
|
866
|
+
hint,
|
|
867
|
+
inputIndex,
|
|
868
|
+
extra: `Missing variables: ${variableNames.join(", ")}.`,
|
|
869
|
+
example
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (typeof rawValue !== "string") return buildMultiVariableTextErrorMessage({
|
|
873
|
+
hint,
|
|
874
|
+
inputIndex,
|
|
875
|
+
extra: `Received ${describeValue(rawValue)}.`,
|
|
876
|
+
example
|
|
877
|
+
});
|
|
878
|
+
let parsedValue;
|
|
879
|
+
try {
|
|
880
|
+
parsedValue = JSON.parse(rawValue);
|
|
881
|
+
} catch {
|
|
882
|
+
return buildMultiVariableTextErrorMessage({
|
|
883
|
+
hint,
|
|
884
|
+
inputIndex,
|
|
885
|
+
extra: `Received ${describeValue(rawValue)}.`,
|
|
886
|
+
example
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
if (typeof parsedValue !== "object" || parsedValue === null || Array.isArray(parsedValue)) return buildMultiVariableTextErrorMessage({
|
|
890
|
+
hint,
|
|
891
|
+
inputIndex,
|
|
892
|
+
extra: `Received ${describeValue(parsedValue)}.`,
|
|
893
|
+
example
|
|
894
|
+
});
|
|
895
|
+
if (!hint.required || variableNames.length === 0) return null;
|
|
896
|
+
const values = parsedValue;
|
|
897
|
+
const missingVariables = variableNames.filter((variableName) => !values[variableName]);
|
|
898
|
+
if (missingVariables.length > 0) return buildMultiVariableTextErrorMessage({
|
|
899
|
+
hint,
|
|
900
|
+
inputIndex,
|
|
901
|
+
extra: `Missing variables: ${missingVariables.join(", ")}.`,
|
|
902
|
+
example
|
|
903
|
+
});
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
function getRadioGroupSelectionIssues(hints, input, inputIndex) {
|
|
907
|
+
const groups = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const hint of hints) {
|
|
909
|
+
if (hint.type !== "radioGroup" || !hint.expectedInput.groupName || (hint.expectedInput.groupMemberNames?.length ?? 0) <= 1) continue;
|
|
910
|
+
const groupName = hint.expectedInput.groupName;
|
|
911
|
+
const existing = groups.get(groupName);
|
|
912
|
+
if (existing) existing.push(hint);
|
|
913
|
+
else groups.set(groupName, [hint]);
|
|
914
|
+
}
|
|
915
|
+
const issues = [];
|
|
916
|
+
for (const [groupName, groupHints] of groups) {
|
|
917
|
+
const selectedNames = groupHints.filter((hint) => input[hint.name] === "true").map((hint) => hint.name);
|
|
918
|
+
if (selectedNames.length <= 1) continue;
|
|
919
|
+
issues.push(buildRadioGroupSelectionErrorMessage({
|
|
920
|
+
groupName,
|
|
921
|
+
inputIndex,
|
|
922
|
+
groupMemberNames: groupHints[0]?.expectedInput.groupMemberNames ?? groupHints.map((hint) => hint.name),
|
|
923
|
+
selectedNames
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
926
|
+
return issues;
|
|
927
|
+
}
|
|
928
|
+
function getEnumStringInputIssue(hint, input, inputIndex) {
|
|
929
|
+
const rawValue = input[hint.name];
|
|
930
|
+
const allowedValues = hint.expectedInput.allowedValues ?? [];
|
|
931
|
+
const example = hint.expectedInput.example;
|
|
932
|
+
if (rawValue === void 0 || rawValue === "") return null;
|
|
933
|
+
if (typeof rawValue !== "string") return buildEnumStringErrorMessage({
|
|
934
|
+
hint,
|
|
935
|
+
inputIndex,
|
|
936
|
+
extra: `Received ${describeValue(rawValue)}.`,
|
|
937
|
+
example
|
|
938
|
+
});
|
|
939
|
+
if (allowedValues.length === 0 || allowedValues.includes(rawValue)) return null;
|
|
940
|
+
return buildEnumStringErrorMessage({
|
|
941
|
+
hint,
|
|
942
|
+
inputIndex,
|
|
943
|
+
extra: `Received ${describeValue(rawValue)}.`,
|
|
944
|
+
example
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function getStringMatrixInputIssue(hint, input, inputIndex) {
|
|
948
|
+
const rawValue = input[hint.name];
|
|
949
|
+
const example = hint.expectedInput.example;
|
|
950
|
+
if (rawValue === void 0 || rawValue === "") return null;
|
|
951
|
+
const issue = getStringMatrixShapeIssue(typeof rawValue === "string" && hint.expectedInput.acceptsJsonString === true ? parseTableStringMatrix(rawValue) ?? rawValue : rawValue, hint.expectedInput.columnCount);
|
|
952
|
+
if (!issue) return null;
|
|
953
|
+
return buildStringMatrixErrorMessage({
|
|
954
|
+
hint,
|
|
955
|
+
inputIndex,
|
|
956
|
+
extra: issue,
|
|
957
|
+
example
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function isCanonicalDateHint(hint) {
|
|
961
|
+
return (hint.type === "date" || hint.type === "time" || hint.type === "dateTime") && typeof hint.expectedInput.canonicalFormat === "string" && hint.expectedInput.canonicalFormat.length > 0;
|
|
962
|
+
}
|
|
963
|
+
function getCanonicalDateInputIssue(hint, input, inputIndex) {
|
|
964
|
+
const rawValue = input[hint.name];
|
|
965
|
+
const example = hint.expectedInput.example;
|
|
966
|
+
if (rawValue === void 0 || rawValue === "") return null;
|
|
967
|
+
if (typeof rawValue !== "string") return buildCanonicalDateErrorMessage({
|
|
968
|
+
hint,
|
|
969
|
+
inputIndex,
|
|
970
|
+
extra: `Received ${describeValue(rawValue)}.`,
|
|
971
|
+
example
|
|
972
|
+
});
|
|
973
|
+
if (isValidCanonicalDateValue(rawValue, hint.type)) return null;
|
|
974
|
+
return buildCanonicalDateErrorMessage({
|
|
975
|
+
hint,
|
|
976
|
+
inputIndex,
|
|
977
|
+
extra: `Received ${describeValue(rawValue)}.`,
|
|
978
|
+
example
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function getStringMatrixShapeIssue(value, expectedColumnCount) {
|
|
982
|
+
if (!Array.isArray(value)) return `Received ${describeValue(value)}.`;
|
|
983
|
+
const columnCount = expectedColumnCount ?? getFirstArrayLength(value);
|
|
984
|
+
for (let rowIndex = 0; rowIndex < value.length; rowIndex++) {
|
|
985
|
+
const row = value[rowIndex];
|
|
986
|
+
if (!Array.isArray(row)) return `Row ${rowIndex + 1} must be an array of strings. Received ${describeValue(row)}.`;
|
|
987
|
+
if (columnCount > 0 && row.length !== columnCount) return `Row ${rowIndex + 1} must contain ${columnCount} cells. Received ${row.length}.`;
|
|
988
|
+
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
|
989
|
+
const cell = row[colIndex];
|
|
990
|
+
if (typeof cell !== "string") return `Cell [${rowIndex + 1}, ${colIndex + 1}] must be a string. Received ${describeValue(cell)}.`;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
function getFirstArrayLength(rows) {
|
|
996
|
+
for (const row of rows) if (Array.isArray(row)) return row.length;
|
|
997
|
+
return 0;
|
|
998
|
+
}
|
|
999
|
+
function parseTableStringMatrix(rawValue) {
|
|
1000
|
+
if (typeof rawValue !== "string") return null;
|
|
1001
|
+
try {
|
|
1002
|
+
return JSON.parse(rawValue);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function isValidCanonicalDateValue(value, type) {
|
|
1008
|
+
switch (type) {
|
|
1009
|
+
case "date": return isValidCanonicalDate(value);
|
|
1010
|
+
case "time": return isValidCanonicalTime(value);
|
|
1011
|
+
case "dateTime": return isValidCanonicalDateTime(value);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function isValidCanonicalDate(value) {
|
|
1015
|
+
const match = value.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
|
|
1016
|
+
if (!match) return false;
|
|
1017
|
+
const [, year, month, day] = match;
|
|
1018
|
+
if (!isValidCalendarDate(Number(year), Number(month), Number(day))) return false;
|
|
1019
|
+
const parsed = parseRendererDateValue(value, "date");
|
|
1020
|
+
return parsed !== null && formatCanonicalDateValue(parsed, "date") === value;
|
|
1021
|
+
}
|
|
1022
|
+
function isValidCanonicalTime(value) {
|
|
1023
|
+
const match = value.match(/^(\d{2}):(\d{2})$/);
|
|
1024
|
+
if (!match) return false;
|
|
1025
|
+
const [, hours, minutes] = match;
|
|
1026
|
+
return isValidClockTime(Number(hours), Number(minutes));
|
|
1027
|
+
}
|
|
1028
|
+
function isValidCanonicalDateTime(value) {
|
|
1029
|
+
const match = value.match(/^(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2})$/);
|
|
1030
|
+
if (!match) return false;
|
|
1031
|
+
const [, year, month, day, hours, minutes] = match;
|
|
1032
|
+
if (!isValidCalendarDate(Number(year), Number(month), Number(day)) || !isValidClockTime(Number(hours), Number(minutes))) return false;
|
|
1033
|
+
const parsed = parseRendererDateValue(value, "dateTime");
|
|
1034
|
+
return parsed !== null && formatCanonicalDateValue(parsed, "dateTime") === value;
|
|
1035
|
+
}
|
|
1036
|
+
function isValidCalendarDate(year, month, day) {
|
|
1037
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return false;
|
|
1038
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
1039
|
+
const candidate = new Date(year, month - 1, day);
|
|
1040
|
+
return candidate.getFullYear() === year && candidate.getMonth() === month - 1 && candidate.getDate() === day;
|
|
1041
|
+
}
|
|
1042
|
+
function isValidClockTime(hours, minutes) {
|
|
1043
|
+
return Number.isInteger(hours) && Number.isInteger(minutes) && hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
|
|
1044
|
+
}
|
|
1045
|
+
function parseRendererDateValue(value, type) {
|
|
1046
|
+
const parsed = type === "time" ? /* @__PURE__ */ new Date(`2021-01-01T${value}`) : new Date(value);
|
|
1047
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
1048
|
+
}
|
|
1049
|
+
function formatCanonicalDateValue(date, type) {
|
|
1050
|
+
const year = String(date.getFullYear()).padStart(4, "0");
|
|
1051
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1052
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1053
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
1054
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
1055
|
+
switch (type) {
|
|
1056
|
+
case "date": return `${year}/${month}/${day}`;
|
|
1057
|
+
case "time": return `${hours}:${minutes}`;
|
|
1058
|
+
case "dateTime": return `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function buildMultiVariableTextErrorMessage(args) {
|
|
1062
|
+
const variableLabel = args.hint.expectedInput.variableNames && args.hint.expectedInput.variableNames.length > 0 ? ` with variables: ${args.hint.expectedInput.variableNames.join(", ")}` : "";
|
|
1063
|
+
return `Field "${args.hint.name}" (multiVariableText) in input ${args.inputIndex + 1} expects a JSON string object${variableLabel}. Example: ${args.example}. ${args.extra}`;
|
|
1064
|
+
}
|
|
1065
|
+
function buildEnumStringErrorMessage(args) {
|
|
1066
|
+
const allowedValues = (args.hint.expectedInput.allowedValues ?? []).map((value) => JSON.stringify(value));
|
|
1067
|
+
const allowedLabel = allowedValues.length > 0 ? ` one of: ${allowedValues.join(", ")}` : " a supported string value";
|
|
1068
|
+
const exampleLabel = args.example !== void 0 ? ` Example: ${JSON.stringify(args.example)}.` : "";
|
|
1069
|
+
return `Field "${args.hint.name}" (${args.hint.type}) in input ${args.inputIndex + 1} expects${allowedLabel}.${exampleLabel} ${args.extra}`.trim();
|
|
1070
|
+
}
|
|
1071
|
+
function buildStringMatrixErrorMessage(args) {
|
|
1072
|
+
const columnCount = args.hint.expectedInput.columnCount;
|
|
1073
|
+
const columnHeaders = args.hint.expectedInput.columnHeaders ?? [];
|
|
1074
|
+
const columnLabel = typeof columnCount === "number" && columnCount > 0 ? ` with ${columnCount} cells per row` : "";
|
|
1075
|
+
const headerLabel = columnHeaders.length > 0 ? ` Column headers: ${columnHeaders.join(", ")}.` : "";
|
|
1076
|
+
const exampleLabel = args.example !== void 0 ? ` Example: ${JSON.stringify(args.example)}.` : "";
|
|
1077
|
+
const compatibilityLabel = args.hint.expectedInput.acceptsJsonString === true ? " JSON string input is also accepted for compatibility." : "";
|
|
1078
|
+
return `Field "${args.hint.name}" (${args.hint.type}) in input ${args.inputIndex + 1} expects a JSON array of string arrays${columnLabel}.${headerLabel}${exampleLabel}${compatibilityLabel} ${args.extra}`.trim();
|
|
1079
|
+
}
|
|
1080
|
+
function buildCanonicalDateErrorMessage(args) {
|
|
1081
|
+
const displayFormat = args.hint.expectedInput.format;
|
|
1082
|
+
const displayLabel = typeof displayFormat === "string" && displayFormat.length > 0 && displayFormat !== args.hint.expectedInput.canonicalFormat ? ` Display format hint: ${displayFormat}.` : "";
|
|
1083
|
+
const exampleLabel = args.example !== void 0 ? ` Example: ${JSON.stringify(args.example)}.` : "";
|
|
1084
|
+
return `Field "${args.hint.name}" (${args.hint.type}) in input ${args.inputIndex + 1} expects canonical stored content in format ${args.hint.expectedInput.canonicalFormat}.${displayLabel}${exampleLabel} ${args.extra}`.trim();
|
|
1085
|
+
}
|
|
1086
|
+
function buildRadioGroupSelectionErrorMessage(args) {
|
|
1087
|
+
return `Radio group "${args.groupName}" in input ${args.inputIndex + 1} allows at most one "true" value across fields: ${args.groupMemberNames.join(", ")}. Received "true" for: ${args.selectedNames.join(", ")}. Set one field to "true" and the others to "false".`;
|
|
1088
|
+
}
|
|
1089
|
+
function describeValue(value) {
|
|
1090
|
+
if (typeof value === "string") {
|
|
1091
|
+
const trimmed = value.trim();
|
|
1092
|
+
return `${trimmed.startsWith("{") || trimmed.startsWith("[") ? "string" : "plain string"} ${JSON.stringify(value)}`;
|
|
1093
|
+
}
|
|
1094
|
+
if (value === null) return "null";
|
|
1095
|
+
if (Array.isArray(value)) return "array";
|
|
1096
|
+
if (typeof value === "number" || typeof value === "boolean") return `${typeof value} ${JSON.stringify(value)}`;
|
|
1097
|
+
return typeof value;
|
|
1098
|
+
}
|
|
1099
|
+
function getUniqueStringValues(values) {
|
|
1100
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))].sort();
|
|
1101
|
+
}
|
|
1102
|
+
function getUniqueOrderedStringValues(values) {
|
|
1103
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
|
1104
|
+
}
|
|
1105
|
+
function getOrderedStringValues(values) {
|
|
1106
|
+
return values.filter((value) => typeof value === "string" && value.length > 0);
|
|
1107
|
+
}
|
|
1108
|
+
function collectRadioGroupMembers(schemaPages) {
|
|
1109
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1110
|
+
for (const page of schemaPages) for (const schema of page) {
|
|
1111
|
+
if (schema.readOnly === true || schema.type !== "radioGroup") continue;
|
|
1112
|
+
const name = typeof schema.name === "string" ? schema.name : "";
|
|
1113
|
+
const groupName = typeof schema.group === "string" ? schema.group : "";
|
|
1114
|
+
if (!name || !groupName) continue;
|
|
1115
|
+
const existing = groups.get(groupName);
|
|
1116
|
+
if (existing) {
|
|
1117
|
+
if (!existing.includes(name)) existing.push(name);
|
|
1118
|
+
} else groups.set(groupName, [name]);
|
|
1119
|
+
}
|
|
1120
|
+
return groups;
|
|
1121
|
+
}
|
|
1122
|
+
function summarizeBasePdf(basePdf, templateDir) {
|
|
1123
|
+
if (typeof basePdf === "string") {
|
|
1124
|
+
if (basePdf.startsWith("data:")) return { kind: "dataUri" };
|
|
1125
|
+
if (basePdf.endsWith(".pdf")) return {
|
|
1126
|
+
kind: "pdfPath",
|
|
1127
|
+
path: basePdf,
|
|
1128
|
+
resolvedPath: templateDir ? resolve(templateDir, basePdf) : resolve(basePdf)
|
|
1129
|
+
};
|
|
1130
|
+
return { kind: "string" };
|
|
1131
|
+
}
|
|
1132
|
+
if (basePdf && typeof basePdf === "object") {
|
|
1133
|
+
if ("width" in basePdf && "height" in basePdf) {
|
|
1134
|
+
const width = typeof basePdf.width === "number" ? basePdf.width : void 0;
|
|
1135
|
+
const height = typeof basePdf.height === "number" ? basePdf.height : void 0;
|
|
1136
|
+
return {
|
|
1137
|
+
kind: "blank",
|
|
1138
|
+
width,
|
|
1139
|
+
height,
|
|
1140
|
+
paperSize: width !== void 0 && height !== void 0 ? detectPaperSize(width, height) : null
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return { kind: "object" };
|
|
1144
|
+
}
|
|
1145
|
+
return { kind: "missing" };
|
|
1146
|
+
}
|
|
1147
|
+
//#endregion
|
|
1148
|
+
//#region src/fonts.ts
|
|
1149
|
+
var CACHE_DIR = join(homedir(), ".pdfme", "fonts");
|
|
1150
|
+
var NOTO_SANS_JP_URL = "https://github.com/google/fonts/raw/main/ofl/notosansjp/NotoSansJP%5Bwght%5D.ttf";
|
|
1151
|
+
var NOTO_CACHE_FILE = join(CACHE_DIR, "NotoSansJP-Regular.ttf");
|
|
1152
|
+
var REMOTE_FONT_TIMEOUT_MS = 15e3;
|
|
1153
|
+
var MAX_REMOTE_FONT_BYTES = 32 * 1024 * 1024;
|
|
1154
|
+
function ensureCacheDir() {
|
|
1155
|
+
if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
|
|
1156
|
+
}
|
|
1157
|
+
async function downloadNotoSansJP(verbose) {
|
|
1158
|
+
if (existsSync(NOTO_CACHE_FILE)) {
|
|
1159
|
+
if (verbose) console.error("Using cached NotoSansJP from", NOTO_CACHE_FILE);
|
|
1160
|
+
return new Uint8Array(readFileSync(NOTO_CACHE_FILE));
|
|
1161
|
+
}
|
|
1162
|
+
ensureCacheDir();
|
|
1163
|
+
console.error("Downloading NotoSansJP for CJK support...");
|
|
1164
|
+
try {
|
|
1165
|
+
const response = await fetch(NOTO_SANS_JP_URL);
|
|
1166
|
+
if (!response.ok) {
|
|
1167
|
+
console.error(`Warning: Failed to download NotoSansJP (HTTP ${response.status})`);
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
1171
|
+
writeFileSync(NOTO_CACHE_FILE, buffer);
|
|
1172
|
+
console.error("Cached NotoSansJP to", NOTO_CACHE_FILE);
|
|
1173
|
+
return buffer;
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
console.error("Warning: Could not download NotoSansJP. CJK text may not render correctly.", error instanceof Error ? error.message : "");
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function parseCustomFonts(fontArgs) {
|
|
1180
|
+
const font = {};
|
|
1181
|
+
for (let i = 0; i < fontArgs.length; i++) {
|
|
1182
|
+
const arg = fontArgs[i];
|
|
1183
|
+
const eqIndex = arg.indexOf("=");
|
|
1184
|
+
if (eqIndex === -1) fail(`Invalid --font format ${JSON.stringify(arg)}. Expected name=path, for example "NotoSansJP=./fonts/NotoSansJP.ttf".`, {
|
|
1185
|
+
code: "EARG",
|
|
1186
|
+
exitCode: 1
|
|
1187
|
+
});
|
|
1188
|
+
const name = arg.slice(0, eqIndex);
|
|
1189
|
+
const filePath = resolve(arg.slice(eqIndex + 1));
|
|
1190
|
+
if (!existsSync(filePath)) fail(`Font file not found: ${filePath}`, {
|
|
1191
|
+
code: "EIO",
|
|
1192
|
+
exitCode: 3
|
|
1193
|
+
});
|
|
1194
|
+
if (extname(filePath).toLowerCase() !== ".ttf") fail(`Unsupported font format for ${filePath}. @pdfme/cli currently guarantees only .ttf custom fonts.`, {
|
|
1195
|
+
code: "EUNSUPPORTED",
|
|
1196
|
+
exitCode: 1
|
|
1197
|
+
});
|
|
1198
|
+
font[name] = {
|
|
1199
|
+
data: new Uint8Array(readFileSync(filePath)),
|
|
1200
|
+
fallback: i === 0,
|
|
1201
|
+
subset: true
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
return font;
|
|
1205
|
+
}
|
|
1206
|
+
function analyzeExplicitFontRecord(fontRecord, templateDir) {
|
|
1207
|
+
const issues = [];
|
|
1208
|
+
const warnings = [];
|
|
1209
|
+
const sources = [];
|
|
1210
|
+
for (const fontName of Object.keys(fontRecord).sort()) {
|
|
1211
|
+
const result = analyzeExplicitFontSource(fontName, fontRecord[fontName], templateDir);
|
|
1212
|
+
sources.push(result.source);
|
|
1213
|
+
issues.push(...result.issues);
|
|
1214
|
+
warnings.push(...result.warnings);
|
|
1215
|
+
}
|
|
1216
|
+
return {
|
|
1217
|
+
sources,
|
|
1218
|
+
issues,
|
|
1219
|
+
warnings
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
async function normalizeExplicitFontOption(jobFont, templateDir) {
|
|
1223
|
+
if (jobFont === void 0) return;
|
|
1224
|
+
if (typeof jobFont !== "object" || jobFont === null || Array.isArray(jobFont)) fail("Unified job options.font must be an object.", {
|
|
1225
|
+
code: "EARG",
|
|
1226
|
+
exitCode: 1
|
|
1227
|
+
});
|
|
1228
|
+
const normalized = {};
|
|
1229
|
+
const fontRecord = jobFont;
|
|
1230
|
+
for (const fontName of Object.keys(fontRecord).sort()) normalized[fontName] = await normalizeExplicitFontSource(fontName, fontRecord[fontName], templateDir);
|
|
1231
|
+
return normalized;
|
|
1232
|
+
}
|
|
1233
|
+
function analyzeExplicitFontSource(fontName, value, templateDir) {
|
|
1234
|
+
const issues = [];
|
|
1235
|
+
const warnings = [];
|
|
1236
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1237
|
+
issues.push(`Font config for ${fontName} must be an object with a "data" field.`);
|
|
1238
|
+
return {
|
|
1239
|
+
source: {
|
|
1240
|
+
fontName,
|
|
1241
|
+
kind: "invalid",
|
|
1242
|
+
needsNetwork: false,
|
|
1243
|
+
dataType: getValueType(value)
|
|
1244
|
+
},
|
|
1245
|
+
issues,
|
|
1246
|
+
warnings
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
const data = value.data;
|
|
1250
|
+
if (data === void 0) {
|
|
1251
|
+
issues.push(`Font config for ${fontName} is missing "data".`);
|
|
1252
|
+
return {
|
|
1253
|
+
source: {
|
|
1254
|
+
fontName,
|
|
1255
|
+
kind: "invalid",
|
|
1256
|
+
needsNetwork: false,
|
|
1257
|
+
dataType: "missing"
|
|
1258
|
+
},
|
|
1259
|
+
issues,
|
|
1260
|
+
warnings
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
if (typeof data === "string") {
|
|
1264
|
+
if (data.startsWith("data:")) return analyzeDataUriFontSource(fontName, data);
|
|
1265
|
+
const parsedUrl = tryParseUrl(data);
|
|
1266
|
+
if (parsedUrl) {
|
|
1267
|
+
if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return analyzeUrlFontSource(fontName, parsedUrl);
|
|
1268
|
+
issues.push(`Font source for ${fontName} uses unsupported URL protocol "${parsedUrl.protocol}". Use a local .ttf path, a data URI, or an https URL.`);
|
|
1269
|
+
return {
|
|
1270
|
+
source: {
|
|
1271
|
+
fontName,
|
|
1272
|
+
kind: "invalid",
|
|
1273
|
+
needsNetwork: false,
|
|
1274
|
+
dataType: "string"
|
|
1275
|
+
},
|
|
1276
|
+
issues,
|
|
1277
|
+
warnings
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
return analyzeLocalFontSource(fontName, data, templateDir);
|
|
1281
|
+
}
|
|
1282
|
+
if (data instanceof Uint8Array || data instanceof ArrayBuffer) return {
|
|
1283
|
+
source: {
|
|
1284
|
+
fontName,
|
|
1285
|
+
kind: "inlineBytes",
|
|
1286
|
+
needsNetwork: false,
|
|
1287
|
+
dataType: getValueType(data)
|
|
1288
|
+
},
|
|
1289
|
+
issues,
|
|
1290
|
+
warnings
|
|
1291
|
+
};
|
|
1292
|
+
issues.push(`Font source for ${fontName} has unsupported data type ${getValueType(data)}.`);
|
|
1293
|
+
return {
|
|
1294
|
+
source: {
|
|
1295
|
+
fontName,
|
|
1296
|
+
kind: "invalid",
|
|
1297
|
+
needsNetwork: false,
|
|
1298
|
+
dataType: getValueType(data)
|
|
1299
|
+
},
|
|
1300
|
+
issues,
|
|
1301
|
+
warnings
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
function analyzeLocalFontSource(fontName, pathValue, templateDir) {
|
|
1305
|
+
const issues = [];
|
|
1306
|
+
const warnings = [];
|
|
1307
|
+
const resolvedPath = templateDir ? resolve(templateDir, pathValue) : resolve(pathValue);
|
|
1308
|
+
const exists = existsSync(resolvedPath);
|
|
1309
|
+
const formatHint = detectPathFormatHint(resolvedPath);
|
|
1310
|
+
const formatResult = evaluateFontFormat(fontName, formatHint, `Font file for ${fontName}`);
|
|
1311
|
+
if (!exists) issues.push(`Font file for ${fontName} not found: ${resolvedPath}`);
|
|
1312
|
+
if (formatResult.issue) issues.push(formatResult.issue);
|
|
1313
|
+
if (formatResult.warning) warnings.push(formatResult.warning);
|
|
1314
|
+
return {
|
|
1315
|
+
source: {
|
|
1316
|
+
fontName,
|
|
1317
|
+
kind: "localPath",
|
|
1318
|
+
path: pathValue,
|
|
1319
|
+
resolvedPath,
|
|
1320
|
+
exists,
|
|
1321
|
+
formatHint,
|
|
1322
|
+
supportedFormat: formatResult.supportedFormat,
|
|
1323
|
+
needsNetwork: false,
|
|
1324
|
+
dataType: "string"
|
|
1325
|
+
},
|
|
1326
|
+
issues,
|
|
1327
|
+
warnings
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function analyzeUrlFontSource(fontName, url) {
|
|
1331
|
+
const issues = [];
|
|
1332
|
+
const warnings = [];
|
|
1333
|
+
const provider = detectRemoteFontProvider(url);
|
|
1334
|
+
const formatHint = detectPathFormatHint(url.pathname);
|
|
1335
|
+
const formatResult = evaluateFontFormat(fontName, formatHint, `Font URL for ${fontName}`);
|
|
1336
|
+
if (provider === "googleFontsStylesheet") issues.push(`Font URL for ${fontName} uses the unsupported Google Fonts stylesheet API. Use the direct fonts.gstatic.com asset URL or download the font locally.`);
|
|
1337
|
+
if (!isUrlSafeToFetch(url.toString())) issues.push(`Font URL for ${fontName} is invalid or unsafe. Only http: and https: URLs pointing to public hosts are allowed.`);
|
|
1338
|
+
if (provider !== "googleFontsStylesheet" && formatResult.issue) issues.push(formatResult.issue);
|
|
1339
|
+
if (provider !== "googleFontsStylesheet" && formatResult.warning) warnings.push(formatResult.warning);
|
|
1340
|
+
return {
|
|
1341
|
+
source: {
|
|
1342
|
+
fontName,
|
|
1343
|
+
kind: "url",
|
|
1344
|
+
provider,
|
|
1345
|
+
url: url.toString(),
|
|
1346
|
+
formatHint,
|
|
1347
|
+
supportedFormat: formatResult.supportedFormat,
|
|
1348
|
+
needsNetwork: true,
|
|
1349
|
+
dataType: "string"
|
|
1350
|
+
},
|
|
1351
|
+
issues,
|
|
1352
|
+
warnings
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function analyzeDataUriFontSource(fontName, dataUri) {
|
|
1356
|
+
const issues = [];
|
|
1357
|
+
const warnings = [];
|
|
1358
|
+
const mediaType = getDataUriMediaType(dataUri);
|
|
1359
|
+
const formatHint = detectDataUriFormatHint(mediaType);
|
|
1360
|
+
const formatResult = evaluateFontFormat(fontName, formatHint, `Font data URI for ${fontName}`);
|
|
1361
|
+
if (formatResult.issue) issues.push(formatResult.issue);
|
|
1362
|
+
if (formatResult.warning) warnings.push(formatResult.warning);
|
|
1363
|
+
return {
|
|
1364
|
+
source: {
|
|
1365
|
+
fontName,
|
|
1366
|
+
kind: "dataUri",
|
|
1367
|
+
mediaType,
|
|
1368
|
+
formatHint,
|
|
1369
|
+
supportedFormat: formatResult.supportedFormat,
|
|
1370
|
+
needsNetwork: false,
|
|
1371
|
+
dataType: "string"
|
|
1372
|
+
},
|
|
1373
|
+
issues,
|
|
1374
|
+
warnings
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
async function normalizeExplicitFontSource(fontName, value, templateDir) {
|
|
1378
|
+
const analysis = analyzeExplicitFontSource(fontName, value, templateDir);
|
|
1379
|
+
for (const issue of analysis.issues) {
|
|
1380
|
+
const code = issue.includes("not found") ? "EIO" : issue.includes("unsupported") || issue.includes("unsafe") || issue.includes("uses .") ? "EUNSUPPORTED" : "EARG";
|
|
1381
|
+
fail(issue, {
|
|
1382
|
+
code,
|
|
1383
|
+
exitCode: code === "EIO" ? 3 : 1
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
const record = value;
|
|
1387
|
+
const data = record.data;
|
|
1388
|
+
if (analysis.source.kind === "localPath") return {
|
|
1389
|
+
...record,
|
|
1390
|
+
data: new Uint8Array(readFileSync(analysis.source.resolvedPath))
|
|
1391
|
+
};
|
|
1392
|
+
if (analysis.source.kind === "url") return {
|
|
1393
|
+
...record,
|
|
1394
|
+
data: await fetchRemoteFontSource(analysis.source)
|
|
1395
|
+
};
|
|
1396
|
+
if (analysis.source.kind === "dataUri" || analysis.source.kind === "inlineBytes") {
|
|
1397
|
+
const normalizedData = typeof data === "string" ? data : data instanceof Uint8Array ? data : data;
|
|
1398
|
+
return {
|
|
1399
|
+
...record,
|
|
1400
|
+
data: normalizedData
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
fail(`Font source for ${fontName} has unsupported data type ${getValueType(data)}.`, {
|
|
1404
|
+
code: "EARG",
|
|
1405
|
+
exitCode: 1
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
async function fetchRemoteFontSource(source) {
|
|
1409
|
+
const url = source.url;
|
|
1410
|
+
try {
|
|
1411
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(REMOTE_FONT_TIMEOUT_MS) });
|
|
1412
|
+
if (!response.ok) failRemoteFontFetch(source, `Failed to fetch remote font data from ${url}. HTTP ${response.status}`);
|
|
1413
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
1414
|
+
const declaredLength = contentLengthHeader ? Number(contentLengthHeader) : NaN;
|
|
1415
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_FONT_BYTES) failRemoteFontFetch(source, `Remote font data from ${url} exceeds the ${MAX_REMOTE_FONT_BYTES}-byte safety limit.`);
|
|
1416
|
+
return await readResponseBodyWithLimit(response, source);
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
if (error instanceof CliError) throw error;
|
|
1419
|
+
failRemoteFontFetch(source, `Failed to fetch remote font data from ${url}. ${error instanceof Error ? error.message : String(error)}`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
function failRemoteFontFetch(source, message) {
|
|
1423
|
+
fail(message, {
|
|
1424
|
+
code: "EFONT",
|
|
1425
|
+
exitCode: 2,
|
|
1426
|
+
details: {
|
|
1427
|
+
fontName: source.fontName,
|
|
1428
|
+
url: source.url,
|
|
1429
|
+
provider: source.provider,
|
|
1430
|
+
timeoutMs: REMOTE_FONT_TIMEOUT_MS,
|
|
1431
|
+
maxBytes: MAX_REMOTE_FONT_BYTES
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
async function readResponseBodyWithLimit(response, source) {
|
|
1436
|
+
if (!response.body) {
|
|
1437
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
1438
|
+
if (buffer.byteLength > MAX_REMOTE_FONT_BYTES) failRemoteFontFetch(source, `Remote font data from ${source.url} exceeds the ${MAX_REMOTE_FONT_BYTES}-byte safety limit.`);
|
|
1439
|
+
return buffer;
|
|
1440
|
+
}
|
|
1441
|
+
const reader = response.body.getReader();
|
|
1442
|
+
const chunks = [];
|
|
1443
|
+
let total = 0;
|
|
1444
|
+
while (true) {
|
|
1445
|
+
const { done, value } = await reader.read();
|
|
1446
|
+
if (done) break;
|
|
1447
|
+
if (!value) continue;
|
|
1448
|
+
total += value.byteLength;
|
|
1449
|
+
if (total > MAX_REMOTE_FONT_BYTES) failRemoteFontFetch(source, `Remote font data from ${source.url} exceeds the ${MAX_REMOTE_FONT_BYTES}-byte safety limit.`);
|
|
1450
|
+
chunks.push(value);
|
|
1451
|
+
}
|
|
1452
|
+
const merged = new Uint8Array(total);
|
|
1453
|
+
let offset = 0;
|
|
1454
|
+
for (const chunk of chunks) {
|
|
1455
|
+
merged.set(chunk, offset);
|
|
1456
|
+
offset += chunk.byteLength;
|
|
1457
|
+
}
|
|
1458
|
+
return merged;
|
|
1459
|
+
}
|
|
1460
|
+
function tryParseUrl(value) {
|
|
1461
|
+
try {
|
|
1462
|
+
return new URL(value);
|
|
1463
|
+
} catch {
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function detectRemoteFontProvider(url) {
|
|
1468
|
+
const hostname = url.hostname.toLowerCase();
|
|
1469
|
+
if (hostname === "fonts.gstatic.com" || hostname.endsWith(".fonts.gstatic.com")) return "googleFontsAsset";
|
|
1470
|
+
if (hostname === "fonts.googleapis.com" || hostname.endsWith(".fonts.googleapis.com")) return "googleFontsStylesheet";
|
|
1471
|
+
return "genericPublic";
|
|
1472
|
+
}
|
|
1473
|
+
function getDataUriMediaType(value) {
|
|
1474
|
+
const match = value.match(/^data:([^;,]+)/i);
|
|
1475
|
+
return match ? match[1] : void 0;
|
|
1476
|
+
}
|
|
1477
|
+
function detectPathFormatHint(value) {
|
|
1478
|
+
const extension = extname(value).toLowerCase();
|
|
1479
|
+
return extension ? extension.slice(1) : null;
|
|
1480
|
+
}
|
|
1481
|
+
function detectDataUriFormatHint(mediaType) {
|
|
1482
|
+
if (!mediaType) return null;
|
|
1483
|
+
const lower = mediaType.toLowerCase();
|
|
1484
|
+
if (lower.includes("ttf") || lower.endsWith("/sfnt")) return "ttf";
|
|
1485
|
+
if (lower.includes("otf")) return "otf";
|
|
1486
|
+
if (lower.includes("ttc")) return "ttc";
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
function evaluateFontFormat(fontName, formatHint, sourceLabel) {
|
|
1490
|
+
if (formatHint === "ttf") return { supportedFormat: true };
|
|
1491
|
+
if (formatHint === null) return { warning: `${sourceLabel} does not clearly advertise a .ttf format. @pdfme/cli currently guarantees only .ttf custom fonts.` };
|
|
1492
|
+
return {
|
|
1493
|
+
supportedFormat: false,
|
|
1494
|
+
issue: `${sourceLabel} uses .${formatHint}. @pdfme/cli currently guarantees only .ttf custom fonts for ${fontName}.`
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function getValueType(value) {
|
|
1498
|
+
if (value === void 0) return "undefined";
|
|
1499
|
+
if (value === null) return "null";
|
|
1500
|
+
if (value instanceof Uint8Array) return "Uint8Array";
|
|
1501
|
+
if (value instanceof ArrayBuffer) return "ArrayBuffer";
|
|
1502
|
+
if (Array.isArray(value)) return "array";
|
|
1503
|
+
return typeof value;
|
|
1504
|
+
}
|
|
1505
|
+
async function resolveFont(options) {
|
|
1506
|
+
const { fontArgs, hasCJK, noAutoFont, verbose, hasExplicitFontConfig = false } = options;
|
|
1507
|
+
if (fontArgs && fontArgs.length > 0) return parseCustomFonts(fontArgs);
|
|
1508
|
+
const defaultFont = getDefaultFont();
|
|
1509
|
+
if (!hasCJK || hasExplicitFontConfig) return defaultFont;
|
|
1510
|
+
if (noAutoFont) fail("CJK text detected, but automatic NotoSansJP download is disabled by --noAutoFont and no explicit font source was provided. Provide --font or options.font.", {
|
|
1511
|
+
code: "EFONT",
|
|
1512
|
+
exitCode: 2,
|
|
1513
|
+
details: {
|
|
1514
|
+
fontName: "NotoSansJP",
|
|
1515
|
+
cacheFile: NOTO_CACHE_FILE,
|
|
1516
|
+
autoFont: false
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
const notoData = await downloadNotoSansJP(verbose);
|
|
1520
|
+
if (!notoData) fail("CJK text detected, but NotoSansJP could not be resolved automatically. Re-run with network access, warm the font cache, or provide --font / options.font.", {
|
|
1521
|
+
code: "EFONT",
|
|
1522
|
+
exitCode: 2,
|
|
1523
|
+
details: {
|
|
1524
|
+
fontName: "NotoSansJP",
|
|
1525
|
+
cacheFile: NOTO_CACHE_FILE,
|
|
1526
|
+
downloadUrl: NOTO_SANS_JP_URL,
|
|
1527
|
+
autoFont: true
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
return {
|
|
1531
|
+
NotoSansJP: {
|
|
1532
|
+
data: notoData,
|
|
1533
|
+
fallback: true,
|
|
1534
|
+
subset: true
|
|
1535
|
+
},
|
|
1536
|
+
...Object.fromEntries(Object.entries(defaultFont).map(([k, v]) => [k, {
|
|
1537
|
+
...v,
|
|
1538
|
+
fallback: false
|
|
1539
|
+
}]))
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
//#endregion
|
|
1543
|
+
//#region src/cjk-detect.ts
|
|
1544
|
+
var CJK_RANGES = [
|
|
1545
|
+
[19968, 40959],
|
|
1546
|
+
[13312, 19903],
|
|
1547
|
+
[12352, 12447],
|
|
1548
|
+
[12448, 12543],
|
|
1549
|
+
[44032, 55215],
|
|
1550
|
+
[12288, 12351],
|
|
1551
|
+
[65280, 65519]
|
|
1552
|
+
];
|
|
1553
|
+
function containsCJK(text) {
|
|
1554
|
+
for (let i = 0; i < text.length; i++) {
|
|
1555
|
+
const code = text.charCodeAt(i);
|
|
1556
|
+
for (const [start, end] of CJK_RANGES) if (code >= start && code <= end) return true;
|
|
1557
|
+
}
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
function detectCJKInTemplate(template) {
|
|
1561
|
+
if (!Array.isArray(template.schemas)) return false;
|
|
1562
|
+
for (const page of template.schemas) {
|
|
1563
|
+
if (!Array.isArray(page)) continue;
|
|
1564
|
+
for (const schema of page) {
|
|
1565
|
+
if (typeof schema !== "object" || schema === null) continue;
|
|
1566
|
+
if (typeof schema.content === "string" && containsCJK(schema.content)) return true;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
function detectCJKInInputs(inputs) {
|
|
1572
|
+
if (!Array.isArray(inputs)) return false;
|
|
1573
|
+
for (const input of inputs) {
|
|
1574
|
+
if (typeof input !== "object" || input === null) continue;
|
|
1575
|
+
for (const value of Object.values(input)) if (typeof value === "string" && containsCJK(value)) return true;
|
|
1576
|
+
}
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
//#endregion
|
|
1580
|
+
//#region src/grid.ts
|
|
1581
|
+
var schemaColorCache = /* @__PURE__ */ new Map();
|
|
1582
|
+
function hashString(value) {
|
|
1583
|
+
let hash = 0;
|
|
1584
|
+
for (let i = 0; i < value.length; i++) hash = hash * 31 + value.charCodeAt(i) >>> 0;
|
|
1585
|
+
return hash;
|
|
1586
|
+
}
|
|
1587
|
+
function hslToHex(hue, saturation, lightness) {
|
|
1588
|
+
const s = saturation / 100;
|
|
1589
|
+
const l = lightness / 100;
|
|
1590
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
1591
|
+
const h = hue / 60;
|
|
1592
|
+
const x = c * (1 - Math.abs(h % 2 - 1));
|
|
1593
|
+
let r = 0;
|
|
1594
|
+
let g = 0;
|
|
1595
|
+
let b = 0;
|
|
1596
|
+
if (h >= 0 && h < 1) {
|
|
1597
|
+
r = c;
|
|
1598
|
+
g = x;
|
|
1599
|
+
} else if (h < 2) {
|
|
1600
|
+
r = x;
|
|
1601
|
+
g = c;
|
|
1602
|
+
} else if (h < 3) {
|
|
1603
|
+
g = c;
|
|
1604
|
+
b = x;
|
|
1605
|
+
} else if (h < 4) {
|
|
1606
|
+
g = x;
|
|
1607
|
+
b = c;
|
|
1608
|
+
} else if (h < 5) {
|
|
1609
|
+
r = x;
|
|
1610
|
+
b = c;
|
|
1611
|
+
} else {
|
|
1612
|
+
r = c;
|
|
1613
|
+
b = x;
|
|
1614
|
+
}
|
|
1615
|
+
const m = l - c / 2;
|
|
1616
|
+
const toHex = (channel) => Math.round((channel + m) * 255).toString(16).padStart(2, "0");
|
|
1617
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
1618
|
+
}
|
|
1619
|
+
function getSchemaColor(type) {
|
|
1620
|
+
const normalizedType = type.trim().toLowerCase();
|
|
1621
|
+
const cached = schemaColorCache.get(normalizedType);
|
|
1622
|
+
if (cached) return cached;
|
|
1623
|
+
const hash = hashString(normalizedType);
|
|
1624
|
+
const color = hslToHex(hash % 360, 65 + hash % 12, 42 + (hash >> 8) % 10);
|
|
1625
|
+
schemaColorCache.set(normalizedType, color);
|
|
1626
|
+
return color;
|
|
1627
|
+
}
|
|
1628
|
+
function drawGridLines(ctx, pxPerMm, pageWidthMm, pageHeightMm, imgWidth, imgHeight, gridSizeMm, withLabels) {
|
|
1629
|
+
ctx.strokeStyle = "rgba(128, 128, 128, 0.3)";
|
|
1630
|
+
ctx.lineWidth = 1;
|
|
1631
|
+
for (let x = 0; x <= pageWidthMm; x += gridSizeMm) {
|
|
1632
|
+
const px = x * pxPerMm;
|
|
1633
|
+
ctx.beginPath();
|
|
1634
|
+
ctx.moveTo(px, 0);
|
|
1635
|
+
ctx.lineTo(px, imgHeight);
|
|
1636
|
+
ctx.stroke();
|
|
1637
|
+
if (withLabels && x % (gridSizeMm * 2) === 0 && x > 0) {
|
|
1638
|
+
ctx.fillStyle = "rgba(100, 100, 100, 0.7)";
|
|
1639
|
+
ctx.font = `${Math.max(9, pxPerMm * 2.5)}px sans-serif`;
|
|
1640
|
+
ctx.fillText(`${x}`, px + 2, 12);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
for (let y = 0; y <= pageHeightMm; y += gridSizeMm) {
|
|
1644
|
+
const px = y * pxPerMm;
|
|
1645
|
+
ctx.beginPath();
|
|
1646
|
+
ctx.moveTo(0, px);
|
|
1647
|
+
ctx.lineTo(imgWidth, px);
|
|
1648
|
+
ctx.stroke();
|
|
1649
|
+
if (withLabels && y % (gridSizeMm * 2) === 0 && y > 0) {
|
|
1650
|
+
ctx.fillStyle = "rgba(100, 100, 100, 0.7)";
|
|
1651
|
+
ctx.font = `${Math.max(9, pxPerMm * 2.5)}px sans-serif`;
|
|
1652
|
+
ctx.fillText(`${y}`, 2, px - 2);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function drawSchemaOverlays(ctx, schemas, pxPerMm) {
|
|
1657
|
+
for (const schema of schemas) {
|
|
1658
|
+
const color = getSchemaColor(schema.type);
|
|
1659
|
+
const x = schema.position.x * pxPerMm;
|
|
1660
|
+
const y = schema.position.y * pxPerMm;
|
|
1661
|
+
const w = schema.width * pxPerMm;
|
|
1662
|
+
const h = schema.height * pxPerMm;
|
|
1663
|
+
ctx.strokeStyle = color;
|
|
1664
|
+
ctx.lineWidth = 2;
|
|
1665
|
+
ctx.setLineDash([4, 2]);
|
|
1666
|
+
ctx.strokeRect(x, y, w, h);
|
|
1667
|
+
ctx.setLineDash([]);
|
|
1668
|
+
const label = `${schema.name} (${schema.type})`;
|
|
1669
|
+
const fontSize = Math.max(10, Math.min(14, pxPerMm * 3));
|
|
1670
|
+
ctx.font = `${fontSize}px sans-serif`;
|
|
1671
|
+
const metrics = ctx.measureText(label);
|
|
1672
|
+
const labelHeight = fontSize + 4;
|
|
1673
|
+
const labelWidth = metrics.width + 6;
|
|
1674
|
+
ctx.fillStyle = color;
|
|
1675
|
+
ctx.globalAlpha = .85;
|
|
1676
|
+
ctx.fillRect(x, y - labelHeight, labelWidth, labelHeight);
|
|
1677
|
+
ctx.globalAlpha = 1;
|
|
1678
|
+
ctx.fillStyle = "#FFFFFF";
|
|
1679
|
+
ctx.fillText(label, x + 3, y - 3);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
async function loadAndPrepareCanvas(imageBuffer) {
|
|
1683
|
+
const { createCanvas, loadImage } = await import("@napi-rs/canvas");
|
|
1684
|
+
const img = await loadImage(Buffer.from(imageBuffer));
|
|
1685
|
+
const canvas = createCanvas(img.width, img.height);
|
|
1686
|
+
const ctx = canvas.getContext("2d");
|
|
1687
|
+
ctx.drawImage(img, 0, 0);
|
|
1688
|
+
return {
|
|
1689
|
+
canvas,
|
|
1690
|
+
ctx,
|
|
1691
|
+
img
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
function bufferToArrayBuffer(buffer) {
|
|
1695
|
+
const arrayBuffer = new ArrayBuffer(buffer.byteLength);
|
|
1696
|
+
new Uint8Array(arrayBuffer).set(buffer);
|
|
1697
|
+
return arrayBuffer;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Draw grid lines and schema boundary overlays on a generated PDF page image.
|
|
1701
|
+
* Used by `pdfme generate --grid`.
|
|
1702
|
+
*/
|
|
1703
|
+
async function drawGridOnImage(imageBuffer, schemas, gridSizeMm, pageWidthMm, pageHeightMm, imageType) {
|
|
1704
|
+
const { canvas, ctx, img } = await loadAndPrepareCanvas(imageBuffer);
|
|
1705
|
+
const pxPerMm = img.width / pageWidthMm;
|
|
1706
|
+
drawGridLines(ctx, pxPerMm, pageWidthMm, pageHeightMm, img.width, img.height, gridSizeMm, false);
|
|
1707
|
+
drawSchemaOverlays(ctx, schemas, pxPerMm);
|
|
1708
|
+
const mimeType = imageType === "jpeg" ? "image/jpeg" : "image/png";
|
|
1709
|
+
return bufferToArrayBuffer(canvas.toBuffer(mimeType));
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Draw grid lines with mm coordinate labels on a plain PDF image.
|
|
1713
|
+
* Used by `pdfme pdf2img --grid`.
|
|
1714
|
+
*/
|
|
1715
|
+
async function drawGridOnPdfImage(imageBuffer, gridSizeMm, pageWidthMm, pageHeightMm, imageType) {
|
|
1716
|
+
const { canvas, ctx, img } = await loadAndPrepareCanvas(imageBuffer);
|
|
1717
|
+
drawGridLines(ctx, img.width / pageWidthMm, pageWidthMm, pageHeightMm, img.width, img.height, gridSizeMm, true);
|
|
1718
|
+
const mimeType = imageType === "jpeg" ? "image/jpeg" : "image/png";
|
|
1719
|
+
return bufferToArrayBuffer(canvas.toBuffer(mimeType));
|
|
1720
|
+
}
|
|
1721
|
+
//#endregion
|
|
1722
|
+
//#region src/commands/generate.ts
|
|
1723
|
+
var generateArgs = {
|
|
1724
|
+
file: {
|
|
1725
|
+
type: "positional",
|
|
1726
|
+
description: "Unified JSON file: { template, inputs }",
|
|
1727
|
+
required: false
|
|
1728
|
+
},
|
|
1729
|
+
template: {
|
|
1730
|
+
type: "string",
|
|
1731
|
+
alias: "t",
|
|
1732
|
+
description: "Template JSON file"
|
|
1733
|
+
},
|
|
1734
|
+
inputs: {
|
|
1735
|
+
type: "string",
|
|
1736
|
+
alias: "i",
|
|
1737
|
+
description: "Input data JSON file"
|
|
1738
|
+
},
|
|
1739
|
+
output: {
|
|
1740
|
+
type: "string",
|
|
1741
|
+
alias: "o",
|
|
1742
|
+
description: "Output PDF path",
|
|
1743
|
+
default: "output.pdf"
|
|
1744
|
+
},
|
|
1745
|
+
force: {
|
|
1746
|
+
type: "boolean",
|
|
1747
|
+
description: "Allow overwriting the implicit default output path",
|
|
1748
|
+
default: false
|
|
1749
|
+
},
|
|
1750
|
+
image: {
|
|
1751
|
+
type: "boolean",
|
|
1752
|
+
description: "Also output PNG images per page",
|
|
1753
|
+
default: false
|
|
1754
|
+
},
|
|
1755
|
+
imageFormat: {
|
|
1756
|
+
type: "string",
|
|
1757
|
+
description: "Image format: png | jpeg",
|
|
1758
|
+
default: "png"
|
|
1759
|
+
},
|
|
1760
|
+
scale: {
|
|
1761
|
+
type: "string",
|
|
1762
|
+
description: "Image render scale",
|
|
1763
|
+
default: "1"
|
|
1764
|
+
},
|
|
1765
|
+
grid: {
|
|
1766
|
+
type: "boolean",
|
|
1767
|
+
description: "Overlay grid + schema boundaries on images",
|
|
1768
|
+
default: false
|
|
1769
|
+
},
|
|
1770
|
+
gridSize: {
|
|
1771
|
+
type: "string",
|
|
1772
|
+
description: "Grid spacing in mm",
|
|
1773
|
+
default: "10"
|
|
1774
|
+
},
|
|
1775
|
+
font: {
|
|
1776
|
+
type: "string",
|
|
1777
|
+
description: "Custom font(s): name=path (comma-separated for multiple)"
|
|
1778
|
+
},
|
|
1779
|
+
basePdf: {
|
|
1780
|
+
type: "string",
|
|
1781
|
+
description: "Override basePdf with PDF file path"
|
|
1782
|
+
},
|
|
1783
|
+
noAutoFont: {
|
|
1784
|
+
type: "boolean",
|
|
1785
|
+
description: "Disable automatic CJK font download",
|
|
1786
|
+
default: false
|
|
1787
|
+
},
|
|
1788
|
+
verbose: {
|
|
1789
|
+
type: "boolean",
|
|
1790
|
+
alias: "v",
|
|
1791
|
+
description: "Verbose output",
|
|
1792
|
+
default: false
|
|
1793
|
+
},
|
|
1794
|
+
json: {
|
|
1795
|
+
type: "boolean",
|
|
1796
|
+
description: "Machine-readable JSON output",
|
|
1797
|
+
default: false
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
var generate_default = defineCommand({
|
|
1801
|
+
meta: {
|
|
1802
|
+
name: "generate",
|
|
1803
|
+
description: "Generate PDF from template and inputs"
|
|
1804
|
+
},
|
|
1805
|
+
args: generateArgs,
|
|
1806
|
+
async run({ args, rawArgs }) {
|
|
1807
|
+
return runWithContract({ json: Boolean(args.json) }, async () => {
|
|
1808
|
+
assertNoUnknownFlags(rawArgs, generateArgs);
|
|
1809
|
+
const imageFormat = parseEnumArg("imageFormat", args.imageFormat, ["png", "jpeg"]);
|
|
1810
|
+
const scale = parsePositiveNumberArg("scale", args.scale);
|
|
1811
|
+
const gridSize = parsePositiveNumberArg("gridSize", args.gridSize);
|
|
1812
|
+
ensureSafeDefaultOutputPath({
|
|
1813
|
+
filePath: args.output,
|
|
1814
|
+
rawArgs,
|
|
1815
|
+
optionName: "output",
|
|
1816
|
+
optionAlias: "o",
|
|
1817
|
+
defaultValue: "output.pdf",
|
|
1818
|
+
force: Boolean(args.force)
|
|
1819
|
+
});
|
|
1820
|
+
const { template: rawTemplate, inputs, options: rawJobOptions, templateDir } = loadInput({
|
|
1821
|
+
_: args.file ? [args.file] : [],
|
|
1822
|
+
template: args.template,
|
|
1823
|
+
inputs: args.inputs
|
|
1824
|
+
});
|
|
1825
|
+
const template = resolveBasePdf(rawTemplate, args.basePdf, templateDir);
|
|
1826
|
+
const mode = args.file ? "job" : "template+inputs";
|
|
1827
|
+
const templatePageCount = normalizeSchemaPages$1(template.schemas).length;
|
|
1828
|
+
const jobOptions = normalizeJobOptions(rawJobOptions);
|
|
1829
|
+
assertSupportedSchemaTypes(template);
|
|
1830
|
+
const resolvedFont = await resolveFont({
|
|
1831
|
+
fontArgs: args.font ? args.font.split(",").map((value) => value.trim()).filter(Boolean) : void 0,
|
|
1832
|
+
hasCJK: detectCJKInTemplate(template) || detectCJKInInputs(inputs),
|
|
1833
|
+
noAutoFont: Boolean(args.noAutoFont),
|
|
1834
|
+
verbose: Boolean(args.verbose),
|
|
1835
|
+
hasExplicitFontConfig: hasExplicitFontEntries(jobOptions.font)
|
|
1836
|
+
});
|
|
1837
|
+
const font = await normalizeExplicitFontOption(mergeFontConfig(jobOptions.font, resolvedFont), templateDir) ?? resolvedFont;
|
|
1838
|
+
const generateOptions = {
|
|
1839
|
+
...jobOptions,
|
|
1840
|
+
font
|
|
1841
|
+
};
|
|
1842
|
+
try {
|
|
1843
|
+
checkGenerateProps({
|
|
1844
|
+
template,
|
|
1845
|
+
inputs,
|
|
1846
|
+
options: generateOptions
|
|
1847
|
+
});
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
fail(`Invalid generation input. ${error instanceof Error ? error.message : String(error)}`, {
|
|
1850
|
+
code: "EVALIDATE",
|
|
1851
|
+
exitCode: 1,
|
|
1852
|
+
cause: error
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
validateInputContracts(template, inputs);
|
|
1856
|
+
if (args.verbose) {
|
|
1857
|
+
console.error(`Input: ${describeGenerateInput(args.file, args.template, args.inputs)}`);
|
|
1858
|
+
console.error(`Mode: ${mode}`);
|
|
1859
|
+
console.error(`Template pages: ${templatePageCount}`);
|
|
1860
|
+
console.error(`Inputs: ${inputs.length} set(s)`);
|
|
1861
|
+
console.error(`Output: ${args.output}`);
|
|
1862
|
+
console.error(`Images: ${args.image || args.grid ? `enabled (${imageFormat}, scale=${scale}, grid=${args.grid ? `${gridSize}mm` : "disabled"})` : "disabled"}`);
|
|
1863
|
+
console.error(`Fonts: ${Object.keys(font).join(", ")}`);
|
|
1864
|
+
if (args.basePdf) console.error(`Base PDF override: ${args.basePdf}`);
|
|
1865
|
+
}
|
|
1866
|
+
const pdf = await generate({
|
|
1867
|
+
template,
|
|
1868
|
+
inputs,
|
|
1869
|
+
options: generateOptions,
|
|
1870
|
+
plugins: schemaPlugins
|
|
1871
|
+
});
|
|
1872
|
+
const pageCount = (await PDFDocument.load(pdf, { updateMetadata: false })).getPageCount();
|
|
1873
|
+
writeOutput(args.output, pdf);
|
|
1874
|
+
const result = {
|
|
1875
|
+
command: "generate",
|
|
1876
|
+
mode,
|
|
1877
|
+
templatePageCount,
|
|
1878
|
+
inputCount: inputs.length,
|
|
1879
|
+
pageCount,
|
|
1880
|
+
outputPath: args.output,
|
|
1881
|
+
outputBytes: pdf.byteLength
|
|
1882
|
+
};
|
|
1883
|
+
if (args.image || args.grid) {
|
|
1884
|
+
const images = await pdf2img(pdf, {
|
|
1885
|
+
scale,
|
|
1886
|
+
imageType: imageFormat
|
|
1887
|
+
});
|
|
1888
|
+
const imagePaths = getImageOutputPaths(args.output, images.length, imageFormat);
|
|
1889
|
+
if (args.grid) {
|
|
1890
|
+
const renderedPageSizes = await pdf2size(pdf);
|
|
1891
|
+
for (let i = 0; i < images.length; i++) {
|
|
1892
|
+
const templateSchemas = template.schemas ?? [];
|
|
1893
|
+
const pageSchemas = templateSchemas[i % templateSchemas.length] ?? [];
|
|
1894
|
+
const size = renderedPageSizes[i] ?? renderedPageSizes[0] ?? {
|
|
1895
|
+
width: 210,
|
|
1896
|
+
height: 297
|
|
1897
|
+
};
|
|
1898
|
+
const gridImage = await drawGridOnImage(images[i], pageSchemas, gridSize, size.width, size.height, imageFormat);
|
|
1899
|
+
writeOutput(imagePaths[i], gridImage);
|
|
1900
|
+
}
|
|
1901
|
+
} else for (let i = 0; i < images.length; i++) writeOutput(imagePaths[i], images[i]);
|
|
1902
|
+
result.imagePaths = imagePaths;
|
|
1903
|
+
}
|
|
1904
|
+
if (args.json) printJson({
|
|
1905
|
+
ok: true,
|
|
1906
|
+
...result
|
|
1907
|
+
});
|
|
1908
|
+
else {
|
|
1909
|
+
console.log(`\u2713 Output: ${args.output} (${formatBytes(pdf.byteLength)})`);
|
|
1910
|
+
if (result.imagePaths) for (const img of result.imagePaths) console.log(`\u2713 Image: ${img}`);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
function formatBytes(bytes) {
|
|
1916
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1917
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1918
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1919
|
+
}
|
|
1920
|
+
function normalizeJobOptions(rawJobOptions) {
|
|
1921
|
+
if (rawJobOptions === void 0) return {};
|
|
1922
|
+
if (typeof rawJobOptions !== "object" || rawJobOptions === null || Array.isArray(rawJobOptions)) fail("Unified job options must be a JSON object.", {
|
|
1923
|
+
code: "EARG",
|
|
1924
|
+
exitCode: 1
|
|
1925
|
+
});
|
|
1926
|
+
return rawJobOptions;
|
|
1927
|
+
}
|
|
1928
|
+
function mergeFontConfig(jobFont, resolvedFont) {
|
|
1929
|
+
if (jobFont === void 0) return resolvedFont;
|
|
1930
|
+
if (typeof jobFont !== "object" || jobFont === null || Array.isArray(jobFont)) fail("Unified job options.font must be an object.", {
|
|
1931
|
+
code: "EARG",
|
|
1932
|
+
exitCode: 1
|
|
1933
|
+
});
|
|
1934
|
+
return {
|
|
1935
|
+
...jobFont,
|
|
1936
|
+
...resolvedFont
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
function hasExplicitFontEntries(jobFont) {
|
|
1940
|
+
if (typeof jobFont !== "object" || jobFont === null || Array.isArray(jobFont)) return false;
|
|
1941
|
+
return Object.keys(jobFont).length > 0;
|
|
1942
|
+
}
|
|
1943
|
+
function assertSupportedSchemaTypes(template) {
|
|
1944
|
+
const unsupported = [];
|
|
1945
|
+
for (const page of normalizeSchemaPages$1(template.schemas)) for (const schema of page) {
|
|
1946
|
+
const type = schema.type;
|
|
1947
|
+
if (typeof type === "string" && !schemaTypes.has(type)) {
|
|
1948
|
+
const name = typeof schema.name === "string" && schema.name.length > 0 ? schema.name : null;
|
|
1949
|
+
unsupported.push(name ? `Field "${name}" has unknown type "${type}"` : `Unknown schema type "${type}"`);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (unsupported.length > 0) fail(`Invalid generation input. ${unsupported.join("; ")}`, {
|
|
1953
|
+
code: "EVALIDATE",
|
|
1954
|
+
exitCode: 1
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
function describeGenerateInput(file, template, inputs) {
|
|
1958
|
+
if (file) return file;
|
|
1959
|
+
if (template || inputs) return `${template ?? "(missing template)"} + ${inputs ?? "(missing inputs)"}`;
|
|
1960
|
+
return "(unknown)";
|
|
1961
|
+
}
|
|
1962
|
+
function normalizeSchemaPages$1(rawSchemas) {
|
|
1963
|
+
if (!Array.isArray(rawSchemas)) return [];
|
|
1964
|
+
return rawSchemas.map((page) => {
|
|
1965
|
+
if (Array.isArray(page)) return page.filter((schema) => typeof schema === "object" && schema !== null);
|
|
1966
|
+
if (typeof page === "object" && page !== null) return Object.values(page).filter((schema) => typeof schema === "object" && schema !== null);
|
|
1967
|
+
return [];
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
//#endregion
|
|
1971
|
+
//#region src/commands/validate.ts
|
|
1972
|
+
var validateArgs = {
|
|
1973
|
+
file: {
|
|
1974
|
+
type: "positional",
|
|
1975
|
+
description: "Template JSON file, unified job file, or \"-\" for stdin",
|
|
1976
|
+
required: false
|
|
1977
|
+
},
|
|
1978
|
+
verbose: {
|
|
1979
|
+
type: "boolean",
|
|
1980
|
+
alias: "v",
|
|
1981
|
+
description: "Verbose output",
|
|
1982
|
+
default: false
|
|
1983
|
+
},
|
|
1984
|
+
json: {
|
|
1985
|
+
type: "boolean",
|
|
1986
|
+
description: "Machine-readable JSON output",
|
|
1987
|
+
default: false
|
|
1988
|
+
},
|
|
1989
|
+
strict: {
|
|
1990
|
+
type: "boolean",
|
|
1991
|
+
description: "Treat warnings as errors",
|
|
1992
|
+
default: false
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
var validate_default = defineCommand({
|
|
1996
|
+
meta: {
|
|
1997
|
+
name: "validate",
|
|
1998
|
+
description: "Validate a pdfme template JSON file"
|
|
1999
|
+
},
|
|
2000
|
+
args: validateArgs,
|
|
2001
|
+
async run({ args, rawArgs }) {
|
|
2002
|
+
return runWithContract({ json: Boolean(args.json) }, async () => {
|
|
2003
|
+
assertNoUnknownFlags(rawArgs, validateArgs);
|
|
2004
|
+
const source = await loadValidationSource(args.file, { noInputMessage: "No validation input provided. Pass a file path or pipe JSON via stdin." });
|
|
2005
|
+
const templateUnknownKeys = Object.keys(source.template).filter((key) => !KNOWN_TEMPLATE_KEYS.has(key)).sort();
|
|
2006
|
+
const inspection = inspectTemplate(source.template, source.templateDir);
|
|
2007
|
+
const resolvedTemplate = resolveBasePdf(source.template, void 0, source.templateDir);
|
|
2008
|
+
const result = validateTemplate(resolvedTemplate);
|
|
2009
|
+
result.warnings.push(...source.jobWarnings);
|
|
2010
|
+
if (templateUnknownKeys.length > 0) result.warnings.push(`Unknown template top-level field(s): ${templateUnknownKeys.join(", ")}`);
|
|
2011
|
+
if (source.mode === "job") {
|
|
2012
|
+
try {
|
|
2013
|
+
checkGenerateProps({
|
|
2014
|
+
template: resolvedTemplate,
|
|
2015
|
+
inputs: source.inputs,
|
|
2016
|
+
options: source.options
|
|
2017
|
+
});
|
|
2018
|
+
} catch (error) {
|
|
2019
|
+
result.errors.unshift(error instanceof Error ? error.message : String(error));
|
|
2020
|
+
}
|
|
2021
|
+
result.errors.push(...getInputContractIssues(resolvedTemplate, source.inputs ?? []));
|
|
2022
|
+
}
|
|
2023
|
+
const valid = result.errors.length === 0 && (!args.strict || result.warnings.length === 0);
|
|
2024
|
+
const inputCount = source.mode === "job" ? source.inputs?.length ?? 0 : void 0;
|
|
2025
|
+
if (args.verbose) {
|
|
2026
|
+
console.error(`Input: ${describeValidationInput(args.file)}`);
|
|
2027
|
+
console.error(`Mode: ${source.mode}`);
|
|
2028
|
+
console.error(`Template pages: ${result.pages}`);
|
|
2029
|
+
console.error(`Fields: ${result.fields}`);
|
|
2030
|
+
if (inputCount !== void 0) console.error(`Inputs: ${inputCount} set(s)`);
|
|
2031
|
+
console.error(`Valid: ${valid ? "yes" : "no"}`);
|
|
2032
|
+
console.error(`Strict: ${args.strict ? "enabled" : "disabled"}`);
|
|
2033
|
+
console.error(`Errors: ${result.errors.length}`);
|
|
2034
|
+
console.error(`Warnings: ${result.warnings.length}`);
|
|
2035
|
+
}
|
|
2036
|
+
if (args.json) printJson({
|
|
2037
|
+
ok: true,
|
|
2038
|
+
command: "validate",
|
|
2039
|
+
valid,
|
|
2040
|
+
mode: source.mode,
|
|
2041
|
+
templatePageCount: result.pages,
|
|
2042
|
+
fieldCount: result.fields,
|
|
2043
|
+
...inputCount !== void 0 ? { inputCount } : {},
|
|
2044
|
+
errors: result.errors,
|
|
2045
|
+
warnings: result.warnings,
|
|
2046
|
+
inspection,
|
|
2047
|
+
inputHints: collectInputHints(source.template)
|
|
2048
|
+
});
|
|
2049
|
+
else {
|
|
2050
|
+
if (result.errors.length === 0 && result.warnings.length === 0) console.log(`\u2713 Template is valid (${result.pages} page(s), ${result.fields} field(s))`);
|
|
2051
|
+
for (const err of result.errors) console.log(`\u2717 Error: ${err}`);
|
|
2052
|
+
for (const warn of result.warnings) console.log(`\u26a0 Warning: ${warn}`);
|
|
2053
|
+
}
|
|
2054
|
+
if (!valid) process.exit(1);
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
function describeValidationInput(file) {
|
|
2059
|
+
if (!file || file === "-") return "stdin";
|
|
2060
|
+
return file;
|
|
2061
|
+
}
|
|
2062
|
+
//#endregion
|
|
2063
|
+
//#region src/commands/pdf2img.ts
|
|
2064
|
+
var pdf2imgArgs = {
|
|
2065
|
+
file: {
|
|
2066
|
+
type: "positional",
|
|
2067
|
+
description: "Input PDF file",
|
|
2068
|
+
required: false
|
|
2069
|
+
},
|
|
2070
|
+
output: {
|
|
2071
|
+
type: "string",
|
|
2072
|
+
alias: "o",
|
|
2073
|
+
description: "Output directory"
|
|
2074
|
+
},
|
|
2075
|
+
grid: {
|
|
2076
|
+
type: "boolean",
|
|
2077
|
+
description: "Overlay mm grid on images",
|
|
2078
|
+
default: false
|
|
2079
|
+
},
|
|
2080
|
+
gridSize: {
|
|
2081
|
+
type: "string",
|
|
2082
|
+
description: "Grid spacing in mm",
|
|
2083
|
+
default: "10"
|
|
2084
|
+
},
|
|
2085
|
+
scale: {
|
|
2086
|
+
type: "string",
|
|
2087
|
+
description: "Render scale",
|
|
2088
|
+
default: "1"
|
|
2089
|
+
},
|
|
2090
|
+
imageFormat: {
|
|
2091
|
+
type: "string",
|
|
2092
|
+
description: "Image format: png | jpeg",
|
|
2093
|
+
default: "png"
|
|
2094
|
+
},
|
|
2095
|
+
pages: {
|
|
2096
|
+
type: "string",
|
|
2097
|
+
description: "Page range (e.g., 1-3, 1,3,5)"
|
|
2098
|
+
},
|
|
2099
|
+
verbose: {
|
|
2100
|
+
type: "boolean",
|
|
2101
|
+
alias: "v",
|
|
2102
|
+
description: "Verbose output",
|
|
2103
|
+
default: false
|
|
2104
|
+
},
|
|
2105
|
+
json: {
|
|
2106
|
+
type: "boolean",
|
|
2107
|
+
description: "Machine-readable JSON output (includes size info)",
|
|
2108
|
+
default: false
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
var pdf2img_default = defineCommand({
|
|
2112
|
+
meta: {
|
|
2113
|
+
name: "pdf2img",
|
|
2114
|
+
description: "Convert PDF pages to images"
|
|
2115
|
+
},
|
|
2116
|
+
args: pdf2imgArgs,
|
|
2117
|
+
async run({ args, rawArgs }) {
|
|
2118
|
+
return runWithContract({ json: Boolean(args.json) }, async () => {
|
|
2119
|
+
assertNoUnknownFlags(rawArgs, pdf2imgArgs);
|
|
2120
|
+
if (!args.file) fail("No input PDF provided.", {
|
|
2121
|
+
code: "EARG",
|
|
2122
|
+
exitCode: 1
|
|
2123
|
+
});
|
|
2124
|
+
const scale = parsePositiveNumberArg("scale", args.scale);
|
|
2125
|
+
const gridSize = parsePositiveNumberArg("gridSize", args.gridSize);
|
|
2126
|
+
const imageFormat = parseEnumArg("imageFormat", args.imageFormat, ["png", "jpeg"]);
|
|
2127
|
+
const pdfData = readPdfFile(args.file);
|
|
2128
|
+
const sizes = await pdf2size(pdfData);
|
|
2129
|
+
const pageIndices = args.pages ? parsePageRange(args.pages, sizes.length).map((page) => page - 1) : Array.from({ length: sizes.length }, (_, index) => index);
|
|
2130
|
+
const allImages = await pdf2img(pdfData, {
|
|
2131
|
+
scale,
|
|
2132
|
+
imageType: imageFormat
|
|
2133
|
+
});
|
|
2134
|
+
const inputBase = basename(args.file, extname(args.file));
|
|
2135
|
+
const ext = imageFormat === "jpeg" ? "jpg" : "png";
|
|
2136
|
+
let outputDir = ".";
|
|
2137
|
+
if (args.output) {
|
|
2138
|
+
outputDir = args.output;
|
|
2139
|
+
if (existsSync(outputDir)) {
|
|
2140
|
+
if (!statSync(outputDir).isDirectory()) fail(`Output path must be a directory for pdf2img: ${args.output}`, {
|
|
2141
|
+
code: "EIO",
|
|
2142
|
+
exitCode: 3
|
|
2143
|
+
});
|
|
2144
|
+
} else mkdirSync(outputDir, { recursive: true });
|
|
2145
|
+
}
|
|
2146
|
+
if (args.verbose) {
|
|
2147
|
+
console.error(`Input: ${args.file}`);
|
|
2148
|
+
console.error(`Pages: ${sizes.length}`);
|
|
2149
|
+
console.error(`Selected pages: ${pageIndices.map((pageIdx) => pageIdx + 1).join(", ")}`);
|
|
2150
|
+
console.error(`Output: ${outputDir}`);
|
|
2151
|
+
console.error(`Image format: ${imageFormat}`);
|
|
2152
|
+
console.error(`Scale: ${scale}`);
|
|
2153
|
+
console.error(`Grid: ${args.grid ? `enabled (${gridSize}mm)` : "disabled"}`);
|
|
2154
|
+
}
|
|
2155
|
+
const results = [];
|
|
2156
|
+
for (const pageIdx of pageIndices) {
|
|
2157
|
+
let imageData = allImages[pageIdx];
|
|
2158
|
+
const size = sizes[pageIdx] ?? {
|
|
2159
|
+
width: 210,
|
|
2160
|
+
height: 297
|
|
2161
|
+
};
|
|
2162
|
+
if (args.grid) imageData = await drawGridOnPdfImage(imageData, gridSize, size.width, size.height, imageFormat);
|
|
2163
|
+
const outputPath = join(outputDir, `${inputBase}-${pageIdx + 1}.${ext}`);
|
|
2164
|
+
writeOutput(outputPath, imageData);
|
|
2165
|
+
const paperSize = detectPaperSize(size.width, size.height);
|
|
2166
|
+
results.push({
|
|
2167
|
+
outputPath,
|
|
2168
|
+
pageNumber: pageIdx + 1,
|
|
2169
|
+
width: Math.round(size.width * 100) / 100,
|
|
2170
|
+
height: Math.round(size.height * 100) / 100
|
|
2171
|
+
});
|
|
2172
|
+
if (!args.json) {
|
|
2173
|
+
const sizeLabel = paperSize ? `, ${paperSize}` : "";
|
|
2174
|
+
console.log(`\u2713 ${outputPath} (${size.width.toFixed(0)}\u00d7${size.height.toFixed(0)}mm${sizeLabel})`);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (args.json) printJson({
|
|
2178
|
+
ok: true,
|
|
2179
|
+
command: "pdf2img",
|
|
2180
|
+
pageCount: sizes.length,
|
|
2181
|
+
selectedPageCount: results.length,
|
|
2182
|
+
outputDir,
|
|
2183
|
+
outputPaths: results.map((result) => result.outputPath),
|
|
2184
|
+
pages: results
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
});
|
|
2189
|
+
//#endregion
|
|
2190
|
+
//#region src/commands/pdf2size.ts
|
|
2191
|
+
var pdf2sizeArgs = {
|
|
2192
|
+
file: {
|
|
2193
|
+
type: "positional",
|
|
2194
|
+
description: "Input PDF file",
|
|
2195
|
+
required: false
|
|
2196
|
+
},
|
|
2197
|
+
verbose: {
|
|
2198
|
+
type: "boolean",
|
|
2199
|
+
alias: "v",
|
|
2200
|
+
description: "Verbose output",
|
|
2201
|
+
default: false
|
|
2202
|
+
},
|
|
2203
|
+
json: {
|
|
2204
|
+
type: "boolean",
|
|
2205
|
+
description: "Machine-readable JSON output",
|
|
2206
|
+
default: false
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
var pdf2size_default = defineCommand({
|
|
2210
|
+
meta: {
|
|
2211
|
+
name: "pdf2size",
|
|
2212
|
+
description: "Get page dimensions of a PDF file"
|
|
2213
|
+
},
|
|
2214
|
+
args: pdf2sizeArgs,
|
|
2215
|
+
async run({ args, rawArgs }) {
|
|
2216
|
+
return runWithContract({ json: Boolean(args.json) }, async () => {
|
|
2217
|
+
assertNoUnknownFlags(rawArgs, pdf2sizeArgs);
|
|
2218
|
+
if (!args.file) fail("No input PDF provided.", {
|
|
2219
|
+
code: "EARG",
|
|
2220
|
+
exitCode: 1
|
|
2221
|
+
});
|
|
2222
|
+
const sizes = await pdf2size(readPdfFile(args.file));
|
|
2223
|
+
if (args.verbose) {
|
|
2224
|
+
console.error(`Input: ${args.file}`);
|
|
2225
|
+
console.error(`Pages: ${sizes.length}`);
|
|
2226
|
+
}
|
|
2227
|
+
const result = sizes.map((size, index) => ({
|
|
2228
|
+
pageNumber: index + 1,
|
|
2229
|
+
width: Math.round(size.width * 100) / 100,
|
|
2230
|
+
height: Math.round(size.height * 100) / 100
|
|
2231
|
+
}));
|
|
2232
|
+
if (args.json) printJson({
|
|
2233
|
+
ok: true,
|
|
2234
|
+
command: "pdf2size",
|
|
2235
|
+
pageCount: result.length,
|
|
2236
|
+
pages: result
|
|
2237
|
+
});
|
|
2238
|
+
else for (let i = 0; i < sizes.length; i++) {
|
|
2239
|
+
const size = sizes[i];
|
|
2240
|
+
const paperSize = detectPaperSize(size.width, size.height);
|
|
2241
|
+
const sizeLabel = paperSize ? ` (${paperSize})` : "";
|
|
2242
|
+
console.log(`Page ${i + 1}: ${size.width.toFixed(0)} \u00d7 ${size.height.toFixed(0)} mm${sizeLabel}`);
|
|
2243
|
+
}
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
//#endregion
|
|
2248
|
+
//#region src/version.ts
|
|
2249
|
+
var CLI_VERSION = "0.1.0-alpha.0";
|
|
2250
|
+
//#endregion
|
|
2251
|
+
//#region src/example-templates.ts
|
|
2252
|
+
function getExamplesBaseUrl() {
|
|
2253
|
+
return process.env.PDFME_EXAMPLES_BASE_URL ?? "https://playground.pdfme.com/template-assets";
|
|
2254
|
+
}
|
|
2255
|
+
async function fetchJson(url) {
|
|
2256
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
2257
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2258
|
+
return await response.json();
|
|
2259
|
+
}
|
|
2260
|
+
async function getExampleManifest() {
|
|
2261
|
+
let lastError;
|
|
2262
|
+
for (const url of getManifestUrls()) try {
|
|
2263
|
+
return {
|
|
2264
|
+
manifest: normalizeManifest(await fetchJson(url)),
|
|
2265
|
+
source: "remote",
|
|
2266
|
+
url
|
|
2267
|
+
};
|
|
2268
|
+
} catch (error) {
|
|
2269
|
+
lastError = error;
|
|
2270
|
+
}
|
|
2271
|
+
throw new Error(`Could not load examples manifest. ${formatError(lastError)}`);
|
|
2272
|
+
}
|
|
2273
|
+
async function fetchExampleTemplateWithSource(name, options = {}) {
|
|
2274
|
+
const entry = (options.manifest ?? (await getExampleManifest()).manifest).templates.find((template) => template.name === name);
|
|
2275
|
+
if (!entry) throw new Error(`Template "${name}" is not present in the examples manifest.`);
|
|
2276
|
+
const relativePath = entry.path;
|
|
2277
|
+
const templateUrl = `${getExamplesBaseUrl().replace(/\/$/, "")}/${relativePath}`;
|
|
2278
|
+
return {
|
|
2279
|
+
template: await fetchJson(templateUrl),
|
|
2280
|
+
source: "remote",
|
|
2281
|
+
url: templateUrl
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function getManifestUrls() {
|
|
2285
|
+
const baseUrl = getExamplesBaseUrl().replace(/\/$/, "");
|
|
2286
|
+
return [`${baseUrl}/manifest.json`, `${baseUrl}/index.json`];
|
|
2287
|
+
}
|
|
2288
|
+
function normalizeManifest(raw) {
|
|
2289
|
+
if (Array.isArray(raw)) return {
|
|
2290
|
+
schemaVersion: 1,
|
|
2291
|
+
cliVersion: CLI_VERSION,
|
|
2292
|
+
templates: normalizeEntries(raw)
|
|
2293
|
+
};
|
|
2294
|
+
if (typeof raw !== "object" || raw === null) throw new Error("Examples manifest must be a JSON object or array.");
|
|
2295
|
+
const record = raw;
|
|
2296
|
+
const rawTemplates = Array.isArray(record.templates) ? record.templates : Array.isArray(record.entries) ? record.entries : void 0;
|
|
2297
|
+
if (!rawTemplates) throw new Error("Examples manifest is missing templates.");
|
|
2298
|
+
return {
|
|
2299
|
+
schemaVersion: typeof record.schemaVersion === "number" && Number.isFinite(record.schemaVersion) ? record.schemaVersion : 1,
|
|
2300
|
+
cliVersion: typeof record.cliVersion === "string" ? record.cliVersion : CLI_VERSION,
|
|
2301
|
+
templates: normalizeEntries(rawTemplates)
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
function normalizeEntries(rawTemplates) {
|
|
2305
|
+
return rawTemplates.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => {
|
|
2306
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
2307
|
+
return {
|
|
2308
|
+
name,
|
|
2309
|
+
author: typeof entry.author === "string" && entry.author.length > 0 ? entry.author : "pdfme",
|
|
2310
|
+
path: typeof entry.path === "string" && entry.path.length > 0 ? entry.path : `${name}/template.json`,
|
|
2311
|
+
thumbnailPath: typeof entry.thumbnailPath === "string" && entry.thumbnailPath.length > 0 ? entry.thumbnailPath : `${name}/thumbnail.png`,
|
|
2312
|
+
pageCount: typeof entry.pageCount === "number" && Number.isFinite(entry.pageCount) ? entry.pageCount : 0,
|
|
2313
|
+
fieldCount: typeof entry.fieldCount === "number" && Number.isFinite(entry.fieldCount) ? entry.fieldCount : 0,
|
|
2314
|
+
schemaTypes: normalizeStringArray(entry.schemaTypes),
|
|
2315
|
+
fontNames: normalizeStringArray(entry.fontNames),
|
|
2316
|
+
hasCJK: typeof entry.hasCJK === "boolean" ? entry.hasCJK : false,
|
|
2317
|
+
basePdfKind: typeof entry.basePdfKind === "string" && entry.basePdfKind.length > 0 ? entry.basePdfKind : "unknown"
|
|
2318
|
+
};
|
|
2319
|
+
}).filter((entry) => entry.name.length > 0);
|
|
2320
|
+
}
|
|
2321
|
+
function normalizeStringArray(value) {
|
|
2322
|
+
if (!Array.isArray(value)) return [];
|
|
2323
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
2324
|
+
}
|
|
2325
|
+
function formatError(error) {
|
|
2326
|
+
return error instanceof Error ? error.message : String(error);
|
|
2327
|
+
}
|
|
2328
|
+
//#endregion
|
|
2329
|
+
//#region src/example-fonts.ts
|
|
2330
|
+
var OFFICIAL_EXAMPLE_FONT_URLS = {
|
|
2331
|
+
NotoSansJP: "https://fonts.gstatic.com/s/notosansjp/v53/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEj75vY0rw-oME.ttf",
|
|
2332
|
+
NotoSerifJP: "https://fonts.gstatic.com/s/notoserifjp/v30/xn71YHs72GKoTvER4Gn3b5eMRtWGkp6o7MjQ2bwxOubAILO5wBCU.ttf",
|
|
2333
|
+
"PinyonScript-Regular": "https://fonts.gstatic.com/s/pinyonscript/v22/6xKpdSJbL9-e9LuoeQiDRQR8aOLQO4bhiDY.ttf"
|
|
2334
|
+
};
|
|
2335
|
+
function collectTemplateFontNames(template) {
|
|
2336
|
+
const schemas = template.schemas;
|
|
2337
|
+
if (!Array.isArray(schemas)) return [];
|
|
2338
|
+
const fontNames = /* @__PURE__ */ new Set();
|
|
2339
|
+
for (const page of schemas) {
|
|
2340
|
+
const pageSchemas = Array.isArray(page) ? page : typeof page === "object" && page !== null ? Object.values(page) : [];
|
|
2341
|
+
for (const schema of pageSchemas) {
|
|
2342
|
+
if (typeof schema !== "object" || schema === null) continue;
|
|
2343
|
+
const fontName = schema.fontName;
|
|
2344
|
+
if (typeof fontName === "string" && fontName.length > 0) fontNames.add(fontName);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return [...fontNames].sort();
|
|
2348
|
+
}
|
|
2349
|
+
function getOfficialExampleFonts(template) {
|
|
2350
|
+
const entries = collectTemplateFontNames(template).flatMap((fontName) => {
|
|
2351
|
+
const url = OFFICIAL_EXAMPLE_FONT_URLS[fontName];
|
|
2352
|
+
return url ? [[fontName, {
|
|
2353
|
+
data: url,
|
|
2354
|
+
fallback: false,
|
|
2355
|
+
subset: true
|
|
2356
|
+
}]] : [];
|
|
2357
|
+
});
|
|
2358
|
+
if (entries.length === 0) return;
|
|
2359
|
+
return Object.fromEntries(entries);
|
|
2360
|
+
}
|
|
2361
|
+
//#endregion
|
|
2362
|
+
//#region src/commands/examples.ts
|
|
2363
|
+
var examplesArgs = {
|
|
2364
|
+
name: {
|
|
2365
|
+
type: "positional",
|
|
2366
|
+
description: "Template name to output",
|
|
2367
|
+
required: false
|
|
2368
|
+
},
|
|
2369
|
+
list: {
|
|
2370
|
+
type: "boolean",
|
|
2371
|
+
description: "List available templates",
|
|
2372
|
+
default: false
|
|
2373
|
+
},
|
|
2374
|
+
output: {
|
|
2375
|
+
type: "string",
|
|
2376
|
+
alias: "o",
|
|
2377
|
+
description: "Output file path"
|
|
2378
|
+
},
|
|
2379
|
+
withInputs: {
|
|
2380
|
+
type: "boolean",
|
|
2381
|
+
alias: "w",
|
|
2382
|
+
description: "Output unified format with sample inputs",
|
|
2383
|
+
default: false
|
|
2384
|
+
},
|
|
2385
|
+
verbose: {
|
|
2386
|
+
type: "boolean",
|
|
2387
|
+
alias: "v",
|
|
2388
|
+
description: "Verbose output",
|
|
2389
|
+
default: false
|
|
2390
|
+
},
|
|
2391
|
+
json: {
|
|
2392
|
+
type: "boolean",
|
|
2393
|
+
description: "Machine-readable JSON output",
|
|
2394
|
+
default: false
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
function generateSampleInputs(template) {
|
|
2398
|
+
const fields = normalizeSchemaPages(template.schemas).flat();
|
|
2399
|
+
if (fields.length === 0) return [{}];
|
|
2400
|
+
const input = {};
|
|
2401
|
+
for (const schema of fields) {
|
|
2402
|
+
if (typeof schema !== "object" || schema === null) continue;
|
|
2403
|
+
const name = schema.name;
|
|
2404
|
+
const content = schema.content;
|
|
2405
|
+
if (schema.readOnly) continue;
|
|
2406
|
+
if (name) input[name] = content || `Sample ${name}`;
|
|
2407
|
+
}
|
|
2408
|
+
return [input];
|
|
2409
|
+
}
|
|
2410
|
+
var examples_default = defineCommand({
|
|
2411
|
+
meta: {
|
|
2412
|
+
name: "examples",
|
|
2413
|
+
description: "List and output example pdfme templates"
|
|
2414
|
+
},
|
|
2415
|
+
args: examplesArgs,
|
|
2416
|
+
async run({ args, rawArgs }) {
|
|
2417
|
+
return runWithContract({ json: Boolean(args.json) }, async () => {
|
|
2418
|
+
assertNoUnknownFlags(rawArgs, examplesArgs);
|
|
2419
|
+
let manifestResult;
|
|
2420
|
+
try {
|
|
2421
|
+
manifestResult = await getExampleManifest();
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
fail(`Failed to load examples manifest. ${error instanceof Error ? error.message : String(error)}`, {
|
|
2424
|
+
code: "EIO",
|
|
2425
|
+
exitCode: 3,
|
|
2426
|
+
cause: error
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
const templateEntries = manifestResult.manifest.templates;
|
|
2430
|
+
const templateNames = templateEntries.map((entry) => entry.name).filter((name) => typeof name === "string" && name.length > 0).sort();
|
|
2431
|
+
if (args.verbose) {
|
|
2432
|
+
console.error(`Base URL: ${getExamplesBaseUrl()}`);
|
|
2433
|
+
console.error(`Manifest source: ${manifestResult.source}`);
|
|
2434
|
+
if (manifestResult.url) console.error(`Manifest URL: ${manifestResult.url}`);
|
|
2435
|
+
console.error(`Templates: ${templateNames.length}`);
|
|
2436
|
+
}
|
|
2437
|
+
if (args.list || !args.name) {
|
|
2438
|
+
if (args.json) printJson({
|
|
2439
|
+
ok: true,
|
|
2440
|
+
command: "examples",
|
|
2441
|
+
mode: "list",
|
|
2442
|
+
templateCount: templateNames.length,
|
|
2443
|
+
source: manifestResult.source,
|
|
2444
|
+
baseUrl: getExamplesBaseUrl(),
|
|
2445
|
+
manifest: manifestResult.manifest
|
|
2446
|
+
});
|
|
2447
|
+
else {
|
|
2448
|
+
console.log("Available templates:");
|
|
2449
|
+
for (const name of templateNames) console.log(` ${name}`);
|
|
2450
|
+
}
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
if (!templateEntries.find((template) => template.name === args.name)) fail(`Template "${args.name}" not found. Available templates: ${templateNames.join(", ")}`, {
|
|
2454
|
+
code: "EARG",
|
|
2455
|
+
exitCode: 1
|
|
2456
|
+
});
|
|
2457
|
+
let templateResult;
|
|
2458
|
+
try {
|
|
2459
|
+
templateResult = await fetchExampleTemplateWithSource(args.name, { manifest: manifestResult.manifest });
|
|
2460
|
+
} catch (error) {
|
|
2461
|
+
fail(`Failed to load example template "${args.name}". ${error instanceof Error ? error.message : String(error)}`, {
|
|
2462
|
+
code: "EIO",
|
|
2463
|
+
exitCode: 3,
|
|
2464
|
+
cause: error
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
const output = args.withInputs ? buildExampleJob(templateResult.template) : templateResult.template;
|
|
2468
|
+
const mode = args.withInputs ? "job" : "template";
|
|
2469
|
+
const stats = countTemplateStats(templateResult.template);
|
|
2470
|
+
const inputCount = args.withInputs ? output.inputs?.length ?? 0 : void 0;
|
|
2471
|
+
if (args.verbose) {
|
|
2472
|
+
console.error(`Template: ${args.name}`);
|
|
2473
|
+
console.error(`Template source: ${templateResult.source}`);
|
|
2474
|
+
if (templateResult.url) console.error(`Template URL: ${templateResult.url}`);
|
|
2475
|
+
console.error(`Mode: ${mode}`);
|
|
2476
|
+
console.error(`Template pages: ${stats.templatePageCount}`);
|
|
2477
|
+
console.error(`Fields: ${stats.fieldCount}`);
|
|
2478
|
+
if (inputCount !== void 0) console.error(`Inputs: ${inputCount} set(s)`);
|
|
2479
|
+
console.error(`Output: ${args.output ?? "stdout"}`);
|
|
2480
|
+
}
|
|
2481
|
+
if (args.output) {
|
|
2482
|
+
writeOutput(args.output, new TextEncoder().encode(JSON.stringify(output, null, 2)));
|
|
2483
|
+
if (args.json) printJson({
|
|
2484
|
+
ok: true,
|
|
2485
|
+
command: "examples",
|
|
2486
|
+
name: args.name,
|
|
2487
|
+
mode,
|
|
2488
|
+
source: templateResult.source,
|
|
2489
|
+
templatePageCount: stats.templatePageCount,
|
|
2490
|
+
fieldCount: stats.fieldCount,
|
|
2491
|
+
...inputCount !== void 0 ? { inputCount } : {},
|
|
2492
|
+
outputPath: args.output
|
|
2493
|
+
});
|
|
2494
|
+
else {
|
|
2495
|
+
const label = args.withInputs ? "Job file" : "Template";
|
|
2496
|
+
console.log(`\u2713 ${label} written to ${args.output}`);
|
|
2497
|
+
}
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if (args.json) printJson({
|
|
2501
|
+
ok: true,
|
|
2502
|
+
command: "examples",
|
|
2503
|
+
name: args.name,
|
|
2504
|
+
source: templateResult.source,
|
|
2505
|
+
mode,
|
|
2506
|
+
templatePageCount: stats.templatePageCount,
|
|
2507
|
+
fieldCount: stats.fieldCount,
|
|
2508
|
+
...inputCount !== void 0 ? { inputCount } : {},
|
|
2509
|
+
data: output
|
|
2510
|
+
});
|
|
2511
|
+
else console.log(JSON.stringify(output, null, 2));
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
});
|
|
2515
|
+
function buildExampleJob(template) {
|
|
2516
|
+
const job = {
|
|
2517
|
+
template,
|
|
2518
|
+
inputs: generateSampleInputs(template)
|
|
2519
|
+
};
|
|
2520
|
+
const font = getOfficialExampleFonts(template);
|
|
2521
|
+
if (font) job.options = { font };
|
|
2522
|
+
return job;
|
|
2523
|
+
}
|
|
2524
|
+
function countTemplateStats(template) {
|
|
2525
|
+
const schemaPages = normalizeSchemaPages(template.schemas);
|
|
2526
|
+
return {
|
|
2527
|
+
templatePageCount: schemaPages.length,
|
|
2528
|
+
fieldCount: schemaPages.reduce((count, page) => count + page.length, 0)
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function normalizeSchemaPages(rawSchemas) {
|
|
2532
|
+
if (!Array.isArray(rawSchemas)) return [];
|
|
2533
|
+
return rawSchemas.map((page) => {
|
|
2534
|
+
if (Array.isArray(page)) return page.filter((schema) => typeof schema === "object" && schema !== null);
|
|
2535
|
+
if (typeof page === "object" && page !== null) return Object.entries(page).map(([name, schema]) => typeof schema === "object" && schema !== null ? {
|
|
2536
|
+
...schema,
|
|
2537
|
+
name: schema.name ?? name
|
|
2538
|
+
} : null).filter((schema) => schema !== null);
|
|
2539
|
+
return [];
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
//#endregion
|
|
2543
|
+
//#region src/commands/doctor.ts
|
|
2544
|
+
var doctorArgs = {
|
|
2545
|
+
target: {
|
|
2546
|
+
type: "positional",
|
|
2547
|
+
description: "Optional job/template JSON file, or \"fonts\" for font-focused diagnosis",
|
|
2548
|
+
required: false
|
|
2549
|
+
},
|
|
2550
|
+
file: {
|
|
2551
|
+
type: "positional",
|
|
2552
|
+
description: "Job/template JSON file for \"doctor fonts\", or \"-\" for stdin",
|
|
2553
|
+
required: false
|
|
2554
|
+
},
|
|
2555
|
+
verbose: {
|
|
2556
|
+
type: "boolean",
|
|
2557
|
+
alias: "v",
|
|
2558
|
+
description: "Verbose output",
|
|
2559
|
+
default: false
|
|
2560
|
+
},
|
|
2561
|
+
json: {
|
|
2562
|
+
type: "boolean",
|
|
2563
|
+
description: "Machine-readable JSON output",
|
|
2564
|
+
default: false
|
|
2565
|
+
},
|
|
2566
|
+
noAutoFont: {
|
|
2567
|
+
type: "boolean",
|
|
2568
|
+
description: "Simulate generate with automatic CJK font download disabled",
|
|
2569
|
+
default: false
|
|
2570
|
+
},
|
|
2571
|
+
output: {
|
|
2572
|
+
type: "string",
|
|
2573
|
+
alias: "o",
|
|
2574
|
+
description: "Simulate generate output PDF path for runtime/path diagnosis",
|
|
2575
|
+
default: "output.pdf"
|
|
2576
|
+
},
|
|
2577
|
+
force: {
|
|
2578
|
+
type: "boolean",
|
|
2579
|
+
description: "Simulate generate --force for implicit default output path checks",
|
|
2580
|
+
default: false
|
|
2581
|
+
},
|
|
2582
|
+
image: {
|
|
2583
|
+
type: "boolean",
|
|
2584
|
+
description: "Simulate generate --image when previewing runtime output paths",
|
|
2585
|
+
default: false
|
|
2586
|
+
},
|
|
2587
|
+
imageFormat: {
|
|
2588
|
+
type: "string",
|
|
2589
|
+
description: "Image format to use when previewing runtime output paths: png | jpeg",
|
|
2590
|
+
default: "png"
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
var doctor_default = defineCommand({
|
|
2594
|
+
meta: {
|
|
2595
|
+
name: "doctor",
|
|
2596
|
+
description: "Diagnose the local pdfme CLI environment and input readiness"
|
|
2597
|
+
},
|
|
2598
|
+
args: doctorArgs,
|
|
2599
|
+
async run({ args, rawArgs }) {
|
|
2600
|
+
return runWithContract({ json: Boolean(args.json) }, async () => {
|
|
2601
|
+
assertNoUnknownFlags(rawArgs, doctorArgs);
|
|
2602
|
+
const invocation = resolveDoctorInvocation(args);
|
|
2603
|
+
const imageFormat = parseEnumArg("imageFormat", args.imageFormat, ["png", "jpeg"]);
|
|
2604
|
+
const environment = getEnvironmentReport();
|
|
2605
|
+
if (invocation.target === "environment") {
|
|
2606
|
+
const issues = collectEnvironmentIssues(environment);
|
|
2607
|
+
const warnings = collectEnvironmentWarnings(environment);
|
|
2608
|
+
const healthy = issues.length === 0;
|
|
2609
|
+
if (args.verbose) printDoctorEnvironmentVerbose(environment, healthy, issues, warnings);
|
|
2610
|
+
if (args.json) printJson({
|
|
2611
|
+
ok: true,
|
|
2612
|
+
command: "doctor",
|
|
2613
|
+
target: "environment",
|
|
2614
|
+
healthy,
|
|
2615
|
+
environment,
|
|
2616
|
+
issues,
|
|
2617
|
+
warnings
|
|
2618
|
+
});
|
|
2619
|
+
else printEnvironmentReport(environment, issues, warnings);
|
|
2620
|
+
if (!healthy) process.exit(1);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
const source = await loadValidationSource(invocation.file, { noInputMessage: invocation.target === "fonts" ? "No font diagnostic input provided. Pass a file path or pipe JSON via stdin." : "No diagnostic input provided. Pass a file path or pipe JSON via stdin." });
|
|
2624
|
+
const diagnosis = buildInputDiagnosis(source, environment, Boolean(args.noAutoFont), {
|
|
2625
|
+
includeBasePdfIssue: invocation.target === "input",
|
|
2626
|
+
includeRuntimeIssue: invocation.target === "input",
|
|
2627
|
+
runtime: {
|
|
2628
|
+
output: args.output,
|
|
2629
|
+
force: Boolean(args.force),
|
|
2630
|
+
image: Boolean(args.image),
|
|
2631
|
+
imageFormat,
|
|
2632
|
+
rawArgs
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
if (args.verbose) printDoctorInputVerbose(invocation, source, diagnosis);
|
|
2636
|
+
const payload = invocation.target === "fonts" ? {
|
|
2637
|
+
ok: true,
|
|
2638
|
+
command: "doctor",
|
|
2639
|
+
target: "fonts",
|
|
2640
|
+
healthy: diagnosis.healthy,
|
|
2641
|
+
mode: source.mode,
|
|
2642
|
+
templatePageCount: diagnosis.validation.pages,
|
|
2643
|
+
fieldCount: diagnosis.validation.fields,
|
|
2644
|
+
...source.mode === "job" ? { inputCount: source.inputs?.length ?? 0 } : {},
|
|
2645
|
+
environment,
|
|
2646
|
+
validation: createValidationPayload(diagnosis.validation),
|
|
2647
|
+
inspection: {
|
|
2648
|
+
schemaTypes: diagnosis.inspection.schemaTypes,
|
|
2649
|
+
requiredPlugins: diagnosis.inspection.requiredPlugins,
|
|
2650
|
+
requiredFonts: diagnosis.inspection.requiredFonts
|
|
2651
|
+
},
|
|
2652
|
+
inputHints: collectInputHints(source.template),
|
|
2653
|
+
diagnosis: { fonts: createFontPayload(diagnosis.fontDiagnosis) },
|
|
2654
|
+
issues: diagnosis.issues,
|
|
2655
|
+
warnings: diagnosis.warnings
|
|
2656
|
+
} : {
|
|
2657
|
+
ok: true,
|
|
2658
|
+
command: "doctor",
|
|
2659
|
+
target: "input",
|
|
2660
|
+
healthy: diagnosis.healthy,
|
|
2661
|
+
mode: source.mode,
|
|
2662
|
+
templatePageCount: diagnosis.validation.pages,
|
|
2663
|
+
fieldCount: diagnosis.validation.fields,
|
|
2664
|
+
...source.mode === "job" ? { inputCount: source.inputs?.length ?? 0 } : {},
|
|
2665
|
+
estimatedPageCount: diagnosis.runtimeDiagnosis.estimatedPages,
|
|
2666
|
+
environment,
|
|
2667
|
+
validation: createValidationPayload(diagnosis.validation),
|
|
2668
|
+
inspection: diagnosis.inspection,
|
|
2669
|
+
inputHints: collectInputHints(source.template),
|
|
2670
|
+
diagnosis: {
|
|
2671
|
+
basePdf: diagnosis.basePdfDiagnosis,
|
|
2672
|
+
fonts: createFontPayload(diagnosis.fontDiagnosis),
|
|
2673
|
+
runtime: diagnosis.runtimeDiagnosis,
|
|
2674
|
+
plugins: {
|
|
2675
|
+
required: diagnosis.inspection.requiredPlugins,
|
|
2676
|
+
unsupportedSchemaTypes: diagnosis.inspection.schemaTypes.filter((type) => !diagnosis.inspection.requiredPlugins.includes(type))
|
|
2677
|
+
}
|
|
2678
|
+
},
|
|
2679
|
+
issues: diagnosis.issues,
|
|
2680
|
+
warnings: diagnosis.warnings
|
|
2681
|
+
};
|
|
2682
|
+
if (args.json) printJson(payload);
|
|
2683
|
+
else if (invocation.target === "fonts") printFontReport(payload);
|
|
2684
|
+
else printInputReport(payload);
|
|
2685
|
+
if (!diagnosis.healthy) process.exit(1);
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
function resolveDoctorInvocation(args) {
|
|
2690
|
+
const positionals = Array.isArray(args._) ? args._ : [];
|
|
2691
|
+
if (args.target === "fonts") {
|
|
2692
|
+
if (positionals.length > 2) fail(`Unexpected extra positional argument: ${JSON.stringify(positionals[2])}. Usage: pdfme doctor fonts <job-or-template>.`, {
|
|
2693
|
+
code: "EARG",
|
|
2694
|
+
exitCode: 1
|
|
2695
|
+
});
|
|
2696
|
+
return {
|
|
2697
|
+
target: "fonts",
|
|
2698
|
+
file: args.file
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
if (positionals.length > 1) fail(`Unexpected extra positional argument: ${JSON.stringify(positionals[1])}. Usage: pdfme doctor [job-or-template].`, {
|
|
2702
|
+
code: "EARG",
|
|
2703
|
+
exitCode: 1
|
|
2704
|
+
});
|
|
2705
|
+
if (args.target) return {
|
|
2706
|
+
target: "input",
|
|
2707
|
+
file: args.target
|
|
2708
|
+
};
|
|
2709
|
+
return { target: "environment" };
|
|
2710
|
+
}
|
|
2711
|
+
function buildInputDiagnosis(source, environment, noAutoFont, options) {
|
|
2712
|
+
const inspection = inspectTemplate(source.template, source.templateDir);
|
|
2713
|
+
const validation = validateTemplate(source.template);
|
|
2714
|
+
validation.warnings.push(...source.jobWarnings);
|
|
2715
|
+
const templateUnknownKeys = Object.keys(source.template).filter((key) => !KNOWN_TEMPLATE_KEYS.has(key)).sort();
|
|
2716
|
+
if (templateUnknownKeys.length > 0) validation.warnings.push(`Unknown template top-level field(s): ${templateUnknownKeys.join(", ")}`);
|
|
2717
|
+
const fontDiagnosis = diagnoseFonts(source, environment, noAutoFont, inspection.requiredFonts);
|
|
2718
|
+
const basePdfDiagnosis = diagnoseBasePdf(source.template.basePdf, source.templateDir);
|
|
2719
|
+
const runtimeDiagnosis = diagnoseRuntime(source, options.runtime);
|
|
2720
|
+
const issues = [...validation.errors];
|
|
2721
|
+
const warnings = [...validation.warnings, ...fontDiagnosis.warnings];
|
|
2722
|
+
if (options.includeBasePdfIssue && basePdfDiagnosis.issue) issues.push(basePdfDiagnosis.issue);
|
|
2723
|
+
issues.push(...fontDiagnosis.issues);
|
|
2724
|
+
if (options.includeRuntimeIssue && runtimeDiagnosis.output.issue) issues.push(runtimeDiagnosis.output.issue);
|
|
2725
|
+
if (source.mode === "job" && fontDiagnosis.effectiveOptions) {
|
|
2726
|
+
try {
|
|
2727
|
+
checkGenerateProps({
|
|
2728
|
+
template: source.template,
|
|
2729
|
+
inputs: source.inputs,
|
|
2730
|
+
options: fontDiagnosis.effectiveOptions
|
|
2731
|
+
});
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
issues.unshift(error instanceof Error ? error.message : String(error));
|
|
2734
|
+
}
|
|
2735
|
+
issues.push(...getInputContractIssues(source.template, source.inputs ?? []));
|
|
2736
|
+
}
|
|
2737
|
+
return {
|
|
2738
|
+
validation: {
|
|
2739
|
+
valid: validation.errors.length === 0,
|
|
2740
|
+
pages: validation.pages,
|
|
2741
|
+
fields: validation.fields,
|
|
2742
|
+
errors: validation.errors,
|
|
2743
|
+
warnings: validation.warnings
|
|
2744
|
+
},
|
|
2745
|
+
inspection,
|
|
2746
|
+
basePdfDiagnosis,
|
|
2747
|
+
fontDiagnosis,
|
|
2748
|
+
runtimeDiagnosis,
|
|
2749
|
+
issues,
|
|
2750
|
+
warnings,
|
|
2751
|
+
healthy: issues.length === 0
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function createValidationPayload(validation) {
|
|
2755
|
+
return {
|
|
2756
|
+
valid: validation.valid,
|
|
2757
|
+
errors: validation.errors,
|
|
2758
|
+
warnings: validation.warnings
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
function createFontPayload(fontDiagnosis) {
|
|
2762
|
+
return {
|
|
2763
|
+
hasCJK: fontDiagnosis.hasCJK,
|
|
2764
|
+
requiredFonts: fontDiagnosis.requiredFonts,
|
|
2765
|
+
explicitFonts: fontDiagnosis.explicitFonts,
|
|
2766
|
+
effectiveFonts: fontDiagnosis.effectiveFonts,
|
|
2767
|
+
missingFonts: fontDiagnosis.missingFonts,
|
|
2768
|
+
explicitSources: fontDiagnosis.explicitSources,
|
|
2769
|
+
implicitSources: fontDiagnosis.implicitSources,
|
|
2770
|
+
autoNotoSansJP: {
|
|
2771
|
+
needed: fontDiagnosis.autoFontNeeded,
|
|
2772
|
+
cached: fontDiagnosis.autoFontCached,
|
|
2773
|
+
cacheFile: NOTO_CACHE_FILE
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function getEnvironmentReport() {
|
|
2778
|
+
const fontCacheDir = dirname(NOTO_CACHE_FILE);
|
|
2779
|
+
const cwdStatus = getWritableStatus(process.cwd());
|
|
2780
|
+
const tempStatus = getWritableStatus(tmpdir());
|
|
2781
|
+
const fontCacheStatus = getWritableStatus(fontCacheDir);
|
|
2782
|
+
return {
|
|
2783
|
+
nodeVersion: process.version,
|
|
2784
|
+
cliVersion: CLI_VERSION,
|
|
2785
|
+
platform: process.platform,
|
|
2786
|
+
arch: process.arch,
|
|
2787
|
+
cwd: cwdStatus,
|
|
2788
|
+
tempDir: tempStatus,
|
|
2789
|
+
homeDir: homedir(),
|
|
2790
|
+
fontCache: {
|
|
2791
|
+
file: NOTO_CACHE_FILE,
|
|
2792
|
+
dir: fontCacheDir,
|
|
2793
|
+
cached: existsSync(NOTO_CACHE_FILE),
|
|
2794
|
+
writable: fontCacheStatus.writable,
|
|
2795
|
+
checkedPath: fontCacheStatus.checkedPath,
|
|
2796
|
+
error: fontCacheStatus.error
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
function getWritableStatus(path) {
|
|
2801
|
+
const checkedPath = findExistingParent(path);
|
|
2802
|
+
try {
|
|
2803
|
+
accessSync(checkedPath, constants.W_OK);
|
|
2804
|
+
return {
|
|
2805
|
+
path,
|
|
2806
|
+
writable: true,
|
|
2807
|
+
checkedPath: checkedPath !== path ? checkedPath : void 0
|
|
2808
|
+
};
|
|
2809
|
+
} catch (error) {
|
|
2810
|
+
return {
|
|
2811
|
+
path,
|
|
2812
|
+
writable: false,
|
|
2813
|
+
checkedPath: checkedPath !== path ? checkedPath : void 0,
|
|
2814
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
function findExistingParent(path) {
|
|
2819
|
+
let current = path;
|
|
2820
|
+
while (!existsSync(current)) {
|
|
2821
|
+
const parent = dirname(current);
|
|
2822
|
+
if (parent === current) break;
|
|
2823
|
+
current = parent;
|
|
2824
|
+
}
|
|
2825
|
+
return current;
|
|
2826
|
+
}
|
|
2827
|
+
function collectEnvironmentIssues(environment) {
|
|
2828
|
+
const issues = [];
|
|
2829
|
+
if (!environment.cwd.writable) issues.push(`Current working directory is not writable: ${environment.cwd.path}`);
|
|
2830
|
+
if (!environment.tempDir.writable) issues.push(`Temporary directory is not writable: ${environment.tempDir.path}`);
|
|
2831
|
+
return issues;
|
|
2832
|
+
}
|
|
2833
|
+
function collectEnvironmentWarnings(environment) {
|
|
2834
|
+
const warnings = [];
|
|
2835
|
+
if (!environment.fontCache.writable) warnings.push(`Font cache directory is not writable: ${environment.fontCache.dir}`);
|
|
2836
|
+
if (!environment.fontCache.cached) warnings.push(`NotoSansJP is not cached at ${environment.fontCache.file}`);
|
|
2837
|
+
return warnings;
|
|
2838
|
+
}
|
|
2839
|
+
function diagnoseBasePdf(basePdf, templateDir) {
|
|
2840
|
+
const summary = summarizeBasePdf(basePdf, templateDir);
|
|
2841
|
+
if (summary.kind !== "pdfPath" || !summary.resolvedPath) return summary;
|
|
2842
|
+
const exists = existsSync(summary.resolvedPath);
|
|
2843
|
+
return {
|
|
2844
|
+
...summary,
|
|
2845
|
+
exists,
|
|
2846
|
+
issue: exists ? void 0 : `Base PDF file not found: ${summary.resolvedPath}`
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
function diagnoseRuntime(source, options) {
|
|
2850
|
+
const estimatedPages = source.mode === "job" ? (source.inputs?.length ?? 0) * getTemplatePageCount(source.template) : getTemplatePageCount(source.template);
|
|
2851
|
+
const output = diagnoseOutputPath(options);
|
|
2852
|
+
return {
|
|
2853
|
+
estimatedPages,
|
|
2854
|
+
output,
|
|
2855
|
+
imageOutputs: {
|
|
2856
|
+
enabled: options.image,
|
|
2857
|
+
format: options.imageFormat,
|
|
2858
|
+
paths: options.image ? getImageOutputPaths(options.output, estimatedPages, options.imageFormat) : [],
|
|
2859
|
+
directory: dirname(output.resolvedPath)
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
function diagnoseOutputPath(options) {
|
|
2864
|
+
const inspection = inspectWriteTarget(options.output);
|
|
2865
|
+
const implicitDefaultIssue = getSafeDefaultOutputPathIssue({
|
|
2866
|
+
filePath: options.output,
|
|
2867
|
+
rawArgs: options.rawArgs,
|
|
2868
|
+
optionName: "output",
|
|
2869
|
+
optionAlias: "o",
|
|
2870
|
+
defaultValue: "output.pdf",
|
|
2871
|
+
force: options.force
|
|
2872
|
+
});
|
|
2873
|
+
let issue = implicitDefaultIssue;
|
|
2874
|
+
if (!issue && inspection.exists && inspection.existingType === "directory") issue = `Output path points to a directory: ${inspection.resolvedPath}. Choose a file path like out.pdf.`;
|
|
2875
|
+
else if (!issue && inspection.exists && inspection.existingType === "other") issue = `Output path is not a regular file: ${inspection.resolvedPath}.`;
|
|
2876
|
+
else if (!issue && inspection.checkedType && inspection.checkedType !== "directory" && inspection.existingType !== "file") issue = `Output directory cannot be created because an existing path segment is not a directory: ${inspection.checkedPath ?? inspection.parentDir}.`;
|
|
2877
|
+
else if (!issue && !inspection.writable) issue = inspection.exists && inspection.existingType === "file" ? `Output file is not writable: ${inspection.resolvedPath}.` : `Output directory is not writable for ${inspection.resolvedPath}: ${inspection.checkedPath ?? inspection.parentDir}.`;
|
|
2878
|
+
return {
|
|
2879
|
+
...inspection,
|
|
2880
|
+
implicitDefaultProtected: Boolean(implicitDefaultIssue),
|
|
2881
|
+
issue
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
function getTemplatePageCount(template) {
|
|
2885
|
+
return Array.isArray(template.schemas) ? template.schemas.length : 0;
|
|
2886
|
+
}
|
|
2887
|
+
function diagnoseFonts(source, environment, noAutoFont, requiredFonts) {
|
|
2888
|
+
const hasCJK = detectCJKInTemplate(source.template) || detectCJKInInputs(source.inputs ?? []);
|
|
2889
|
+
const issues = [];
|
|
2890
|
+
const warnings = [];
|
|
2891
|
+
const autoFontCached = existsSync(NOTO_CACHE_FILE);
|
|
2892
|
+
const explicit = normalizeExplicitFontConfig(source.options, source.templateDir);
|
|
2893
|
+
issues.push(...explicit.issues);
|
|
2894
|
+
warnings.push(...explicit.warnings);
|
|
2895
|
+
const autoFontNeeded = hasCJK && explicit.fontNames.length === 0;
|
|
2896
|
+
if (autoFontNeeded && noAutoFont) issues.push("CJK text detected, but automatic NotoSansJP download is disabled by --noAutoFont and no explicit font source was provided.");
|
|
2897
|
+
else if (autoFontNeeded && !autoFontCached && !environment.fontCache.writable) issues.push(`CJK text detected and NotoSansJP is not cached at ${NOTO_CACHE_FILE}, but the font cache directory is not writable: ${environment.fontCache.dir}. Provide --font / options.font or warm the cache in a writable HOME.`);
|
|
2898
|
+
else if (autoFontNeeded && !autoFontCached) warnings.push(`CJK text detected and NotoSansJP is not cached at ${NOTO_CACHE_FILE}. Generate will require network access to fetch it.`);
|
|
2899
|
+
const resolvedFont = autoFontNeeded ? {
|
|
2900
|
+
NotoSansJP: {
|
|
2901
|
+
data: new Uint8Array(),
|
|
2902
|
+
fallback: true,
|
|
2903
|
+
subset: true
|
|
2904
|
+
},
|
|
2905
|
+
[DEFAULT_FONT_NAME]: {
|
|
2906
|
+
data: new Uint8Array(),
|
|
2907
|
+
fallback: false,
|
|
2908
|
+
subset: true
|
|
2909
|
+
}
|
|
2910
|
+
} : { [DEFAULT_FONT_NAME]: {
|
|
2911
|
+
data: new Uint8Array(),
|
|
2912
|
+
fallback: true,
|
|
2913
|
+
subset: true
|
|
2914
|
+
} };
|
|
2915
|
+
const effectiveFont = explicit.fontRecord ? {
|
|
2916
|
+
...explicit.fontRecord,
|
|
2917
|
+
...resolvedFont
|
|
2918
|
+
} : resolvedFont;
|
|
2919
|
+
const effectiveFonts = Object.keys(effectiveFont).sort();
|
|
2920
|
+
const missingFonts = requiredFonts.filter((fontName) => !effectiveFonts.includes(fontName));
|
|
2921
|
+
if (source.mode === "template" && missingFonts.length > 0) issues.push(`Template references font(s) that are not available by default: ${missingFonts.join(", ")}. Provide them via generate --font or unified job options.font.`);
|
|
2922
|
+
return {
|
|
2923
|
+
hasCJK,
|
|
2924
|
+
requiredFonts,
|
|
2925
|
+
explicitFonts: explicit.fontNames,
|
|
2926
|
+
effectiveFonts,
|
|
2927
|
+
missingFonts,
|
|
2928
|
+
autoFontNeeded,
|
|
2929
|
+
autoFontCached,
|
|
2930
|
+
issues,
|
|
2931
|
+
warnings,
|
|
2932
|
+
explicitSources: explicit.sources,
|
|
2933
|
+
implicitSources: buildImplicitFontSources(autoFontNeeded, autoFontCached),
|
|
2934
|
+
effectiveOptions: explicit.optionsRecord ? {
|
|
2935
|
+
...explicit.optionsRecord,
|
|
2936
|
+
font: effectiveFont
|
|
2937
|
+
} : { font: effectiveFont }
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
function normalizeExplicitFontConfig(options, templateDir) {
|
|
2941
|
+
if (options === void 0) return {
|
|
2942
|
+
issues: [],
|
|
2943
|
+
warnings: [],
|
|
2944
|
+
fontNames: [],
|
|
2945
|
+
sources: []
|
|
2946
|
+
};
|
|
2947
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) return {
|
|
2948
|
+
issues: ["Unified job options must be a JSON object."],
|
|
2949
|
+
warnings: [],
|
|
2950
|
+
fontNames: [],
|
|
2951
|
+
sources: []
|
|
2952
|
+
};
|
|
2953
|
+
const optionsRecord = options;
|
|
2954
|
+
const font = optionsRecord.font;
|
|
2955
|
+
if (font === void 0) return {
|
|
2956
|
+
issues: [],
|
|
2957
|
+
warnings: [],
|
|
2958
|
+
fontNames: [],
|
|
2959
|
+
optionsRecord,
|
|
2960
|
+
sources: []
|
|
2961
|
+
};
|
|
2962
|
+
if (typeof font !== "object" || font === null || Array.isArray(font)) return {
|
|
2963
|
+
issues: ["Unified job options.font must be an object."],
|
|
2964
|
+
warnings: [],
|
|
2965
|
+
fontNames: [],
|
|
2966
|
+
optionsRecord,
|
|
2967
|
+
sources: []
|
|
2968
|
+
};
|
|
2969
|
+
const fontRecord = font;
|
|
2970
|
+
const sourceDiagnostics = analyzeExplicitFontRecord(fontRecord, templateDir);
|
|
2971
|
+
return {
|
|
2972
|
+
issues: sourceDiagnostics.issues,
|
|
2973
|
+
warnings: sourceDiagnostics.warnings,
|
|
2974
|
+
fontNames: Object.keys(fontRecord).sort(),
|
|
2975
|
+
fontRecord,
|
|
2976
|
+
optionsRecord,
|
|
2977
|
+
sources: sortFontSources(sourceDiagnostics.sources.map(toDoctorExplicitSource))
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
function toDoctorExplicitSource(source) {
|
|
2981
|
+
return {
|
|
2982
|
+
...source,
|
|
2983
|
+
source: "explicit"
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
function buildImplicitFontSources(autoFontNeeded, autoFontCached) {
|
|
2987
|
+
const sources = [{
|
|
2988
|
+
fontName: DEFAULT_FONT_NAME,
|
|
2989
|
+
source: "implicit",
|
|
2990
|
+
kind: "default",
|
|
2991
|
+
formatHint: "ttf",
|
|
2992
|
+
supportedFormat: true,
|
|
2993
|
+
needsNetwork: false
|
|
2994
|
+
}];
|
|
2995
|
+
if (autoFontNeeded) sources.push({
|
|
2996
|
+
fontName: "NotoSansJP",
|
|
2997
|
+
source: "implicit",
|
|
2998
|
+
kind: autoFontCached ? "autoCache" : "autoDownload",
|
|
2999
|
+
path: NOTO_CACHE_FILE,
|
|
3000
|
+
resolvedPath: NOTO_CACHE_FILE,
|
|
3001
|
+
exists: autoFontCached,
|
|
3002
|
+
formatHint: "ttf",
|
|
3003
|
+
supportedFormat: true,
|
|
3004
|
+
needsNetwork: !autoFontCached
|
|
3005
|
+
});
|
|
3006
|
+
return sortFontSources(sources);
|
|
3007
|
+
}
|
|
3008
|
+
function sortFontSources(sources) {
|
|
3009
|
+
return [...sources].sort((a, b) => a.fontName.localeCompare(b.fontName) || a.kind.localeCompare(b.kind));
|
|
3010
|
+
}
|
|
3011
|
+
function printEnvironmentReport(environment, issues, warnings) {
|
|
3012
|
+
const header = issues.length === 0 ? "✓ Environment looks ready" : "✗ Environment has blocking issues";
|
|
3013
|
+
console.log(header);
|
|
3014
|
+
console.log(`Node: ${environment.nodeVersion}`);
|
|
3015
|
+
console.log(`CLI: ${environment.cliVersion}`);
|
|
3016
|
+
console.log(`Platform: ${environment.platform} ${environment.arch}`);
|
|
3017
|
+
console.log(`cwd: ${environment.cwd.path} (${environment.cwd.writable ? "writable" : "not writable"})`);
|
|
3018
|
+
console.log(`temp: ${environment.tempDir.path} (${environment.tempDir.writable ? "writable" : "not writable"})`);
|
|
3019
|
+
console.log(`font cache: ${environment.fontCache.file} (${environment.fontCache.cached ? "cached" : "not cached"})`);
|
|
3020
|
+
for (const issue of issues) console.log(`\u2717 Issue: ${issue}`);
|
|
3021
|
+
for (const warning of warnings) console.log(`\u26a0 Warning: ${warning}`);
|
|
3022
|
+
}
|
|
3023
|
+
function printDoctorEnvironmentVerbose(environment, healthy, issues, warnings) {
|
|
3024
|
+
console.error("Target: environment");
|
|
3025
|
+
console.error(`Healthy: ${healthy ? "yes" : "no"}`);
|
|
3026
|
+
console.error(`Node: ${environment.nodeVersion}`);
|
|
3027
|
+
console.error(`CLI: ${environment.cliVersion}`);
|
|
3028
|
+
console.error(`Platform: ${environment.platform} ${environment.arch}`);
|
|
3029
|
+
console.error(`cwd: ${environment.cwd.path}`);
|
|
3030
|
+
console.error(`temp: ${environment.tempDir.path}`);
|
|
3031
|
+
console.error(`Font cache: ${environment.fontCache.cached ? "cached" : "not cached"} (${environment.fontCache.file})`);
|
|
3032
|
+
console.error(`Issues: ${issues.length}`);
|
|
3033
|
+
console.error(`Warnings: ${warnings.length}`);
|
|
3034
|
+
}
|
|
3035
|
+
function printInputReport(payload) {
|
|
3036
|
+
const healthy = Boolean(payload.healthy);
|
|
3037
|
+
const inspection = payload.inspection;
|
|
3038
|
+
const diagnosis = payload.diagnosis;
|
|
3039
|
+
const issues = payload.issues;
|
|
3040
|
+
const warnings = payload.warnings;
|
|
3041
|
+
console.log(healthy ? "✓ Doctor checks passed" : "✗ Doctor found blocking issues");
|
|
3042
|
+
console.log(`Mode: ${payload.mode}`);
|
|
3043
|
+
console.log(`Template pages: ${payload.templatePageCount}`);
|
|
3044
|
+
console.log(`Fields: ${payload.fieldCount}`);
|
|
3045
|
+
console.log(`Schema types: ${inspection.schemaTypes.join(", ") || "(none)"}`);
|
|
3046
|
+
if (diagnosis.runtime) {
|
|
3047
|
+
console.log(`Output: ${diagnosis.runtime.output.path} (${diagnosis.runtime.output.writable ? "writable" : "not writable"})`);
|
|
3048
|
+
if (diagnosis.runtime.imageOutputs.enabled) console.log(`Images: ${diagnosis.runtime.imageOutputs.paths.length} ${diagnosis.runtime.imageOutputs.format.toUpperCase()} file(s) in ${diagnosis.runtime.imageOutputs.directory}`);
|
|
3049
|
+
}
|
|
3050
|
+
for (const issue of issues) console.log(`\u2717 Issue: ${issue}`);
|
|
3051
|
+
for (const warning of warnings) console.log(`\u26a0 Warning: ${warning}`);
|
|
3052
|
+
}
|
|
3053
|
+
function printDoctorInputVerbose(invocation, source, diagnosis) {
|
|
3054
|
+
console.error(`Target: ${invocation.target}`);
|
|
3055
|
+
console.error(`Input: ${describeDoctorInput(invocation.file)}`);
|
|
3056
|
+
console.error(`Mode: ${source.mode}`);
|
|
3057
|
+
console.error(`Template pages: ${diagnosis.validation.pages}`);
|
|
3058
|
+
console.error(`Fields: ${diagnosis.validation.fields}`);
|
|
3059
|
+
if (source.mode === "job") console.error(`Inputs: ${source.inputs?.length ?? 0} set(s)`);
|
|
3060
|
+
if (invocation.target === "input") {
|
|
3061
|
+
console.error(`Estimated pages: ${diagnosis.runtimeDiagnosis.estimatedPages}`);
|
|
3062
|
+
console.error(`Output: ${diagnosis.runtimeDiagnosis.output.path}`);
|
|
3063
|
+
console.error(`Images: ${diagnosis.runtimeDiagnosis.imageOutputs.enabled ? `enabled (${diagnosis.runtimeDiagnosis.imageOutputs.format}, ${diagnosis.runtimeDiagnosis.imageOutputs.paths.length} file(s))` : "disabled"}`);
|
|
3064
|
+
}
|
|
3065
|
+
console.error(`Healthy: ${diagnosis.healthy ? "yes" : "no"}`);
|
|
3066
|
+
console.error(`Issues: ${diagnosis.issues.length}`);
|
|
3067
|
+
console.error(`Warnings: ${diagnosis.warnings.length}`);
|
|
3068
|
+
}
|
|
3069
|
+
function printFontReport(payload) {
|
|
3070
|
+
const healthy = Boolean(payload.healthy);
|
|
3071
|
+
const diagnosis = payload.diagnosis;
|
|
3072
|
+
const issues = payload.issues;
|
|
3073
|
+
const warnings = payload.warnings;
|
|
3074
|
+
console.log(healthy ? "✓ Font checks passed" : "✗ Font checks found blocking issues");
|
|
3075
|
+
console.log(`Mode: ${payload.mode}`);
|
|
3076
|
+
console.log(`Required fonts: ${diagnosis.fonts.requiredFonts.join(", ") || "(none)"}`);
|
|
3077
|
+
console.log(`Explicit fonts: ${diagnosis.fonts.explicitFonts.join(", ") || "(none)"}`);
|
|
3078
|
+
console.log(`Effective fonts: ${diagnosis.fonts.effectiveFonts.join(", ") || "(none)"}`);
|
|
3079
|
+
for (const source of diagnosis.fonts.explicitSources) console.log(`- explicit ${source.fontName}: ${source.kind}${source.path ? ` (${source.path})` : source.url ? ` (${source.url})` : ""}`);
|
|
3080
|
+
for (const source of diagnosis.fonts.implicitSources) console.log(`- implicit ${source.fontName}: ${source.kind}${source.path ? ` (${source.path})` : ""}`);
|
|
3081
|
+
for (const issue of issues) console.log(`\u2717 Issue: ${issue}`);
|
|
3082
|
+
for (const warning of warnings) console.log(`\u26a0 Warning: ${warning}`);
|
|
3083
|
+
}
|
|
3084
|
+
function describeDoctorInput(file) {
|
|
3085
|
+
if (!file || file === "-") return "stdin";
|
|
3086
|
+
return file;
|
|
3087
|
+
}
|
|
3088
|
+
//#endregion
|
|
3089
|
+
//#region src/index.ts
|
|
3090
|
+
runMain(defineCommand({
|
|
3091
|
+
meta: {
|
|
3092
|
+
name: "pdfme",
|
|
3093
|
+
version: CLI_VERSION,
|
|
3094
|
+
description: "CLI tool for pdfme - generate PDFs, convert images, validate templates"
|
|
3095
|
+
},
|
|
3096
|
+
subCommands: {
|
|
3097
|
+
generate: generate_default,
|
|
3098
|
+
validate: validate_default,
|
|
3099
|
+
pdf2img: pdf2img_default,
|
|
3100
|
+
pdf2size: pdf2size_default,
|
|
3101
|
+
examples: examples_default,
|
|
3102
|
+
doctor: doctor_default
|
|
3103
|
+
}
|
|
3104
|
+
}));
|
|
3105
|
+
//#endregion
|
|
3106
|
+
|
|
3107
|
+
//# sourceMappingURL=index.js.map
|