2020-08-04 02:44:04 +09:00
|
|
|
/* Copyright 2020 Mozilla Foundation
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-11-16 20:18:13 +09:00
|
|
|
import { bytesToString, warn } from "../shared/util.js";
|
2022-02-21 20:45:00 +09:00
|
|
|
import { Dict, Name, Ref } from "./primitives.js";
|
2022-11-16 20:18:13 +09:00
|
|
|
import {
|
|
|
|
escapePDFName,
|
|
|
|
escapeString,
|
|
|
|
numberToString,
|
|
|
|
parseXFAPath,
|
|
|
|
} from "./core_utils.js";
|
2021-02-16 22:13:46 +09:00
|
|
|
import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js";
|
2022-02-17 21:45:42 +09:00
|
|
|
import { BaseStream } from "./base_stream.js";
|
2020-08-04 02:44:04 +09:00
|
|
|
import { calculateMD5 } from "./crypto.js";
|
|
|
|
|
2022-06-01 22:42:46 +09:00
|
|
|
function writeObject(ref, obj, buffer, transform) {
|
|
|
|
buffer.push(`${ref.num} ${ref.gen} obj\n`);
|
|
|
|
if (obj instanceof Dict) {
|
|
|
|
writeDict(obj, buffer, transform);
|
|
|
|
} else if (obj instanceof BaseStream) {
|
|
|
|
writeStream(obj, buffer, transform);
|
|
|
|
}
|
|
|
|
buffer.push("\nendobj\n");
|
|
|
|
}
|
|
|
|
|
2020-08-04 02:44:04 +09:00
|
|
|
function writeDict(dict, buffer, transform) {
|
|
|
|
buffer.push("<<");
|
|
|
|
for (const key of dict.getKeys()) {
|
2020-09-11 19:25:05 +09:00
|
|
|
buffer.push(` /${escapePDFName(key)} `);
|
2020-08-04 02:44:04 +09:00
|
|
|
writeValue(dict.getRaw(key), buffer, transform);
|
|
|
|
}
|
|
|
|
buffer.push(">>");
|
|
|
|
}
|
|
|
|
|
|
|
|
function writeStream(stream, buffer, transform) {
|
2021-05-01 19:11:09 +09:00
|
|
|
let string = stream.getString();
|
2020-08-04 02:44:04 +09:00
|
|
|
if (transform !== null) {
|
|
|
|
string = transform.encryptString(string);
|
|
|
|
}
|
2023-02-08 03:26:46 +09:00
|
|
|
stream.dict.set("Length", string.length);
|
|
|
|
writeDict(stream.dict, buffer, transform);
|
|
|
|
buffer.push(" stream\n", string, "\nendstream");
|
2020-08-04 02:44:04 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
function writeArray(array, buffer, transform) {
|
|
|
|
buffer.push("[");
|
|
|
|
let first = true;
|
|
|
|
for (const val of array) {
|
|
|
|
if (!first) {
|
|
|
|
buffer.push(" ");
|
|
|
|
} else {
|
|
|
|
first = false;
|
|
|
|
}
|
|
|
|
writeValue(val, buffer, transform);
|
|
|
|
}
|
|
|
|
buffer.push("]");
|
|
|
|
}
|
|
|
|
|
|
|
|
function writeValue(value, buffer, transform) {
|
2022-02-21 20:45:00 +09:00
|
|
|
if (value instanceof Name) {
|
2020-09-10 01:39:14 +09:00
|
|
|
buffer.push(`/${escapePDFName(value.name)}`);
|
2022-02-18 20:11:45 +09:00
|
|
|
} else if (value instanceof Ref) {
|
2020-08-04 02:44:04 +09:00
|
|
|
buffer.push(`${value.num} ${value.gen} R`);
|
|
|
|
} else if (Array.isArray(value)) {
|
|
|
|
writeArray(value, buffer, transform);
|
|
|
|
} else if (typeof value === "string") {
|
|
|
|
if (transform !== null) {
|
|
|
|
value = transform.encryptString(value);
|
|
|
|
}
|
|
|
|
buffer.push(`(${escapeString(value)})`);
|
|
|
|
} else if (typeof value === "number") {
|
|
|
|
buffer.push(numberToString(value));
|
2021-09-10 21:10:21 +09:00
|
|
|
} else if (typeof value === "boolean") {
|
2021-09-12 22:45:45 +09:00
|
|
|
buffer.push(value.toString());
|
2022-02-21 20:44:56 +09:00
|
|
|
} else if (value instanceof Dict) {
|
2020-08-04 02:44:04 +09:00
|
|
|
writeDict(value, buffer, transform);
|
2022-02-17 21:45:42 +09:00
|
|
|
} else if (value instanceof BaseStream) {
|
2020-08-04 02:44:04 +09:00
|
|
|
writeStream(value, buffer, transform);
|
[src/core/writer.js] Support `null` values in the `writeValue` function
*This fixes something that I noticed, having recently looked at both the `Lexer.getObj` and `writeValue` code.*
Please note that I unfortunately don't have an example of a form where saving fails without this patch. However, given its overall simplicity and that unit-tests are added, it's hopefully deemed useful to fix this potential issue pro-actively rather than waiting for a bug report.
At this point one might, and rightly so, wonder if there's actually any real-world PDF documents where a `null` value is being used?
Unfortunately the answer is *yes*, and we have a couple of examples in the test-suite (although none of those are related to forms); please see: `issue1015`, `issue2642`, `issue10402`, `issue12823`, `issue13823`, and `pr12564`.
2021-09-13 01:13:36 +09:00
|
|
|
} else if (value === null) {
|
|
|
|
buffer.push("null");
|
2021-09-10 21:10:21 +09:00
|
|
|
} else {
|
|
|
|
warn(`Unhandled value in writer: ${typeof value}, please file a bug.`);
|
2020-08-04 02:44:04 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function writeInt(number, size, offset, buffer) {
|
|
|
|
for (let i = size + offset - 1; i > offset - 1; i--) {
|
|
|
|
buffer[i] = number & 0xff;
|
|
|
|
number >>= 8;
|
|
|
|
}
|
|
|
|
return offset + size;
|
|
|
|
}
|
|
|
|
|
|
|
|
function writeString(string, offset, buffer) {
|
|
|
|
for (let i = 0, len = string.length; i < len; i++) {
|
|
|
|
buffer[offset + i] = string.charCodeAt(i) & 0xff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function computeMD5(filesize, xrefInfo) {
|
|
|
|
const time = Math.floor(Date.now() / 1000);
|
|
|
|
const filename = xrefInfo.filename || "";
|
|
|
|
const md5Buffer = [time.toString(), filename, filesize.toString()];
|
|
|
|
let md5BufferLen = md5Buffer.reduce((a, str) => a + str.length, 0);
|
|
|
|
for (const value of Object.values(xrefInfo.info)) {
|
|
|
|
md5Buffer.push(value);
|
|
|
|
md5BufferLen += value.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
const array = new Uint8Array(md5BufferLen);
|
|
|
|
let offset = 0;
|
|
|
|
for (const str of md5Buffer) {
|
|
|
|
writeString(str, offset, array);
|
|
|
|
offset += str.length;
|
|
|
|
}
|
|
|
|
return bytesToString(calculateMD5(array));
|
|
|
|
}
|
|
|
|
|
2021-06-25 21:31:55 +09:00
|
|
|
function writeXFADataForAcroform(str, newRefs) {
|
2021-01-09 02:40:09 +09:00
|
|
|
const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str);
|
2020-09-09 07:13:52 +09:00
|
|
|
|
|
|
|
for (const { xfa } of newRefs) {
|
|
|
|
if (!xfa) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const { path, value } = xfa;
|
|
|
|
if (!path) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-02-23 06:08:21 +09:00
|
|
|
const nodePath = parseXFAPath(path);
|
|
|
|
let node = xml.documentElement.searchNode(nodePath, 0);
|
|
|
|
if (!node && nodePath.length > 1) {
|
|
|
|
// If we're lucky the last element in the path will identify the node.
|
|
|
|
node = xml.documentElement.searchNode([nodePath.at(-1)], 0);
|
|
|
|
}
|
2020-09-09 07:13:52 +09:00
|
|
|
if (node) {
|
2022-03-27 06:45:50 +09:00
|
|
|
if (Array.isArray(value)) {
|
|
|
|
node.childNodes = value.map(val => new SimpleDOMNode("value", val));
|
|
|
|
} else {
|
|
|
|
node.childNodes = [new SimpleDOMNode("#text", value)];
|
|
|
|
}
|
2020-09-09 07:13:52 +09:00
|
|
|
} else {
|
|
|
|
warn(`Node not found for path: ${path}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const buffer = [];
|
|
|
|
xml.documentElement.dump(buffer);
|
2021-06-25 21:31:55 +09:00
|
|
|
return buffer.join("");
|
|
|
|
}
|
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
function updateAcroform({
|
|
|
|
xref,
|
2021-09-03 21:28:31 +09:00
|
|
|
acroForm,
|
2022-10-19 00:07:47 +09:00
|
|
|
acroFormRef,
|
|
|
|
hasXfa,
|
|
|
|
hasXfaDatasetsEntry,
|
|
|
|
xfaDatasetsRef,
|
|
|
|
needAppearances,
|
2021-09-03 21:28:31 +09:00
|
|
|
newRefs,
|
|
|
|
}) {
|
2022-10-19 00:07:47 +09:00
|
|
|
if (hasXfa && !hasXfaDatasetsEntry && !xfaDatasetsRef) {
|
|
|
|
warn("XFA - Cannot save it");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!needAppearances && (!hasXfa || !xfaDatasetsRef)) {
|
2021-06-25 21:31:55 +09:00
|
|
|
return;
|
|
|
|
}
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
// Clone the acroForm.
|
|
|
|
const dict = new Dict(xref);
|
|
|
|
for (const key of acroForm.getKeys()) {
|
|
|
|
dict.set(key, acroForm.getRaw(key));
|
|
|
|
}
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
if (hasXfa && !hasXfaDatasetsEntry) {
|
2021-09-03 21:28:31 +09:00
|
|
|
// 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.
|
2022-10-19 00:07:47 +09:00
|
|
|
const newXfa = acroForm.get("XFA").slice();
|
2021-09-03 21:28:31 +09:00
|
|
|
newXfa.splice(2, 0, "datasets");
|
2021-09-24 01:18:55 +09:00
|
|
|
newXfa.splice(3, 0, xfaDatasetsRef);
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
dict.set("XFA", newXfa);
|
|
|
|
}
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
if (needAppearances) {
|
|
|
|
dict.set("NeedAppearances", true);
|
|
|
|
}
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
const encrypt = xref.encrypt;
|
|
|
|
let transform = null;
|
|
|
|
if (encrypt) {
|
|
|
|
transform = encrypt.createCipherTransform(acroFormRef.num, acroFormRef.gen);
|
|
|
|
}
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
const buffer = [];
|
|
|
|
writeObject(acroFormRef, dict, buffer, transform);
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
newRefs.push({ ref: acroFormRef, data: buffer.join("") });
|
|
|
|
}
|
2021-09-03 21:28:31 +09:00
|
|
|
|
2022-10-19 00:07:47 +09:00
|
|
|
function updateXFA({ xfaData, xfaDatasetsRef, newRefs, xref }) {
|
2021-06-25 21:31:55 +09:00
|
|
|
if (xfaData === null) {
|
2021-09-24 01:18:55 +09:00
|
|
|
const datasets = xref.fetchIfRef(xfaDatasetsRef);
|
2021-06-25 21:31:55 +09:00
|
|
|
xfaData = writeXFADataForAcroform(datasets.getString(), newRefs);
|
|
|
|
}
|
2020-09-09 07:13:52 +09:00
|
|
|
|
|
|
|
const encrypt = xref.encrypt;
|
|
|
|
if (encrypt) {
|
|
|
|
const transform = encrypt.createCipherTransform(
|
2021-09-24 01:18:55 +09:00
|
|
|
xfaDatasetsRef.num,
|
|
|
|
xfaDatasetsRef.gen
|
2020-09-09 07:13:52 +09:00
|
|
|
);
|
2021-06-25 21:31:55 +09:00
|
|
|
xfaData = transform.encryptString(xfaData);
|
2020-09-09 07:13:52 +09:00
|
|
|
}
|
|
|
|
const data =
|
2021-09-24 01:18:55 +09:00
|
|
|
`${xfaDatasetsRef.num} ${xfaDatasetsRef.gen} obj\n` +
|
2021-06-25 21:31:55 +09:00
|
|
|
`<< /Type /EmbeddedFile /Length ${xfaData.length}>>\nstream\n` +
|
|
|
|
xfaData +
|
2020-09-09 07:13:52 +09:00
|
|
|
"\nendstream\nendobj\n";
|
|
|
|
|
2021-09-24 01:18:55 +09:00
|
|
|
newRefs.push({ ref: xfaDatasetsRef, data });
|
2020-09-09 07:13:52 +09:00
|
|
|
}
|
|
|
|
|
2020-09-09 18:46:02 +09:00
|
|
|
function incrementalUpdate({
|
|
|
|
originalData,
|
|
|
|
xrefInfo,
|
|
|
|
newRefs,
|
|
|
|
xref = null,
|
2021-09-24 01:18:55 +09:00
|
|
|
hasXfa = false,
|
|
|
|
xfaDatasetsRef = null,
|
|
|
|
hasXfaDatasetsEntry = false,
|
2022-10-19 00:07:47 +09:00
|
|
|
needAppearances,
|
2021-09-03 21:28:31 +09:00
|
|
|
acroFormRef = null,
|
|
|
|
acroForm = null,
|
2021-06-25 21:31:55 +09:00
|
|
|
xfaData = null,
|
2020-09-09 18:46:02 +09:00
|
|
|
}) {
|
2022-10-19 00:07:47 +09:00
|
|
|
updateAcroform({
|
|
|
|
xref,
|
|
|
|
acroForm,
|
|
|
|
acroFormRef,
|
|
|
|
hasXfa,
|
|
|
|
hasXfaDatasetsEntry,
|
|
|
|
xfaDatasetsRef,
|
|
|
|
needAppearances,
|
|
|
|
newRefs,
|
|
|
|
});
|
|
|
|
|
2021-09-24 01:18:55 +09:00
|
|
|
if (hasXfa) {
|
|
|
|
updateXFA({
|
|
|
|
xfaData,
|
|
|
|
xfaDatasetsRef,
|
|
|
|
newRefs,
|
|
|
|
xref,
|
|
|
|
});
|
|
|
|
}
|
2020-09-09 07:13:52 +09:00
|
|
|
|
2020-08-04 02:44:04 +09:00
|
|
|
const newXref = new Dict(null);
|
|
|
|
const refForXrefTable = xrefInfo.newRef;
|
|
|
|
|
|
|
|
let buffer, baseOffset;
|
2022-06-09 19:53:39 +09:00
|
|
|
const lastByte = originalData.at(-1);
|
2020-08-04 02:44:04 +09:00
|
|
|
if (lastByte === /* \n */ 0x0a || lastByte === /* \r */ 0x0d) {
|
|
|
|
buffer = [];
|
|
|
|
baseOffset = originalData.length;
|
|
|
|
} else {
|
|
|
|
// Avoid to concatenate %%EOF with an object definition
|
|
|
|
buffer = ["\n"];
|
|
|
|
baseOffset = originalData.length + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
newXref.set("Size", refForXrefTable.num + 1);
|
|
|
|
newXref.set("Prev", xrefInfo.startXRef);
|
|
|
|
newXref.set("Type", Name.get("XRef"));
|
|
|
|
|
|
|
|
if (xrefInfo.rootRef !== null) {
|
|
|
|
newXref.set("Root", xrefInfo.rootRef);
|
|
|
|
}
|
|
|
|
if (xrefInfo.infoRef !== null) {
|
|
|
|
newXref.set("Info", xrefInfo.infoRef);
|
|
|
|
}
|
2021-04-22 19:08:56 +09:00
|
|
|
if (xrefInfo.encryptRef !== null) {
|
|
|
|
newXref.set("Encrypt", xrefInfo.encryptRef);
|
2020-08-04 02:44:04 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add a ref for the new xref and sort them
|
|
|
|
newRefs.push({ ref: refForXrefTable, data: "" });
|
|
|
|
newRefs = newRefs.sort((a, b) => {
|
|
|
|
// compare the refs
|
|
|
|
return a.ref.num - b.ref.num;
|
|
|
|
});
|
|
|
|
|
|
|
|
const xrefTableData = [[0, 1, 0xffff]];
|
|
|
|
const indexes = [0, 1];
|
|
|
|
let maxOffset = 0;
|
|
|
|
for (const { ref, data } of newRefs) {
|
|
|
|
maxOffset = Math.max(maxOffset, baseOffset);
|
|
|
|
xrefTableData.push([1, baseOffset, Math.min(ref.gen, 0xffff)]);
|
|
|
|
baseOffset += data.length;
|
2021-05-24 20:20:19 +09:00
|
|
|
indexes.push(ref.num, 1);
|
2020-08-04 02:44:04 +09:00
|
|
|
buffer.push(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
newXref.set("Index", indexes);
|
|
|
|
|
2021-04-22 19:08:56 +09:00
|
|
|
if (Array.isArray(xrefInfo.fileIds) && xrefInfo.fileIds.length > 0) {
|
2020-08-04 02:44:04 +09:00
|
|
|
const md5 = computeMD5(baseOffset, xrefInfo);
|
|
|
|
newXref.set("ID", [xrefInfo.fileIds[0], md5]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const offsetSize = Math.ceil(Math.log2(maxOffset) / 8);
|
|
|
|
const sizes = [1, offsetSize, 2];
|
|
|
|
const structSize = sizes[0] + sizes[1] + sizes[2];
|
|
|
|
const tableLength = structSize * xrefTableData.length;
|
|
|
|
newXref.set("W", sizes);
|
|
|
|
newXref.set("Length", tableLength);
|
|
|
|
|
|
|
|
buffer.push(`${refForXrefTable.num} ${refForXrefTable.gen} obj\n`);
|
|
|
|
writeDict(newXref, buffer, null);
|
|
|
|
buffer.push(" stream\n");
|
|
|
|
|
|
|
|
const bufferLen = buffer.reduce((a, str) => a + str.length, 0);
|
|
|
|
const footer = `\nendstream\nendobj\nstartxref\n${baseOffset}\n%%EOF\n`;
|
|
|
|
const array = new Uint8Array(
|
|
|
|
originalData.length + bufferLen + tableLength + footer.length
|
|
|
|
);
|
|
|
|
|
|
|
|
// Original data
|
|
|
|
array.set(originalData);
|
|
|
|
let offset = originalData.length;
|
|
|
|
|
|
|
|
// New data
|
|
|
|
for (const str of buffer) {
|
|
|
|
writeString(str, offset, array);
|
|
|
|
offset += str.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
// New xref table
|
|
|
|
for (const [type, objOffset, gen] of xrefTableData) {
|
|
|
|
offset = writeInt(type, sizes[0], offset, array);
|
|
|
|
offset = writeInt(objOffset, sizes[1], offset, array);
|
|
|
|
offset = writeInt(gen, sizes[2], offset, array);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the footer
|
|
|
|
writeString(footer, offset, array);
|
|
|
|
|
|
|
|
return array;
|
|
|
|
}
|
|
|
|
|
2022-06-01 22:42:46 +09:00
|
|
|
export { incrementalUpdate, writeDict, writeObject };
|