1
0
Fork 0
mirror of https://github.com/codedread/bitjs synced 2025-10-03 09:39:16 +02:00
bitjs/image/parsers/exif.js

234 lines
7 KiB
JavaScript

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;
}