From 0351852d746b2b7bf1b555989978fad8d2432edf Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 20 Jan 2020 12:10:16 +0100 Subject: [PATCH 1/4] [api-minor] Decode all JPEG images with the built-in PDF.js decoder in `src/core/jpg.js` Currently some JPEG images are decoded by the built-in PDF.js decoder in `src/core/jpg.js`, while others attempt to use the browser JPEG decoder. This inconsistency seem unfortunate for a number of reasons: - It adds, compared to the other image formats supported in the PDF specification, a fair amount of code/complexity to the image handling in the PDF.js library. - The PDF specification support JPEG images with features, e.g. certain ColorSpaces, that browsers are unable to decode natively. Hence, determining if a JPEG image is possible to decode natively in the browser require a non-trivial amount of parsing. In particular, we're parsing (part of) the raw JPEG data to extract certain marker data and we also need to parse the ColorSpace for the JPEG image. - While some JPEG images may, for all intents and purposes, appear to be natively supported there's still cases where the browser may fail to decode some JPEG images. In order to support those cases, we've had to implement a fallback to the PDF.js JPEG decoder if there's any issues during the native decoding. This also means that it's no longer possible to simply send the JPEG image to the main-thread and continue parsing, but you now need to actually wait for the main-thread to indicate success/failure first. In practice this means that there's a code-path where the worker-thread is forced to wait for the main-thread, while the reverse should *always* be the case. - The native decoding, for anything except the *simplest* of JPEG images, result in increased peak memory usage because there's a handful of short-lived copies of the JPEG data (see PR 11707). Furthermore this also leads to data being *parsed* on the main-thread, rather than the worker-thread, which you usually want to avoid for e.g. performance and UI-reponsiveness reasons. - Not all environments, e.g. Node.js, fully support native JPEG decoding. This has, historically, lead to some issues and support requests. - Different browsers may use different JPEG decoders, possibly leading to images being rendered slightly differently depending on the platform/browser where the PDF.js library is used. Originally the implementation in `src/core/jpg.js` were unable to handle all of the JPEG images in the test-suite, but over the last couple of years I've fixed (hopefully) all of those issues. At this point in time, there's two kinds of failure with this patch: - Changes which are basically imperceivable to the naked eye, where some pixels in the images are essentially off-by-one (in all components), which could probably be attributed to things such as different rounding behaviour in the browser/PDF.js JPEG decoder. This type of "failure" accounts for the *vast* majority of the total number of changes in the reference tests. - Changes where the JPEG images now looks *ever so slightly* blurrier than with the native browser decoder. For quite some time I've just assumed that this pointed to a general deficiency in the `src/core/jpg.js` implementation, however I've discovered when comparing two viewers side-by-side that the differences vanish at higher zoom levels (usually around 200% is enough). Basically if you disable [this downscaling in canvas.js](https://github.com/mozilla/pdf.js/blob/8fb82e939cf0c8618a4e775ff17fc96f726872b5/src/display/canvas.js#L2356-L2395), which is what happens when zooming in, the differences simply vanish! Hence I'm pretty satisfied that there's no significant problems with the `src/core/jpg.js` implementation, and the problems are rather tied to the general quality of the downscaling algorithm used. It could even be seen as a positive that *all* images now share the same downscaling behaviour, since this actually fixes one old bug; see issue 7041. --- examples/node/pdf2svg.js | 3 - src/core/evaluator.js | 105 +-------------------- src/core/image.js | 70 ++++---------- src/core/image_utils.js | 98 +------------------- src/core/jpeg_stream.js | 153 +------------------------------ src/core/worker.js | 1 - src/display/api.js | 103 --------------------- src/display/api_compatibility.js | 5 +- src/display/canvas.js | 44 +-------- src/display/svg.js | 20 ---- src/pdf.js | 2 - src/shared/util.js | 7 -- test/driver.js | 1 - test/test_manifest.json | 15 +-- test/unit/display_svg_spec.js | 7 +- 15 files changed, 32 insertions(+), 602 deletions(-) diff --git a/examples/node/pdf2svg.js b/examples/node/pdf2svg.js index bce275ce62c23..24ca75ba86173 100644 --- a/examples/node/pdf2svg.js +++ b/examples/node/pdf2svg.js @@ -87,9 +87,6 @@ function writeSvgToFile(svgElement, filePath) { var loadingTask = pdfjsLib.getDocument({ data: data, fontExtraProperties: true, - // Try to export JPEG images directly if they don't need any further - // processing. - nativeImageDecoderSupport: pdfjsLib.NativeImageDecoding.DISPLAY, }); loadingTask.promise .then(function (doc) { diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 7bac0edde38d8..2d99beb8ed1fd 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -25,7 +25,6 @@ import { isArrayEqual, isNum, isString, - NativeImageDecoding, OPS, stringToPDFString, TextRenderingMode, @@ -80,9 +79,7 @@ import { DecodeStream } from "./stream.js"; import { getGlyphsUnicode } from "./glyphlist.js"; import { getMetrics } from "./metrics.js"; import { isPDFFunction } from "./function.js"; -import { JpegStream } from "./jpeg_stream.js"; import { MurmurHash3_64 } from "./murmurhash3.js"; -import { NativeImageDecoder } from "./image_utils.js"; import { OperatorList } from "./operator_list.js"; import { PDFImage } from "./image.js"; @@ -91,7 +88,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { forceDataSchema: false, maxImageSize: -1, disableFontFace: false, - nativeImageDecoderSupport: NativeImageDecoding.DECODE, ignoreErrors: false, isEvalSupported: true, fontExtraProperties: false, @@ -450,7 +446,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { operatorList, cacheKey, imageCache, - forceDisableNativeImageDecoder = false, }) { var dict = image.dict; const imageRef = dict.objId; @@ -510,13 +505,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var SMALL_IMAGE_DIMENSIONS = 200; // Inlining small images into the queue as RGB data - if ( - isInline && - !softMask && - !mask && - !(image instanceof JpegStream) && - w + h < SMALL_IMAGE_DIMENSIONS - ) { + if (isInline && !softMask && !mask && w + h < SMALL_IMAGE_DIMENSIONS) { const imageObj = new PDFImage({ xref: this.xref, res: resources, @@ -531,20 +520,12 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { return undefined; } - let nativeImageDecoderSupport = forceDisableNativeImageDecoder - ? NativeImageDecoding.NONE - : this.options.nativeImageDecoderSupport; // If there is no imageMask, create the PDFImage and a lot // of image processing can be done here. let objId = `img_${this.idFactory.createObjId()}`, cacheGlobally = false; if (this.parsingType3Font) { - assert( - nativeImageDecoderSupport === NativeImageDecoding.NONE, - "Type3 image resources should be completely decoded in the worker." - ); - objId = `${this.idFactory.getDocId()}_type3res_${objId}`; } else if (imageRef) { cacheGlobally = this.globalImageCache.shouldCache( @@ -553,102 +534,19 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { ); if (cacheGlobally) { - // Ensure that the image is *completely* decoded on the worker-thread, - // in order to simplify the caching/rendering code on the main-thread. - nativeImageDecoderSupport = NativeImageDecoding.NONE; - objId = `${this.idFactory.getDocId()}_${objId}`; } } - if ( - nativeImageDecoderSupport !== NativeImageDecoding.NONE && - !softMask && - !mask && - image instanceof JpegStream && - image.maybeValidDimensions && - NativeImageDecoder.isSupported( - image, - this.xref, - resources, - this.pdfFunctionFactory - ) - ) { - // These JPEGs don't need any more processing so we can just send it. - return this.handler - .sendWithPromise("obj", [ - objId, - this.pageIndex, - "JpegStream", - image.getIR(this.options.forceDataSchema), - ]) - .then( - () => { - // Only add the dependency once we know that the native JPEG - // decoding succeeded, to ensure that rendering will always - // complete. - operatorList.addDependency(objId); - args = [objId, w, h]; - - operatorList.addOp(OPS.paintJpegXObject, args); - if (cacheKey) { - imageCache[cacheKey] = { - fn: OPS.paintJpegXObject, - args, - }; - - if (imageRef) { - this.globalImageCache.addPageIndex(imageRef, this.pageIndex); - } - } - }, - reason => { - warn( - "Native JPEG decoding failed -- trying to recover: " + - (reason && reason.message) - ); - // Try to decode the JPEG image with the built-in decoder instead. - return this.buildPaintImageXObject({ - resources, - image, - isInline, - operatorList, - cacheKey, - imageCache, - forceDisableNativeImageDecoder: true, - }); - } - ); - } - - // Creates native image decoder only if a JPEG image or mask is present. - var nativeImageDecoder = null; - if ( - nativeImageDecoderSupport === NativeImageDecoding.DECODE && - (image instanceof JpegStream || - mask instanceof JpegStream || - softMask instanceof JpegStream) - ) { - nativeImageDecoder = new NativeImageDecoder({ - xref: this.xref, - resources, - handler: this.handler, - forceDataSchema: this.options.forceDataSchema, - pdfFunctionFactory: this.pdfFunctionFactory, - }); - } - // Ensure that the dependency is added before the image is decoded. operatorList.addDependency(objId); args = [objId, w, h]; const imgPromise = PDFImage.buildImage({ - handler: this.handler, xref: this.xref, res: resources, image, isInline, - nativeDecoder: nativeImageDecoder, pdfFunctionFactory: this.pdfFunctionFactory, }) .then(imageObj => { @@ -3393,7 +3291,6 @@ class TranslatedFont { // the rendering code on the main-thread (see issue10717.pdf). var type3Options = Object.create(evaluator.options); type3Options.ignoreErrors = false; - type3Options.nativeImageDecoderSupport = NativeImageDecoding.NONE; var type3Evaluator = evaluator.clone(type3Options); type3Evaluator.parsingType3Font = true; diff --git a/src/core/image.js b/src/core/image.js index d0cecd766811f..bab5affa0e4f5 100644 --- a/src/core/image.js +++ b/src/core/image.js @@ -21,23 +21,6 @@ import { JpegStream } from "./jpeg_stream.js"; import { JpxImage } from "./jpx.js"; var PDFImage = (function PDFImageClosure() { - /** - * Decodes the image using native decoder if possible. Resolves the promise - * when the image data is ready. - */ - function handleImageData(image, nativeDecoder) { - if (nativeDecoder && nativeDecoder.canDecode(image)) { - return nativeDecoder.decode(image).catch(reason => { - warn( - "Native image decoding failed -- trying to recover: " + - (reason && reason.message) - ); - return image; - }); - } - return Promise.resolve(image); - } - /** * Decode and clamp a value. The formula is different from the spec because we * don't decode to float range [0,1], we decode it in the [0,max] range. @@ -266,51 +249,38 @@ var PDFImage = (function PDFImageClosure() { * with a PDFImage when the image is ready to be used. */ PDFImage.buildImage = function ({ - handler, xref, res, image, isInline = false, - nativeDecoder = null, pdfFunctionFactory, }) { - var imagePromise = handleImageData(image, nativeDecoder); - var smaskPromise; - var maskPromise; + const imageData = image; + let smaskData = null; + let maskData = null; - var smask = image.dict.get("SMask"); - var mask = image.dict.get("Mask"); + const smask = image.dict.get("SMask"); + const mask = image.dict.get("Mask"); if (smask) { - smaskPromise = handleImageData(smask, nativeDecoder); - maskPromise = Promise.resolve(null); - } else { - smaskPromise = Promise.resolve(null); - if (mask) { - if (isStream(mask)) { - maskPromise = handleImageData(mask, nativeDecoder); - } else if (Array.isArray(mask)) { - maskPromise = Promise.resolve(mask); - } else { - warn("Unsupported mask format."); - maskPromise = Promise.resolve(null); - } + smaskData = smask; + } else if (mask) { + if (isStream(mask) || Array.isArray(mask)) { + maskData = mask; } else { - maskPromise = Promise.resolve(null); + warn("Unsupported mask format."); } } - return Promise.all([imagePromise, smaskPromise, maskPromise]).then( - function ([imageData, smaskData, maskData]) { - return new PDFImage({ - xref, - res, - image: imageData, - isInline, - smask: smaskData, - mask: maskData, - pdfFunctionFactory, - }); - } + return Promise.resolve( + new PDFImage({ + xref, + res, + image: imageData, + isInline, + smask: smaskData, + mask: maskData, + pdfFunctionFactory, + }) ); }; diff --git a/src/core/image_utils.js b/src/core/image_utils.js index 1a6a8bd87a630..2e89e1adc988d 100644 --- a/src/core/image_utils.js +++ b/src/core/image_utils.js @@ -15,103 +15,7 @@ /* eslint no-var: error */ import { assert, info, shadow } from "../shared/util.js"; -import { ColorSpace } from "./colorspace.js"; -import { JpegStream } from "./jpeg_stream.js"; import { RefSetCache } from "./primitives.js"; -import { Stream } from "./stream.js"; - -class NativeImageDecoder { - constructor({ - xref, - resources, - handler, - forceDataSchema = false, - pdfFunctionFactory, - }) { - this.xref = xref; - this.resources = resources; - this.handler = handler; - this.forceDataSchema = forceDataSchema; - this.pdfFunctionFactory = pdfFunctionFactory; - } - - canDecode(image) { - return ( - image instanceof JpegStream && - image.maybeValidDimensions && - NativeImageDecoder.isDecodable( - image, - this.xref, - this.resources, - this.pdfFunctionFactory - ) - ); - } - - decode(image) { - // For natively supported JPEGs send them to the main thread for decoding. - const dict = image.dict; - let colorSpace = dict.get("ColorSpace", "CS"); - colorSpace = ColorSpace.parse( - colorSpace, - this.xref, - this.resources, - this.pdfFunctionFactory - ); - - return this.handler - .sendWithPromise("JpegDecode", [ - image.getIR(this.forceDataSchema), - colorSpace.numComps, - ]) - .then(function ({ data, width, height }) { - return new Stream(data, 0, data.length, dict); - }); - } - - /** - * Checks if the image can be decoded and displayed by the browser without any - * further processing such as color space conversions. - */ - static isSupported(image, xref, res, pdfFunctionFactory) { - const dict = image.dict; - if (dict.has("DecodeParms") || dict.has("DP")) { - return false; - } - const cs = ColorSpace.parse( - dict.get("ColorSpace", "CS"), - xref, - res, - pdfFunctionFactory - ); - // isDefaultDecode() of DeviceGray and DeviceRGB needs no `bpc` argument. - return ( - (cs.name === "DeviceGray" || cs.name === "DeviceRGB") && - cs.isDefaultDecode(dict.getArray("Decode", "D")) - ); - } - - /** - * Checks if the image can be decoded by the browser. - */ - static isDecodable(image, xref, res, pdfFunctionFactory) { - const dict = image.dict; - if (dict.has("DecodeParms") || dict.has("DP")) { - return false; - } - const cs = ColorSpace.parse( - dict.get("ColorSpace", "CS"), - xref, - res, - pdfFunctionFactory - ); - const bpc = dict.get("BitsPerComponent", "BPC") || 1; - return ( - (cs.numComps === 1 || cs.numComps === 3) && - cs.isDefaultDecode(dict.getArray("Decode", "D"), bpc) - ); - } -} class GlobalImageCache { static get NUM_PAGES_THRESHOLD() { @@ -205,4 +109,4 @@ class GlobalImageCache { } } -export { NativeImageDecoder, GlobalImageCache }; +export { GlobalImageCache }; diff --git a/src/core/jpeg_stream.js b/src/core/jpeg_stream.js index eb8e9a5f7fbb6..f936626ea3c03 100644 --- a/src/core/jpeg_stream.js +++ b/src/core/jpeg_stream.js @@ -13,17 +13,14 @@ * limitations under the License. */ -import { createObjectURL, shadow } from "../shared/util.js"; import { DecodeStream } from "./stream.js"; import { isDict } from "./primitives.js"; import { JpegImage } from "./jpg.js"; +import { shadow } from "../shared/util.js"; /** - * Depending on the type of JPEG a JpegStream is handled in different ways. For - * JPEG's that are supported natively such as DeviceGray and DeviceRGB the image - * data is stored and then loaded by the browser. For unsupported JPEG's we use - * a library to decode these images and the stream behaves like all the other - * DecodeStreams. + * For JPEG's we use a library to decode these images and the stream behaves + * like all the other DecodeStreams. */ const JpegStream = (function JpegStreamClosure() { // eslint-disable-next-line no-shadow @@ -110,150 +107,6 @@ const JpegStream = (function JpegStreamClosure() { this.eof = true; }; - Object.defineProperty(JpegStream.prototype, "maybeValidDimensions", { - get: function JpegStream_maybeValidDimensions() { - const { dict, stream } = this; - const dictHeight = dict.get("Height", "H"); - const startPos = stream.pos; - - let validDimensions = true, - foundSOF = false, - b; - while ((b = stream.getByte()) !== -1) { - if (b !== 0xff) { - // Not a valid marker. - continue; - } - switch (stream.getByte()) { - case 0xc0: // SOF0 - case 0xc1: // SOF1 - case 0xc2: // SOF2 - // These three SOF{n} markers are the only ones that the built-in - // PDF.js JPEG decoder currently supports. - foundSOF = true; - - stream.pos += 2; // Skip marker length. - stream.pos += 1; // Skip precision. - const scanLines = stream.getUint16(); - const samplesPerLine = stream.getUint16(); - - // Letting the browser handle the JPEG decoding, on the main-thread, - // will cause a *large* increase in peak memory usage since there's - // a handful of short-lived copies of the image data. For very big - // JPEG images, always let the PDF.js image decoder handle them to - // reduce overall memory usage during decoding (see issue 11694). - if (scanLines * samplesPerLine > 1e6) { - validDimensions = false; - break; - } - - // The "normal" case, where the image data and dictionary agrees. - if (scanLines === dictHeight) { - break; - } - // A DNL (Define Number of Lines) marker is expected, - // which browsers (usually) cannot decode natively. - if (scanLines === 0) { - validDimensions = false; - break; - } - // The dimensions of the image, among other properties, should - // always be taken from the image data *itself* rather than the - // XObject dictionary. However there's cases of corrupt images that - // browsers cannot decode natively, for example: - // - JPEG images with DNL markers, where the SOF `scanLines` - // parameter has an unexpected value (see issue 8614). - // - JPEG images with too large SOF `scanLines` parameter, where - // the EOI marker is encountered prematurely (see issue 10880). - // In an attempt to handle these kinds of corrupt images, compare - // the dimensions in the image data with the dictionary and *always* - // let the PDF.js JPEG decoder (rather than the browser) handle the - // image if the difference is larger than one order of magnitude - // (since that would generally suggest that something is off). - if (scanLines > dictHeight * 10) { - validDimensions = false; - break; - } - break; - - case 0xc3: // SOF3 - /* falls through */ - case 0xc5: // SOF5 - case 0xc6: // SOF6 - case 0xc7: // SOF7 - /* falls through */ - case 0xc9: // SOF9 - case 0xca: // SOF10 - case 0xcb: // SOF11 - /* falls through */ - case 0xcd: // SOF13 - case 0xce: // SOF14 - case 0xcf: // SOF15 - foundSOF = true; - break; - - case 0xc4: // DHT - case 0xcc: // DAC - /* falls through */ - case 0xda: // SOS - case 0xdb: // DQT - case 0xdc: // DNL - case 0xdd: // DRI - case 0xde: // DHP - case 0xdf: // EXP - /* falls through */ - case 0xe0: // APP0 - case 0xe1: // APP1 - case 0xe2: // APP2 - case 0xe3: // APP3 - case 0xe4: // APP4 - case 0xe5: // APP5 - case 0xe6: // APP6 - case 0xe7: // APP7 - case 0xe8: // APP8 - case 0xe9: // APP9 - case 0xea: // APP10 - case 0xeb: // APP11 - case 0xec: // APP12 - case 0xed: // APP13 - case 0xee: // APP14 - case 0xef: // APP15 - /* falls through */ - case 0xfe: // COM - const markerLength = stream.getUint16(); - if (markerLength > 2) { - stream.skip(markerLength - 2); // Jump to the next marker. - } else { - // The marker length is invalid, resetting the stream position. - stream.skip(-2); - } - break; - - case 0xff: // Fill byte. - // Avoid skipping a valid marker, resetting the stream position. - stream.skip(-1); - break; - - case 0xd9: // EOI - foundSOF = true; - break; - } - if (foundSOF) { - break; - } - } - // Finally, don't forget to reset the stream position. - stream.pos = startPos; - - return shadow(this, "maybeValidDimensions", validDimensions); - }, - configurable: true, - }); - - JpegStream.prototype.getIR = function (forceDataSchema = false) { - return createObjectURL(this.bytes, "image/jpeg", forceDataSchema); - }; - return JpegStream; })(); diff --git a/src/core/worker.js b/src/core/worker.js index 91a70db63d597..fb37d7ee3677d 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -402,7 +402,6 @@ var WorkerMessageHandler = { forceDataSchema: data.disableCreateObjectURL, maxImageSize: data.maxImageSize, disableFontFace: data.disableFontFace, - nativeImageDecoderSupport: data.nativeImageDecoderSupport, ignoreErrors: data.ignoreErrors, isEvalSupported: data.isEvalSupported, fontExtraProperties: data.fontExtraProperties, diff --git a/src/display/api.js b/src/display/api.js index a99cffeef896a..5d37779c8d053 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -28,7 +28,6 @@ import { isArrayBuffer, isSameOrigin, MissingPDFException, - NativeImageDecoding, PasswordException, setVerbosityLevel, shadow, @@ -116,14 +115,6 @@ function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) { * @property {string} [docBaseUrl] - The base URL of the document, * used when attempting to recover valid absolute URLs for annotations, and * outline items, that (incorrectly) only specify relative URLs. - * @property {string} [nativeImageDecoderSupport] - Strategy for - * decoding certain (simple) JPEG images in the browser. This is useful for - * environments without DOM image and canvas support, such as e.g. Node.js. - * Valid values are 'decode', 'display' or 'none'; where 'decode' is intended - * for browsers with full image/canvas support, 'display' for environments - * with limited image support through stubs (useful for SVG conversion), - * and 'none' where JPEG images will be decoded entirely by PDF.js. - * The default value is 'decode'. * @property {string} [cMapUrl] - The URL where the predefined * Adobe CMaps are located. Include trailing slash. * @property {boolean} [cMapPacked] - Specifies if the Adobe CMaps are @@ -260,15 +251,6 @@ function getDocument(src) { params.fontExtraProperties = params.fontExtraProperties === true; params.pdfBug = params.pdfBug === true; - const NativeImageDecoderValues = Object.values(NativeImageDecoding); - if ( - params.nativeImageDecoderSupport === undefined || - !NativeImageDecoderValues.includes(params.nativeImageDecoderSupport) - ) { - params.nativeImageDecoderSupport = - apiCompatibilityParams.nativeImageDecoderSupport || - NativeImageDecoding.DECODE; - } if (!Number.isInteger(params.maxImageSize)) { params.maxImageSize = -1; } @@ -417,7 +399,6 @@ function _fetchDocument(worker, source, pdfDataRangeTransport, docId) { disableCreateObjectURL: source.disableCreateObjectURL, postMessageTransfers: worker.postMessageTransfers, docBaseUrl: source.docBaseUrl, - nativeImageDecoderSupport: source.nativeImageDecoderSupport, ignoreErrors: source.ignoreErrors, isEvalSupported: source.isEvalSupported, fontExtraProperties: source.fontExtraProperties, @@ -2309,26 +2290,6 @@ class WorkerTransport { } switch (type) { - case "JpegStream": - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = function () { - resolve(img); - }; - img.onerror = function () { - // Note that when the browser image loading/decoding fails, - // we'll fallback to the built-in PDF.js JPEG decoder; see - // `PartialEvaluator.buildPaintImageXObject` in the - // `src/core/evaluator.js` file. - reject(new Error("Error during JPEG image loading")); - - // Always remember to release the image data if errors occurred. - releaseImageResources(img); - }; - img.src = imageData; - }).then(img => { - pageProxy.objs.resolve(id, img); - }); case "Image": pageProxy.objs.resolve(id, imageData); @@ -2366,69 +2327,6 @@ class WorkerTransport { this._onUnsupportedFeature.bind(this) ); - messageHandler.on("JpegDecode", ([imageUrl, components]) => { - if (this.destroyed) { - return Promise.reject(new Error("Worker was destroyed")); - } - - if (typeof document === "undefined") { - // Make sure that this code is not executing in node.js, as - // it's using DOM image, and there is no library to support that. - return Promise.reject(new Error('"document" is not defined.')); - } - - if (components !== 3 && components !== 1) { - return Promise.reject( - new Error("Only 3 components or 1 component can be returned") - ); - } - - return new Promise(function (resolve, reject) { - const img = new Image(); - img.onload = function () { - const { width, height } = img; - const size = width * height; - const rgbaLength = size * 4; - const buf = new Uint8ClampedArray(size * components); - let tmpCanvas = document.createElement("canvas"); - tmpCanvas.width = width; - tmpCanvas.height = height; - let tmpCtx = tmpCanvas.getContext("2d"); - tmpCtx.drawImage(img, 0, 0); - const data = tmpCtx.getImageData(0, 0, width, height).data; - - if (components === 3) { - for (let i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { - buf[j] = data[i]; - buf[j + 1] = data[i + 1]; - buf[j + 2] = data[i + 2]; - } - } else if (components === 1) { - for (let i = 0, j = 0; i < rgbaLength; i += 4, j++) { - buf[j] = data[i]; - } - } - resolve({ data: buf, width, height }); - - // Immediately release the image data once decoding has finished. - releaseImageResources(img); - // Zeroing the width and height cause Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - tmpCanvas.width = 0; - tmpCanvas.height = 0; - tmpCanvas = null; - tmpCtx = null; - }; - img.onerror = function () { - reject(new Error("JpegDecode failed to load image")); - - // Always remember to release the image data if errors occurred. - releaseImageResources(img); - }; - img.src = imageUrl; - }); - }); - messageHandler.on("FetchBuiltInCMap", (data, sink) => { if (this.destroyed) { sink.error(new Error("Worker was destroyed")); @@ -2610,7 +2508,6 @@ class WorkerTransport { disableAutoFetch: params.disableAutoFetch, disableCreateObjectURL: params.disableCreateObjectURL, disableFontFace: params.disableFontFace, - nativeImageDecoderSupport: params.nativeImageDecoderSupport, }); } } diff --git a/src/display/api_compatibility.js b/src/display/api_compatibility.js index 01c185b991960..dd0a35641c94d 100644 --- a/src/display/api_compatibility.js +++ b/src/display/api_compatibility.js @@ -33,11 +33,10 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { })(); // Support: Node.js - (function checkFontFaceAndImage() { - // Node.js is missing native support for `@font-face` and `Image`. + (function checkFontFace() { + // Node.js is missing native support for `@font-face`. if (isNodeJS) { compatibilityParams.disableFontFace = true; - compatibilityParams.nativeImageDecoderSupport = "none"; } })(); } diff --git a/src/display/canvas.js b/src/display/canvas.js index 0cd2108f34dba..8cae8db99de58 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -2113,46 +2113,6 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.restore(); }, - paintJpegXObject: function CanvasGraphics_paintJpegXObject(objId, w, h) { - const domImage = objId.startsWith("g_") - ? this.commonObjs.get(objId) - : this.objs.get(objId); - if (!domImage) { - warn("Dependent image isn't ready yet"); - return; - } - - this.save(); - - var ctx = this.ctx; - // scale the image to the unit square - ctx.scale(1 / w, -1 / h); - - ctx.drawImage( - domImage, - 0, - 0, - domImage.width, - domImage.height, - 0, - -h, - w, - h - ); - if (this.imageLayer) { - var currentTransform = ctx.mozCurrentTransformInverse; - var position = this.getCanvasPosition(0, 0); - this.imageLayer.appendImage({ - objId, - left: position[0], - top: position[1], - width: w / currentTransform[0], - height: h / currentTransform[3], - }); - } - this.restore(); - }, - paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) { var ctx = this.ctx; var width = img.width, @@ -2353,9 +2313,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var paintWidth = width, paintHeight = height; var tmpCanvasId = "prescale1"; - // Vertial or horizontal scaling shall not be more than 2 to not loose the + // Vertical or horizontal scaling shall not be more than 2 to not lose the // pixels during drawImage operation, painting on the temporary canvas(es) - // that are twice smaller in size + // that are twice smaller in size. while ( (widthScale > 2 && paintWidth > 1) || (heightScale > 2 && paintHeight > 1) diff --git a/src/display/svg.js b/src/display/svg.js index 8e27f38d5c1d1..dccd3a22ca7bc 100644 --- a/src/display/svg.js +++ b/src/display/svg.js @@ -664,9 +664,6 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { case OPS.paintSolidColorImageMask: this.paintSolidColorImageMask(); break; - case OPS.paintJpegXObject: - this.paintJpegXObject(args[0], args[1], args[2]); - break; case OPS.paintImageXObject: this.paintImageXObject(args[0]); break; @@ -1559,23 +1556,6 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { this._ensureTransformGroup().appendChild(rect); } - paintJpegXObject(objId, w, h) { - const imgObj = this.objs.get(objId); - const imgEl = this.svgFactory.createElement("svg:image"); - imgEl.setAttributeNS(XLINK_NS, "xlink:href", imgObj.src); - imgEl.setAttributeNS(null, "width", pf(w)); - imgEl.setAttributeNS(null, "height", pf(h)); - imgEl.setAttributeNS(null, "x", "0"); - imgEl.setAttributeNS(null, "y", pf(-h)); - imgEl.setAttributeNS( - null, - "transform", - `scale(${pf(1 / w)} ${pf(-1 / h)})` - ); - - this._ensureTransformGroup().appendChild(imgEl); - } - paintImageXObject(objId) { const imgData = this.objs.get(objId); if (!imgData) { diff --git a/src/pdf.js b/src/pdf.js index bd6f129f112a9..b318f4675335d 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -39,7 +39,6 @@ import { createValidAbsoluteUrl, InvalidPDFException, MissingPDFException, - NativeImageDecoding, OPS, PasswordResponses, PermissionFlag, @@ -143,7 +142,6 @@ export { createValidAbsoluteUrl, InvalidPDFException, MissingPDFException, - NativeImageDecoding, OPS, PasswordResponses, PermissionFlag, diff --git a/src/shared/util.js b/src/shared/util.js index dcbf5b28dfb7b..eeb666d204c30 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -19,12 +19,6 @@ import "./compatibility.js"; const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; -const NativeImageDecoding = { - NONE: "none", - DECODE: "decode", - DISPLAY: "display", -}; - // Permission flags from Table 22, Section 7.6.3.2 of the PDF specification. const PermissionFlag = { PRINT: 0x04, @@ -917,7 +911,6 @@ export { AbortException, InvalidPDFException, MissingPDFException, - NativeImageDecoding, PasswordException, PasswordResponses, PermissionFlag, diff --git a/test/driver.js b/test/driver.js index 53729a785690f..8a2720892a490 100644 --- a/test/driver.js +++ b/test/driver.js @@ -387,7 +387,6 @@ var Driver = (function DriverClosure() { const loadingTask = pdfjsLib.getDocument({ url: absoluteUrl, password: task.password, - nativeImageDecoderSupport: task.nativeImageDecoderSupport, cMapUrl: CMAP_URL, cMapPacked: CMAP_PACKED, disableRange: task.disableRange, diff --git a/test/test_manifest.json b/test/test_manifest.json index e804550d9421e..69b35409f8e9a 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1631,8 +1631,7 @@ "link": true, "firstPage": 2, "lastPage": 2, - "type": "eq", - "nativeImageDecoderSupport": "none" + "type": "eq" }, { "id": "issue10529", "file": "pdfs/issue10529.pdf", @@ -4175,22 +4174,12 @@ "link": true, "type": "eq" }, - { "id": "issue4926-built-in-jpg", - "file": "pdfs/issue4926.pdf", - "md5": "ed881c8ea2f9bc4be94ecb7f2b2c149b", - "rounds": 1, - "link": true, - "lastPage": 1, - "type": "eq", - "nativeImageDecoderSupport": "none" - }, { "id": "decodeACSuccessive", "file": "pdfs/decodeACSuccessive.pdf", "md5": "7749c032624fe27ab8e8d7d5e9a4a93f", "rounds": 1, "link": false, - "type": "eq", - "nativeImageDecoderSupport": "none" + "type": "eq" }, { "id": "issue5592", "file": "pdfs/issue5592.pdf", diff --git a/test/unit/display_svg_spec.js b/test/unit/display_svg_spec.js index 0dce86043e443..30eec2000f2ba 100644 --- a/test/unit/display_svg_spec.js +++ b/test/unit/display_svg_spec.js @@ -18,7 +18,6 @@ import { setStubs, unsetStubs } from "../../examples/node/domstubs.js"; import { buildGetDocumentParams } from "./test_utils.js"; import { getDocument } from "../../src/display/api.js"; import { isNodeJS } from "../../src/shared/is_node.js"; -import { NativeImageDecoding } from "../../src/shared/util.js"; import { SVGGraphics } from "../../src/display/svg.js"; const XLINK_NS = "http://www.w3.org/1999/xlink"; @@ -62,11 +61,7 @@ describe("SVGGraphics", function () { var loadingTask; var page; beforeAll(function (done) { - loadingTask = getDocument( - buildGetDocumentParams("xobject-image.pdf", { - nativeImageDecoderSupport: NativeImageDecoding.DISPLAY, - }) - ); + loadingTask = getDocument(buildGetDocumentParams("xobject-image.pdf")); loadingTask.promise.then(function (doc) { doc.getPage(1).then(function (firstPage) { page = firstPage; From cc4cc8b11b7f346cb272fd21fa0e8fbdaf9ed61d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Wed, 29 Jan 2020 11:14:01 +0100 Subject: [PATCH 2/4] Remove the, now unused, `releaseImageResources` helper function With the changes in the previous patch, this is now dead code which should thus be removed. --- src/display/api.js | 9 --------- src/display/display_utils.js | 15 --------------- 2 files changed, 24 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 5d37779c8d053..ca517edc7bdea 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -43,7 +43,6 @@ import { DOMCMapReaderFactory, loadScript, PageViewport, - releaseImageResources, RenderingCancelledException, StatTimer, } from "./display_utils.js"; @@ -2581,14 +2580,6 @@ class PDFObjects { } clear() { - for (const objId in this._objs) { - const { data } = this._objs[objId]; - - if (typeof Image !== "undefined" && data instanceof Image) { - // Always release the image data when clearing out the cached objects. - releaseImageResources(data); - } - } this._objs = Object.create(null); } } diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 3be6ba0219608..4578a7d594edd 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -517,20 +517,6 @@ function deprecated(details) { console.log("Deprecated API usage: " + details); } -function releaseImageResources(img) { - assert(img instanceof Image, "Invalid `img` parameter."); - - const url = img.src; - if ( - typeof url === "string" && - url.startsWith("blob:") && - URL.revokeObjectURL - ) { - URL.revokeObjectURL(url); - } - img.removeAttribute("src"); -} - let pdfDateStringRegex; class PDFDateString { @@ -631,6 +617,5 @@ export { isValidFetchUrl, loadScript, deprecated, - releaseImageResources, PDFDateString, }; From 18e0b10d3cb142313f45855878afec496c4e9496 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Wed, 29 Jan 2020 11:37:52 +0100 Subject: [PATCH 3/4] [api-minor] Remove the `disableCreateObjectURL` option from the `getDocument` parameters, since it's now unused in the API With the changes in previous patches, the `disableCreateObjectURL` option/functionality is no longer used for anything in the API and/or in the Worker code. Note however that there's some functionality, mainly related to file loading/downloading, in the GENERIC version of the default viewer which still depends on this option. Hence the `disableCreateObjectURL` option (and related compatibility code) is moved into the viewer, see e.g. `web/app_options.js`, such that it's still available in the default viewer. --- src/core/evaluator.js | 1 - src/core/worker.js | 1 - src/display/api.js | 9 --------- src/display/api_compatibility.js | 15 --------------- src/display/svg.js | 2 +- web/app.js | 2 +- web/app_options.js | 13 ++++++------- web/download_manager.js | 9 +++------ web/pdf_attachment_viewer.js | 6 +----- web/pdf_print_service.js | 3 +-- web/viewer_compatibility.js | 12 ++++++++++++ 11 files changed, 25 insertions(+), 48 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 2d99beb8ed1fd..4a307f1425fd7 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -85,7 +85,6 @@ import { PDFImage } from "./image.js"; var PartialEvaluator = (function PartialEvaluatorClosure() { const DefaultPartialEvaluatorOptions = { - forceDataSchema: false, maxImageSize: -1, disableFontFace: false, ignoreErrors: false, diff --git a/src/core/worker.js b/src/core/worker.js index fb37d7ee3677d..ce63121d33120 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -399,7 +399,6 @@ var WorkerMessageHandler = { ensureNotTerminated(); var evaluatorOptions = { - forceDataSchema: data.disableCreateObjectURL, maxImageSize: data.maxImageSize, disableFontFace: data.disableFontFace, ignoreErrors: data.ignoreErrors, diff --git a/src/display/api.js b/src/display/api.js index ca517edc7bdea..3d6ee47cc0c22 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -154,9 +154,6 @@ function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) { * The default value is `false`. * NOTE: It is also necessary to disable streaming, see above, * in order for disabling of pre-fetching to work correctly. - * @property {boolean} [disableCreateObjectURL] - Disable the use of - * `URL.createObjectURL`, for compatibility with older browsers. - * The default value is `false`. * @property {boolean} [pdfBug] - Enables special hooks for debugging * PDF.js (see `web/debugger.js`). The default value is `false`. */ @@ -269,10 +266,6 @@ function getDocument(src) { if (typeof params.disableAutoFetch !== "boolean") { params.disableAutoFetch = false; } - if (typeof params.disableCreateObjectURL !== "boolean") { - params.disableCreateObjectURL = - apiCompatibilityParams.disableCreateObjectURL || false; - } // Set the main-thread verbosity level. setVerbosityLevel(params.verbosity); @@ -395,7 +388,6 @@ function _fetchDocument(worker, source, pdfDataRangeTransport, docId) { }, maxImageSize: source.maxImageSize, disableFontFace: source.disableFontFace, - disableCreateObjectURL: source.disableCreateObjectURL, postMessageTransfers: worker.postMessageTransfers, docBaseUrl: source.docBaseUrl, ignoreErrors: source.ignoreErrors, @@ -2505,7 +2497,6 @@ class WorkerTransport { const params = this._params; return shadow(this, "loadingParams", { disableAutoFetch: params.disableAutoFetch, - disableCreateObjectURL: params.disableCreateObjectURL, disableFontFace: params.disableFontFace, }); } diff --git a/src/display/api_compatibility.js b/src/display/api_compatibility.js index dd0a35641c94d..d610e3fae43c2 100644 --- a/src/display/api_compatibility.js +++ b/src/display/api_compatibility.js @@ -17,21 +17,6 @@ import { isNodeJS } from "../shared/is_node.js"; const compatibilityParams = Object.create(null); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { - const userAgent = - (typeof navigator !== "undefined" && navigator.userAgent) || ""; - const isIE = /Trident/.test(userAgent); - const isIOSChrome = /CriOS/.test(userAgent); - - // Checks if possible to use URL.createObjectURL() - // Support: IE, Chrome on iOS - (function checkOnBlobSupport() { - // Sometimes IE and Chrome on iOS losing the data created with - // createObjectURL(), see issues #3977 and #8081. - if (isIE || isIOSChrome) { - compatibilityParams.disableCreateObjectURL = true; - } - })(); - // Support: Node.js (function checkFontFace() { // Node.js is missing native support for `@font-face`. diff --git a/src/display/svg.js b/src/display/svg.js index dccd3a22ca7bc..4631b5d4fd08e 100644 --- a/src/display/svg.js +++ b/src/display/svg.js @@ -440,7 +440,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // eslint-disable-next-line no-shadow SVGGraphics = class SVGGraphics { - constructor(commonObjs, objs, forceDataSchema) { + constructor(commonObjs, objs, forceDataSchema = false) { this.svgFactory = new DOMSVGFactory(); this.current = new SVGExtraState(); diff --git a/web/app.js b/web/app.js index f4956dd65b6fa..1d442f0a189aa 100644 --- a/web/app.js +++ b/web/app.js @@ -2223,7 +2223,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { } const file = evt.fileInput.files[0]; - if (URL.createObjectURL && !AppOptions.get("disableCreateObjectURL")) { + if (!AppOptions.get("disableCreateObjectURL")) { let url = URL.createObjectURL(file); if (file.name) { url = { url, originalUrl: file.name }; diff --git a/web/app_options.js b/web/app_options.js index f3ac76d8a2357..8b68d7525c530 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -13,7 +13,6 @@ * limitations under the License. */ -import { apiCompatibilityParams } from "pdfjs-lib"; import { viewerCompatibilityParams } from "./viewer_compatibility.js"; const OptionKind = { @@ -43,6 +42,12 @@ const defaultOptions = { value: "", kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + disableCreateObjectURL: { + /** @type {boolean} */ + value: false, + compatibility: viewerCompatibilityParams.disableCreateObjectURL, + kind: OptionKind.VIEWER, + }, disableHistory: { /** @type {boolean} */ value: false, @@ -174,12 +179,6 @@ const defaultOptions = { value: false, kind: OptionKind.API + OptionKind.PREFERENCE, }, - disableCreateObjectURL: { - /** @type {boolean} */ - value: false, - compatibility: apiCompatibilityParams.disableCreateObjectURL, - kind: OptionKind.API, - }, disableFontFace: { /** @type {boolean} */ value: false, diff --git a/web/download_manager.js b/web/download_manager.js index 5c54015503bae..00ec1675d3540 100644 --- a/web/download_manager.js +++ b/web/download_manager.js @@ -13,11 +13,8 @@ * limitations under the License. */ -import { - apiCompatibilityParams, - createObjectURL, - createValidAbsoluteUrl, -} from "pdfjs-lib"; +import { createObjectURL, createValidAbsoluteUrl } from "pdfjs-lib"; +import { viewerCompatibilityParams } from "./viewer_compatibility.js"; if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) { throw new Error( @@ -27,7 +24,7 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) { } const DISABLE_CREATE_OBJECT_URL = - apiCompatibilityParams.disableCreateObjectURL || false; + viewerCompatibilityParams.disableCreateObjectURL || false; function download(blobUrl, filename) { const a = document.createElement("a"); diff --git a/web/pdf_attachment_viewer.js b/web/pdf_attachment_viewer.js index 198162b93009b..529c7712162f4 100644 --- a/web/pdf_attachment_viewer.js +++ b/web/pdf_attachment_viewer.js @@ -74,14 +74,10 @@ class PDFAttachmentViewer { } /** + * NOTE: Should only be used when `URL.createObjectURL` is natively supported. * @private */ _bindPdfLink(button, content, filename) { - if (this.downloadManager.disableCreateObjectURL) { - throw new Error( - 'bindPdfLink: Unsupported "disableCreateObjectURL" value.' - ); - } let blobUrl; button.onclick = () => { if (!blobUrl) { diff --git a/web/pdf_print_service.js b/web/pdf_print_service.js index d6f5b331187e0..d16792c7fac49 100644 --- a/web/pdf_print_service.js +++ b/web/pdf_print_service.js @@ -65,8 +65,7 @@ function PDFPrintService(pdfDocument, pagesOverview, printContainer, l10n) { this.pagesOverview = pagesOverview; this.printContainer = printContainer; this.l10n = l10n || NullL10n; - this.disableCreateObjectURL = - pdfDocument.loadingParams.disableCreateObjectURL; + this.disableCreateObjectURL = AppOptions.get("disableCreateObjectURL"); this.currentPage = -1; // The temporary canvas where renderPage paints one page at a time. this.scratchCanvas = document.createElement("canvas"); diff --git a/web/viewer_compatibility.js b/web/viewer_compatibility.js index db8421a79f330..b998f49ae87b0 100644 --- a/web/viewer_compatibility.js +++ b/web/viewer_compatibility.js @@ -23,9 +23,21 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { (typeof navigator !== "undefined" && navigator.maxTouchPoints) || 1; const isAndroid = /Android/.test(userAgent); + const isIE = /Trident/.test(userAgent); const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || (platform === "MacIntel" && maxTouchPoints > 1); + const isIOSChrome = /CriOS/.test(userAgent); + + // Checks if possible to use URL.createObjectURL() + // Support: IE, Chrome on iOS + (function checkOnBlobSupport() { + // Sometimes IE and Chrome on iOS losing the data created with + // createObjectURL(), see issues #3977 and #8081. + if (isIE || isIOSChrome) { + compatibilityParams.disableCreateObjectURL = true; + } + })(); // Limit canvas size to 5 mega-pixels on mobile. // Support: Android, iOS From ebef67b3549d8612cbece0e8a3b3355a41c80fdf Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 21 Mar 2020 12:53:20 +0100 Subject: [PATCH 4/4] Stop building any `src/` files during the `gulp default_preferences` task With the changes made in the previous patch, the `web/app_options.js` file no longer depends on anything *except* files residing in the `web/` folder. Hence the `gulp default_preferences` task can now be further simplified and thus becomes even faster than before; see also PR 11724. --- gulpfile.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 18a2ab9fac447..120d4fdbad354 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -563,14 +563,6 @@ gulp.task("default_preferences-pre", function () { }; var preprocessor2 = require("./external/builder/preprocessor2.js"); return merge([ - gulp.src( - [ - "src/{display,shared}/*.js", - "!src/shared/{cffStandardStrings,fonts_utils}.js", - "src/pdf.js", - ], - { base: "src/" } - ), gulp.src(["web/{app_options,viewer_compatibility}.js"], { base: ".", }),