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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Fix Interlaced PNG with indexed transparency rendered incorrectly
- Preserve existing PageMode instead of overwriting when adding outlines
- Support outlines that jump to specific page positions with custom zoom level
- Replace deprecated jpeg-exif with jay-peg for JPEG parsing

### [v0.17.2] - 2025-08-30

Expand Down
45 changes: 15 additions & 30 deletions lib/image/jpeg.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import exif from 'jpeg-exif';

const MARKERS = [
0xffc0, 0xffc1, 0xffc2, 0xffc3, 0xffc5, 0xffc6, 0xffc7, 0xffc8, 0xffc9,
0xffca, 0xffcb, 0xffcc, 0xffcd, 0xffce, 0xffcf,
];
import _JPEG from 'jay-peg';

const COLOR_SPACE_MAP = {
1: 'DeviceGray',
Expand All @@ -13,40 +8,30 @@ const COLOR_SPACE_MAP = {

class JPEG {
constructor(data, label) {
let marker;
this.data = data;
this.label = label;
this.orientation = 1;

if (this.data.readUInt16BE(0) !== 0xffd8) {
throw 'SOI not found in JPEG';
}

// Parse the EXIF orientation
this.orientation = exif.fromBuffer(this.data).Orientation || 1;
const markers = _JPEG.decode(this.data);

let pos = 2;
while (pos < this.data.length) {
marker = this.data.readUInt16BE(pos);
pos += 2;
if (MARKERS.includes(marker)) {
break;
for (let i = 0; i < markers.length; i += 1) {
const marker = markers[i];

if (marker.name === 'EXIF' && marker.entries.orientation) {
this.orientation = marker.entries.orientation;
}
pos += this.data.readUInt16BE(pos);
}

if (!MARKERS.includes(marker)) {
throw 'Invalid JPEG.';
if (marker.name === 'SOF') {
this.bits ||= marker.precision;
this.width ||= marker.width;
this.height ||= marker.height;
this.colorSpace ||= COLOR_SPACE_MAP[marker.numberOfComponents];
}
}
pos += 2;

this.bits = this.data[pos++];
this.height = this.data.readUInt16BE(pos);
pos += 2;

this.width = this.data.readUInt16BE(pos);
pos += 2;

const channels = this.data[pos++];
this.colorSpace = COLOR_SPACE_MAP[channels];

this.obj = null;
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"dependencies": {
"crypto-js": "^4.2.0",
"fontkit": "^2.0.4",
"jpeg-exif": "^1.1.4",
"jay-peg": "^1.1.1",
"linebreak": "^1.1.0",
"png-js": "^1.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const external = [
'png-js',
'crypto-js',
'saslprep',
'jpeg-exif'
'jay-peg'
];

const supportedBrowsers = [
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/jpeg.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from 'fs';
import JPEG from '../../lib/image/jpeg';

describe('JPEG', () => {
describe('parsing', () => {
test('parses basic JPEG properties', () => {
const data = fs.readFileSync('tests/images/bee.jpg');
const jpeg = new JPEG(data, 'bee');

expect(jpeg.width).toBeGreaterThan(0);
expect(jpeg.height).toBeGreaterThan(0);
expect(jpeg.bits).toBe(8);
expect(jpeg.colorSpace).toBe('DeviceRGB');
});

test('parses EXIF orientation', () => {
const data1 = fs.readFileSync('tests/images/orientation-3.jpeg');
const jpeg1 = new JPEG(data1, 'ori3');
expect(jpeg1.orientation).toBe(3);

const data6 = fs.readFileSync('tests/images/orientation-6.jpeg');
const jpeg6 = new JPEG(data6, 'ori-6');
expect(jpeg6.orientation).toBe(6);
});

test('defaults orientation to 1 when EXIF not present', () => {
const data = fs.readFileSync('tests/images/bee.jpg');
const jpeg = new JPEG(data, 'bee');
expect(jpeg.orientation).toBe(1);
});

test('throws on invalid JPEG (missing SOI marker)', () => {
const invalidData = Buffer.from([0x00, 0x00, 0x00, 0x00]);
expect(() => new JPEG(invalidData, 'invalid')).toThrow(
'SOI not found in JPEG',
);
});
});
});
18 changes: 10 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4864,6 +4864,15 @@ __metadata:
languageName: node
linkType: hard

"jay-peg@npm:^1.1.1":
version: 1.1.1
resolution: "jay-peg@npm:1.1.1"
dependencies:
restructure: "npm:^3.0.0"
checksum: 10c0/654ea1e1938dac5af24d4bf8fc9a2ac91faf39a18295b40e02b1c8bd08e9f17df78d383a538891e29ed5f7097e098bbae256d5dab39854314f5771eceeea2088
languageName: node
linkType: hard

"jest-changed-files@npm:^29.7.0":
version: 29.7.0
resolution: "jest-changed-files@npm:29.7.0"
Expand Down Expand Up @@ -5324,13 +5333,6 @@ __metadata:
languageName: node
linkType: hard

"jpeg-exif@npm:^1.1.4":
version: 1.1.4
resolution: "jpeg-exif@npm:1.1.4"
checksum: 10c0/0f9225b2423184d60c66b3d7361176801c17ede92fc9b3c044fcf00f379a5a1d424b360ecf0027dda47d405d253c7b62bf5b353fb08b2589e3650f38cc575e82
languageName: node
linkType: hard

"js-stringify@npm:^1.0.2":
version: 1.0.2
resolution: "js-stringify@npm:1.0.2"
Expand Down Expand Up @@ -6323,9 +6325,9 @@ __metadata:
fontkit: "npm:^2.0.4"
gh-pages: "npm:^6.2.0"
globals: "npm:^15.14.0"
jay-peg: "npm:^1.1.1"
jest: "npm:^29.7.0"
jest-image-snapshot: "npm:^6.4.0"
jpeg-exif: "npm:^1.1.4"
linebreak: "npm:^1.1.0"
markdown: "npm:~0.5.0"
pdfjs-dist: "npm:^2.14.305"
Expand Down