From 9bd4b4c2eef6fbfd175a957459b76539765b1522 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 19 Jan 2025 10:49:47 +0100 Subject: [PATCH] Use the built-in `jpg.js` decoder for JPEG images with non-default EXIF orientation (bug 1942064) The new EXIF parsing, during `JpegImage.canUseImageDecoder`, takes *well below* `1 ms` in my testing which should be fast enough to not be an issue. The EXIF parsing is done using https://github.com/exif-js/exif-js/, which is released under the MIT License. Note that while it's available on NPM we're however not using that since: - The package is not fully up-to-date, given the latest changes in the Git repository. - We only need a *subset* of the functionality, and this way we're able to simplify and shorten the code. - Given the age of that project the code isn't directly compatible with JavaScript modules. --- external/exif-js/LICENSE.md | 21 ++++ external/exif-js/exif.js | 235 ++++++++++++++++++++++++++++++++++++ gulpfile.mjs | 1 + src/core/jpg.js | 37 ++++++ test/pdfs/.gitignore | 1 + test/pdfs/bug1942064.pdf | Bin 0 -> 10719 bytes test/test_manifest.json | 7 ++ 7 files changed, 302 insertions(+) create mode 100644 external/exif-js/LICENSE.md create mode 100644 external/exif-js/exif.js create mode 100644 test/pdfs/bug1942064.pdf diff --git a/external/exif-js/LICENSE.md b/external/exif-js/LICENSE.md new file mode 100644 index 0000000000000..455a8fc587c24 --- /dev/null +++ b/external/exif-js/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2008 Jacob Seidelin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/external/exif-js/exif.js b/external/exif-js/exif.js new file mode 100644 index 0000000000000..7857417c06042 --- /dev/null +++ b/external/exif-js/exif.js @@ -0,0 +1,235 @@ +/* The MIT License (MIT) + * + * Copyright (c) 2008 Jacob Seidelin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * This implementation is based on + * https://github.com/exif-js/exif-js/blob/51a8f7d2f3aa71cb03463c84088067c9a4ebe8cb/exif.js + * + * with the following modifications: + * - Removal of, for the PDF.js use-case, unneeded and unused methods. + * - Skip `ExifIFDPointer` tags, `GPSInfoIFDPointer` tags and + * thumbnail image extraction. + * - Removal of debug logging. + * - Make it pass PDF.js linting, and general modernization of the code. + */ + +const TiffTags = { + 0x0100: "ImageWidth", + 0x0101: "ImageHeight", + 0x8769: "ExifIFDPointer", + 0x8825: "GPSInfoIFDPointer", + 0xa005: "InteroperabilityIFDPointer", + 0x0102: "BitsPerSample", + 0x0103: "Compression", + 0x0106: "PhotometricInterpretation", + 0x0112: "Orientation", + 0x0115: "SamplesPerPixel", + 0x011c: "PlanarConfiguration", + 0x0212: "YCbCrSubSampling", + 0x0213: "YCbCrPositioning", + 0x011a: "XResolution", + 0x011b: "YResolution", + 0x0128: "ResolutionUnit", + 0x0111: "StripOffsets", + 0x0116: "RowsPerStrip", + 0x0117: "StripByteCounts", + 0x0201: "JPEGInterchangeFormat", + 0x0202: "JPEGInterchangeFormatLength", + 0x012d: "TransferFunction", + 0x013e: "WhitePoint", + 0x013f: "PrimaryChromaticities", + 0x0211: "YCbCrCoefficients", + 0x0214: "ReferenceBlackWhite", + 0x0132: "DateTime", + 0x010e: "ImageDescription", + 0x010f: "Make", + 0x0110: "Model", + 0x0131: "Software", + 0x013b: "Artist", + 0x8298: "Copyright", +}; + +function readTags(file, tiffStart, dirStart, strings, bigEnd) { + const entries = file.getUint16(dirStart, !bigEnd), + tags = Object.create(null); + + for (let i = 0; i < entries; i++) { + const entryOffset = dirStart + i * 12 + 2; + const tag = strings[file.getUint16(entryOffset, !bigEnd)]; + tags[tag] = readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd); + } + return tags; +} + +function readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd) { + const type = file.getUint16(entryOffset + 2, !bigEnd), + numValues = file.getUint32(entryOffset + 4, !bigEnd), + valueOffset = file.getUint32(entryOffset + 8, !bigEnd) + tiffStart; + let offset, vals, val, n, numerator, denominator; + + switch (type) { + case 1: // byte, 8-bit unsigned int + case 7: // undefined, 8-bit byte, value depending on field + if (numValues === 1) { + return file.getUint8(entryOffset + 8, !bigEnd); + } + offset = numValues > 4 ? valueOffset : entryOffset + 8; + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getUint8(offset + n); + } + return vals; + + case 2: // ascii, 8-bit byte + offset = numValues > 4 ? valueOffset : entryOffset + 8; + return getStringFromDB(file, offset, numValues - 1); + + case 3: // short, 16 bit int + if (numValues === 1) { + return file.getUint16(entryOffset + 8, !bigEnd); + } + offset = numValues > 2 ? valueOffset : entryOffset + 8; + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getUint16(offset + 2 * n, !bigEnd); + } + return vals; + + case 4: // long, 32 bit int + if (numValues === 1) { + return file.getUint32(entryOffset + 8, !bigEnd); + } + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getUint32(valueOffset + 4 * n, !bigEnd); + } + return vals; + + case 5: // rational = two long values, first is numerator, second is denominator + if (numValues === 1) { + numerator = file.getUint32(valueOffset, !bigEnd); + denominator = file.getUint32(valueOffset + 4, !bigEnd); + val = numerator / denominator; + return val; + } + vals = []; + for (n = 0; n < numValues; n++) { + numerator = file.getUint32(valueOffset + 8 * n, !bigEnd); + denominator = file.getUint32(valueOffset + 4 + 8 * n, !bigEnd); + vals[n] = numerator / denominator; + } + return vals; + + case 9: // slong, 32 bit signed int + if (numValues === 1) { + return file.getInt32(entryOffset + 8, !bigEnd); + } + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getInt32(valueOffset + 4 * n, !bigEnd); + } + return vals; + + case 10: // signed rational, two slongs, first is numerator, second is denominator + if (numValues === 1) { + return ( + file.getInt32(valueOffset, !bigEnd) / + file.getInt32(valueOffset + 4, !bigEnd) + ); + } + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = + file.getInt32(valueOffset + 8 * n, !bigEnd) / + file.getInt32(valueOffset + 4 + 8 * n, !bigEnd); + } + return vals; + } + throw new Error(`readTagValue - unsupported type: "${type}".`); +} + +function getStringFromDB(buffer, start, length) { + let outstr = ""; + for (let n = start; n < start + length; n++) { + outstr += String.fromCharCode(buffer.getUint8(n)); + } + return outstr; +} + +/** + * @param [DataView] file + * @param [number] start + * @return {Object | null} + */ +function readEXIFData(file, start) { + if (getStringFromDB(file, start, 4) !== "Exif") { + // console.log("Not valid EXIF data! " + getStringFromDB(file, start, 4)); + return null; + } + + const tiffOffset = start + 6; + let bigEnd; + + // test for TIFF validity and endianness + if (file.getUint16(tiffOffset) === 0x4949) { + bigEnd = false; + } else if (file.getUint16(tiffOffset) === 0x4d4d) { + bigEnd = true; + } else { + // console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); + return null; + } + + if (file.getUint16(tiffOffset + 2, !bigEnd) !== 0x002a) { + // console.log("Not valid TIFF data! (no 0x002A)"); + return null; + } + + const firstIFDOffset = file.getUint32(tiffOffset + 4, !bigEnd); + + if (firstIFDOffset < 0x00000008) { + // console.log( + // "Not valid TIFF data! (First offset less than 8)", + // file.getUint32(tiffOffset + 4, !bigEnd) + // ); + return null; + } + + const tags = readTags( + file, + tiffOffset, + tiffOffset + firstIFDOffset, + TiffTags, + bigEnd + ); + + // Skip `ExifIFDPointer` tags. + // + // Skip `GPSInfoIFDPointer` tags. + // + // Skip thumbnail image extraction. + + return tags; +} + +export { readEXIFData }; diff --git a/gulpfile.mjs b/gulpfile.mjs index a204a39847093..e2fd96cb855c2 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1646,6 +1646,7 @@ function buildLib(defines, dir) { encoding: false, }), gulp.src("test/unit/*.js", { base: ".", encoding: false }), + gulp.src("external/exif-js/*.js", { base: "exif-js/", encoding: false }), gulp.src("external/openjpeg/*.js", { base: "openjpeg/", encoding: false }), ]); diff --git a/src/core/jpg.js b/src/core/jpg.js index a7640a6d46dc1..845a649a6df0d 100644 --- a/src/core/jpg.js +++ b/src/core/jpg.js @@ -15,6 +15,7 @@ import { assert, BaseException, warn } from "../shared/util.js"; import { grayToRGBA } from "../shared/image_utils.js"; +import { readEXIFData } from "../../external/exif-js/exif.js"; import { readUint16 } from "./core_utils.js"; class JpegError extends BaseException { @@ -804,6 +805,16 @@ class JpegImage { this._colorTransform = colorTransform; } + static #getEXIF(data) { + try { + const dv = new DataView(data.buffer, data.byteOffset, data.byteLength); + return readEXIFData(dv, 0); + } catch (ex) { + warn(`#getEXIF - EXIF parsing failed: "${ex}".`); + } + return null; + } + static canUseImageDecoder(data, colorTransform = -1) { let offset = 0; let numComponents = null; @@ -817,6 +828,32 @@ class JpegImage { markerLoop: while (fileMarker !== /* EOI (End of Image) = */ 0xffd9) { switch (fileMarker) { + case 0xffe1: // APP1 - Exif + const { appData, newOffset } = readDataBlock(data, offset); + offset = newOffset; + + // 'Exif\x00\x00' + if ( + appData[0] === 0x45 && + appData[1] === 0x78 && + appData[2] === 0x69 && + appData[3] === 0x66 && + appData[4] === 0 && + appData[5] === 0 + ) { + const exif = this.#getEXIF(appData); + + if (exif) { + // Skip images with non-default orientation + // (fixes bug1942064.pdf). + if (exif.Orientation !== undefined && exif.Orientation !== 1) { + return false; + } + } + } + fileMarker = readUint16(data, offset); + offset += 2; + continue; case 0xffc0: // SOF0 (Start of Frame, Baseline DCT) case 0xffc1: // SOF1 (Start of Frame, Extended DCT) case 0xffc2: // SOF2 (Start of Frame, Progressive DCT) diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 74bc63780255f..9105960cb9147 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -503,6 +503,7 @@ !annotation-line-without-appearance.pdf !bug1669099.pdf !annotation-square-circle.pdf +!bug1942064.pdf !annotation-square-circle-without-appearance.pdf !annotation-stamp.pdf !issue14048.pdf diff --git a/test/pdfs/bug1942064.pdf b/test/pdfs/bug1942064.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cb9dff3344578b4755e304b37e3de956488023ca GIT binary patch literal 10719 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 L_U*=2CW!w9R=zfq literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index 1a7418b9a5c9f..03c40750cac59 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -8997,6 +8997,13 @@ "link": true, "type": "other" }, + { + "id": "bug1942064", + "file": "pdfs/bug1942064.pdf", + "md5": "d50b5ebb8cab1211609d16faa54ec47d", + "rounds": 1, + "type": "eq" + }, { "id": "issue16221-text", "file": "pdfs/issue16221.pdf",