Skip to content

Commit 35e8ca9

Browse files
committed
First version of JPEG and EXIF parser with starter unit tests and updated docs.
1 parent 24edc8c commit 35e8ca9

File tree

7 files changed

+910
-7
lines changed

7 files changed

+910
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.
66

77
### Added
88

9-
- Added a GIF parser to bitjs.image.
9+
- Added GIF and JPEG parsers to bitjs.image.
1010
- Added a skip() method to ByteStream.
1111

1212
### Changed

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Includes:
1414
* bitjs/codecs: Get the codec info of media containers in a ISO RFC6381
1515
MIME type string
1616
* bitjs/file: Detect the type of file from its binary signature.
17-
* bitjs/image: Parsing GIF. Conversion of WebP to PNG or JPEG.
17+
* bitjs/image: Parsing GIF, JPEG. Conversion of WebP to PNG or JPEG.
1818
* bitjs/io: Low-level classes for interpreting binary data (BitStream
1919
ByteStream). For example, reading or peeking at N bits at a time.
2020

@@ -108,17 +108,17 @@ const mimeType = findMimeType(someArrayBuffer);
108108
### bitjs.image
109109

110110
This package includes code for dealing with binary images. It includes general event-based parsers
111-
for images (GIF only, at the moment). It also includes a module for converting WebP images into
112-
alternative raster graphics formats (PNG/JPG). This latter module is deprecated, now that WebP
111+
for images (GIF and JPEG only, at the moment). It also includes a module for converting WebP images
112+
into alternative raster graphics formats (PNG/JPG). This latter module is deprecated, now that WebP
113113
images are well-supported in all browsers.
114114

115115
#### GIF Parser
116116
```javascript
117117
import { GifParser } from './bitjs/image/parsers/gif.js'
118118

119119
const parser = new GifParser(someArrayBuffer);
120-
parser.addEventListener('application_extension', evt => {
121-
const appId = evt.applicationExtension.applicationIdentifier
120+
parser.onApplicationExtension(evt => {
121+
const appId = evt.applicationExtension.applicationIdentifier;
122122
const appAuthCode = new TextDecoder().decode(
123123
evt.applicationExtension.applicationAuthenticationCode);
124124
if (appId === 'XMP Data' && appAuthCode === 'XMP') {
@@ -130,6 +130,20 @@ parser.addEventListener('application_extension', evt => {
130130
parser.start();
131131
```
132132

133+
#### JPEG Parser
134+
```javascript
135+
import { JpegParser } from './bitjs/image/parsers/jpeg.js'
136+
import { ExifTagNumber } from './bitjs/image/parsers/exif.js';
137+
138+
const parser = new JpegParser(someArrayBuffer);
139+
let exif;
140+
const parser = new JpegParser(ab);
141+
parser.onApp1Exif(evt => {
142+
console.log(evt.exifValueMap.get(ExifTagNumber.IMAGE_DESCRIPTION).stringValue);
143+
});
144+
await parser.start();
145+
```
146+
133147
#### WebP Converter
134148
```javascript
135149
import { convertWebPtoPNG, convertWebPtoJPG } from './bitjs/image/webp-shim/webp-shim.js';

image/parsers/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
General-purpose, event-based parsers for digital images.
22

3-
Currently only supports GIF.
3+
Currently only supports GIF and JPEG.
44

55
Some nice implementations for HEIF, JPEG, PNG, TIFF here:
66
https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers

image/parsers/exif.js

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { ByteStream } from '../../io/bytestream.js';
2+
3+
/** @enum {number} */
4+
export const ExifTagNumber = {
5+
// Tags used by IFD0.
6+
IMAGE_DESCRIPTION: 0x010e,
7+
MAKE: 0x010f,
8+
MODEL: 0x0110,
9+
ORIENTATION: 0x0112,
10+
X_RESOLUTION: 0x011a,
11+
Y_RESOLUTION: 0x011b,
12+
RESOLUTION_UNIT: 0x0128,
13+
SOFTWARE: 0x0131,
14+
DATE_TIME: 0x0132,
15+
WHITE_POINT: 0x013e,
16+
PRIMARY_CHROMATICITIES: 0x013f,
17+
Y_CB_CR_COEFFICIENTS: 0x0211,
18+
Y_CB_CR_POSITIONING: 0x0213,
19+
REFERENCE_BLACK_WHITE: 0x0214,
20+
COPYRIGHT: 0x8298,
21+
EXIF_OFFSET: 0x8769,
22+
23+
// Tags used by Exif SubIFD.
24+
EXPOSURE_TIME: 0x829a,
25+
F_NUMBER: 0x829d,
26+
EXPOSURE_PROGRAM: 0x8822,
27+
ISO_SPEED_RATINGS: 0x8827,
28+
EXIF_VERSION: 0x9000,
29+
DATE_TIME_ORIGINAL: 0x9003,
30+
DATE_TIME_DIGITIZED: 0x9004,
31+
COMPONENT_CONFIGURATION: 0x9101,
32+
COMPRESSED_BITS_PER_PIXEL: 0x9102,
33+
SHUTTER_SPEED_VALUE: 0x9201,
34+
APERTURE_VALUE: 0x9202,
35+
BRIGHTNESS_VALUE: 0x9203,
36+
EXPOSURE_BIAS_VALUE: 0x9204,
37+
MAX_APERTURE_VALUE: 0x9205,
38+
SUBJECT_DISTANCE: 0x9206,
39+
METERING_MODE: 0x9207,
40+
LIGHT_SOURCE: 0x9208,
41+
FLASH: 0x9209,
42+
FOCAL_LENGTH: 0x920a,
43+
MAKER_NOTE: 0x927c,
44+
USER_COMMENT: 0x9286,
45+
FLASH_PIX_VERSION: 0xa000,
46+
COLOR_SPACE: 0xa001,
47+
EXIF_IMAGE_WIDTH: 0xa002,
48+
EXIF_IMAGE_HEIGHT: 0xa003,
49+
RELATED_SOUND_FILE: 0xa004,
50+
EXIF_INTEROPERABILITY_OFFSET: 0xa005,
51+
FOCAL_PLANE_X_RESOLUTION: 0xa20e,
52+
FOCAL_PLANE_Y_RESOLUTION: 0x20f,
53+
FOCAL_PLANE_RESOLUTION_UNIT: 0xa210,
54+
SENSING_METHOD: 0xa217,
55+
FILE_SOURCE: 0xa300,
56+
SCENE_TYPE: 0xa301,
57+
58+
// Tags used by IFD1.
59+
IMAGE_WIDTH: 0x0100,
60+
IMAGE_LENGTH: 0x0101,
61+
BITS_PER_SAMPLE: 0x0102,
62+
COMPRESSION: 0x0103,
63+
PHOTOMETRIC_INTERPRETATION: 0x0106,
64+
STRIP_OFFSETS: 0x0111,
65+
SAMPLES_PER_PIXEL: 0x0115,
66+
ROWS_PER_STRIP: 0x0116,
67+
STRIP_BYTE_COUNTS: 0x0117,
68+
// X_RESOLUTION, Y_RESOLUTION
69+
PLANAR_CONFIGURATION: 0x011c,
70+
// RESOLUTION_UNIT
71+
JPEG_IF_OFFSET: 0x0201,
72+
JPEG_IF_BYTE_COUNT: 0x0202,
73+
// Y_CB_CR_COEFFICIENTS
74+
Y_CB_CR_SUB_SAMPLING: 0x0212,
75+
// Y_CB_CR_POSITIONING, REFERENCE_BLACK_WHITE
76+
};
77+
78+
/** @enum {number} */
79+
export const ExifDataFormat = {
80+
UNSIGNED_BYTE: 1,
81+
ASCII_STRING: 2,
82+
UNSIGNED_SHORT: 3,
83+
UNSIGNED_LONG: 4,
84+
UNSIGNED_RATIONAL: 5,
85+
SIGNED_BYTE: 6,
86+
UNDEFINED: 7,
87+
SIGNED_SHORT: 8,
88+
SIGNED_LONG: 9,
89+
SIGNED_RATIONAL: 10,
90+
SINGLE_FLOAT: 11,
91+
DOUBLE_FLOAT: 12,
92+
};
93+
94+
/**
95+
* @typedef ExifValue
96+
* @property {ExifTagNumber} tagNumber The numerical value of the tag.
97+
* @property {string=} tagName A string representing the tag number.
98+
* @property {ExifDataFormat} dataFormat The data format.
99+
* @property {number=} numericalValue Populated for SIGNED/UNSIGNED BYTE/SHORT/LONG/FLOAT.
100+
* @property {string=} stringValue Populated only for ASCII_STRING.
101+
* @property {number=} numeratorValue Populated only for SIGNED/UNSIGNED RATIONAL.
102+
* @property {number=} denominatorValue Populated only for SIGNED/UNSIGNED RATIONAL.
103+
* @property {number=} numComponents Populated only for UNDEFINED data format.
104+
* @property {number=} offsetValue Populated only for UNDEFINED data format.
105+
*/
106+
107+
/**
108+
* @param {number} tagNumber
109+
* @param {string} type
110+
* @param {number} len
111+
* @param {number} dataVal
112+
*/
113+
function warnBadLength(tagNumber, type, len, dataVal) {
114+
const hexTag = tagNumber.toString(16);
115+
console.warn(`Tag 0x${hexTag} is ${type} with len=${len} and data=${dataVal}`);
116+
}
117+
118+
/**
119+
* @param {ByteStream} stream
120+
* @param {ByteStream} lookAheadStream
121+
* @param {boolean} debug
122+
* @returns {ExifValue}
123+
*/
124+
export function getExifValue(stream, lookAheadStream, DEBUG = false) {
125+
const tagNumber = stream.readNumber(2);
126+
let tagName = findNameWithValue(ExifTagNumber, tagNumber);
127+
if (!tagName) {
128+
tagName = `UNKNOWN (0x${tagNumber.toString(16)})`;
129+
}
130+
131+
let dataFormat = stream.readNumber(2);
132+
133+
// Handle bad types for special tags.
134+
if (tagNumber === ExifTagNumber.EXIF_OFFSET) {
135+
dataFormat = ExifDataFormat.UNSIGNED_LONG;
136+
}
137+
138+
const dataFormatName = findNameWithValue(ExifDataFormat, dataFormat);
139+
if (!dataFormatName) throw `Invalid data format: ${dataFormat}`;
140+
141+
/** @type {ExifValue} */
142+
const exifValue = {
143+
tagNumber,
144+
tagName,
145+
dataFormat,
146+
};
147+
148+
let len = stream.readNumber(4);
149+
switch (dataFormat) {
150+
case ExifDataFormat.UNSIGNED_BYTE:
151+
if (len !== 1 && DEBUG) {
152+
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
153+
}
154+
exifValue.numericalValue = stream.readNumber(1);
155+
stream.skip(3);
156+
break;
157+
case ExifDataFormat.ASCII_STRING:
158+
if (len <= 4) {
159+
exifValue.stringValue = stream.readString(4);
160+
} else {
161+
const strOffset = stream.readNumber(4);
162+
exifValue.stringValue = lookAheadStream.tee().skip(strOffset).readString(len - 1);
163+
}
164+
break;
165+
case ExifDataFormat.UNSIGNED_SHORT:
166+
if (len !== 1 && DEBUG) {
167+
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
168+
}
169+
exifValue.numericalValue = stream.readNumber(2);
170+
stream.skip(2);
171+
break;
172+
case ExifDataFormat.UNSIGNED_LONG:
173+
if (len !== 1 && DEBUG) {
174+
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
175+
}
176+
exifValue.numericalValue = stream.readNumber(4);
177+
break;
178+
case ExifDataFormat.UNSIGNED_RATIONAL:
179+
if (len !== 1 && DEBUG) {
180+
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
181+
}
182+
183+
const uratStream = lookAheadStream.tee().skip(stream.readNumber(4));
184+
exifValue.numeratorValue = uratStream.readNumber(4);
185+
exifValue.denominatorValue = uratStream.readNumber(4);
186+
break;
187+
case ExifDataFormat.SIGNED_BYTE:
188+
if (len !== 1 && DEBUG) {
189+
warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4));
190+
}
191+
exifValue.numericalValue = stream.readSignedNumber(1);
192+
stream.skip(3);
193+
break;
194+
case ExifDataFormat.UNDEFINED:
195+
exifValue.numComponents = len;
196+
exifValue.offsetValue = stream.readNumber(4);
197+
break;
198+
case ExifDataFormat.SIGNED_SHORT:
199+
if (len !== 1 && DEBUG) {
200+
warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4));
201+
}
202+
exifValue.numericalValue = stream.readSignedNumber(2);
203+
stream.skip(2);
204+
break;
205+
case ExifDataFormat.SIGNED_LONG:
206+
if (len !== 1) {
207+
warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4));
208+
}
209+
exifValue.numericalValue = stream.readSignedNumber(4);
210+
break;
211+
case ExifDataFormat.SIGNED_RATIONAL:
212+
if (len !== 1 && DEBUG) {
213+
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
214+
}
215+
216+
const ratStream = lookAheadStream.tee().skip(stream.readNumber(4));
217+
exifValue.numeratorValue = ratStream.readSignedNumber(4);
218+
exifValue.denominatorValue = ratStream.readSignedNumber(4);
219+
break;
220+
default:
221+
throw `Bad data format: ${dataFormat}`;
222+
}
223+
return exifValue;
224+
}
225+
226+
/**
227+
* @param {Object} obj A numeric enum.
228+
* @param {number} valToFind The value to find.
229+
* @returns {string|null}
230+
*/
231+
function findNameWithValue(obj, valToFind) {
232+
const entry = Object.entries(obj).find(([k,v]) => v === valToFind);
233+
return entry ? entry[0] : null;
234+
}

0 commit comments

Comments
 (0)