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