[api-minor] Support accessing both the original and modified PDF fingerprint

The PDF.js API has only ever supported accessing the original file ID, however the second one that (should) exist in *modified* documents have thus far been completely inaccessible through the API.
That seems like a simple oversight, caused e.g. by the viewer not needing it, since it really shouldn't hurt to provide API-users with the ability to check if a PDF document has been modified since its creation.[1]

Please refer to https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf#G13.2261661 for additional information.

For an example of how to update existing code to use the new API, please see the changes in the `web/app.js` file included in this patch.

*Please note:* While I'm not sure if we'll ever be able to remove the old `PDFDocumentProxy.fingerprint` getter, given that it's existed since "forever", that probably isn't a big deal given that it's now limited to only `GENERIC`-builds.

---
[1] Although this obviously depends on the PDF software following the specification, by updating the second file ID as intended.
This commit is contained in:
Jonas Jenwald 2021-07-02 16:36:27 +02:00
parent f9d506cf50
commit 661c60ecc9
5 changed files with 77 additions and 33 deletions

View File

@ -1153,30 +1153,44 @@ class PDFDocument {
return shadow(this, "documentInfo", docInfo); return shadow(this, "documentInfo", docInfo);
} }
get fingerprint() { get fingerprints() {
let hash; function validate(data) {
return (
typeof data === "string" &&
data.length > 0 &&
data !== EMPTY_FINGERPRINT
);
}
function hexString(hash) {
const buf = [];
for (let i = 0, ii = hash.length; i < ii; i++) {
const hex = hash[i].toString(16);
buf.push(hex.padStart(2, "0"));
}
return buf.join("");
}
const idArray = this.xref.trailer.get("ID"); const idArray = this.xref.trailer.get("ID");
if ( let hashOriginal, hashModified;
Array.isArray(idArray) && if (Array.isArray(idArray) && validate(idArray[0])) {
idArray[0] && hashOriginal = stringToBytes(idArray[0]);
isString(idArray[0]) &&
idArray[0] !== EMPTY_FINGERPRINT if (idArray[1] !== idArray[0] && validate(idArray[1])) {
) { hashModified = stringToBytes(idArray[1]);
hash = stringToBytes(idArray[0]); }
} else { } else {
hash = calculateMD5( hashOriginal = calculateMD5(
this.stream.getByteRange(0, FINGERPRINT_FIRST_BYTES), this.stream.getByteRange(0, FINGERPRINT_FIRST_BYTES),
0, 0,
FINGERPRINT_FIRST_BYTES FINGERPRINT_FIRST_BYTES
); );
} }
const fingerprintBuf = []; return shadow(this, "fingerprints", [
for (let i = 0, ii = hash.length; i < ii; i++) { hexString(hashOriginal),
const hex = hash[i].toString(16); hashModified ? hexString(hashModified) : null,
fingerprintBuf.push(hex.padStart(2, "0")); ]);
}
return shadow(this, "fingerprint", fingerprintBuf.join(""));
} }
_getLinearizationPage(pageIndex) { _getLinearizationPage(pageIndex) {

View File

@ -199,9 +199,9 @@ class WorkerMessageHandler {
.then(() => finishWorkerTask(task)); .then(() => finishWorkerTask(task));
} }
const [numPages, fingerprint] = await Promise.all([ const [numPages, fingerprints] = await Promise.all([
pdfManager.ensureDoc("numPages"), pdfManager.ensureDoc("numPages"),
pdfManager.ensureDoc("fingerprint"), pdfManager.ensureDoc("fingerprints"),
]); ]);
// Get htmlForXfa after numPages to avoid to create HTML twice. // Get htmlForXfa after numPages to avoid to create HTML twice.
@ -209,7 +209,7 @@ class WorkerMessageHandler {
? await pdfManager.ensureDoc("htmlForXfa") ? await pdfManager.ensureDoc("htmlForXfa")
: null; : null;
return { numPages, fingerprint, htmlForXfa }; return { numPages, fingerprints, htmlForXfa };
} }
function getPdfManager(data, evaluatorOptions, enableXfa) { function getPdfManager(data, evaluatorOptions, enableXfa) {

View File

@ -722,6 +722,18 @@ class PDFDocumentProxy {
constructor(pdfInfo, transport) { constructor(pdfInfo, transport) {
this._pdfInfo = pdfInfo; this._pdfInfo = pdfInfo;
this._transport = transport; this._transport = transport;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
Object.defineProperty(this, "fingerprint", {
get() {
deprecated(
"`PDFDocumentProxy.fingerprint`, " +
"please use `PDFDocumentProxy.fingerprints` instead."
);
return this.fingerprints[0];
},
});
}
} }
/** /**
@ -739,10 +751,13 @@ class PDFDocumentProxy {
} }
/** /**
* @type {string} A (not guaranteed to be) unique ID to identify a PDF. * @type {Array<string, string|null>} A (not guaranteed to be) unique ID to
* identify the PDF document.
* NOTE: The first element will always be defined for all PDF documents,
* whereas the second element is only defined for *modified* PDF documents.
*/ */
get fingerprint() { get fingerprints() {
return this._pdfInfo.fingerprint; return this._pdfInfo.fingerprints;
} }
/** /**

View File

@ -503,10 +503,25 @@ describe("api", function () {
expect(pdfDocument.numPages).toEqual(3); expect(pdfDocument.numPages).toEqual(3);
}); });
it("gets fingerprint", function () { it("gets fingerprints", function () {
expect(pdfDocument.fingerprint).toEqual( expect(pdfDocument.fingerprints).toEqual([
"ea8b35919d6279a369e835bde778611b" "ea8b35919d6279a369e835bde778611b",
null,
]);
});
it("gets fingerprints, from modified document", async function () {
const loadingTask = getDocument(
buildGetDocumentParams("annotation-tx.pdf")
); );
const pdfDoc = await loadingTask.promise;
expect(pdfDoc.fingerprints).toEqual([
"3ebd77c320274649a68f10dbf3b9f882",
"e7087346aa4b4ae0911c1f1643b57345",
]);
await loadingTask.destroy();
}); });
it("gets page", async function () { it("gets page", async function () {
@ -1203,13 +1218,13 @@ describe("api", function () {
loadingTask1.promise, loadingTask1.promise,
loadingTask2.promise, loadingTask2.promise,
]); ]);
const fingerprint1 = data[0].fingerprint; const fingerprints1 = data[0].fingerprints;
const fingerprint2 = data[1].fingerprint; const fingerprints2 = data[1].fingerprints;
expect(fingerprint1).not.toEqual(fingerprint2); expect(fingerprints1).not.toEqual(fingerprints2);
expect(fingerprint1).toEqual("2f695a83d6e7553c24fc08b7ac69712d"); expect(fingerprints1).toEqual(["2f695a83d6e7553c24fc08b7ac69712d", null]);
expect(fingerprint2).toEqual("04c7126b34a46b6d4d6e7a1eff7edcb6"); expect(fingerprints2).toEqual(["04c7126b34a46b6d4d6e7a1eff7edcb6", null]);
await Promise.all([loadingTask1.destroy(), loadingTask2.destroy()]); await Promise.all([loadingTask1.destroy(), loadingTask2.destroy()]);
}); });

View File

@ -1220,7 +1220,7 @@ const PDFViewerApplication = {
pdfThumbnailViewer.setDocument(pdfDocument); pdfThumbnailViewer.setDocument(pdfDocument);
const storedPromise = (this.store = new ViewHistory( const storedPromise = (this.store = new ViewHistory(
pdfDocument.fingerprint pdfDocument.fingerprints[0]
)) ))
.getMultiple({ .getMultiple({
page: null, page: null,
@ -1252,7 +1252,7 @@ const PDFViewerApplication = {
const viewOnLoad = AppOptions.get("viewOnLoad"); const viewOnLoad = AppOptions.get("viewOnLoad");
this._initializePdfHistory({ this._initializePdfHistory({
fingerprint: pdfDocument.fingerprint, fingerprint: pdfDocument.fingerprints[0],
viewOnLoad, viewOnLoad,
initialDest: openAction?.dest, initialDest: openAction?.dest,
}); });
@ -1511,7 +1511,7 @@ const PDFViewerApplication = {
// Provides some basic debug information // Provides some basic debug information
console.log( console.log(
`PDF ${pdfDocument.fingerprint} [${info.PDFFormatVersion} ` + `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` +
`${(info.Producer || "-").trim()} / ${(info.Creator || "-").trim()}] ` + `${(info.Producer || "-").trim()} / ${(info.Creator || "-").trim()}] ` +
`(PDF.js: ${version || "-"})` `(PDF.js: ${version || "-"})`
); );