Merge pull request #9427 from Snuffleupagus/native-JPEG-decoding-fallback
Fallback to the built-in JPEG decoder when browser decoding fails, and attempt to handle JPEG images with DNL (Define Number of Lines) markers (issue 8614)
This commit is contained in:
commit
7bb066494f
@ -80,11 +80,10 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
var colorSpace = dict.get('ColorSpace', 'CS');
|
||||
colorSpace = ColorSpace.parse(colorSpace, this.xref, this.resources,
|
||||
this.pdfFunctionFactory);
|
||||
var numComps = colorSpace.numComps;
|
||||
var decodePromise = this.handler.sendWithPromise('JpegDecode',
|
||||
[image.getIR(this.forceDataSchema), numComps]);
|
||||
return decodePromise.then(function (message) {
|
||||
var data = message.data;
|
||||
|
||||
return this.handler.sendWithPromise('JpegDecode', [
|
||||
image.getIR(this.forceDataSchema), colorSpace.numComps
|
||||
]).then(function({ data, width, height, }) {
|
||||
return new Stream(data, 0, data.length, image.dict);
|
||||
});
|
||||
},
|
||||
@ -349,22 +348,21 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
});
|
||||
},
|
||||
|
||||
buildPaintImageXObject:
|
||||
function PartialEvaluator_buildPaintImageXObject(resources, image,
|
||||
inline, operatorList,
|
||||
cacheKey, imageCache) {
|
||||
buildPaintImageXObject({ resources, image, isInline = false, operatorList,
|
||||
cacheKey, imageCache,
|
||||
forceDisableNativeImageDecoder = false, }) {
|
||||
var dict = image.dict;
|
||||
var w = dict.get('Width', 'W');
|
||||
var h = dict.get('Height', 'H');
|
||||
|
||||
if (!(w && isNum(w)) || !(h && isNum(h))) {
|
||||
warn('Image dimensions are missing, or not numbers.');
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
var maxImageSize = this.options.maxImageSize;
|
||||
if (maxImageSize !== -1 && w * h > maxImageSize) {
|
||||
warn('Image exceeded maximum allowed size and was removed.');
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var imageMask = (dict.get('ImageMask', 'IM') || false);
|
||||
@ -398,7 +396,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
args,
|
||||
};
|
||||
}
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var softMask = (dict.get('SMask', 'SM') || false);
|
||||
@ -406,44 +404,63 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
|
||||
var SMALL_IMAGE_DIMENSIONS = 200;
|
||||
// Inlining small images into the queue as RGB data
|
||||
if (inline && !softMask && !mask && !(image instanceof JpegStream) &&
|
||||
if (isInline && !softMask && !mask && !(image instanceof JpegStream) &&
|
||||
(w + h) < SMALL_IMAGE_DIMENSIONS) {
|
||||
let imageObj = new PDFImage({
|
||||
xref: this.xref,
|
||||
res: resources,
|
||||
image,
|
||||
isInline: inline,
|
||||
isInline,
|
||||
pdfFunctionFactory: this.pdfFunctionFactory,
|
||||
});
|
||||
// We force the use of RGBA_32BPP images here, because we can't handle
|
||||
// any other kind.
|
||||
imgData = imageObj.createImageData(/* forceRGBA = */ true);
|
||||
operatorList.addOp(OPS.paintInlineImageXObject, [imgData]);
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var nativeImageDecoderSupport = this.options.nativeImageDecoderSupport;
|
||||
const 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.
|
||||
var objId = 'img_' + this.idFactory.createObjId();
|
||||
operatorList.addDependency(objId);
|
||||
args = [objId, w, h];
|
||||
|
||||
if (nativeImageDecoderSupport !== NativeImageDecoding.NONE &&
|
||||
!softMask && !mask && image instanceof JpegStream &&
|
||||
NativeImageDecoder.isSupported(image, this.xref, resources,
|
||||
this.pdfFunctionFactory)) {
|
||||
// These JPEGs don't need any more processing so we can just send it.
|
||||
operatorList.addOp(OPS.paintJpegXObject, args);
|
||||
this.handler.send('obj', [objId, this.pageIndex, 'JpegStream',
|
||||
image.getIR(this.options.forceDataSchema)]);
|
||||
if (cacheKey) {
|
||||
imageCache[cacheKey] = {
|
||||
fn: OPS.paintJpegXObject,
|
||||
args,
|
||||
};
|
||||
}
|
||||
return;
|
||||
return this.handler.sendWithPromise('obj', [
|
||||
objId, this.pageIndex, 'JpegStream',
|
||||
image.getIR(this.options.forceDataSchema)
|
||||
]).then(function() {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
}, (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.
|
||||
@ -460,12 +477,16 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that the dependency is added before the image is decoded.
|
||||
operatorList.addDependency(objId);
|
||||
args = [objId, w, h];
|
||||
|
||||
PDFImage.buildImage({
|
||||
handler: this.handler,
|
||||
xref: this.xref,
|
||||
res: resources,
|
||||
image,
|
||||
isInline: inline,
|
||||
isInline,
|
||||
nativeDecoder: nativeImageDecoder,
|
||||
pdfFunctionFactory: this.pdfFunctionFactory,
|
||||
}).then((imageObj) => {
|
||||
@ -484,6 +505,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
args,
|
||||
};
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
handleSMask: function PartialEvaluator_handleSmask(smask, resources,
|
||||
@ -989,8 +1011,14 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
}, rejectXObject);
|
||||
return;
|
||||
} else if (type.name === 'Image') {
|
||||
self.buildPaintImageXObject(resources, xobj, false,
|
||||
operatorList, name, imageCache);
|
||||
self.buildPaintImageXObject({
|
||||
resources,
|
||||
image: xobj,
|
||||
operatorList,
|
||||
cacheKey: name,
|
||||
imageCache,
|
||||
}).then(resolveXObject, rejectXObject);
|
||||
return;
|
||||
} else if (type.name === 'PS') {
|
||||
// PostScript XObjects are unused when viewing documents.
|
||||
// See section 4.7.1 of Adobe's PDF reference.
|
||||
@ -1032,10 +1060,15 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.buildPaintImageXObject(resources, args[0], true,
|
||||
operatorList, cacheKey, imageCache);
|
||||
args = null;
|
||||
continue;
|
||||
next(self.buildPaintImageXObject({
|
||||
resources,
|
||||
image: args[0],
|
||||
isInline: true,
|
||||
operatorList,
|
||||
cacheKey,
|
||||
imageCache,
|
||||
}));
|
||||
return;
|
||||
case OPS.showText:
|
||||
args[0] = self.handleText(args[0], stateManager.state);
|
||||
break;
|
||||
|
@ -27,7 +27,11 @@ var PDFImage = (function PDFImageClosure() {
|
||||
*/
|
||||
function handleImageData(image, nativeDecoder) {
|
||||
if (nativeDecoder && nativeDecoder.canDecode(image)) {
|
||||
return nativeDecoder.decode(image);
|
||||
return nativeDecoder.decode(image).catch((reason) => {
|
||||
warn('Native image decoding failed -- trying to recover: ' +
|
||||
(reason && reason.message));
|
||||
return image;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(image);
|
||||
}
|
||||
|
@ -28,6 +28,19 @@ let JpegError = (function JpegErrorClosure() {
|
||||
return JpegError;
|
||||
})();
|
||||
|
||||
let DNLMarkerError = (function DNLMarkerErrorClosure() {
|
||||
function DNLMarkerError(message, scanLines) {
|
||||
this.message = message;
|
||||
this.scanLines = scanLines;
|
||||
}
|
||||
|
||||
DNLMarkerError.prototype = new Error();
|
||||
DNLMarkerError.prototype.name = 'DNLMarkerError';
|
||||
DNLMarkerError.constructor = DNLMarkerError;
|
||||
|
||||
return DNLMarkerError;
|
||||
})();
|
||||
|
||||
/**
|
||||
* This code was forked from https://github.com/notmasteryet/jpgjs.
|
||||
* The original version was created by GitHub user notmasteryet.
|
||||
@ -112,7 +125,8 @@ var JpegImage = (function JpegImageClosure() {
|
||||
}
|
||||
|
||||
function decodeScan(data, offset, frame, components, resetInterval,
|
||||
spectralStart, spectralEnd, successivePrev, successive) {
|
||||
spectralStart, spectralEnd, successivePrev, successive,
|
||||
parseDNLMarker = false) {
|
||||
var mcusPerLine = frame.mcusPerLine;
|
||||
var progressive = frame.progressive;
|
||||
|
||||
@ -127,6 +141,14 @@ var JpegImage = (function JpegImageClosure() {
|
||||
if (bitsData === 0xFF) {
|
||||
var nextByte = data[offset++];
|
||||
if (nextByte) {
|
||||
if (nextByte === 0xDC && parseDNLMarker) { // DNL == 0xFFDC
|
||||
offset += 2; // Skip data length.
|
||||
const scanLines = (data[offset++] << 8) | data[offset++];
|
||||
if (scanLines > 0 && scanLines !== frame.scanLines) {
|
||||
throw new DNLMarkerError(
|
||||
'Found DNL marker (0xFFDC) while parsing scan data', scanLines);
|
||||
}
|
||||
}
|
||||
throw new JpegError(
|
||||
`unexpected marker ${((bitsData << 8) | nextByte).toString(16)}`);
|
||||
}
|
||||
@ -635,7 +657,7 @@ var JpegImage = (function JpegImageClosure() {
|
||||
}
|
||||
|
||||
JpegImage.prototype = {
|
||||
parse: function parse(data) {
|
||||
parse(data, { dnlScanLines = null, } = {}) {
|
||||
|
||||
function readUint16() {
|
||||
var value = (data[offset] << 8) | data[offset + 1];
|
||||
@ -685,6 +707,7 @@ var JpegImage = (function JpegImageClosure() {
|
||||
var jfif = null;
|
||||
var adobe = null;
|
||||
var frame, resetInterval;
|
||||
let numSOSMarkers = 0;
|
||||
var quantizationTables = [];
|
||||
var huffmanTablesAC = [], huffmanTablesDC = [];
|
||||
var fileMarker = readUint16();
|
||||
@ -781,7 +804,8 @@ var JpegImage = (function JpegImageClosure() {
|
||||
frame.extended = (fileMarker === 0xFFC1);
|
||||
frame.progressive = (fileMarker === 0xFFC2);
|
||||
frame.precision = data[offset++];
|
||||
frame.scanLines = readUint16();
|
||||
const sofScanLines = readUint16();
|
||||
frame.scanLines = dnlScanLines || sofScanLines;
|
||||
frame.samplesPerLine = readUint16();
|
||||
frame.components = [];
|
||||
frame.componentIds = {};
|
||||
@ -839,6 +863,12 @@ var JpegImage = (function JpegImageClosure() {
|
||||
break;
|
||||
|
||||
case 0xFFDA: // SOS (Start of Scan)
|
||||
// A DNL marker (0xFFDC), if it exists, is only allowed at the end
|
||||
// of the first scan segment and may only occur once in an image.
|
||||
// Furthermore, to prevent an infinite loop, do *not* attempt to
|
||||
// parse DNL markers during re-parsing of the JPEG scan data.
|
||||
const parseDNLMarker = (++numSOSMarkers) === 1 && !dnlScanLines;
|
||||
|
||||
readUint16(); // scanLength
|
||||
var selectorsCount = data[offset++];
|
||||
var components = [], component;
|
||||
@ -853,11 +883,26 @@ var JpegImage = (function JpegImageClosure() {
|
||||
var spectralStart = data[offset++];
|
||||
var spectralEnd = data[offset++];
|
||||
var successiveApproximation = data[offset++];
|
||||
var processed = decodeScan(data, offset,
|
||||
frame, components, resetInterval,
|
||||
spectralStart, spectralEnd,
|
||||
successiveApproximation >> 4, successiveApproximation & 15);
|
||||
offset += processed;
|
||||
try {
|
||||
var processed = decodeScan(data, offset,
|
||||
frame, components, resetInterval,
|
||||
spectralStart, spectralEnd,
|
||||
successiveApproximation >> 4, successiveApproximation & 15,
|
||||
parseDNLMarker);
|
||||
offset += processed;
|
||||
} catch (ex) {
|
||||
if (ex instanceof DNLMarkerError) {
|
||||
warn('Attempting to re-parse JPEG image using "scanLines" ' +
|
||||
'parameter found in DNL marker (0xFFDC) segment.');
|
||||
return this.parse(data, { dnlScanLines: ex.scanLines, });
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xFFDC: // DNL (Define Number of Lines)
|
||||
// Ignore the marker, since it's being handled in `decodeScan`.
|
||||
offset += 4;
|
||||
break;
|
||||
|
||||
case 0xFFFF: // Fill bytes
|
||||
|
@ -16,10 +16,9 @@
|
||||
|
||||
import {
|
||||
assert, createPromiseCapability, getVerbosityLevel, info, InvalidPDFException,
|
||||
isArrayBuffer, isSameOrigin, loadJpegStream, MessageHandler,
|
||||
MissingPDFException, NativeImageDecoding, PageViewport, PasswordException,
|
||||
stringToBytes, UnexpectedResponseException, UnknownErrorException,
|
||||
unreachable, Util, warn
|
||||
isArrayBuffer, isSameOrigin, MessageHandler, MissingPDFException,
|
||||
NativeImageDecoding, PageViewport, PasswordException, stringToBytes,
|
||||
UnexpectedResponseException, UnknownErrorException, unreachable, Util, warn
|
||||
} from '../shared/util';
|
||||
import {
|
||||
DOMCanvasFactory, DOMCMapReaderFactory, DummyStatTimer, getDefaultSetting,
|
||||
@ -1818,8 +1817,22 @@ var WorkerTransport = (function WorkerTransportClosure() {
|
||||
switch (type) {
|
||||
case 'JpegStream':
|
||||
imageData = data[3];
|
||||
loadJpegStream(id, imageData, pageProxy.objs);
|
||||
break;
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = function() {
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = function() {
|
||||
reject(new Error('Error during JPEG image loading'));
|
||||
// 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.
|
||||
};
|
||||
img.src = imageData;
|
||||
}).then((img) => {
|
||||
pageProxy.objs.resolve(id, img);
|
||||
});
|
||||
case 'Image':
|
||||
imageData = data[3];
|
||||
pageProxy.objs.resolve(id, imageData);
|
||||
|
@ -1569,18 +1569,6 @@ MessageHandler.prototype = {
|
||||
},
|
||||
};
|
||||
|
||||
function loadJpegStream(id, imageUrl, objs) {
|
||||
var img = new Image();
|
||||
img.onload = (function loadJpegStream_onloadClosure() {
|
||||
objs.resolve(id, img);
|
||||
});
|
||||
img.onerror = (function loadJpegStream_onerrorClosure() {
|
||||
objs.resolve(id, null);
|
||||
warn('Error during JPEG image loading');
|
||||
});
|
||||
img.src = imageUrl;
|
||||
}
|
||||
|
||||
export {
|
||||
FONT_IDENTITY_MATRIX,
|
||||
IDENTITY_MATRIX,
|
||||
@ -1632,7 +1620,6 @@ export {
|
||||
createValidAbsoluteUrl,
|
||||
isLittleEndian,
|
||||
isEvalSupported,
|
||||
loadJpegStream,
|
||||
log2,
|
||||
readInt8,
|
||||
readUint16,
|
||||
|
1
test/pdfs/issue8614.pdf.link
Normal file
1
test/pdfs/issue8614.pdf.link
Normal file
@ -0,0 +1 @@
|
||||
https://github.com/mozilla/pdf.js/files/1125123/OBW-OVK.pdf
|
@ -3201,6 +3201,14 @@
|
||||
"link": true,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "issue8614",
|
||||
"file": "pdfs/issue8614.pdf",
|
||||
"md5": "7e8b66cf674ac2b79d6b267d0c6f2fa2",
|
||||
"rounds": 1,
|
||||
"link": true,
|
||||
"lastPage": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "bug1108753",
|
||||
"file": "pdfs/bug1108753.pdf",
|
||||
"md5": "a7aaf92d55b4602afb0ca3d75198b56b",
|
||||
|
Loading…
x
Reference in New Issue
Block a user