improves block functionality
adds cli compatible prompts/logs adds logfile function for debug adds multiselect support new fieldRename adds listFieldNames
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import { loadPdfForm } from "../util/saveLoadPdf.ts";
|
||||
import { callWithArgPrompt } from "util/call.ts";
|
||||
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import {
|
||||
PDFAcroField,
|
||||
PDFHexString,
|
||||
PDFName,
|
||||
PDFString,
|
||||
toHexString,
|
||||
} from "pdf-lib";
|
||||
import { loadPdfForm, savePdf } from "util/saveLoadPdf.ts";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import { call, callWithArgPrompt } from "util/call.ts";
|
||||
import { type PDFAcroField, type PDFField, PDFName, PDFString } from "pdf-lib";
|
||||
import { loadPdf, loadPdfForm, savePdf } from "util/saveLoadPdf.ts";
|
||||
import { callWithArgPrompt } from "util/call.ts";
|
||||
import { TerminalBlock } from "../cli/TerminalLayout.ts";
|
||||
import { forceArgs } from "../cli/forceArgs.ts";
|
||||
import { colorize } from "../cli/style.ts";
|
||||
import { cliLog, cliPrompt } from "../cli/prompts.ts";
|
||||
import { multiSelectMenuInteractive } from "../cli/selectMenu.ts";
|
||||
|
||||
// const thing = PDFAcroField.prototype.getFullyQualifiedName;
|
||||
// PDFAcroField.prototype.getFullyQualifiedName = function () {
|
||||
@@ -80,18 +78,108 @@ async function renameFields(
|
||||
}
|
||||
}
|
||||
|
||||
function applyRename(
|
||||
field: PDFField,
|
||||
name: string,
|
||||
pattern: RegExp,
|
||||
change: string,
|
||||
) {
|
||||
const segments = name.split(".");
|
||||
const matchingSegments = segments.filter((s) => pattern.test(s));
|
||||
let cField: PDFAcroField | undefined = field.acroField;
|
||||
while (cField) {
|
||||
if (
|
||||
cField.getPartialName() &&
|
||||
matchingSegments.includes(cField.getPartialName()!)
|
||||
) {
|
||||
const mName = cField.getPartialName()?.replace(pattern, change);
|
||||
if (mName) {
|
||||
cField.dict.set(PDFName.of("T"), PDFString.of(mName));
|
||||
// console.log(cField.getPartialName())
|
||||
}
|
||||
}
|
||||
cField = cField.getParent();
|
||||
// console.log(cField?.getPartialName())
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateChange(change: string, match: RegExpExecArray) {
|
||||
return change.replace(
|
||||
/\$(\d+)(i?)/g,
|
||||
(_, i, indexed) =>
|
||||
indexed
|
||||
? (parseInt(match[i]) ? (parseInt(match[i]) - 1).toString() : match[i])
|
||||
: match[i],
|
||||
);
|
||||
}
|
||||
|
||||
class RenameFields implements ITool {
|
||||
name = "renamefields";
|
||||
description = "Renames fields in a PDF form";
|
||||
help() {
|
||||
console.log("Usage: renamefields <pdfPath> <pattern> <change>");
|
||||
block: TerminalBlock | undefined;
|
||||
|
||||
setBlock(block: TerminalBlock) {
|
||||
this.block = block;
|
||||
}
|
||||
|
||||
help(standalone = false) {
|
||||
cliLog(
|
||||
"Usage: renamefields <pdfPath> <pattern> <change>",
|
||||
standalone ? undefined : this.block,
|
||||
);
|
||||
}
|
||||
async run(pdfPath: string = "", pattern: string = "", change: string = "") {
|
||||
await callWithArgPrompt(renameFields, [
|
||||
["Please provide path to PDF:", (p) => !!p && p.endsWith(".pdf")],
|
||||
"Please provide search string:",
|
||||
"Please provide requested change:",
|
||||
], [pdfPath, pattern, change]);
|
||||
if (!this.block) {
|
||||
this.block = new TerminalBlock();
|
||||
}
|
||||
this.block.setPreserveHistory(true);
|
||||
|
||||
[pdfPath, pattern, change] = await forceArgs(
|
||||
[pdfPath, pattern, change],
|
||||
[
|
||||
["Please provide path to PDF:", (p) => !!p && p.endsWith(".pdf")],
|
||||
"Please provide search string:",
|
||||
"Please provide requested change:",
|
||||
],
|
||||
this.block,
|
||||
);
|
||||
|
||||
const patternRegex = new RegExp(pattern);
|
||||
|
||||
const pdf = await loadPdf(pdfPath);
|
||||
const form = pdf.getForm();
|
||||
const fields = form.getFields();
|
||||
|
||||
const foundUpdates: [string, callback][] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
const name = field.getName();
|
||||
const match = patternRegex.exec(name);
|
||||
if (match) {
|
||||
const toChange = evaluateChange(change, match);
|
||||
foundUpdates.push([
|
||||
`${colorize(name, "red")} -> ${colorize(toChange, "green")}`,
|
||||
() => {
|
||||
applyRename(field, name, patternRegex, toChange);
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundUpdates.length) {
|
||||
cliLog("Found updates:", this.block);
|
||||
await multiSelectMenuInteractive(
|
||||
"Please select an option to apply",
|
||||
foundUpdates,
|
||||
{ terminalBlock: this.block },
|
||||
);
|
||||
}
|
||||
|
||||
const path = await cliPrompt(
|
||||
"Save to path (or hit enter to keep current):",
|
||||
this.block,
|
||||
);
|
||||
await savePdf(pdf, path || pdfPath);
|
||||
}
|
||||
}
|
||||
export default new RenameFields();
|
||||
|
56
tools/listFormFields.ts
Normal file
56
tools/listFormFields.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { forceArgs } from "../cli/forceArgs.ts";
|
||||
import { cliAlert } from "../cli/prompts.ts";
|
||||
import { TerminalBlock } from "../cli/TerminalLayout.ts";
|
||||
import { loadPdfForm } from "util/saveLoadPdf.ts";
|
||||
|
||||
export class ListFormFields implements ITool {
|
||||
name = "listformfields";
|
||||
description = "Lists fields in a PDF form";
|
||||
block?: TerminalBlock;
|
||||
async run(pdfPath: string = "") {
|
||||
if (!this.block) {
|
||||
this.block = new TerminalBlock();
|
||||
}
|
||||
this.block.setPreserveHistory(true);
|
||||
[pdfPath] = await forceArgs([pdfPath], [[
|
||||
"Please provide path to PDF:",
|
||||
(p) => !!p && p.endsWith(".pdf"),
|
||||
]], this.block);
|
||||
|
||||
const form = await loadPdfForm(pdfPath);
|
||||
const fields = form.getFields();
|
||||
const height = this.block.getRenderHeight() - 1;
|
||||
const fieldNames = fields.sort((a, b) => {
|
||||
const aRect = a.acroField.getWidgets().find((e) => e.Rect())?.Rect()
|
||||
?.asRectangle();
|
||||
const bRect = b.acroField.getWidgets().find((e) => e.Rect())?.Rect()
|
||||
?.asRectangle();
|
||||
|
||||
if (aRect && bRect) {
|
||||
if (aRect.x !== bRect.x) {
|
||||
return aRect.x - bRect.x; // Sort left to right
|
||||
} else {
|
||||
return bRect.y - aRect.y; // If x is equal, sort top to bottom
|
||||
}
|
||||
}
|
||||
return a.getName().localeCompare(b.getName());
|
||||
}).map((f) => f.getName());
|
||||
const maxLength = Math.max(...fieldNames.map((f) => f.length)) + 4;
|
||||
const lines = [];
|
||||
for (let i = 0; i < height; i++) {
|
||||
let line = "";
|
||||
for (let j = 0; j < fieldNames.length; j += height) {
|
||||
const fieldName = fieldNames[i + j] ?? "";
|
||||
line += fieldName.padEnd(maxLength, " ");
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
this.block.setLines(lines, [0, 1]);
|
||||
await cliAlert("", this.block);
|
||||
}
|
||||
setBlock(terminalBlock: TerminalBlock) {
|
||||
this.block = terminalBlock;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ListFormFields();
|
Reference in New Issue
Block a user