mirror of
https://github.com/codedread/bitjs
synced 2025-10-05 10:29:24 +02:00
First version of JPEG and EXIF parser with starter unit tests and updated docs.
This commit is contained in:
parent
24edc8cbaf
commit
35e8ca9458
7 changed files with 910 additions and 7 deletions
234
image/parsers/exif.js
Normal file
234
image/parsers/exif.js
Normal file
|
@ -0,0 +1,234 @@
|
|||
import { ByteStream } from '../../io/bytestream.js';
|
||||
|
||||
/** @enum {number} */
|
||||
export const ExifTagNumber = {
|
||||
// Tags used by IFD0.
|
||||
IMAGE_DESCRIPTION: 0x010e,
|
||||
MAKE: 0x010f,
|
||||
MODEL: 0x0110,
|
||||
ORIENTATION: 0x0112,
|
||||
X_RESOLUTION: 0x011a,
|
||||
Y_RESOLUTION: 0x011b,
|
||||
RESOLUTION_UNIT: 0x0128,
|
||||
SOFTWARE: 0x0131,
|
||||
DATE_TIME: 0x0132,
|
||||
WHITE_POINT: 0x013e,
|
||||
PRIMARY_CHROMATICITIES: 0x013f,
|
||||
Y_CB_CR_COEFFICIENTS: 0x0211,
|
||||
Y_CB_CR_POSITIONING: 0x0213,
|
||||
REFERENCE_BLACK_WHITE: 0x0214,
|
||||
COPYRIGHT: 0x8298,
|
||||
EXIF_OFFSET: 0x8769,
|
||||
|
||||
// Tags used by Exif SubIFD.
|
||||
EXPOSURE_TIME: 0x829a,
|
||||
F_NUMBER: 0x829d,
|
||||
EXPOSURE_PROGRAM: 0x8822,
|
||||
ISO_SPEED_RATINGS: 0x8827,
|
||||
EXIF_VERSION: 0x9000,
|
||||
DATE_TIME_ORIGINAL: 0x9003,
|
||||
DATE_TIME_DIGITIZED: 0x9004,
|
||||
COMPONENT_CONFIGURATION: 0x9101,
|
||||
COMPRESSED_BITS_PER_PIXEL: 0x9102,
|
||||
SHUTTER_SPEED_VALUE: 0x9201,
|
||||
APERTURE_VALUE: 0x9202,
|
||||
BRIGHTNESS_VALUE: 0x9203,
|
||||
EXPOSURE_BIAS_VALUE: 0x9204,
|
||||
MAX_APERTURE_VALUE: 0x9205,
|
||||
SUBJECT_DISTANCE: 0x9206,
|
||||
METERING_MODE: 0x9207,
|
||||
LIGHT_SOURCE: 0x9208,
|
||||
FLASH: 0x9209,
|
||||
FOCAL_LENGTH: 0x920a,
|
||||
MAKER_NOTE: 0x927c,
|
||||
USER_COMMENT: 0x9286,
|
||||
FLASH_PIX_VERSION: 0xa000,
|
||||
COLOR_SPACE: 0xa001,
|
||||
EXIF_IMAGE_WIDTH: 0xa002,
|
||||
EXIF_IMAGE_HEIGHT: 0xa003,
|
||||
RELATED_SOUND_FILE: 0xa004,
|
||||
EXIF_INTEROPERABILITY_OFFSET: 0xa005,
|
||||
FOCAL_PLANE_X_RESOLUTION: 0xa20e,
|
||||
FOCAL_PLANE_Y_RESOLUTION: 0x20f,
|
||||
FOCAL_PLANE_RESOLUTION_UNIT: 0xa210,
|
||||
SENSING_METHOD: 0xa217,
|
||||
FILE_SOURCE: 0xa300,
|
||||
SCENE_TYPE: 0xa301,
|
||||
|
||||
// Tags used by IFD1.
|
||||
IMAGE_WIDTH: 0x0100,
|
||||
IMAGE_LENGTH: 0x0101,
|
||||
BITS_PER_SAMPLE: 0x0102,
|
||||
COMPRESSION: 0x0103,
|
||||
PHOTOMETRIC_INTERPRETATION: 0x0106,
|
||||
STRIP_OFFSETS: 0x0111,
|
||||
SAMPLES_PER_PIXEL: 0x0115,
|
||||
ROWS_PER_STRIP: 0x0116,
|
||||
STRIP_BYTE_COUNTS: 0x0117,
|
||||
// X_RESOLUTION, Y_RESOLUTION
|
||||
PLANAR_CONFIGURATION: 0x011c,
|
||||
// RESOLUTION_UNIT
|
||||
JPEG_IF_OFFSET: 0x0201,
|
||||
JPEG_IF_BYTE_COUNT: 0x0202,
|
||||
// Y_CB_CR_COEFFICIENTS
|
||||
Y_CB_CR_SUB_SAMPLING: 0x0212,
|
||||
// Y_CB_CR_POSITIONING, REFERENCE_BLACK_WHITE
|
||||
};
|
||||
|
||||
/** @enum {number} */
|
||||
export const ExifDataFormat = {
|
||||
UNSIGNED_BYTE: 1,
|
||||
ASCII_STRING: 2,
|
||||
UNSIGNED_SHORT: 3,
|
||||
UNSIGNED_LONG: 4,
|
||||
UNSIGNED_RATIONAL: 5,
|
||||
SIGNED_BYTE: 6,
|
||||
UNDEFINED: 7,
|
||||
SIGNED_SHORT: 8,
|
||||
SIGNED_LONG: 9,
|
||||
SIGNED_RATIONAL: 10,
|
||||
SINGLE_FLOAT: 11,
|
||||
DOUBLE_FLOAT: 12,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef ExifValue
|
||||
* @property {ExifTagNumber} tagNumber The numerical value of the tag.
|
||||
* @property {string=} tagName A string representing the tag number.
|
||||
* @property {ExifDataFormat} dataFormat The data format.
|
||||
* @property {number=} numericalValue Populated for SIGNED/UNSIGNED BYTE/SHORT/LONG/FLOAT.
|
||||
* @property {string=} stringValue Populated only for ASCII_STRING.
|
||||
* @property {number=} numeratorValue Populated only for SIGNED/UNSIGNED RATIONAL.
|
||||
* @property {number=} denominatorValue Populated only for SIGNED/UNSIGNED RATIONAL.
|
||||
* @property {number=} numComponents Populated only for UNDEFINED data format.
|
||||
* @property {number=} offsetValue Populated only for UNDEFINED data format.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} tagNumber
|
||||
* @param {string} type
|
||||
* @param {number} len
|
||||
* @param {number} dataVal
|
||||
*/
|
||||
function warnBadLength(tagNumber, type, len, dataVal) {
|
||||
const hexTag = tagNumber.toString(16);
|
||||
console.warn(`Tag 0x${hexTag} is ${type} with len=${len} and data=${dataVal}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ByteStream} stream
|
||||
* @param {ByteStream} lookAheadStream
|
||||
* @param {boolean} debug
|
||||
* @returns {ExifValue}
|
||||
*/
|
||||
export function getExifValue(stream, lookAheadStream, DEBUG = false) {
|
||||
const tagNumber = stream.readNumber(2);
|
||||
let tagName = findNameWithValue(ExifTagNumber, tagNumber);
|
||||
if (!tagName) {
|
||||
tagName = `UNKNOWN (0x${tagNumber.toString(16)})`;
|
||||
}
|
||||
|
||||
let dataFormat = stream.readNumber(2);
|
||||
|
||||
// Handle bad types for special tags.
|
||||
if (tagNumber === ExifTagNumber.EXIF_OFFSET) {
|
||||
dataFormat = ExifDataFormat.UNSIGNED_LONG;
|
||||
}
|
||||
|
||||
const dataFormatName = findNameWithValue(ExifDataFormat, dataFormat);
|
||||
if (!dataFormatName) throw `Invalid data format: ${dataFormat}`;
|
||||
|
||||
/** @type {ExifValue} */
|
||||
const exifValue = {
|
||||
tagNumber,
|
||||
tagName,
|
||||
dataFormat,
|
||||
};
|
||||
|
||||
let len = stream.readNumber(4);
|
||||
switch (dataFormat) {
|
||||
case ExifDataFormat.UNSIGNED_BYTE:
|
||||
if (len !== 1 && DEBUG) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
|
||||
}
|
||||
exifValue.numericalValue = stream.readNumber(1);
|
||||
stream.skip(3);
|
||||
break;
|
||||
case ExifDataFormat.ASCII_STRING:
|
||||
if (len <= 4) {
|
||||
exifValue.stringValue = stream.readString(4);
|
||||
} else {
|
||||
const strOffset = stream.readNumber(4);
|
||||
exifValue.stringValue = lookAheadStream.tee().skip(strOffset).readString(len - 1);
|
||||
}
|
||||
break;
|
||||
case ExifDataFormat.UNSIGNED_SHORT:
|
||||
if (len !== 1 && DEBUG) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
|
||||
}
|
||||
exifValue.numericalValue = stream.readNumber(2);
|
||||
stream.skip(2);
|
||||
break;
|
||||
case ExifDataFormat.UNSIGNED_LONG:
|
||||
if (len !== 1 && DEBUG) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
|
||||
}
|
||||
exifValue.numericalValue = stream.readNumber(4);
|
||||
break;
|
||||
case ExifDataFormat.UNSIGNED_RATIONAL:
|
||||
if (len !== 1 && DEBUG) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
|
||||
}
|
||||
|
||||
const uratStream = lookAheadStream.tee().skip(stream.readNumber(4));
|
||||
exifValue.numeratorValue = uratStream.readNumber(4);
|
||||
exifValue.denominatorValue = uratStream.readNumber(4);
|
||||
break;
|
||||
case ExifDataFormat.SIGNED_BYTE:
|
||||
if (len !== 1 && DEBUG) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4));
|
||||
}
|
||||
exifValue.numericalValue = stream.readSignedNumber(1);
|
||||
stream.skip(3);
|
||||
break;
|
||||
case ExifDataFormat.UNDEFINED:
|
||||
exifValue.numComponents = len;
|
||||
exifValue.offsetValue = stream.readNumber(4);
|
||||
break;
|
||||
case ExifDataFormat.SIGNED_SHORT:
|
||||
if (len !== 1 && DEBUG) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4));
|
||||
}
|
||||
exifValue.numericalValue = stream.readSignedNumber(2);
|
||||
stream.skip(2);
|
||||
break;
|
||||
case ExifDataFormat.SIGNED_LONG:
|
||||
if (len !== 1) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4));
|
||||
}
|
||||
exifValue.numericalValue = stream.readSignedNumber(4);
|
||||
break;
|
||||
case ExifDataFormat.SIGNED_RATIONAL:
|
||||
if (len !== 1 && DEBUG) {
|
||||
warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4));
|
||||
}
|
||||
|
||||
const ratStream = lookAheadStream.tee().skip(stream.readNumber(4));
|
||||
exifValue.numeratorValue = ratStream.readSignedNumber(4);
|
||||
exifValue.denominatorValue = ratStream.readSignedNumber(4);
|
||||
break;
|
||||
default:
|
||||
throw `Bad data format: ${dataFormat}`;
|
||||
}
|
||||
return exifValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} obj A numeric enum.
|
||||
* @param {number} valToFind The value to find.
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function findNameWithValue(obj, valToFind) {
|
||||
const entry = Object.entries(obj).find(([k,v]) => v === valToFind);
|
||||
return entry ? entry[0] : null;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue