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 b975d45..7ace2e1 100644 Binary files a/testing/test.pdf and b/testing/test.pdf differ 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); }