From b611e3472b28a63ef51e8bd3a771948f06da7b6f Mon Sep 17 00:00:00 2001 From: darshan-solanki Date: Sat, 18 Oct 2025 14:14:13 +0530 Subject: [PATCH] Add XFA support to AcroForm module --- src/modules/acroform.js | 215 ++++++++++++++++++++++++++++++++++- test/reference/xfa-basic.pdf | Bin 0 -> 3356 bytes test/specs/acroform.spec.js | 39 +++++++ types/index.d.ts | 7 ++ 4 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 test/reference/xfa-basic.pdf diff --git a/src/modules/acroform.js b/src/modules/acroform.js index d440d51f6..08cd233da 100644 --- a/src/modules/acroform.js +++ b/src/modules/acroform.js @@ -431,6 +431,7 @@ var calculateFontSpace = function(text, formObject, fontSize) { var acroformPluginTemplate = { fields: [], xForms: [], + xfaStreams: [], /** * acroFormDictionaryRoot contains information about the AcroForm * Dictionary 0: The Event-Token, the AcroFormDictionaryCallback has @@ -444,7 +445,8 @@ var acroformPluginTemplate = { */ printedOut: false, internal: null, - isInitialized: false + isInitialized: false, + needsRendering: false }; var annotReferenceCallback = function(scope) { @@ -464,6 +466,12 @@ var annotReferenceCallback = function(scope) { } } } + var xfaStreams = scope.internal.acroformPlugin.xfaStreams || []; + for (var j = 0; j < xfaStreams.length; j++) { + if (xfaStreams[j]) { + xfaStreams[j].objId = undefined; + } + } }; var putForm = function(formObject) { @@ -513,6 +521,9 @@ var putCatalogCallback = function(scope) { 0 + " R" ); + if (scope.internal.acroformPlugin.needsRendering === true) { + scope.internal.write("/NeedsRendering true"); + } } else { throw new Error("putCatalogCallback: Root missing."); } @@ -651,6 +662,7 @@ var createFieldCallback = function(fieldArray, scope) { } if (standardFields) { createXFormObjectCallback(scope.internal.acroformPlugin.xForms, scope); + createXFAPacketCallback(scope); } }; @@ -673,8 +685,87 @@ var createXFormObjectCallback = function(fieldArray, scope) { } }; +var ARRAY_APPLY_BATCH = 8192; + +var isArrayBufferLike = function(value) { + if (typeof ArrayBuffer === "undefined") { + return false; + } + if (value instanceof ArrayBuffer) { + return true; + } + if (typeof ArrayBuffer.isView === "function" && ArrayBuffer.isView(value)) { + return true; + } + return ( + value && typeof value === "object" && value.buffer instanceof ArrayBuffer + ); +}; + +var getUint8View = function(value) { + if (typeof Uint8Array === "undefined") { + return null; + } + if (value instanceof ArrayBuffer) { + return new Uint8Array(value); + } + if (typeof ArrayBuffer.isView === "function" && ArrayBuffer.isView(value)) { + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + if ( + value && + typeof value === "object" && + value.buffer instanceof ArrayBuffer + ) { + var byteOffset = value.byteOffset || 0; + var byteLength = + typeof value.byteLength === "number" + ? value.byteLength + : value.buffer.byteLength - byteOffset; + return new Uint8Array(value.buffer, byteOffset, byteLength); + } + return null; +}; + +var arrayBufferToBinaryString = function(buffer) { + var view = getUint8View(buffer); + if (!view) { + throw new Error("Invalid XFA packet stream provided."); + } + var out = ""; + for (var i = 0; i < view.length; i += ARRAY_APPLY_BATCH) { + out += String.fromCharCode.apply( + null, + view.subarray(i, i + ARRAY_APPLY_BATCH) + ); + } + return out; +}; + +var normalizeXFAPacketStream = function(stream) { + if (typeof stream === "string" || stream instanceof String) { + return stream.toString(); + } + if (isArrayBufferLike(stream)) { + return arrayBufferToBinaryString(stream); + } + throw new Error("Invalid XFA packet stream provided."); +}; + +var normalizeXFAPacketName = function(name) { + if (typeof name === "string" || name instanceof String) { + return name.toString(); + } + if (name !== null && typeof name !== "undefined") { + return String(name); + } + throw new Error("XFA packet name must be defined."); +}; + var initializeAcroForm = function(scope, formObject) { - formObject.scope = scope; + if (formObject) { + formObject.scope = scope; + } if ( scope.internal !== undefined && (scope.internal.acroformPlugin === undefined || @@ -941,6 +1032,25 @@ var AcroFormXObject = function() { inherit(AcroFormXObject, AcroFormPDFObject); +var AcroFormXFAPacket = function(stream) { + AcroFormPDFObject.call(this); + + var _stream = typeof stream === "string" ? stream : ""; + + Object.defineProperty(this, "stream", { + enumerable: false, + configurable: true, + get: function() { + return _stream; + }, + set: function(value) { + _stream = typeof value === "string" ? value : ""; + } + }); +}; + +inherit(AcroFormXFAPacket, AcroFormPDFObject); + var AcroFormDictionary = function() { AcroFormPDFObject.call(this); @@ -984,10 +1094,102 @@ var AcroFormDictionary = function() { _DA = value; } }); + + var _XFA; + Object.defineProperty(this, "XFA", { + enumerable: false, + configurable: false, + get: function() { + return _XFA; + }, + set: function(value) { + if (value === null || typeof value === "undefined") { + _XFA = undefined; + } else { + _XFA = value; + } + } + }); }; inherit(AcroFormDictionary, AcroFormPDFObject); +var createXFAPacket = function(scope, stream) { + var plugin = scope.internal.acroformPlugin; + if (!plugin.xfaStreams) { + plugin.xfaStreams = []; + } + var packet = new AcroFormXFAPacket(normalizeXFAPacketStream(stream)); + packet.scope = scope; + plugin.xfaStreams.push(packet); + return packet; +}; + +var setXFAPayload = function(scope, payload) { + if (payload === null || typeof payload === "undefined") { + throw new Error("Invalid XFA payload provided."); + } + + var plugin = scope.internal.acroformPlugin; + if (!plugin.xfaStreams) { + plugin.xfaStreams = []; + } else { + plugin.xfaStreams.length = 0; + } + + var dictionary = plugin.acroFormDictionaryRoot; + if (Array.isArray(payload)) { + if (payload.length === 0) { + throw new Error("XFA payload array must contain at least one packet."); + } + var xfaArray = []; + if (Array.isArray(payload[0])) { + for (var pairIndex = 0; pairIndex < payload.length; pairIndex++) { + var pair = payload[pairIndex]; + if (!Array.isArray(pair) || pair.length !== 2) { + throw new Error("XFA payload pairs must be [name, stream] tuples."); + } + var tupleName = normalizeXFAPacketName(pair[0]); + var tupleStream = pair[1]; + xfaArray.push(tupleName); + xfaArray.push(createXFAPacket(scope, tupleStream)); + } + } else { + if (payload.length % 2 !== 0) { + throw new Error( + "XFA payload array must contain an even number of entries." + ); + } + for (var i = 0; i < payload.length; i += 2) { + var name = normalizeXFAPacketName(payload[i]); + var data = payload[i + 1]; + xfaArray.push(name); + xfaArray.push(createXFAPacket(scope, data)); + } + } + dictionary.XFA = xfaArray; + } else { + dictionary.XFA = createXFAPacket(scope, payload); + } +}; + +var createXFAPacketCallback = function(scope) { + var packets = scope.internal.acroformPlugin.xfaStreams; + if (!Array.isArray(packets) || packets.length === 0) { + return; + } + for (var i = 0; i < packets.length; i++) { + var packet = packets[i]; + if (!packet) { + continue; + } + packet.scope = scope; + scope.internal.newObjectDeferredBegin(packet.objId, true); + packet.putStream(); + } + packets.length = 0; +}; + /** * The Field Object contains the Variables, that every Field needs * @@ -3107,6 +3309,15 @@ AcroFormAppearance.internal.getHeight = function(formObject) { * @param {Object} fieldObject * @returns {jsPDF} */ +var addXFA = (jsPDFAPI.addXFA = function(payload, needsRendering) { + initializeAcroForm(this); + setXFAPayload(this, payload); + if (typeof needsRendering !== "undefined") { + this.internal.acroformPlugin.needsRendering = Boolean(needsRendering); + } + return this; +}); + var addField = (jsPDFAPI.addField = function(fieldObject) { initializeAcroForm(this, fieldObject); diff --git a/test/reference/xfa-basic.pdf b/test/reference/xfa-basic.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1f945f767fc2e300ad0999198ac8fbfcc63f5a6e GIT binary patch literal 3356 zcmcImU2fY(5Ps(==EaDO!s70d%b!9pAX8yuG_4HTK!OT+ps2M%gCbpWmD)X1PLRtq zN68FDMl|VIMiKHsS`KGtzWLeN@91(Gp7N1p9DVuk^H;+H2Kn;V7>~{ChdTx4WwcfX z{uV0J!13$cFz2eww?(W<5IeyiYL!Ii`8_Nc3ZySb!u9u$D_rnwKJvW1%=aAogJA}F zrqOZ~q1WB6q4+0BjPr#5@7&AWPrm~g{9N9?o$^NmVBDV@ZT zVP0*Qx~e&8a>IO`#rZ19)?mI%vPo7Zjeycolsfnr6<`UhPgK7MDO+LtFL!==o~Nsy zkAPD|LJU*CVy|S*?)E(_`U;;`c(a}3RNP8Lqg~E`FsR;vJ+JdkcItd`B4^X z-$OM+d!#(w3=M$c3ecV@&tsqyF&vKeNO~H_b+S?A={(;=*#OPZo@&qeA80Ndo{PKi zFLab9@c{83o{c*I6#q~l#Np}qS9EtX#VcSLX}mGJgYiYTfH*w;t{yhaJpI4+TO;kV z$m1)eVPPVx0rOhj;|+JQK?-wT{|EK|1zzOU1vnn#f&(=KGMi8URfbj;RJR8$ZKy^# zEhX@p^MnrYiaemy1-|1iX6ON@Gfuib@QNGY5>4@n7SNglZ``3V-Z(^SRYhnknyd0Q zr|Z;wKU?0aSOa3k*F8ZI?QxE=uePx&;P|$r=&cbOStrP_kS^&Y&!)&t;CTA0U_x>x zJR!I(>|dCCWwBQ$ZL9r39U$3B^EL6nHL|{%2F~tOHi^l0L!*!v=r@pU(-hN7x^JV0 ze2X=hWh%(i?FL85BrftWFE&u8=})Crn;fd1rDc!e9A*H hu>*(ME|XypoV`S)qUxoN3SD)ZaJg?B9lf4~-vG(ksz3k$ literal 0 HcmV?d00001 diff --git a/test/specs/acroform.spec.js b/test/specs/acroform.spec.js index b7582a826..e5afc1302 100644 --- a/test/specs/acroform.spec.js +++ b/test/specs/acroform.spec.js @@ -1116,4 +1116,43 @@ describe("Module: Acroform Integration Test", function() { expect(jsPDF.API.AcroFormRadioButton); expect(jsPDF.API.AcroFormTextField); }); + + it("addXFA embeds single stream payload", function() { + var doc = new jsPDF({ compress: false }); + doc.addXFA("example"); + var pdf = doc.output(); + expect(pdf).toMatch(/\/XFA\s+\d+\s0\sR/); + expect(pdf).toContain("example"); + expect(pdf).not.toContain("/NeedsRendering true"); + }); + + it("addXFA embeds packet array and sets NeedRendering", function() { + var doc = new jsPDF({ compress: false }); + doc.addXFA( + [ + ["datasets", ""], + ["template", "