mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
Add support for saving forms
This commit is contained in:
parent
3380f2a7fc
commit
1a6816ba98
16 changed files with 1060 additions and 8 deletions
|
@ -22,6 +22,7 @@ import {
|
|||
AnnotationType,
|
||||
assert,
|
||||
escapeString,
|
||||
getModificationDate,
|
||||
isString,
|
||||
OPS,
|
||||
stringToPDFString,
|
||||
|
@ -29,11 +30,12 @@ import {
|
|||
warn,
|
||||
} from "../shared/util.js";
|
||||
import { Catalog, FileSpec, ObjectLoader } from "./obj.js";
|
||||
import { Dict, isDict, isName, isRef, isStream } from "./primitives.js";
|
||||
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
|
||||
import { ColorSpace } from "./colorspace.js";
|
||||
import { getInheritableProperty } from "./core_utils.js";
|
||||
import { OperatorList } from "./operator_list.js";
|
||||
import { StringStream } from "./stream.js";
|
||||
import { writeDict } from "./writer.js";
|
||||
|
||||
class AnnotationFactory {
|
||||
/**
|
||||
|
@ -68,6 +70,7 @@ class AnnotationFactory {
|
|||
if (!isDict(dict)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const id = isRef(ref) ? ref.toString() : `annot_${idFactory.createObjId()}`;
|
||||
|
||||
// Determine the annotation's subtype.
|
||||
|
@ -77,6 +80,7 @@ class AnnotationFactory {
|
|||
// Return the right annotation object based on the subtype and field type.
|
||||
const parameters = {
|
||||
xref,
|
||||
ref,
|
||||
dict,
|
||||
subtype,
|
||||
id,
|
||||
|
@ -550,6 +554,10 @@ class Annotation {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
async save(evaluator, task, annotationStorage) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -791,6 +799,7 @@ class WidgetAnnotation extends Annotation {
|
|||
|
||||
const dict = params.dict;
|
||||
const data = this.data;
|
||||
this.ref = params.ref;
|
||||
|
||||
data.annotationType = AnnotationType.WIDGET;
|
||||
data.fieldName = this._constructFieldName(dict);
|
||||
|
@ -953,6 +962,78 @@ class WidgetAnnotation extends Annotation {
|
|||
);
|
||||
}
|
||||
|
||||
async save(evaluator, task, annotationStorage) {
|
||||
if (this.data.fieldValue === annotationStorage[this.data.id]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let appearance = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
annotationStorage
|
||||
);
|
||||
if (appearance === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dict = evaluator.xref.fetchIfRef(this.ref);
|
||||
if (!isDict(dict)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bbox = [
|
||||
0,
|
||||
0,
|
||||
this.data.rect[2] - this.data.rect[0],
|
||||
this.data.rect[3] - this.data.rect[1],
|
||||
];
|
||||
|
||||
const newRef = evaluator.xref.getNewRef();
|
||||
const AP = new Dict(evaluator.xref);
|
||||
AP.set("N", newRef);
|
||||
|
||||
const value = annotationStorage[this.data.id];
|
||||
const encrypt = evaluator.xref.encrypt;
|
||||
let originalTransform = null;
|
||||
let newTransform = null;
|
||||
if (encrypt) {
|
||||
originalTransform = encrypt.createCipherTransform(
|
||||
this.ref.num,
|
||||
this.ref.gen
|
||||
);
|
||||
newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
|
||||
appearance = newTransform.encryptString(appearance);
|
||||
}
|
||||
|
||||
dict.set("V", value);
|
||||
dict.set("AP", AP);
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
|
||||
const appearanceDict = new Dict(evaluator.xref);
|
||||
appearanceDict.set("Length", appearance.length);
|
||||
appearanceDict.set("Subtype", Name.get("Form"));
|
||||
appearanceDict.set("Resources", this.fieldResources);
|
||||
appearanceDict.set("BBox", bbox);
|
||||
|
||||
const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`];
|
||||
writeDict(dict, bufferOriginal, originalTransform);
|
||||
bufferOriginal.push("\nendobj\n");
|
||||
|
||||
const bufferNew = [`${newRef.num} ${newRef.gen} obj\n`];
|
||||
writeDict(appearanceDict, bufferNew, newTransform);
|
||||
bufferNew.push(" stream\n");
|
||||
bufferNew.push(appearance);
|
||||
bufferNew.push("\nendstream\nendobj\n");
|
||||
|
||||
return [
|
||||
// data for the original object
|
||||
// V field changed + reference for new AP
|
||||
{ ref: this.ref, data: bufferOriginal.join("") },
|
||||
// data for the new AP
|
||||
{ ref: newRef, data: bufferNew.join("") },
|
||||
];
|
||||
}
|
||||
|
||||
async _getAppearance(evaluator, task, annotationStorage) {
|
||||
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
|
||||
if (!annotationStorage || isPassword) {
|
||||
|
@ -1312,6 +1393,111 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||
);
|
||||
}
|
||||
|
||||
async save(evaluator, task, annotationStorage) {
|
||||
if (this.data.checkBox) {
|
||||
return this._saveCheckbox(evaluator, task, annotationStorage);
|
||||
}
|
||||
|
||||
if (this.data.radioButton) {
|
||||
return this._saveRadioButton(evaluator, task, annotationStorage);
|
||||
}
|
||||
|
||||
return super.save(evaluator, task, annotationStorage);
|
||||
}
|
||||
|
||||
async _saveCheckbox(evaluator, task, annotationStorage) {
|
||||
const defaultValue = this.data.fieldValue && this.data.fieldValue !== "Off";
|
||||
const value = annotationStorage[this.data.id];
|
||||
|
||||
if (defaultValue === value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dict = evaluator.xref.fetchIfRef(this.ref);
|
||||
if (!isDict(dict)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = Name.get(value ? this.data.exportValue : "Off");
|
||||
dict.set("V", name);
|
||||
dict.set("AS", name);
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
|
||||
const encrypt = evaluator.xref.encrypt;
|
||||
let originalTransform = null;
|
||||
if (encrypt) {
|
||||
originalTransform = encrypt.createCipherTransform(
|
||||
this.ref.num,
|
||||
this.ref.gen
|
||||
);
|
||||
}
|
||||
|
||||
const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
|
||||
writeDict(dict, buffer, originalTransform);
|
||||
buffer.push("\nendobj\n");
|
||||
|
||||
return [{ ref: this.ref, data: buffer.join("") }];
|
||||
}
|
||||
|
||||
async _saveRadioButton(evaluator, task, annotationStorage) {
|
||||
const defaultValue = this.data.fieldValue === this.data.buttonValue;
|
||||
const value = annotationStorage[this.data.id];
|
||||
|
||||
if (defaultValue === value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dict = evaluator.xref.fetchIfRef(this.ref);
|
||||
if (!isDict(dict)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = Name.get(value ? this.data.buttonValue : "Off");
|
||||
let parentBuffer = null;
|
||||
const encrypt = evaluator.xref.encrypt;
|
||||
|
||||
if (value) {
|
||||
if (isRef(this.parent)) {
|
||||
const parent = evaluator.xref.fetch(this.parent);
|
||||
let parentTransform = null;
|
||||
if (encrypt) {
|
||||
parentTransform = encrypt.createCipherTransform(
|
||||
this.parent.num,
|
||||
this.parent.gen
|
||||
);
|
||||
}
|
||||
parent.set("V", name);
|
||||
parentBuffer = [`${this.parent.num} ${this.parent.gen} obj\n`];
|
||||
writeDict(parent, parentBuffer, parentTransform);
|
||||
parentBuffer.push("\nendobj\n");
|
||||
} else if (isDict(this.parent)) {
|
||||
this.parent.set("V", name);
|
||||
}
|
||||
}
|
||||
|
||||
dict.set("AS", name);
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
|
||||
let originalTransform = null;
|
||||
if (encrypt) {
|
||||
originalTransform = encrypt.createCipherTransform(
|
||||
this.ref.num,
|
||||
this.ref.gen
|
||||
);
|
||||
}
|
||||
|
||||
const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
|
||||
writeDict(dict, buffer, originalTransform);
|
||||
buffer.push("\nendobj\n");
|
||||
|
||||
const newRefs = [{ ref: this.ref, data: buffer.join("") }];
|
||||
if (parentBuffer !== null) {
|
||||
newRefs.push({ ref: this.parent, data: parentBuffer.join("") });
|
||||
}
|
||||
|
||||
return newRefs;
|
||||
}
|
||||
|
||||
_processCheckBox(params) {
|
||||
if (isName(this.data.fieldValue)) {
|
||||
this.data.fieldValue = this.data.fieldValue.name;
|
||||
|
@ -1354,6 +1540,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||
if (isDict(fieldParent) && fieldParent.has("V")) {
|
||||
const fieldParentValue = fieldParent.get("V");
|
||||
if (isName(fieldParentValue)) {
|
||||
this.parent = params.dict.getRaw("Parent");
|
||||
this.data.fieldValue = fieldParentValue.name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ var ARCFourCipher = (function ARCFourCipherClosure() {
|
|||
},
|
||||
};
|
||||
ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
|
||||
ARCFourCipher.prototype.encrypt = ARCFourCipher.prototype.encryptBlock;
|
||||
|
||||
return ARCFourCipher;
|
||||
})();
|
||||
|
@ -699,6 +700,9 @@ var NullCipher = (function NullCipherClosure() {
|
|||
decryptBlock: function NullCipher_decryptBlock(data) {
|
||||
return data;
|
||||
},
|
||||
encrypt: function NullCipher_encrypt(data) {
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
return NullCipher;
|
||||
|
@ -1097,6 +1101,7 @@ class AESBaseCipher {
|
|||
if (bufferLength < 16) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < 16; ++j) {
|
||||
buffer[j] ^= iv[j];
|
||||
}
|
||||
|
@ -1474,6 +1479,42 @@ var CipherTransform = (function CipherTransformClosure() {
|
|||
data = cipher.decryptBlock(data, true);
|
||||
return bytesToString(data);
|
||||
},
|
||||
encryptString: function CipherTransform_encryptString(s) {
|
||||
const cipher = new this.StringCipherConstructor();
|
||||
if (cipher instanceof AESBaseCipher) {
|
||||
// Append some chars equal to "16 - (M mod 16)"
|
||||
// where M is the string length (see section 7.6.2 in PDF specification)
|
||||
// to have a final string where the length is a multiple of 16.
|
||||
const strLen = s.length;
|
||||
const pad = 16 - (strLen % 16);
|
||||
if (pad !== 16) {
|
||||
s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad));
|
||||
}
|
||||
|
||||
// Generate an initialization vector
|
||||
const iv = new Uint8Array(16);
|
||||
if (typeof crypto !== "undefined") {
|
||||
crypto.getRandomValues(iv);
|
||||
} else {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
iv[i] = Math.floor(256 * Math.random());
|
||||
}
|
||||
}
|
||||
|
||||
let data = stringToBytes(s);
|
||||
data = cipher.encrypt(data, iv);
|
||||
|
||||
const buf = new Uint8Array(16 + data.length);
|
||||
buf.set(iv);
|
||||
buf.set(data, 16);
|
||||
|
||||
return bytesToString(buf);
|
||||
}
|
||||
|
||||
let data = stringToBytes(s);
|
||||
data = cipher.encrypt(data);
|
||||
return bytesToString(data);
|
||||
},
|
||||
};
|
||||
return CipherTransform;
|
||||
})();
|
||||
|
|
|
@ -227,6 +227,43 @@ class Page {
|
|||
return stream;
|
||||
}
|
||||
|
||||
save(handler, task, annotationStorage) {
|
||||
const partialEvaluator = new PartialEvaluator({
|
||||
xref: this.xref,
|
||||
handler,
|
||||
pageIndex: this.pageIndex,
|
||||
idFactory: this._localIdFactory,
|
||||
fontCache: this.fontCache,
|
||||
builtInCMapCache: this.builtInCMapCache,
|
||||
globalImageCache: this.globalImageCache,
|
||||
options: this.evaluatorOptions,
|
||||
});
|
||||
|
||||
// Fetch the page's annotations and save the content
|
||||
// in case of interactive form fields.
|
||||
return this._parsedAnnotations.then(function (annotations) {
|
||||
const newRefsPromises = [];
|
||||
for (const annotation of annotations) {
|
||||
if (!isAnnotationRenderable(annotation, "print")) {
|
||||
continue;
|
||||
}
|
||||
newRefsPromises.push(
|
||||
annotation
|
||||
.save(partialEvaluator, task, annotationStorage)
|
||||
.catch(function (reason) {
|
||||
warn(
|
||||
"save - ignoring annotation data during " +
|
||||
`"${task.name}" task: "${reason}".`
|
||||
);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(newRefsPromises);
|
||||
});
|
||||
}
|
||||
|
||||
loadResources(keys) {
|
||||
if (!this.resourcesPromise) {
|
||||
// TODO: add async `_getInheritableProperty` and remove this.
|
||||
|
|
|
@ -1211,9 +1211,21 @@ var XRef = (function XRefClosure() {
|
|||
streamTypes: Object.create(null),
|
||||
fontTypes: Object.create(null),
|
||||
};
|
||||
this._newRefNum = null;
|
||||
}
|
||||
|
||||
XRef.prototype = {
|
||||
getNewRef: function XRef_getNewRef() {
|
||||
if (this._newRefNum === null) {
|
||||
this._newRefNum = this.entries.length;
|
||||
}
|
||||
return Ref.get(this._newRefNum++, 0);
|
||||
},
|
||||
|
||||
resetNewRef: function XRef_resetNewRef() {
|
||||
this._newRefNum = null;
|
||||
},
|
||||
|
||||
setStartXRef: function XRef_setStartXRef(startXRef) {
|
||||
// Store the starting positions of xref tables as we process them
|
||||
// so we can recover from missing data errors
|
||||
|
|
|
@ -21,9 +21,11 @@ import {
|
|||
getVerbosityLevel,
|
||||
info,
|
||||
InvalidPDFException,
|
||||
isString,
|
||||
MissingPDFException,
|
||||
PasswordException,
|
||||
setVerbosityLevel,
|
||||
stringToPDFString,
|
||||
UnexpectedResponseException,
|
||||
UnknownErrorException,
|
||||
UNSUPPORTED_FEATURES,
|
||||
|
@ -32,6 +34,7 @@ import {
|
|||
} from "../shared/util.js";
|
||||
import { clearPrimitiveCaches, Ref } from "./primitives.js";
|
||||
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
|
||||
import { incrementalUpdate } from "./writer.js";
|
||||
import { isNodeJS } from "../shared/is_node.js";
|
||||
import { MessageHandler } from "../shared/message_handler.js";
|
||||
import { PDFWorkerStream } from "./worker_stream.js";
|
||||
|
@ -513,6 +516,67 @@ class WorkerMessageHandler {
|
|||
});
|
||||
});
|
||||
|
||||
handler.on("SaveDocument", function ({
|
||||
numPages,
|
||||
annotationStorage,
|
||||
filename,
|
||||
}) {
|
||||
pdfManager.requestLoadedStream();
|
||||
const promises = [pdfManager.onLoadedStream()];
|
||||
const document = pdfManager.pdfDocument;
|
||||
for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
|
||||
promises.push(
|
||||
pdfManager.getPage(pageIndex).then(function (page) {
|
||||
const task = new WorkerTask(`Save: page ${pageIndex}`);
|
||||
return page.save(handler, task, annotationStorage);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(([stream, ...refs]) => {
|
||||
let newRefs = [];
|
||||
for (const ref of refs) {
|
||||
newRefs = ref
|
||||
.filter(x => x !== null)
|
||||
.reduce((a, b) => a.concat(b), newRefs);
|
||||
}
|
||||
|
||||
if (newRefs.length === 0) {
|
||||
// No new refs so just return the initial bytes
|
||||
return stream.bytes;
|
||||
}
|
||||
|
||||
const xref = document.xref;
|
||||
let newXrefInfo = Object.create(null);
|
||||
if (xref.trailer) {
|
||||
// Get string info from Info in order to compute fileId
|
||||
const _info = Object.create(null);
|
||||
const xrefInfo = xref.trailer.get("Info") || null;
|
||||
if (xrefInfo) {
|
||||
xrefInfo.forEach((key, value) => {
|
||||
if (isString(key) && isString(value)) {
|
||||
_info[key] = stringToPDFString(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newXrefInfo = {
|
||||
rootRef: xref.trailer.getRaw("Root") || null,
|
||||
encrypt: xref.trailer.getRaw("Encrypt") || null,
|
||||
newRef: xref.getNewRef(),
|
||||
infoRef: xref.trailer.getRaw("Info") || null,
|
||||
info: _info,
|
||||
fileIds: xref.trailer.getRaw("ID") || null,
|
||||
startXRef: document.startXRef,
|
||||
filename,
|
||||
};
|
||||
}
|
||||
xref.resetNewRef();
|
||||
|
||||
return incrementalUpdate(stream.bytes, newXrefInfo, newRefs);
|
||||
});
|
||||
});
|
||||
|
||||
handler.on(
|
||||
"GetOperatorList",
|
||||
function wphSetupRenderPage(data, sink) {
|
||||
|
|
221
src/core/writer.js
Normal file
221
src/core/writer.js
Normal file
|
@ -0,0 +1,221 @@
|
|||
/* 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.
|
||||
*/
|
||||
/* eslint no-var: error */
|
||||
|
||||
import { bytesToString, escapeString } from "../shared/util.js";
|
||||
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
|
||||
import { calculateMD5 } from "./crypto.js";
|
||||
|
||||
function writeDict(dict, buffer, transform) {
|
||||
buffer.push("<<");
|
||||
for (const key of dict.getKeys()) {
|
||||
buffer.push(` /${key} `);
|
||||
writeValue(dict.getRaw(key), buffer, transform);
|
||||
}
|
||||
buffer.push(">>");
|
||||
}
|
||||
|
||||
function writeStream(stream, buffer, transform) {
|
||||
writeDict(stream.dict, buffer, transform);
|
||||
buffer.push(" stream\n");
|
||||
let string = bytesToString(stream.getBytes());
|
||||
if (transform !== null) {
|
||||
string = transform.encryptString(string);
|
||||
}
|
||||
buffer.push(string);
|
||||
buffer.push("\nendstream\n");
|
||||
}
|
||||
|
||||
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 numberToString(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
const roundedValue = Math.round(value * 100);
|
||||
if (roundedValue % 100 === 0) {
|
||||
return (roundedValue / 100).toString();
|
||||
}
|
||||
|
||||
if (roundedValue % 10 === 0) {
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
return value.toFixed(2);
|
||||
}
|
||||
|
||||
function writeValue(value, buffer, transform) {
|
||||
if (isName(value)) {
|
||||
buffer.push(`/${value.name}`);
|
||||
} else if (isRef(value)) {
|
||||
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));
|
||||
} else if (isDict(value)) {
|
||||
writeDict(value, buffer, transform);
|
||||
} else if (isStream(value)) {
|
||||
writeStream(value, buffer, transform);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
function incrementalUpdate(originalData, xrefInfo, newRefs) {
|
||||
const newXref = new Dict(null);
|
||||
const refForXrefTable = xrefInfo.newRef;
|
||||
|
||||
let buffer, baseOffset;
|
||||
const lastByte = originalData[originalData.length - 1];
|
||||
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);
|
||||
}
|
||||
if (xrefInfo.encrypt !== null) {
|
||||
newXref.set("Encrypt", xrefInfo.encrypt);
|
||||
}
|
||||
|
||||
// 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;
|
||||
indexes.push(ref.num);
|
||||
indexes.push(1);
|
||||
buffer.push(data);
|
||||
}
|
||||
|
||||
newXref.set("Index", indexes);
|
||||
|
||||
if (xrefInfo.fileIds.length !== 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
export { writeDict, incrementalUpdate };
|
|
@ -867,6 +867,16 @@ class PDFDocumentProxy {
|
|||
get loadingTask() {
|
||||
return this._transport.loadingTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AnnotationStorage} annotationStorage - Storage for annotation
|
||||
* data in forms.
|
||||
* @returns {Promise<Uint8Array>} A promise that is resolved with a
|
||||
* {Uint8Array} containing the full data of the saved document.
|
||||
*/
|
||||
saveDocument(annotationStorage) {
|
||||
return this._transport.saveDocument(annotationStorage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2520,6 +2530,15 @@ class WorkerTransport {
|
|||
});
|
||||
}
|
||||
|
||||
saveDocument(annotationStorage) {
|
||||
return this.messageHandler.sendWithPromise("SaveDocument", {
|
||||
numPages: this._numPages,
|
||||
annotationStorage:
|
||||
(annotationStorage && annotationStorage.getAll()) || null,
|
||||
filename: this._fullReader.filename,
|
||||
});
|
||||
}
|
||||
|
||||
getDestinations() {
|
||||
return this.messageHandler.sendWithPromise("GetDestinations", null);
|
||||
}
|
||||
|
|
|
@ -832,6 +832,19 @@ function isArrayEqual(arr1, arr2) {
|
|||
});
|
||||
}
|
||||
|
||||
function getModificationDate(date = new Date(Date.now())) {
|
||||
const buffer = [
|
||||
date.getUTCFullYear().toString(),
|
||||
(date.getUTCMonth() + 1).toString().padStart(2, "0"),
|
||||
(date.getUTCDate() + 1).toString().padStart(2, "0"),
|
||||
date.getUTCHours().toString().padStart(2, "0"),
|
||||
date.getUTCMinutes().toString().padStart(2, "0"),
|
||||
date.getUTCSeconds().toString().padStart(2, "0"),
|
||||
];
|
||||
|
||||
return buffer.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise Capability object.
|
||||
*
|
||||
|
@ -934,6 +947,7 @@ export {
|
|||
createPromiseCapability,
|
||||
createObjectURL,
|
||||
escapeString,
|
||||
getModificationDate,
|
||||
getVerbosityLevel,
|
||||
info,
|
||||
isArrayBuffer,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue