diff --git a/src/core/catalog.js b/src/core/catalog.js index 37cc32b28..3ca66bace 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -130,6 +130,11 @@ class Catalog { return shadow(this, "acroForm", acroForm); } + get acroFormRef() { + const value = this._catDict.getRaw("AcroForm"); + return shadow(this, "acroFormRef", isRef(value) ? value : null); + } + get metadata() { const streamRef = this._catDict.getRaw("Metadata"); if (!isRef(streamRef)) { diff --git a/src/core/worker.js b/src/core/worker.js index 6a98cb5ef..023d5307c 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -573,6 +573,7 @@ class WorkerMessageHandler { const promises = [ pdfManager.onLoadedStream(), pdfManager.ensureCatalog("acroForm"), + pdfManager.ensureCatalog("acroFormRef"), pdfManager.ensureDoc("xref"), pdfManager.ensureDoc("startXRef"), ]; @@ -597,6 +598,7 @@ class WorkerMessageHandler { return Promise.all(promises).then(function ([ stream, acroForm, + acroFormRef, xref, startXRef, ...refs @@ -621,15 +623,22 @@ class WorkerMessageHandler { } } - const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || []; + const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || null; let xfaDatasets = null; + let hasDatasets = false; if (Array.isArray(xfa)) { for (let i = 0, ii = xfa.length; i < ii; i += 2) { if (xfa[i] === "datasets") { xfaDatasets = xfa[i + 1]; + acroFormRef = null; + hasDatasets = true; } } + if (xfaDatasets === null) { + xfaDatasets = xref.getNewRef(); + } } else { + acroFormRef = null; // TODO: Support XFA streams. warn("Unsupported XFA type."); } @@ -666,6 +675,9 @@ class WorkerMessageHandler { newRefs, xref, datasetsRef: xfaDatasets, + hasDatasets, + acroFormRef, + acroForm, xfaData, }); }); diff --git a/src/core/writer.js b/src/core/writer.js index 1f598b7a0..c6531979d 100644 --- a/src/core/writer.js +++ b/src/core/writer.js @@ -146,10 +146,54 @@ function writeXFADataForAcroform(str, newRefs) { return buffer.join(""); } -function updateXFA(xfaData, datasetsRef, newRefs, xref) { - if (datasetsRef === null || xref === null) { +function updateXFA({ + xfaData, + datasetsRef, + hasDatasets, + acroFormRef, + acroForm, + newRefs, + xref, + xrefInfo, +}) { + if (xref === null) { return; } + + if (!hasDatasets) { + if (!acroFormRef) { + warn("XFA - Cannot save it"); + return; + } + + // We've a XFA array which doesn't contain a datasets entry. + // So we'll update the AcroForm dictionary to have an XFA containing + // the datasets. + const oldXfa = acroForm.get("XFA"); + const newXfa = oldXfa.slice(); + newXfa.splice(2, 0, "datasets"); + newXfa.splice(3, 0, datasetsRef); + + acroForm.set("XFA", newXfa); + + const encrypt = xref.encrypt; + let transform = null; + if (encrypt) { + transform = encrypt.createCipherTransform( + acroFormRef.num, + acroFormRef.gen + ); + } + + const buffer = [`${acroFormRef.num} ${acroFormRef.gen} obj\n`]; + writeDict(acroForm, buffer, transform); + buffer.push("\n"); + + acroForm.set("XFA", oldXfa); + + newRefs.push({ ref: acroFormRef, data: buffer.join("") }); + } + if (xfaData === null) { const datasets = xref.fetchIfRef(datasetsRef); xfaData = writeXFADataForAcroform(datasets.getString(), newRefs); @@ -178,9 +222,21 @@ function incrementalUpdate({ newRefs, xref = null, datasetsRef = null, + hasDatasets = false, + acroFormRef = null, + acroForm = null, xfaData = null, }) { - updateXFA(xfaData, datasetsRef, newRefs, xref); + updateXFA({ + xfaData, + datasetsRef, + hasDatasets, + acroFormRef, + acroForm, + newRefs, + xref, + xrefInfo, + }); const newXref = new Dict(null); const refForXrefTable = xrefInfo.newRef; diff --git a/test/unit/writer_spec.js b/test/unit/writer_spec.js index 767349aed..81d978547 100644 --- a/test/unit/writer_spec.js +++ b/test/unit/writer_spec.js @@ -142,4 +142,67 @@ describe("Writer", function () { expect(buffer.join("")).toEqual(expected); }); }); + + describe("XFA", function () { + it("should update AcroForm when no datasets in XFA array", function () { + const originalData = new Uint8Array(); + const newRefs = []; + + const acroForm = new Dict(null); + acroForm.set("XFA", [ + "preamble", + Ref.get(123, 0), + "postamble", + Ref.get(456, 0), + ]); + const acroFormRef = Ref.get(789, 0); + const datasetsRef = Ref.get(101112, 0); + const xfaData = "world"; + + const xrefInfo = { + newRef: Ref.get(131415, 0), + startXRef: 314, + fileIds: null, + rootRef: null, + infoRef: null, + encryptRef: null, + filename: "foo.pdf", + info: {}, + }; + + let data = incrementalUpdate({ + originalData, + xrefInfo, + newRefs, + datasetsRef, + hasDatasets: false, + acroFormRef, + acroForm, + xfaData, + xref: {}, + }); + data = bytesToString(data); + + const expected = + "\n" + + "789 0 obj\n" + + "<< /XFA [(preamble) 123 0 R (datasets) 101112 0 R (postamble) 456 0 R]>>\n" + + "101112 0 obj\n" + + "<< /Type /EmbeddedFile /Length 20>>\n" + + "stream\n" + + "world\n" + + "endstream\n" + + "endobj\n" + + "131415 0 obj\n" + + "<< /Size 131416 /Prev 314 /Type /XRef /Index [0 1 789 1 101112 1 131415 1] /W [1 1 2] /Length 16>> stream\n" + + "\u0000\u0001ÿÿ\u0001\u0001\u0000\u0000\u0001T\u0000\u0000\u0001²\u0000\u0000\n" + + "endstream\n" + + "endobj\n" + + "startxref\n" + + "178\n" + + "%%EOF\n"; + + expect(data).toEqual(expected); + }); + }); });