Skip to content

Commit 9ea590c

Browse files
authored
fix regressions in PNG encoding that were introduced in 3.0.2 (#3887)
- fix compression of other than 8-bit images - fix soft mask for other than 8-bit images - fix potential byte order issue for 16-bit images - fix writing an empty mask (error) for indexed images without transparency
1 parent 394d1e7 commit 9ea590c

File tree

47 files changed

+132
-27
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+132
-27
lines changed

src/modules/addimage.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,11 @@ import { atob } from "../libs/AtobBtoa.js";
251251
value: "<<" + image.decodeParameters + ">>"
252252
});
253253
}
254-
if ("transparency" in image && Array.isArray(image.transparency)) {
254+
if (
255+
"transparency" in image &&
256+
Array.isArray(image.transparency) &&
257+
image.transparency.length > 0
258+
) {
255259
var transparency = "",
256260
i = 0,
257261
len = image.transparency.length;
@@ -285,20 +289,17 @@ import { atob } from "../libs/AtobBtoa.js";
285289

286290
// Soft mask
287291
if ("sMask" in image && typeof image.sMask !== "undefined") {
288-
var decodeParameters =
289-
(image.predictor != null ? "/Predictor " + image.predictor : "") +
290-
" /Colors 1 /BitsPerComponent 8" +
291-
" /Columns " +
292-
image.width;
293-
var sMask = {
292+
const sMaskBitsPerComponent =
293+
image.sMaskBitsPerComponent ?? image.bitsPerComponent;
294+
const sMask = {
294295
width: image.width,
295296
height: image.height,
296297
colorSpace: "DeviceGray",
297-
bitsPerComponent: image.bitsPerComponent,
298-
decodeParameters: decodeParameters,
298+
bitsPerComponent: sMaskBitsPerComponent,
299299
data: image.sMask
300300
};
301301
if ("filter" in image) {
302+
sMask.decodeParameters = `/Predictor ${image.predictor} /Colors 1 /BitsPerComponent ${sMaskBitsPerComponent} /Columns ${image.width}`;
302303
sMask.filter = image.filter;
303304
}
304305
putImage.call(this, sMask);

src/modules/png_support.js

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jsPDF.API.processPNG = function(imageData, index, alias, compression) {
8181
const {
8282
colorSpace,
8383
colorsPerPixel,
84+
sMaskBitsPerComponent,
8485
colorBytes,
8586
alphaBytes,
8687
needSMask,
@@ -94,25 +95,36 @@ jsPDF.API.processPNG = function(imageData, index, alias, compression) {
9495
if (canCompress(compression)) {
9596
predictor = getPredictorFromCompression(compression);
9697
filter = this.decode.FLATE_DECODE;
97-
decodeParameters = `/Predictor ${predictor} `;
98+
decodeParameters = `/Predictor ${predictor} /Colors ${colorsPerPixel} /BitsPerComponent ${bitsPerComponent} /Columns ${width}`;
99+
100+
const rowByteLength = Math.ceil(
101+
(width * colorsPerPixel * bitsPerComponent) / 8
102+
);
103+
98104
imageData = compressBytes(
99105
colorBytes,
100-
width * colorsPerPixel,
106+
rowByteLength,
101107
colorsPerPixel,
108+
bitsPerComponent,
102109
compression
103110
);
104111
if (needSMask) {
105-
sMask = compressBytes(alphaBytes, width, 1, compression);
112+
const sMaskRowByteLength = Math.ceil((width * sMaskBitsPerComponent) / 8);
113+
sMask = compressBytes(
114+
alphaBytes,
115+
sMaskRowByteLength,
116+
1,
117+
sMaskBitsPerComponent,
118+
compression
119+
);
106120
}
107121
} else {
108122
filter = undefined;
109-
decodeParameters = "";
123+
decodeParameters = undefined;
110124
imageData = colorBytes;
111125
if (needSMask) sMask = alphaBytes;
112126
}
113127

114-
decodeParameters += `/Colors ${colorsPerPixel} /BitsPerComponent ${bitsPerComponent} /Columns ${width}`;
115-
116128
if (
117129
this.__addimage__.isArrayBuffer(imageData) ||
118130
this.__addimage__.isArrayBufferView(imageData)
@@ -140,6 +152,7 @@ jsPDF.API.processPNG = function(imageData, index, alias, compression) {
140152
width,
141153
height,
142154
bitsPerComponent,
155+
sMaskBitsPerComponent,
143156
colorSpace
144157
};
145158
};
@@ -169,7 +182,13 @@ function canCompress(value) {
169182
function hasCompressionJS() {
170183
return typeof zlibSync === "function";
171184
}
172-
function compressBytes(bytes, lineLength, colorsPerPixel, compression) {
185+
function compressBytes(
186+
bytes,
187+
lineByteLength,
188+
channels,
189+
bitsPerComponent,
190+
compression
191+
) {
173192
let level = 4;
174193
let filter_method = filterUp;
175194

@@ -190,10 +209,11 @@ function compressBytes(bytes, lineLength, colorsPerPixel, compression) {
190209
break;
191210
}
192211

212+
const bytesPerPixel = Math.ceil((channels * bitsPerComponent) / 8);
193213
bytes = applyPngFilterMethod(
194214
bytes,
195-
lineLength,
196-
colorsPerPixel,
215+
lineByteLength,
216+
bytesPerPixel,
197217
filter_method
198218
);
199219
const dat = zlibSync(bytes, { level: level });
@@ -202,27 +222,27 @@ function compressBytes(bytes, lineLength, colorsPerPixel, compression) {
202222

203223
function applyPngFilterMethod(
204224
bytes,
205-
lineLength,
206-
colorsPerPixel,
225+
lineByteLength,
226+
bytesPerPixel,
207227
filter_method
208228
) {
209-
const lines = bytes.length / lineLength;
229+
const lines = bytes.length / lineByteLength;
210230
const result = new Uint8Array(bytes.length + lines);
211231
const filter_methods = getFilterMethods();
212232
let prevLine;
213233

214234
for (let i = 0; i < lines; i += 1) {
215-
const offset = i * lineLength;
216-
const line = bytes.subarray(offset, offset + lineLength);
235+
const offset = i * lineByteLength;
236+
const line = bytes.subarray(offset, offset + lineByteLength);
217237

218238
if (filter_method) {
219-
result.set(filter_method(line, colorsPerPixel, prevLine), offset + i);
239+
result.set(filter_method(line, bytesPerPixel, prevLine), offset + i);
220240
} else {
221241
const len = filter_methods.length;
222242
const results = [];
223243

224244
for (let j = 0; j < len; j += 1) {
225-
results[j] = filter_methods[j](line, colorsPerPixel, prevLine);
245+
results[j] = filter_methods[j](line, bytesPerPixel, prevLine);
226246
}
227247

228248
const ind = getIndexOfSmallestSum(results.concat());
@@ -384,18 +404,22 @@ function processIndexedPNG(decodedPng) {
384404
mask = undefined;
385405

386406
const totalPixels = width * height;
407+
// per PNG spec, palettes always use 8 bits per component
387408
alphaBytes = new Uint8Array(totalPixels);
388409
const dataView = new DataView(data.buffer);
389410
for (let p = 0; p < totalPixels; p++) {
390411
const paletteIndex = readSample(dataView, p, depth);
391412
const [, , , alpha] = decodedPalette[paletteIndex];
392413
alphaBytes[p] = alpha;
393414
}
415+
} else if (maskLength === 0) {
416+
mask = undefined;
394417
}
395418

396419
return {
397420
colorSpace: "Indexed",
398421
colorsPerPixel: 1,
422+
sMaskBitsPerComponent: needSMask ? 8 : undefined,
399423
colorBytes: data,
400424
alphaBytes,
401425
needSMask,
@@ -447,6 +471,7 @@ function processAlphaPNG(decodedPng) {
447471
return {
448472
colorSpace,
449473
colorsPerPixel,
474+
sMaskBitsPerComponent: needSMask ? depth : undefined,
450475
colorBytes,
451476
alphaBytes,
452477
needSMask
@@ -457,11 +482,31 @@ function processOpaquePNG(decodedPng) {
457482
const { data, channels } = decodedPng;
458483
const colorSpace = channels === 1 ? "DeviceGray" : "DeviceRGB";
459484
const colorsPerPixel = colorSpace === "DeviceGray" ? 1 : 3;
460-
const colorBytes =
461-
data instanceof Uint8Array ? data : new Uint8Array(data.buffer);
485+
486+
let colorBytes;
487+
if (data instanceof Uint16Array) {
488+
colorBytes = convertUint16ArrayToUint8Array(data);
489+
} else {
490+
colorBytes = data;
491+
}
492+
462493
return { colorSpace, colorsPerPixel, colorBytes, needSMask: false };
463494
}
464495

496+
function convertUint16ArrayToUint8Array(data) {
497+
// PNG/PDF expect MSB-first byte order. Since EcmaScript does not specify
498+
// the byte order of Uint16Array, we need to use a DataView to ensure the
499+
// correct byte order.
500+
const sampleCount = data.length;
501+
const out = new Uint8Array(sampleCount * 2);
502+
const outView = new DataView(out.buffer, out.byteOffset, out.byteLength);
503+
504+
for (let i = 0; i < sampleCount; i++) {
505+
outView.setUint16(i * 2, data[i], false);
506+
}
507+
return out;
508+
}
509+
465510
function readSample(view, sampleIndex, depth) {
466511
const bitIndex = sampleIndex * depth;
467512
const byteIndex = Math.floor(bitIndex / 8);
-61 Bytes
Binary file not shown.
-61 Bytes
Binary file not shown.
-60 Bytes
Binary file not shown.
-44 Bytes
Binary file not shown.
-45 Bytes
Binary file not shown.
-59 Bytes
Binary file not shown.
-60 Bytes
Binary file not shown.
-45 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)