Version: pdfkit 0.19.0 (works correctly in 0.18.0)
Summary
Since 0.19.0, embedding a standard 3-component (YCbCr/JFIF) RGB JPEG produces an image XObject with /ColorSpace /DeviceGray instead of /DeviceRGB. The image renders in black & white. This affects essentially all ordinary RGB JPEGs, not edge cases.
Root cause
In lib/image/jpeg.js, after parsing the SOF marker, pos points at the number-of-components byte (Nf). The code reads the wrong offset:
// lib/image/jpeg.js (v0.19.0, ~line 135)
const channels = this.data[pos + 1]; // reads the byte AFTER Nf
this.colorSpace = COLOR_SPACE_MAP[channels];
this.data[pos + 1] is the first component's identifier (C1), not the component count. In standard JFIF images C1 === 1, and COLOR_SPACE_MAP[1] === 'DeviceGray', so every 3-channel RGB JPEG is mislabeled grayscale. (CMYK images can break differently depending on their component ids.)
It should read this.data[pos].
Regression source
Introduced by commit 38dd4359a ("Fix lint issues", 2026-06-07):
- const channels = this.data[pos++];
+ const channels = this.data[pos + 1];
This appears to be an incorrect lint autofix. The original this.data[pos++] reads data[pos] (the ++ side effect was unused because pos isn't read again afterward). The intended rewrite was this.data[pos]; this.data[pos + 1] changes the byte being read.
Suggested fix
const channels = this.data[pos];
this.colorSpace = COLOR_SPACE_MAP[channels];
Minimal reproduction
Using any standard RGB JPEG as photo.jpg:
import PDFDocument from 'pdfkit';
const doc = new PDFDocument({ autoFirstPage: true });
const chunks = [];
doc.on('data', (c) => chunks.push(c));
doc.on('end', () => {
const pdf = Buffer.concat(chunks).toString('latin1');
const cs = [...pdf.matchAll(/\/ColorSpace\s*\/(\w+)/g)].map((m) => m[1]);
console.log('ColorSpace:', cs); // 0.18.0 -> ['DeviceRGB'], 0.19.0 -> ['DeviceGray']
});
doc.image('photo.jpg', 0, 0, { width: 200 });
doc.end();
Expected: ['DeviceRGB']
Actual (0.19.0): ['DeviceGray']
Verified the SOF bytes of the test image directly: data[pos] === 3 (3 components → DeviceRGB), while data[pos + 1] === 1 (first component id → DeviceGray), matching the analysis above.
Environment
- pdfkit 0.19.0
- Node.js 24.x
- Reached via pdfmake 0.3.10 (which bumped its pdfkit dependency
^0.18 → ^0.19)
Version: pdfkit 0.19.0 (works correctly in 0.18.0)
Summary
Since 0.19.0, embedding a standard 3-component (YCbCr/JFIF) RGB JPEG produces an image XObject with
/ColorSpace /DeviceGrayinstead of/DeviceRGB. The image renders in black & white. This affects essentially all ordinary RGB JPEGs, not edge cases.Root cause
In
lib/image/jpeg.js, after parsing the SOF marker,pospoints at the number-of-components byte (Nf). The code reads the wrong offset:this.data[pos + 1]is the first component's identifier (C1), not the component count. In standard JFIF imagesC1 === 1, andCOLOR_SPACE_MAP[1] === 'DeviceGray', so every 3-channel RGB JPEG is mislabeled grayscale. (CMYK images can break differently depending on their component ids.)It should read
this.data[pos].Regression source
Introduced by commit
38dd4359a("Fix lint issues", 2026-06-07):This appears to be an incorrect lint autofix. The original
this.data[pos++]readsdata[pos](the++side effect was unused becauseposisn't read again afterward). The intended rewrite wasthis.data[pos];this.data[pos + 1]changes the byte being read.Suggested fix
Minimal reproduction
Using any standard RGB JPEG as
photo.jpg:Expected:
['DeviceRGB']Actual (0.19.0):
['DeviceGray']Verified the SOF bytes of the test image directly:
data[pos] === 3(3 components → DeviceRGB), whiledata[pos + 1] === 1(first component id → DeviceGray), matching the analysis above.Environment
^0.18→^0.19)