Skip to content

Regression in 0.19.0: RGB JPEGs embedded as DeviceGray (rendered grayscale) #1733

@ahoek

Description

@ahoek

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)

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions