Skip to content

Commit 03388a8

Browse files
committed
Add XFA support to AcroForm module
1 parent c10d90c commit 03388a8

File tree

3 files changed

+247
-2
lines changed

3 files changed

+247
-2
lines changed

src/modules/acroform.js

Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ var calculateFontSpace = function(text, formObject, fontSize) {
431431
var acroformPluginTemplate = {
432432
fields: [],
433433
xForms: [],
434+
xfaStreams: [],
434435
/**
435436
* acroFormDictionaryRoot contains information about the AcroForm
436437
* Dictionary 0: The Event-Token, the AcroFormDictionaryCallback has
@@ -444,7 +445,8 @@ var acroformPluginTemplate = {
444445
*/
445446
printedOut: false,
446447
internal: null,
447-
isInitialized: false
448+
isInitialized: false,
449+
needRendering: false
448450
};
449451

450452
var annotReferenceCallback = function(scope) {
@@ -464,6 +466,12 @@ var annotReferenceCallback = function(scope) {
464466
}
465467
}
466468
}
469+
var xfaStreams = scope.internal.acroformPlugin.xfaStreams || [];
470+
for (var j = 0; j < xfaStreams.length; j++) {
471+
if (xfaStreams[j]) {
472+
xfaStreams[j].objId = undefined;
473+
}
474+
}
467475
};
468476

469477
var putForm = function(formObject) {
@@ -513,6 +521,9 @@ var putCatalogCallback = function(scope) {
513521
0 +
514522
" R"
515523
);
524+
if (scope.internal.acroformPlugin.needRendering === true) {
525+
scope.internal.write("/NeedRendering true");
526+
}
516527
} else {
517528
throw new Error("putCatalogCallback: Root missing.");
518529
}
@@ -651,6 +662,7 @@ var createFieldCallback = function(fieldArray, scope) {
651662
}
652663
if (standardFields) {
653664
createXFormObjectCallback(scope.internal.acroformPlugin.xForms, scope);
665+
createXFAPacketCallback(scope);
654666
}
655667
};
656668

@@ -673,8 +685,87 @@ var createXFormObjectCallback = function(fieldArray, scope) {
673685
}
674686
};
675687

688+
var ARRAY_APPLY_BATCH = 8192;
689+
690+
var isArrayBufferLike = function(value) {
691+
if (typeof ArrayBuffer === "undefined") {
692+
return false;
693+
}
694+
if (value instanceof ArrayBuffer) {
695+
return true;
696+
}
697+
if (typeof ArrayBuffer.isView === "function" && ArrayBuffer.isView(value)) {
698+
return true;
699+
}
700+
return (
701+
value && typeof value === "object" && value.buffer instanceof ArrayBuffer
702+
);
703+
};
704+
705+
var getUint8View = function(value) {
706+
if (typeof Uint8Array === "undefined") {
707+
return null;
708+
}
709+
if (value instanceof ArrayBuffer) {
710+
return new Uint8Array(value);
711+
}
712+
if (typeof ArrayBuffer.isView === "function" && ArrayBuffer.isView(value)) {
713+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
714+
}
715+
if (
716+
value &&
717+
typeof value === "object" &&
718+
value.buffer instanceof ArrayBuffer
719+
) {
720+
var byteOffset = value.byteOffset || 0;
721+
var byteLength =
722+
typeof value.byteLength === "number"
723+
? value.byteLength
724+
: value.buffer.byteLength - byteOffset;
725+
return new Uint8Array(value.buffer, byteOffset, byteLength);
726+
}
727+
return null;
728+
};
729+
730+
var arrayBufferToBinaryString = function(buffer) {
731+
var view = getUint8View(buffer);
732+
if (!view) {
733+
throw new Error("Invalid XFA packet stream provided.");
734+
}
735+
var out = "";
736+
for (var i = 0; i < view.length; i += ARRAY_APPLY_BATCH) {
737+
out += String.fromCharCode.apply(
738+
null,
739+
view.subarray(i, i + ARRAY_APPLY_BATCH)
740+
);
741+
}
742+
return out;
743+
};
744+
745+
var normalizeXFAPacketStream = function(stream) {
746+
if (typeof stream === "string" || stream instanceof String) {
747+
return stream.toString();
748+
}
749+
if (isArrayBufferLike(stream)) {
750+
return arrayBufferToBinaryString(stream);
751+
}
752+
throw new Error("Invalid XFA packet stream provided.");
753+
};
754+
755+
var normalizeXFAPacketName = function(name) {
756+
if (typeof name === "string" || name instanceof String) {
757+
return name.toString();
758+
}
759+
if (name !== null && typeof name !== "undefined") {
760+
return String(name);
761+
}
762+
throw new Error("XFA packet name must be defined.");
763+
};
764+
676765
var initializeAcroForm = function(scope, formObject) {
677-
formObject.scope = scope;
766+
if (formObject) {
767+
formObject.scope = scope;
768+
}
678769
if (
679770
scope.internal !== undefined &&
680771
(scope.internal.acroformPlugin === undefined ||
@@ -941,6 +1032,25 @@ var AcroFormXObject = function() {
9411032

9421033
inherit(AcroFormXObject, AcroFormPDFObject);
9431034

1035+
var AcroFormXFAPacket = function(stream) {
1036+
AcroFormPDFObject.call(this);
1037+
1038+
var _stream = typeof stream === "string" ? stream : "";
1039+
1040+
Object.defineProperty(this, "stream", {
1041+
enumerable: false,
1042+
configurable: true,
1043+
get: function() {
1044+
return _stream;
1045+
},
1046+
set: function(value) {
1047+
_stream = typeof value === "string" ? value : "";
1048+
}
1049+
});
1050+
};
1051+
1052+
inherit(AcroFormXFAPacket, AcroFormPDFObject);
1053+
9441054
var AcroFormDictionary = function() {
9451055
AcroFormPDFObject.call(this);
9461056

@@ -984,10 +1094,102 @@ var AcroFormDictionary = function() {
9841094
_DA = value;
9851095
}
9861096
});
1097+
1098+
var _XFA;
1099+
Object.defineProperty(this, "XFA", {
1100+
enumerable: false,
1101+
configurable: false,
1102+
get: function() {
1103+
return _XFA;
1104+
},
1105+
set: function(value) {
1106+
if (value === null || typeof value === "undefined") {
1107+
_XFA = undefined;
1108+
} else {
1109+
_XFA = value;
1110+
}
1111+
}
1112+
});
9871113
};
9881114

9891115
inherit(AcroFormDictionary, AcroFormPDFObject);
9901116

1117+
var createXFAPacket = function(scope, stream) {
1118+
var plugin = scope.internal.acroformPlugin;
1119+
if (!plugin.xfaStreams) {
1120+
plugin.xfaStreams = [];
1121+
}
1122+
var packet = new AcroFormXFAPacket(normalizeXFAPacketStream(stream));
1123+
packet.scope = scope;
1124+
plugin.xfaStreams.push(packet);
1125+
return packet;
1126+
};
1127+
1128+
var setXFAPayload = function(scope, payload) {
1129+
if (payload === null || typeof payload === "undefined") {
1130+
throw new Error("Invalid XFA payload provided.");
1131+
}
1132+
1133+
var plugin = scope.internal.acroformPlugin;
1134+
if (!plugin.xfaStreams) {
1135+
plugin.xfaStreams = [];
1136+
} else {
1137+
plugin.xfaStreams.length = 0;
1138+
}
1139+
1140+
var dictionary = plugin.acroFormDictionaryRoot;
1141+
if (Array.isArray(payload)) {
1142+
if (payload.length === 0) {
1143+
throw new Error("XFA payload array must contain at least one packet.");
1144+
}
1145+
var xfaArray = [];
1146+
if (Array.isArray(payload[0])) {
1147+
for (var pairIndex = 0; pairIndex < payload.length; pairIndex++) {
1148+
var pair = payload[pairIndex];
1149+
if (!Array.isArray(pair) || pair.length !== 2) {
1150+
throw new Error("XFA payload pairs must be [name, stream] tuples.");
1151+
}
1152+
var tupleName = normalizeXFAPacketName(pair[0]);
1153+
var tupleStream = pair[1];
1154+
xfaArray.push(tupleName);
1155+
xfaArray.push(createXFAPacket(scope, tupleStream));
1156+
}
1157+
} else {
1158+
if (payload.length % 2 !== 0) {
1159+
throw new Error(
1160+
"XFA payload array must contain an even number of entries."
1161+
);
1162+
}
1163+
for (var i = 0; i < payload.length; i += 2) {
1164+
var name = normalizeXFAPacketName(payload[i]);
1165+
var data = payload[i + 1];
1166+
xfaArray.push(name);
1167+
xfaArray.push(createXFAPacket(scope, data));
1168+
}
1169+
}
1170+
dictionary.XFA = xfaArray;
1171+
} else {
1172+
dictionary.XFA = createXFAPacket(scope, payload);
1173+
}
1174+
};
1175+
1176+
var createXFAPacketCallback = function(scope) {
1177+
var packets = scope.internal.acroformPlugin.xfaStreams;
1178+
if (!Array.isArray(packets) || packets.length === 0) {
1179+
return;
1180+
}
1181+
for (var i = 0; i < packets.length; i++) {
1182+
var packet = packets[i];
1183+
if (!packet) {
1184+
continue;
1185+
}
1186+
packet.scope = scope;
1187+
scope.internal.newObjectDeferredBegin(packet.objId, true);
1188+
packet.putStream();
1189+
}
1190+
packets.length = 0;
1191+
};
1192+
9911193
/**
9921194
* The Field Object contains the Variables, that every Field needs
9931195
*
@@ -3107,6 +3309,15 @@ AcroFormAppearance.internal.getHeight = function(formObject) {
31073309
* @param {Object} fieldObject
31083310
* @returns {jsPDF}
31093311
*/
3312+
var addXFA = (jsPDFAPI.addXFA = function(payload, needsRendering) {
3313+
initializeAcroForm(this);
3314+
setXFAPayload(this, payload);
3315+
if (typeof needsRendering !== "undefined") {
3316+
this.internal.acroformPlugin.needRendering = Boolean(needsRendering);
3317+
}
3318+
return this;
3319+
});
3320+
31103321
var addField = (jsPDFAPI.addField = function(fieldObject) {
31113322
initializeAcroForm(this, fieldObject);
31123323

test/specs/acroform.spec.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,4 +1116,31 @@ describe("Module: Acroform Integration Test", function() {
11161116
expect(jsPDF.API.AcroFormRadioButton);
11171117
expect(jsPDF.API.AcroFormTextField);
11181118
});
1119+
1120+
it("addXFA embeds single stream payload", function() {
1121+
var doc = new jsPDF({ compress: false });
1122+
doc.addXFA("<xfa>example</xfa>");
1123+
var pdf = doc.output();
1124+
expect(pdf).toMatch(/\/XFA\s+\d+\s0\sR/);
1125+
expect(pdf).toContain("<xfa>example</xfa>");
1126+
expect(pdf).not.toContain("/NeedRendering true");
1127+
});
1128+
1129+
it("addXFA embeds packet array and sets NeedRendering", function() {
1130+
var doc = new jsPDF({ compress: false });
1131+
doc.addXFA(
1132+
[
1133+
["datasets", "<datasets/>"],
1134+
["template", "<template/>"]
1135+
],
1136+
true
1137+
);
1138+
var pdf = doc.output();
1139+
expect(pdf).toMatch(
1140+
/\/XFA\s*\[\s*\(datasets\)\s+\d+\s0\sR\s+\(template\)\s+\d+\s0\sR\s*\]/
1141+
);
1142+
expect(pdf).toContain("<datasets/>");
1143+
expect(pdf).toContain("<template/>");
1144+
expect(pdf).toContain("/NeedRendering true");
1145+
});
11191146
});

types/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ declare module "jspdf" {
276276
pageNumber: number;
277277
}
278278
// jsPDF plugin: AcroForm
279+
export type XFAPacketStream = string | ArrayBuffer | ArrayBufferView;
280+
export type XFAPacketTuple = [string, XFAPacketStream];
281+
export type XFAPayload =
282+
| XFAPacketStream
283+
| XFAPacketTuple[]
284+
| Array<string | XFAPacketStream>;
279285
export abstract class AcroFormField {}
280286
export interface AcroFormField {
281287
constructor(): AcroFormField;
@@ -1019,6 +1025,7 @@ declare module "jspdf" {
10191025
autoPrint(options?: AutoPrintInput): jsPDF;
10201026

10211027
// jsPDF plugin: AcroForm
1028+
addXFA(payload: XFAPayload, needsRendering?: boolean): jsPDF;
10221029
addField(field: AcroFormField): jsPDF;
10231030

10241031
AcroForm: {

0 commit comments

Comments
 (0)