From 5b97a5b2a34f8c97a3bd12fbfcffa9757e04fc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Mon, 26 Jan 2026 08:27:18 +0900 Subject: [PATCH] chore: Replace deprecated jpeg-exif with jay-peg for JPEG parsing --- CHANGELOG.md | 1 + lib/image/jpeg.js | 45 ++++++++++++++--------------------------- package.json | 2 +- rollup.config.js | 2 +- tests/unit/jpeg.spec.js | 39 +++++++++++++++++++++++++++++++++++ yarn.lock | 18 +++++++++-------- 6 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 tests/unit/jpeg.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index edba1fff..9494c6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/image/jpeg.js b/lib/image/jpeg.js index 9fadb8bf..6b09c4ce 100644 --- a/lib/image/jpeg.js +++ b/lib/image/jpeg.js @@ -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', @@ -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; } diff --git a/package.json b/package.json index 25164ae7..481de149 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/rollup.config.js b/rollup.config.js index c3a8b867..27d96be5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -12,7 +12,7 @@ const external = [ 'png-js', 'crypto-js', 'saslprep', - 'jpeg-exif' + 'jay-peg' ]; const supportedBrowsers = [ diff --git a/tests/unit/jpeg.spec.js b/tests/unit/jpeg.spec.js new file mode 100644 index 00000000..c2a5f4a8 --- /dev/null +++ b/tests/unit/jpeg.spec.js @@ -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', + ); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 65ef81cb..69229c85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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"