Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions lib/image/png.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,19 @@ class PNGImage {
return (this.imgData = null);
}

// png-js decodes pixel data through Node's async zlib.inflate and rethrows
// any inflate error from inside that callback, which happens on a later
// tick and so can't be caught by a try/catch around doc.image(). Running
// an inflateSync pass on the same bytes first surfaces the same failure
// synchronously, in a spot the caller can actually catch, the same way
// PDFImage.open() already throws synchronously for other bad input.
decodePixels(callback) {
zlib.inflateSync(this.imgData);
return this.image.decodePixels(callback);
}

splitAlphaChannel() {
return this.image.decodePixels((pixels) => {
return this.decodePixels((pixels) => {
let a, p;
const colorCount = this.image.colors;
const pixelCount = this.width * this.height;
Expand Down Expand Up @@ -151,7 +162,7 @@ class PNGImage {
loadIndexedAlphaChannel() {
const transparency = this.image.transparency.indexed;
const isInterlaced = this.image.interlaceMethod === 1;
return this.image.decodePixels((pixels) => {
return this.decodePixels((pixels) => {
const alphaChannel = new Uint8Array(this.width * this.height);

let i = 0;
Expand All @@ -170,7 +181,7 @@ class PNGImage {
}

decodeData() {
this.image.decodePixels((pixels) => {
this.decodePixels((pixels) => {
this.imgData = zlib.deflateSync(pixels);
this.finalize();
});
Expand Down
3 changes: 2 additions & 1 deletion lib/zlib/browser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { zlibSync } from 'fflate';
import { zlibSync, unzlibSync } from 'fflate';

export default {
deflateSync: (data) => zlibSync(data),
inflateSync: (data) => unzlibSync(data),
};
1 change: 1 addition & 0 deletions lib/zlib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import zlib from 'zlib';

export default {
deflateSync: (data) => zlib.deflateSync(data),
inflateSync: (data) => zlib.inflateSync(data),
};
29 changes: 29 additions & 0 deletions tests/unit/image.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,35 @@ describe('Image', function () {
expect(jpeg.colorSpace).toBe('DeviceRGB');
});

test('a PNG with a corrupted IDAT chunk throws synchronously instead of crashing later (issue #1747)', () => {
const chunk = (type, data) => {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length);
const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
const crc = Buffer.alloc(4);
return Buffer.concat([len, body, crc]);
};

const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(1, 0); // width
ihdr.writeUInt32BE(1, 4); // height
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type: RGBA, forces the decodePixels path

const corruptPng = Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', ihdr),
chunk('IDAT', Buffer.from([0xde, 0xad, 0xbe, 0xef])), // not valid deflate data
chunk('IEND', Buffer.alloc(0)),
]);

// The bug: embed() used to hand this straight to png-js's async
// decodePixels, which rethrows inflate errors from inside a zlib
// callback on a later tick, so this document.image() call would crash
// the process instead of throwing something this test could catch.
expect(() => document.image(corruptPng, 0, 0)).toThrow();
});

describe('opacity', function () {
test('adds an ExtGState with the correct ca value', () => {
document.image('./tests/images/bee.png', 0, 0, { opacity: 0.5 });
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/zlib.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,20 @@ describe('zlib', () => {
expect(compressed.length).toBeLessThan(data.length);
expect(Buffer.from(zlib.inflateSync(compressed))).toEqual(data);
});

test.each([
['node', nodeZlib],
['browser', browserZlib],
])('%s inflateSync reverses deflateSync', (_, impl) => {
const compressed = impl.deflateSync(data);
expect(Buffer.from(impl.inflateSync(compressed))).toEqual(data);
});

test.each([
['node', nodeZlib],
['browser', browserZlib],
])('%s inflateSync throws on data that was never validly compressed', (_, impl) => {
const garbage = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
expect(() => impl.inflateSync(garbage)).toThrow();
});
});