import { parse as parseHtml, HTMLElement, NodeType, } from 'node-html-better-parser'; import { PDFArray, PDFDict, PDFHexString, PDFRawStream, PDFRef, PDFString, decodePDFRawStream, PDFName, } from '../../core'; import { decodeXfaXml } from '../../utils'; export type XfaTemplatePacket = { xfa: PDFArray; templateIndex: number; streamRef: PDFRef | null; stream: PDFRawStream; xml: string; }; export type XfaScriptEntry = { scriptNode: HTMLElement; field: string; event: string; }; /** * Locate the XFA `template` packet on an AcroForm dictionary and decode it. * Only the array form of `/XFA` (alternating name/stream pairs) is supported. */ export const readXfaTemplatePacket = ( acroFormDict: PDFDict, ): XfaTemplatePacket | null => { const context = acroFormDict.context; const xfaObj = acroFormDict.get(PDFName.of('XFA')); if (!xfaObj) return null; const xfa = xfaObj instanceof PDFRef ? context.lookup(xfaObj) : xfaObj; if (!(xfa instanceof PDFArray)) return null; for (let idx = 0; idx < xfa.size(); idx += 2) { const nameObj = xfa.get(idx); const streamObj = xfa.get(idx + 1); if (!nameObj || !streamObj) continue; let sectionName: string; if (nameObj instanceof PDFString) { sectionName = nameObj.asString(); } else if (nameObj instanceof PDFHexString) { sectionName = nameObj.decodeText(); } else { continue; } if (sectionName !== 'template') continue; const streamRef = streamObj instanceof PDFRef ? streamObj : null; const stream = streamRef ? context.lookup(streamRef) : streamObj; if (!(stream instanceof PDFRawStream)) continue; return { xfa, templateIndex: idx + 1, streamRef, stream, xml: decodeXfaXml(decodePDFRawStream(stream).decode()), }; } return null; }; export const parseXfaTemplate = (xml: string): HTMLElement => parseHtml(xml, { script: true }); /** * Walk an XFA template tree. Tracks the enclosing `field` name and invokes * `visit` for every element so callers can collect scripts, signatures, etc. */ export const walkXfaTree = ( node: HTMLElement, visit: (node: HTMLElement, field: string | undefined) => void, currentField?: string, ): void => { const tag = node.tagName?.toLowerCase(); const fieldCtx = tag === 'field' ? (node.getAttribute('name') ?? undefined) : currentField; visit(node, fieldCtx); for (const child of node.childNodes) { if (child.nodeType === NodeType.ELEMENT_NODE) { walkXfaTree(child as HTMLElement, visit, fieldCtx); } } }; export const collectXfaScripts = (root: HTMLElement): XfaScriptEntry[] => { const results: XfaScriptEntry[] = []; // Scripts also need the enclosing event name, so this walk tracks both // field and event (walkXfaTree only tracks field). const walk = (node: HTMLElement, field?: string, event?: string) => { const tag = node.tagName?.toLowerCase(); let fieldCtx = field; let eventCtx = event; if (tag === 'field') { fieldCtx = node.getAttribute('name') ?? undefined; eventCtx = undefined; } else if (tag === 'event') { eventCtx = node.getAttribute('name') ?? undefined; } else if (tag === 'script' && fieldCtx && eventCtx) { results.push({ scriptNode: node, field: fieldCtx, event: eventCtx }); } for (const child of node.childNodes) { if (child.nodeType === NodeType.ELEMENT_NODE) { walk(child as HTMLElement, fieldCtx, eventCtx); } } }; walk(root); return results; }; const collectXfaRefs = (node: HTMLElement): string[] => { const refs: string[] = []; for (const child of node.childNodes) { if (child.nodeType !== NodeType.ELEMENT_NODE) continue; const el = child as HTMLElement; if (el.tagName?.toLowerCase() !== 'ref') continue; const text = el.text?.trim(); if (text) refs.push(text); } return refs; }; export type XfaSignatureRaw = { field: string; manifestUse?: string; inlineRefs: string[]; }; export const collectXfaSignatures = ( root: HTMLElement, ): { signatures: XfaSignatureRaw[]; manifests: Map } => { const signatures: XfaSignatureRaw[] = []; const manifests = new Map(); walkXfaTree(root, (node, fieldCtx) => { const tag = node.tagName?.toLowerCase(); if (tag === 'manifest') { const id = node.getAttribute('id'); if (id) manifests.set(id, collectXfaRefs(node)); return; } if (tag !== 'signature' || !fieldCtx) return; let manifestUse: string | undefined; const inlineRefs: string[] = []; for (const child of node.childNodes) { if (child.nodeType !== NodeType.ELEMENT_NODE) continue; const el = child as HTMLElement; if (el.tagName?.toLowerCase() !== 'manifest') continue; const use = el.getAttribute('use'); if (use) manifestUse = use.replace(/^#/, ''); const id = el.getAttribute('id'); const refs = collectXfaRefs(el); if (id) manifests.set(id, refs); inlineRefs.push(...refs); } signatures.push({ field: fieldCtx, manifestUse, inlineRefs }); }); return { signatures, manifests }; };