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 { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts"; import { multiSelectMenuInteractive } from "../cli/selectMenu.ts"; import type { callback, ITool } from "../types.ts"; async function renameFields( path: string, pattern: string | RegExp, change: string, ) { if (typeof pattern === "string") pattern = new RegExp(pattern); const form = await loadPdfForm(path); const fields = form.getFields(); let changesMade = false; for (const field of fields) { const name = field.getName(); if (pattern.test(name)) { console.log(name + " %cfound", "color: red"); 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) { changesMade = true; cField.dict.set(PDFName.of("T"), PDFString.of(mName)); } } cField = cField.getParent(); } } } if (changesMade) { savePdf(form.doc, path); } } 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"; block: TerminalBlock | undefined; setBlock(block: TerminalBlock) { this.block = block; } async help(standalone = false) { await cliAlert( "Usage: rename-fields \n", standalone ? undefined : this.block, ); } async run(pdfPath: string = "", pattern: string = "", change: string = "") { 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); const preview = name.replace(new RegExp(patternRegex), toChange); foundUpdates.push([ `${colorize(name, "red")} -> ${colorize(preview, "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(); if (import.meta.main) { // await call(renameFields) // while (!path || !path.endsWith('.pdf')) path = prompt("Please provide path to PDF:") || ''; // while (!pattern) pattern = prompt("Please provide search string:") || ''; // while (!change) change = prompt("Please provide requested change:") || ''; await callWithArgPrompt(renameFields, [ ["Please provide path to PDF:", (p) => !!p && p.endsWith(".pdf")], "Please provide search string:", "Please provide requested change:", ]); }