import { forceArgs } from "../cli/forceArgs.ts"; import { selectMenuInteractive } from "../cli/selectMenu.ts"; import { TerminalBlock } from "../cli/TerminalLayout.ts"; import type { ITool } from "../types.ts"; import { loadPdf, savePdf } from "util/saveLoadPdf.ts"; import { PDFName, type PDFNumber, type PDFWidgetAnnotation } from "pdf-lib"; import { cliPrompt } from "../cli/prompts.ts"; type AcrobatVisibility = | "Visible" | "Hidden" | "VisibleButDoesNotPrint" | "HiddenButPrintable"; export function getAcrobatVisibility( widget: PDFWidgetAnnotation, ): AcrobatVisibility { const raw = widget.dict.lookup(PDFName.of("F")) as PDFNumber; const flags = raw?.asNumber?.() ?? 0; const isInvisible = (flags & (1 << 1)) !== 0; const isHidden = (flags & (1 << 2)) !== 0; const isNoPrint = (flags & (1 << 3)) !== 0; const isPrint = (flags & (1 << 2)) === 0 && !isNoPrint; if (isInvisible && isHidden) { return isPrint ? "HiddenButPrintable" : "Hidden"; } else if (isNoPrint) { return "VisibleButDoesNotPrint"; } else { return "Visible"; } } export function setAcrobatVisibility( widget: PDFWidgetAnnotation, visibility: AcrobatVisibility, ) { let flags = 0; switch (visibility) { case "Visible": // No visibility bits set break; case "Hidden": flags |= 1 << 1; // Invisible flags |= 1 << 2; // Hidden break; case "VisibleButDoesNotPrint": flags |= 1 << 3; // NoPrint break; case "HiddenButPrintable": flags |= 1 << 1; // Invisible flags |= 1 << 2; // Hidden flags |= 1 << 3; // NoPrint — UNset this to allow print break; } widget.dict.set(PDFName.of("F"), widget.dict.context.obj(flags)); } export class FieldVisibility implements ITool { name = "Field Visibility"; description = "Change visibility of fields"; block?: TerminalBlock; async run(pdfPath: string) { if (!this.block) this.block = new TerminalBlock(); this.block.setPreserveHistory(false); [pdfPath] = await forceArgs([pdfPath], [[ "Please provide path to PDF:", (e) => e?.endsWith(".pdf"), ]], this.block); const pdf = await loadPdf(pdfPath); const form = pdf.getForm(); const fields = form.getFields(); let changesMade = false; while (true) { const fieldAndVisibility = await selectMenuInteractive( `Select a field to change visibility (ESC to ${ changesMade ? "continue" : "cancel" })`, fields.flatMap((f) => { const name = f.getName(); const visibility = f.acroField.getWidgets().map((w, i, a) => `${name}${a.length > 1 ? "#" + i : ""} :: ${ getAcrobatVisibility(w) }` ); return visibility; }), ); if (!fieldAndVisibility) break; const visibility = await selectMenuInteractive( fieldAndVisibility, [ "Visible", "Hidden", "HiddenButPrintable", "VisibleButDoesNotPrint", ] as AcrobatVisibility[], ) as AcrobatVisibility | null; if (!visibility) continue; const [fName, widgetIndex] = fieldAndVisibility.split("::")[0].trim() .split("#"); const field = fields.find((f) => f.getName() === fName); if (!field) break; const widget = field.acroField.getWidgets()[Number(widgetIndex) || 0]; setAcrobatVisibility(widget, visibility); changesMade = true; } if (changesMade) { const path = await cliPrompt( "Save to path (or hit enter to keep current):", this.block, ) || pdfPath; savePdf(pdf, path); } } help?: (() => Promise | void) | undefined; done?: (() => Promise | void) | undefined; setBlock(block: TerminalBlock) { this.block = block; } } export default new FieldVisibility();