From 0f9c3778530b50cba9d5ad9bdd66a3252ba2d04f Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 27 May 2025 12:44:45 -0600 Subject: [PATCH] change: selects now use inputmanager fix: bad exit logic feat: field rename now supports renaming things with multiple widgets --- .gitignore | 4 +- cli/TerminalLayout.ts | 6 +- cli/index.ts | 9 + cli/prompts.ts | 2 +- cli/selectMenu.ts | 198 ++++++++++-------- deno.json | 2 +- main.ts | 5 + testing/test.pdf | Bin 1908399 -> 1915504 bytes tools/fieldRename.ts | 471 ++++++++++++++++++++++++++++++++++++------ util/asciiArt.ts | 1 - util/logfile.ts | 2 +- util/saveLoadPdf.ts | 4 +- 12 files changed, 543 insertions(+), 161 deletions(-) diff --git a/.gitignore b/.gitignore index 392e83a..3d4b3a9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .env log.txt -log \ No newline at end of file +log + +test2.pdf \ No newline at end of file diff --git a/cli/TerminalLayout.ts b/cli/TerminalLayout.ts index 83ed712..3c34a4c 100644 --- a/cli/TerminalLayout.ts +++ b/cli/TerminalLayout.ts @@ -1,4 +1,5 @@ import { Cursor } from "./cursor.ts"; +import { InputManager } from "./InputManager.ts"; export class TerminalLayout { private static ALT_BUFFER_ENABLE = "\x1b[?1049h"; @@ -22,7 +23,7 @@ export class TerminalLayout { Deno.addSignalListener("SIGINT", () => { this.clearAll(); - Deno.exit(0); + // Deno.exit(0); }); } @@ -121,7 +122,8 @@ export class TerminalBlock { private preserveHistory = false; - constructor(private prepend: string = "") {} + constructor(private prepend: string = "") { + } setPreserveHistory(preserveHistory: boolean) { this.preserveHistory = preserveHistory; diff --git a/cli/index.ts b/cli/index.ts index 39476a5..83f7f91 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -6,6 +6,7 @@ import { selectMenuInteractive } from "./selectMenu.ts"; import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; import { cliAlert, cliLog } from "./prompts.ts"; import type { ITool } from "../types.ts"; +import { InputManager } from "./InputManager.ts"; // Register tools here (filename, no extension) const toolRegistry: [string, Promise<{ default: ITool }>][] = [ @@ -55,6 +56,12 @@ export class PdfToolsCli { } public async run() { + const im = InputManager.getInstance(); + im.activate(); + im.addEventListener("exit", () => { + this.closeMessage = "Exiting..."; + this.cleanup(); + }); try { await this.importTools(); const titleBlock = new TerminalBlock(); @@ -78,11 +85,13 @@ export class PdfToolsCli { } } finally { this.cleanup(); + Deno.exit(0); } } private cleanup() { this.terminalLayout.clearAll(); + InputManager.getInstance().deactivate(); Deno.stdin.setRaw(false); if (this.closeMessage) console.log(this.closeMessage); } diff --git a/cli/prompts.ts b/cli/prompts.ts index d22b3f0..6976ba1 100644 --- a/cli/prompts.ts +++ b/cli/prompts.ts @@ -295,7 +295,7 @@ if (import.meta.main) { Deno.addSignalListener("SIGINT", () => { layout.clearAll(); // console.clear(); - Deno.exit(0); + // Deno.exit(0); }); const name = await cliPrompt("Enter your name:", block); cliLog(`Hello, ${name}!`, block); diff --git a/cli/selectMenu.ts b/cli/selectMenu.ts index 6e2a774..d3740d8 100644 --- a/cli/selectMenu.ts +++ b/cli/selectMenu.ts @@ -1,6 +1,7 @@ import type { callback } from "../types.ts"; +import { type CLICharEvent, InputManager } from "./InputManager.ts"; import { colorize } from "./style.ts"; -import { TerminalBlock } from "./TerminalLayout.ts"; +import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; interface ISelectMenuConfig { terminalBlock?: TerminalBlock; @@ -62,50 +63,65 @@ export async function selectMenuInteractive( let inputBuffer = ""; - // Function to handle input - async function handleInput() { - const buf = new Uint8Array(3); // arrow keys send 3 bytes - while (true) { - renderMenu(); - const n = await Deno.stdin.read(buf); - if (n === null) break; + const im = InputManager.getInstance(); + im.activate(); - const [a, b, c] = buf; + const onUp = (e: Event) => { + e.stopImmediatePropagation(); + selected = (selected - 1 + options.length) % options.length; + renderMenu(); + }; - if (a === 3) { - Deno.stdin.setRaw(false); - terminalBlock?.["layout"]?.clearAll(); - Deno.exit(130); - } - - if (a === 13) { // Enter key - if (inputBuffer) { - const parsed = parseInt(inputBuffer); - if (!isNaN(parsed)) { - selected = parsed - 1; - } - inputBuffer = ""; - } - break; - } else if (a === 27 && b === 91) { // Arrow keys - inputBuffer = ""; - if (c === 65) { // Up - selected = (selected - 1 + options.length) % options.length; - } else if (c === 66) { // Down - selected = (selected + 1) % options.length; - } - } else if (a >= 48 && a <= 57) { - inputBuffer += String.fromCharCode(a); - } else if (a === 8) { - inputBuffer = inputBuffer.slice(0, -1); + const onDown = (e: Event) => { + e.stopImmediatePropagation(); + selected = (selected + 1) % options.length; + renderMenu(); + }; + + const onKey = (e: CLICharEvent) => { + e.stopImmediatePropagation(); + const ke = e.detail; + const char = String.fromCharCode(ke.key); + inputBuffer += char; + }; + + const onBackspace = (e: Event) => { + e.stopImmediatePropagation(); + inputBuffer = inputBuffer.slice(0, -1); + }; + + let resolve: null | ((value: string) => void) = null; + + const onEnter = (e: Event) => { + e.stopImmediatePropagation(); + if (inputBuffer) { + const parsed = parseInt(inputBuffer); + if (!isNaN(parsed)) { + selected = parsed - 1; } + inputBuffer = ""; } + im.removeEventListener("arrow-up", onUp); + im.removeEventListener("arrow-down", onDown); + im.removeEventListener("char", onKey); + im.removeEventListener("backspace", onBackspace); + im.removeEventListener("enter", onEnter); + resolve?.(options[selected]); + }; + + renderMenu(); + await new Promise((res) => { + resolve = res; + im.addEventListener("char", onKey); + im.addEventListener("backspace", onBackspace); + im.addEventListener("enter", onEnter); + im.addEventListener("arrow-up", onUp); + im.addEventListener("arrow-down", onDown); + }); - Deno.stdin.setRaw(false); - return options[selected]; - } terminalBlock.setLines(["Selected: " + options[selected]], range); - return await handleInput(); + + return options[selected]; } export async function multiSelectMenuInteractive( @@ -117,9 +133,7 @@ export async function multiSelectMenuInteractive( let selected = 0; let selectedOptions: number[] = config?.initialSelections || []; - const rawValues = new Set( - options.map((i) => typeof i === "string" ? i : i[0]), - ).values().toArray(); + const rawValues = options.map((i) => typeof i === "string" ? i : i[0]); if (rawValues.length !== options.length) { throw new Error("Duplicate options in multi-select menu"); @@ -158,44 +172,52 @@ export async function multiSelectMenuInteractive( range = terminalBlock.setLines(lines, range); } - // Function to handle input - async function handleInput() { - const buf = new Uint8Array(3); // arrow keys send 3 bytes - while (true) { - renderMenu(); - const n = await Deno.stdin.read(buf); - if (n === null) break; + const im = InputManager.getInstance(); + im.activate(); - const [a, b, c] = buf; + let resolve = null as null | ((value: number[]) => void); - if (a === 3) { - Deno.stdin.setRaw(false); - terminalBlock?.["layout"]?.clearAll(); - Deno.exit(130); - } + const onUp = (e: Event) => { + e.stopImmediatePropagation(); + selected = (selected - 1 + options.length) % options.length; + renderMenu(); + }; - if (a === 13) { // Enter key - break; - } else if (a === 27 && b === 91) { // Arrow keys - if (c === 65) { // Up - selected = (selected - 1 + options.length) % options.length; - } else if (c === 66) { // Down - selected = (selected + 1) % options.length; - } - } else if (a === 32) { // Space - Deno.stdout.writeSync(new TextEncoder().encode("\x07")); - if (selectedOptions.includes(selected)) { - selectedOptions = selectedOptions.filter((i) => i !== selected); - } else { - selectedOptions.push(selected); - } - } + const onDown = (e: Event) => { + e.stopImmediatePropagation(); + selected = (selected + 1) % options.length; + renderMenu(); + }; + + const onSpace = (e: CLICharEvent) => { + if (e.detail.char !== " ") return; + e.stopImmediatePropagation(); + if (selectedOptions.includes(selected)) { + selectedOptions = selectedOptions.filter((i) => i !== selected); + } else { + selectedOptions.push(selected); } + renderMenu(); + }; - Deno.stdin.setRaw(false); - return selectedOptions; - } - const selections = await handleInput(); + const onEnter = (e: Event) => { + e.stopImmediatePropagation(); + resolve?.(selectedOptions); + im.removeEventListener("arrow-up", onUp); + im.removeEventListener("arrow-down", onDown); + im.removeEventListener("char", onSpace); + im.removeEventListener("enter", onEnter); + }; + + renderMenu(); + + const selections = await new Promise((res) => { + resolve = res; + im.addEventListener("arrow-up", onUp); + im.addEventListener("arrow-down", onDown); + im.addEventListener("char", onSpace); + im.addEventListener("enter", onEnter); + }); for (const optionI of selections) { const option = options[optionI]; if (Array.isArray(option)) { @@ -208,17 +230,17 @@ export async function multiSelectMenuInteractive( } if (import.meta.main) { - // const layout = new TerminalLayout(); - // const block = new TerminalBlock(); - // const titleBlock = new TerminalBlock(); - // const postBlock = new TerminalBlock(); - // titleBlock.setLines(["An incredible fruit menu!"]); - // postBlock.setLines(["I'm here too!"]); - // titleBlock.setFixedHeight(1); - // postBlock.setFixedHeight(1); - // layout.register("title", titleBlock); - // layout.register("block", block); - // layout.register("post", postBlock); + const layout = new TerminalLayout(); + const block = new TerminalBlock(); + const titleBlock = new TerminalBlock(); + const postBlock = new TerminalBlock(); + InputManager.addEventListener("exit", () => layout.clearAll()); + titleBlock.setLines(["An incredible fruit menu!"]); + postBlock.setLines(["I'm here too!"]); + titleBlock.setFixedHeight(1); + postBlock.setFixedHeight(1); + layout.register("title", titleBlock); + layout.register("block", block); // const val = await selectMenuInteractive("choose a fruit", [ // "apple", @@ -276,8 +298,8 @@ if (import.meta.main) { "ximenia", "yuzu", "zucchini", - ]); - console.log(val); + ], { terminalBlock: block }); + // console.log(val); // Deno.stdout.writeSync(new TextEncoder().encode("\x07")); } diff --git a/deno.json b/deno.json index 63d55c2..c36933f 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@bearmetal/pdf-tools", - "version": "1.0.8-a", + "version": "1.0.8-h", "license": "GPL 3.0", "tasks": { "dev": "deno run -A --env-file=.env main.ts", diff --git a/main.ts b/main.ts index 0741d37..1d631f5 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,10 @@ /// import { PdfToolsCli } from "./cli/index.ts"; +// import { log } from "util/logfile.ts"; +// try { const app = new PdfToolsCli(); app.run(); +// } catch (e) { +// // log(e); +// } diff --git a/testing/test.pdf b/testing/test.pdf index b975d45c619712a3406221430b3a34c34798bd9e..7ace2e1b7cbf2fc8e4cd9b61ff9d2f84e43c1256 100644 GIT binary patch delta 15629 zcmd^m2{@E%*uOQ5y|Qm(-|0}!ByX~3&mLvTR`v=fB}>VcEoF_A zJ&BNn_>a!!oHLzMeVz0FuIqETyW)C1b3ea(``!2Ryk)hx#T6VNXrQ4b36+uq2?pZ7 zKp+Gd0`|0Z0Vyegq}9|seZiLaUqHZ6`JM0MAZaa6th>n>FANw8lGej`IO3eZ2zik7 zVKffwyz?;h=f^+)i+|h* z?OJSdetYw|c?a#I!P!Ft`a+UI7A8Kp+x?`Of|EH@7PI_7n4C zWDy7ld$_$M6k=~Di9pE6OQI2S_L2^A2&60m4u{!6Zq4wMP!Wo4#O_3`MF{?1HUNYE zt{_M!LZP0BOoE1{t!SE+h>VqpmW#xi01}AZ2~tb-@0N!_WOw1Cj?&n%+fJ2)sQ>=K zx9&=1Qlbcb(C!@dx7$D>cP*DGCqZmPXt~|V;8zPm@eJU%D*jIfR3RpWw#*+w)*0t* zfWfMJx_fzgVDP02zrMPsn=L{(_u_y2-D#QU~Mg=!65q zAUnk57ZsuMNSF*1kK>L{p>~`KjM&+U5lH$7+8w{7`K}$H+|iEsn)^p0B5meuk0(2y zSN)BY1kovm(J2z+g#<68cp<|JIbQbQg#s^>c%i}z2rtxlp}`9+Ug+>bj~52KFydt| zUiRT-KVF#d!i*Odys+Yh4KM6?;i#rl% z>qz$>vi#v6Oa6AM*+~!-&-$UVJKtCg$PDBGQZoTbe`cCcgp>>%1~zd3!KCD6;9!Il zL>>w@u?IonQb;5M4mQyPi6BJ5CN3aN6ObVxH%0DFlmyTLesoLn)zlYs5(JTwOMr3_ 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 delta 9220 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 Iqsl=0Uv}!m-T(jq diff --git a/tools/fieldRename.ts b/tools/fieldRename.ts index 00fa603..1b75e6a 100644 --- a/tools/fieldRename.ts +++ b/tools/fieldRename.ts @@ -1,6 +1,18 @@ -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 { + type PDFAcroField, + PDFArray, + PDFCheckBox, + type PDFDocument, + type PDFField, + PDFName, + PDFNumber, + PDFRadioGroup, + type PDFRef, + PDFString, + PDFTextField, + type PDFWidgetAnnotation, +} from "pdf-lib"; +import { loadPdf, savePdf } from "util/saveLoadPdf.ts"; import { TerminalBlock } from "../cli/TerminalLayout.ts"; import { forceArgs } from "../cli/forceArgs.ts"; import { colorize } from "../cli/style.ts"; @@ -9,42 +21,6 @@ import { multiSelectMenuInteractive } from "../cli/selectMenu.ts"; import type { callback, ITool } from "../types.ts"; import { toCase } from "util/caseManagement.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, @@ -70,6 +46,294 @@ function applyRename( } } +// function applyWidgetRename( +// doc: PDFDocument, +// field: PDFField, +// widget: PDFWidgetAnnotation, +// name: string, +// pattern: RegExp, +// change: string, +// ) { +// if (field.acroField.getWidgets().length > 1) { +// const widgets = field.acroField.getWidgets(); +// const widgetIndex = widgets.indexOf(widget); +// widgets.splice(widgetIndex, 1); + +// const pdfDocContext = doc.context; + +// const originalRef = field.acroField.ref; +// const originalFieldDict = pdfDocContext.lookup(originalRef); +// if (!originalFieldDict) return; + +// const newFieldDict = pdfDocContext.obj({ +// ...originalFieldDict, +// T: PDFString.of(name.replace(pattern, change)), +// Kids: [getWidgetRef(widget, doc.getPages())], +// }); +// const newField = pdfDocContext.register(newFieldDict); + +// const acroForm = doc.catalog.lookup(PDFName.of("AcroForm"), PDFDict); +// const fields = acroForm.lookup(PDFName.of("Fields"), PDFArray); +// fields.push(newField); +// } +// } +function findPageForWidget( + doc: PDFDocument, + widget: PDFWidgetAnnotation, +) { + const pages = doc.getPages(); + for (const page of pages) { + const annots = page.node.Annots(); + if (!annots) continue; + + const annotRefs = annots.asArray(); + for (const ref of annotRefs) { + const annot = doc.context.lookup(ref); + if (annot === widget.dict) { + return page; + } + } + } + return undefined; +} + +function detectFieldType(field: PDFField): string | undefined { + const ft = field.acroField.dict.get(PDFName.of("FT")); + return ft instanceof PDFName ? ft.asString() : undefined; +} + +function getFlag(field: PDFField, bit: number): boolean { + const ff = field.acroField.dict.get(PDFName.of("Ff")); + return ff instanceof PDFNumber ? (ff.asNumber() & (1 << bit)) !== 0 : false; +} + +function getWidgetRef( + widget: PDFWidgetAnnotation, + doc: PDFDocument, +): PDFRef | undefined { + for (const page of doc.getPages()) { + const annots = page.node.Annots()?.asArray() ?? []; + for (const ref of annots) { + const maybeDict = doc.context.lookup(ref); + if (maybeDict === widget.dict) { + return ref as PDFRef; + } + } + } + return undefined; +} + +function applyWidgetRename( + doc: PDFDocument, + field: PDFField, + widget: PDFWidgetAnnotation, + newName: string, + pattern: RegExp, + change: string, +) { + try { + const form = doc.getForm(); + const widgets = field.acroField.getWidgets(); + + if (widgets.length <= 1) return; + const widgetDict = widget.dict; + const widgetIndex = widgets.findIndex((w) => w.dict === widgetDict); + if (widgetIndex === -1) return; + + widgets.splice(widgetIndex, 1); + + const kids = field.acroField.dict.lookup(PDFName.of("Kids"), PDFArray); + if (kids) { + const updatedKids = kids.asArray().filter((ref) => { + const maybeDict = doc.context.lookup(ref); + return maybeDict !== widget.dict; + }); + field.acroField.dict.set( + PDFName.of("Kids"), + doc.context.obj(updatedKids), + ); + } + + const page = findPageForWidget(doc, widget); + if (!page) throw new Error("Widget page not found"); + + const rect = widget.getRectangle(); + if (!rect) throw new Error("Widget has no rectangle"); + + const finalName = newName.replace(pattern, change); + + // Try to get existing field with the new name + let targetField: PDFField | undefined; + + try { + targetField = form.getField(finalName); + } catch { + // Field doesn't exist — that's fine + } + + // Compare field types if field exists + if (targetField) { + const sourceType = detectFieldType(field); + const targetType = detectFieldType(targetField); + + if (sourceType !== targetType) { + throw new Error( + `Field "${finalName}" already exists with a different type (${targetType} vs ${sourceType})`, + ); + } + + // ✅ Same type — attach widget to the existing field + // const targetFieldWidgets = targetField.acroField.getWidgets(); + const targetKidsArray = targetField.acroField.dict.lookup( + PDFName.of("Kids"), + PDFArray, + ); + + // Set /Parent on the widget to point to the existing field + widget.dict.set(PDFName.of("Parent"), targetField.acroField.ref); + + // Add the widget to the field's /Kids array + const widgetRef = getWidgetRef(widget, doc); + if (!widgetRef) throw new Error("Widget ref not found"); + if (targetKidsArray) { + targetKidsArray.push(widgetRef); + } else { + targetField.acroField.dict.set( + PDFName.of("Kids"), + doc.context.obj([widgetRef]), + ); + } + + // Also ensure widget is attached to a page + const page = findPageForWidget(doc, widget); + if (!page) throw new Error("Widget's page not found"); + + const pageAnnots = page.node.Annots(); + const refs = pageAnnots?.asArray() ?? []; + if (!refs.includes(widgetRef)) { + refs.push(widgetRef); + page.node.set(PDFName.of("Annots"), doc.context.obj(refs)); + } + + return; // Done + } + removeWidgetFromPage(widget, doc); + + const fieldType = detectFieldType(field); + + let newField: PDFField; + + switch (fieldType) { + case "/Tx": { + const tf = form.createTextField(finalName); + if (field instanceof PDFTextField) { + const val = field.getText(); + if (val) tf.setText(val); + } + newField = tf; + break; + } + + case "/Btn": { + const isRadio = getFlag(field, 15); + if (isRadio) { + const rf = form.createRadioGroup(finalName); + rf.addOptionToPage(finalName, page, { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + }); + if (field instanceof PDFRadioGroup) { + const selected = field.getSelected(); + if (selected) rf.select(selected); + } + return; + } else { + const cb = form.createCheckBox(finalName); + cb.addToPage(page, { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + }); + if (field instanceof PDFCheckBox && field.isChecked()) { + cb.check(); + } + return; + } + } + + default: + throw new Error(`Unsupported field type: ${fieldType}`); + } + + // Attach the new field to the page if necessary + if ( + newField instanceof PDFTextField || + newField instanceof PDFCheckBox + ) { + newField.addToPage(page, { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + }); + } + } catch { + // log(e); + } +} + +function removeWidgetFromPage(widget: PDFWidgetAnnotation, doc: PDFDocument) { + const pages = doc.getPages(); + + for (const page of pages) { + const annotsArray = page.node.Annots(); + if (!annotsArray) continue; + + const refs = annotsArray.asArray(); + const newRefs = refs.filter((ref) => { + const maybeDict = doc.context.lookup(ref); + return maybeDict !== widget.dict; + }); + + // Replace /Annots with updated array + if (newRefs.length === refs.length) continue; + page.node.set(PDFName.of("Annots"), doc.context.obj(newRefs)); + } +} + +// function getWidgetRef( +// widget: PDFWidgetAnnotation, +// pages: PDFPage[], +// ): PDFRef | undefined { +// const widgetRect = (widget?.dict?.get(PDFName.of("Rect")) as PDFArray) +// ?.asArray(); +// const widgetFT = (widget?.dict?.get(PDFName.of("FT")) as PDFString) +// ?.["value"]; + +// for (const page of pages) { +// const annotsArray = page.node.Annots()?.asArray(); +// if (!annotsArray) continue; + +// for (const annotRef of annotsArray) { +// const annotDict = page.doc.context.lookup(annotRef); +// if (!annotDict) continue; +// if (!(annotDict instanceof PDFDict)) continue; +// const rect = (annotDict.get(PDFName.of("Rect")) as PDFArray)?.asArray(); +// const ft = (annotDict.get(PDFName.of("FT")) as PDFString)?.["value"]; + +// // rudimentary match (you can add more checks like /T, /Subtype, etc.) +// if (rect?.toString() === widgetRect?.toString() && ft === widgetFT) { +// return annotRef as PDFRef; +// } +// } +// } + +// return undefined; +// } + /*** * Evaluates the change string with the match array * @@ -83,7 +347,7 @@ function applyRename( * - $u - capture groups, indexed from 1, transforming a string to upper case * - $t - capture groups, indexed from 1, transforming a string to title case */ -function evaluateChange(change: string, match: RegExpExecArray) { +function evaluateChange(change: string, match: RegExpExecArray, index: number) { return change.replace( /\$(\d+)([icslut]?)/g, (_, i, indexed) => { @@ -106,7 +370,19 @@ function evaluateChange(change: string, match: RegExpExecArray) { return match[i]; } }, - ); + ) + .replace( + /\$I{((\w+,?)+)}/, + (_, offset) => { + const options = offset.split(","); + return options[index % options.length]; + }, + ) + .replace( + /\$I(-?\d+)?/, + (_, offset) => + (parseInt(offset) ? index + parseInt(offset) : index).toString(), + ); } class RenameFields implements ITool { @@ -150,29 +426,92 @@ class RenameFields implements ITool { const pdf = await loadPdf(pdfPath); const form = pdf.getForm(); - const fields = form.getFields(); + const fields = form.getFields().sort((a, b) => { + const aWidgets = a.acroField.getWidgets(); + const bWidgets = b.acroField.getWidgets(); + const aWidget = aWidgets[0]; + const bWidget = bWidgets[0]; + + const aPage = a.doc.findPageForAnnotationRef(a.acroField.ref); + const bPage = b.doc.findPageForAnnotationRef(b.acroField.ref); + + if (aPage && bPage && aPage !== bPage) { + const pages = a.doc.getPages(); + const aPageIndex = pages.indexOf(aPage); + const bPageIndex = pages.indexOf(bPage); + + if (aPageIndex !== bPageIndex) return aPageIndex - bPageIndex; + } + + const aRect = aWidget.Rect()?.asRectangle(); + const bRect = bWidget.Rect()?.asRectangle(); + + if (aRect && bRect) { + const dy = bRect.y - aRect.y; + if (Math.abs(dy) > 5) return dy; + + return aRect.x - bRect.x; + } + + return a.getName().localeCompare(b.getName()); + }); + let badFields = 0; + + for (const field of fields) { + if (field.acroField.getWidgets().length > 1) { + badFields++; + } + } + + badFields && await cliLog( + colorize( + `Warning, ${badFields} fields with shared widgets found`, + "yellow", + ), + this.block, + ); const foundUpdates: [string, callback][] = []; let changesMade = false; + let i = 0; 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); - changesMade = true; - }, - ]); + foundUpdates.push( + ...field.acroField.getWidgets()?.map<[string, callback]>(( + widget, + ) => { + const toChange = evaluateChange(change, match, i); + const preview = name.replace( + new RegExp(patternRegex), + toChange, + ); + i++; + return [ + `${colorize(name, "red")} -> ${colorize(preview, "green")}`, + () => { + field.acroField.getWidgets().length > 1 + ? applyWidgetRename( + pdf, + field, + widget, + name, + new RegExp(patternRegex), + toChange, + ) + : applyRename(field, name, patternRegex, toChange); + changesMade = true; + }, + ]; + }), + ); } } if (foundUpdates.length) { - cliLog("Found updates:", this.block); + await cliLog("Found updates:", this.block); await multiSelectMenuInteractive( "Please select an option to apply", foundUpdates, @@ -185,7 +524,11 @@ class RenameFields implements ITool { "Save to path (or hit enter to keep current):", this.block, ); - await savePdf(pdf, path || pdfPath); + try { + await savePdf(pdf, path || pdfPath); + } catch { + // log(e); + } } else { cliLog("No changes made, skipping", this.block); } @@ -194,14 +537,14 @@ class RenameFields implements ITool { } 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:", - ]); -} +// 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:", +// ]); +// } diff --git a/util/asciiArt.ts b/util/asciiArt.ts index 7f6a8bd..14802f2 100644 --- a/util/asciiArt.ts +++ b/util/asciiArt.ts @@ -1,4 +1,3 @@ -import { log } from "./logfile.ts"; import { join } from "@std/path"; export async function getAsciiArt(art: string) { diff --git a/util/logfile.ts b/util/logfile.ts index ad75a28..a101562 100644 --- a/util/logfile.ts +++ b/util/logfile.ts @@ -9,7 +9,7 @@ logFile.truncateSync(0); export function log(message: any) { if (typeof message === "object") { - message = JSON.stringify(message); + message = Deno.inspect(message); } logFile.writeSync(new TextEncoder().encode(message + "\n")); } diff --git a/util/saveLoadPdf.ts b/util/saveLoadPdf.ts index 815aed6..fc07fe6 100644 --- a/util/saveLoadPdf.ts +++ b/util/saveLoadPdf.ts @@ -15,10 +15,10 @@ export async function loadPdf(path: string) { export async function savePdf(doc: PDFDocument, path: string) { doc.getForm().getFields().forEach((field) => { if (field instanceof PDFTextField) { - field.disableRichFormatting(); + field.disableRichFormatting?.(); } }); - const pdfBytes = await doc.save(); + const pdfBytes = await doc.save({ updateFieldAppearances: true }); if (Deno.env.get("DRYRUN") || path.includes("dryrun")) return; await Deno.writeFile(path, pdfBytes); }