Skip to content

Commit 5b97a5b

Browse files
committed
chore: Replace deprecated jpeg-exif with jay-peg for JPEG parsing
1 parent d2d3720 commit 5b97a5b

6 files changed

Lines changed: 67 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Fix Interlaced PNG with indexed transparency rendered incorrectly
1010
- Preserve existing PageMode instead of overwriting when adding outlines
1111
- Support outlines that jump to specific page positions with custom zoom level
12+
- Replace deprecated jpeg-exif with jay-peg for JPEG parsing
1213

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

lib/image/jpeg.js

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import exif from 'jpeg-exif';
2-
3-
const MARKERS = [
4-
0xffc0, 0xffc1, 0xffc2, 0xffc3, 0xffc5, 0xffc6, 0xffc7, 0xffc8, 0xffc9,
5-
0xffca, 0xffcb, 0xffcc, 0xffcd, 0xffce, 0xffcf,
6-
];
1+
import _JPEG from 'jay-peg';
72

83
const COLOR_SPACE_MAP = {
94
1: 'DeviceGray',
@@ -13,40 +8,30 @@ const COLOR_SPACE_MAP = {
138

149
class JPEG {
1510
constructor(data, label) {
16-
let marker;
1711
this.data = data;
1812
this.label = label;
13+
this.orientation = 1;
14+
1915
if (this.data.readUInt16BE(0) !== 0xffd8) {
2016
throw 'SOI not found in JPEG';
2117
}
2218

23-
// Parse the EXIF orientation
24-
this.orientation = exif.fromBuffer(this.data).Orientation || 1;
19+
const markers = _JPEG.decode(this.data);
2520

26-
let pos = 2;
27-
while (pos < this.data.length) {
28-
marker = this.data.readUInt16BE(pos);
29-
pos += 2;
30-
if (MARKERS.includes(marker)) {
31-
break;
21+
for (let i = 0; i < markers.length; i += 1) {
22+
const marker = markers[i];
23+
24+
if (marker.name === 'EXIF' && marker.entries.orientation) {
25+
this.orientation = marker.entries.orientation;
3226
}
33-
pos += this.data.readUInt16BE(pos);
34-
}
3527

36-
if (!MARKERS.includes(marker)) {
37-
throw 'Invalid JPEG.';
28+
if (marker.name === 'SOF') {
29+
this.bits ||= marker.precision;
30+
this.width ||= marker.width;
31+
this.height ||= marker.height;
32+
this.colorSpace ||= COLOR_SPACE_MAP[marker.numberOfComponents];
33+
}
3834
}
39-
pos += 2;
40-
41-
this.bits = this.data[pos++];
42-
this.height = this.data.readUInt16BE(pos);
43-
pos += 2;
44-
45-
this.width = this.data.readUInt16BE(pos);
46-
pos += 2;
47-
48-
const channels = this.data[pos++];
49-
this.colorSpace = COLOR_SPACE_MAP[channels];
5035

5136
this.obj = null;
5237
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"dependencies": {
5151
"crypto-js": "^4.2.0",
5252
"fontkit": "^2.0.4",
53-
"jpeg-exif": "^1.1.4",
53+
"jay-peg": "^1.1.1",
5454
"linebreak": "^1.1.0",
5555
"png-js": "^1.0.0"
5656
},

rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const external = [
1212
'png-js',
1313
'crypto-js',
1414
'saslprep',
15-
'jpeg-exif'
15+
'jay-peg'
1616
];
1717

1818
const supportedBrowsers = [

tests/unit/jpeg.spec.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import fs from 'fs';
2+
import JPEG from '../../lib/image/jpeg';
3+
4+
describe('JPEG', () => {
5+
describe('parsing', () => {
6+
test('parses basic JPEG properties', () => {
7+
const data = fs.readFileSync('tests/images/bee.jpg');
8+
const jpeg = new JPEG(data, 'bee');
9+
10+
expect(jpeg.width).toBeGreaterThan(0);
11+
expect(jpeg.height).toBeGreaterThan(0);
12+
expect(jpeg.bits).toBe(8);
13+
expect(jpeg.colorSpace).toBe('DeviceRGB');
14+
});
15+
16+
test('parses EXIF orientation', () => {
17+
const data1 = fs.readFileSync('tests/images/orientation-3.jpeg');
18+
const jpeg1 = new JPEG(data1, 'ori3');
19+
expect(jpeg1.orientation).toBe(3);
20+
21+
const data6 = fs.readFileSync('tests/images/orientation-6.jpeg');
22+
const jpeg6 = new JPEG(data6, 'ori-6');
23+
expect(jpeg6.orientation).toBe(6);
24+
});
25+
26+
test('defaults orientation to 1 when EXIF not present', () => {
27+
const data = fs.readFileSync('tests/images/bee.jpg');
28+
const jpeg = new JPEG(data, 'bee');
29+
expect(jpeg.orientation).toBe(1);
30+
});
31+
32+
test('throws on invalid JPEG (missing SOI marker)', () => {
33+
const invalidData = Buffer.from([0x00, 0x00, 0x00, 0x00]);
34+
expect(() => new JPEG(invalidData, 'invalid')).toThrow(
35+
'SOI not found in JPEG',
36+
);
37+
});
38+
});
39+
});

yarn.lock

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4864,6 +4864,15 @@ __metadata:
48644864
languageName: node
48654865
linkType: hard
48664866

4867+
"jay-peg@npm:^1.1.1":
4868+
version: 1.1.1
4869+
resolution: "jay-peg@npm:1.1.1"
4870+
dependencies:
4871+
restructure: "npm:^3.0.0"
4872+
checksum: 10c0/654ea1e1938dac5af24d4bf8fc9a2ac91faf39a18295b40e02b1c8bd08e9f17df78d383a538891e29ed5f7097e098bbae256d5dab39854314f5771eceeea2088
4873+
languageName: node
4874+
linkType: hard
4875+
48674876
"jest-changed-files@npm:^29.7.0":
48684877
version: 29.7.0
48694878
resolution: "jest-changed-files@npm:29.7.0"
@@ -5324,13 +5333,6 @@ __metadata:
53245333
languageName: node
53255334
linkType: hard
53265335

5327-
"jpeg-exif@npm:^1.1.4":
5328-
version: 1.1.4
5329-
resolution: "jpeg-exif@npm:1.1.4"
5330-
checksum: 10c0/0f9225b2423184d60c66b3d7361176801c17ede92fc9b3c044fcf00f379a5a1d424b360ecf0027dda47d405d253c7b62bf5b353fb08b2589e3650f38cc575e82
5331-
languageName: node
5332-
linkType: hard
5333-
53345336
"js-stringify@npm:^1.0.2":
53355337
version: 1.0.2
53365338
resolution: "js-stringify@npm:1.0.2"
@@ -6323,9 +6325,9 @@ __metadata:
63236325
fontkit: "npm:^2.0.4"
63246326
gh-pages: "npm:^6.2.0"
63256327
globals: "npm:^15.14.0"
6328+
jay-peg: "npm:^1.1.1"
63266329
jest: "npm:^29.7.0"
63276330
jest-image-snapshot: "npm:^6.4.0"
6328-
jpeg-exif: "npm:^1.1.4"
63296331
linebreak: "npm:^1.1.0"
63306332
markdown: "npm:~0.5.0"
63316333
pdfjs-dist: "npm:^2.14.305"

0 commit comments

Comments
 (0)