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

First version of JPEG and EXIF parser with starter unit tests and updated docs.

This commit is contained in:
Jeff Schiller 2024-01-15 12:36:23 -08:00
parent 24edc8cbaf
commit 35e8ca9458
7 changed files with 910 additions and 7 deletions

View file

@ -1,6 +1,6 @@
General-purpose, event-based parsers for digital images.
Currently only supports GIF.
Currently only supports GIF and JPEG.
Some nice implementations for HEIF, JPEG, PNG, TIFF here:
https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers

234
image/parsers/exif.js Normal file
View 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;
}

612
image/parsers/jpeg.js Normal file
View file

@ -0,0 +1,612 @@
/*
* jpeg.js
*
* An event-based parser for JPEG images.
*
* Licensed under the MIT License
*
* Copyright(c) 2024 Google Inc.
*/
import { ByteStream } from '../../io/bytestream.js';
import { ExifTagNumber, getExifValue } from './exif.js';
/**
* @typedef {import('./exif.js').ExifValue} ExifValue
*/
// https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
// https://www.media.mit.edu/pia/Research/deepview/exif.html
// https://mykb.cipindanci.com/archive/SuperKB/1294/JPEG%20File%20Layout%20and%20Format.htm
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf
let DEBUG = false;
/** @enum {string} */
export const JpegParseEventType = {
APP0_MARKER: 'app0_marker',
APP0_EXTENSION: 'app0_extension',
APP1_EXIF: 'app1_exif',
DEFINE_QUANTIZATION_TABLE: 'define_quantization_table',
DEFINE_HUFFMAN_TABLE: 'define_huffman_table',
START_OF_FRAME: 'start_of_frame',
START_OF_SCAN: 'start_of_scan',
}
/** @enum {number} */
const JpegSegmentType = {
SOF0: 0xC0,
SOF1: 0xC1,
SOF2: 0xC2,
DHT: 0xC4,
SOI: 0xD8,
EOI: 0xD9,
SOS: 0xDA,
DQT: 0xDB,
APP0: 0xE0,
APP1: 0xE1,
};
/**
* @param {Uint8Array} bytes An array of bytes of size 2.
* @returns {JpegSegmentType} Returns the second byte in bytes.
*/
function getJpegMarker(bytes) {
if (bytes.byteLength < 2) throw `Bad bytes length: ${bytes.byteLength}`;
if (bytes[0] !== 0xFF) throw `Bad marker, first byte=0x${bytes[0].toString(16)}`;
return bytes[1];
}
/** @enum {number} */
export const JpegDensityUnits = {
NO_UNITS: 0,
PIXELS_PER_INCH: 1,
PIXELS_PER_CM: 2,
};
/**
* @typedef JpegApp0Marker
* @property {string} jfifVersion Like '1.02'.
* @property {JpegDensityUnits} densityUnits
* @property {number} xDensity
* @property {number} yDensity
* @property {number} xThumbnail
* @property {number} yThumbnail
* @property {Uint8Array} thumbnailData RGB data. Size is 3 x thumbnailWidth x thumbnailHeight.
*/
export class JpegApp0MarkerEvent extends Event {
/** @param {JpegApp0Marker} */
constructor(segment) {
super(JpegParseEventType.APP0_MARKER);
/** @type {JpegApp0Marker} */
this.app0Marker = segment;
}
}
/** @enum {number} */
export const JpegExtensionThumbnailFormat = {
JPEG: 0x10,
ONE_BYTE_PER_PIXEL_PALETTIZED: 0x11,
THREE_BYTES_PER_PIXEL_RGB: 0x13,
};
/**
* @typedef JpegApp0Extension
* @property {JpegExtensionThumbnailFormat} thumbnailFormat
* @property {Uint8Array} thumbnailData Raw thumbnail data
*/
export class JpegApp0ExtensionEvent extends Event {
/** @param {JpegApp0Extension} */
constructor(segment) {
super(JpegParseEventType.APP0_EXTENSION);
/** @type {JpegApp0Extension} */
this.app0Extension = segment;
}
}
export class JpegApp1ExifEvent extends Event {
/** @param {Map<number, ExifValue>} exifValueMap */
constructor(exifValueMap) {
super(JpegParseEventType.APP1_EXIF);
/** @type {Map<number, ExifValue>} */
this.exifValueMap = exifValueMap;
}
}
/**
* @typedef JpegDefineQuantizationTable
* @property {number} tableNumber Table/component number.
* @property {number} precision (0=byte, 1=word).
* @property {number[]} tableValues 64 numbers representing the quantization table.
*/
export class JpegDefineQuantizationTableEvent extends Event {
/** @param {JpegDefineQuantizationTable} table */
constructor(table) {
super(JpegParseEventType.DEFINE_QUANTIZATION_TABLE);
/** @type {JpegDefineQuantizationTable} */
this.quantizationTable = table;
}
}
/** @enum {number} */
export const JpegHuffmanTableType = {
DC: 0,
AC: 1,
};
/**
* @typedef JpegDefineHuffmanTable
* @property {number} tableNumber Table/component number (0-3).
* @property {JpegHuffmanTableType} tableType Either DC or AC.
* @property {number[]} numberOfSymbols A 16-byte array specifying the # of symbols of each length.
* @property {number[]} symbols
*/
export class JpegDefineHuffmanTableEvent extends Event {
/** @param {JpegDefineHuffmanTable} table */
constructor(table) {
super(JpegParseEventType.DEFINE_HUFFMAN_TABLE);
/** @type {JpegDefineHuffmanTable} */
this.huffmanTable = table;
}
}
/** @enum {number} */
const JpegDctType = {
BASELINE: 0,
EXTENDED_SEQUENTIAL: 1,
PROGRESSIVE: 2,
};
/** @enum {number} */
const JpegComponentType = {
Y: 1,
CB: 2,
CR: 3,
I: 4,
Q: 5,
};
/**
* @typedef JpegComponentDetail
* @property {JpegComponentType} componentId
* @property {number} verticalSamplingFactor
* @property {number} horizontalSamplingFactor
* @property {number} quantizationTableNumber
*/
/**
* @typedef JpegStartOfFrame
* @property {JpegDctType} dctType
* @property {number} dataPrecision
* @property {number} imageHeight
* @property {number} imageWidth
* @property {number} numberOfComponents Usually 1, 3, or 4.
* @property {JpegComponentDetail[]} componentDetails
*/
export class JpegStartOfFrameEvent extends Event {
/** @param {JpegStartOfFrame} sof */
constructor(sof) {
super(JpegParseEventType.START_OF_FRAME);
/** @type {JpegStartOfFrame} */
this.startOfFrame = sof;
}
}
/**
* @typedef JpegStartOfScan
* @property {number} componentsInScan
* @property {number} componentSelectorY
* @property {number} huffmanTableSelectorY
* @property {number} componentSelectorCb
* @property {number} huffmanTableSelectorCb
* @property {number} componentSelectorCr
* @property {number} huffmanTableSelectorCr
* @property {number} scanStartPositionInBlock
* @property {number} scanEndPositionInBlock
* @property {number} successiveApproximationBitPosition
* @property {Uint8Array} rawImageData
*/
export class JpegStartOfScanEvent extends Event {
constructor(sos) {
super(JpegParseEventType.START_OF_SCAN);
/** @type {JpegStartOfScan} */
this.sos = sos;
}
}
export class JpegParser extends EventTarget {
/**
* @type {ByteStream}
* @private
*/
bstream;
/**
* @type {boolean}
* @private
*/
hasApp0MarkerSegment = false;
/** @param {ArrayBuffer} ab */
constructor(ab) {
super();
this.bstream = new ByteStream(ab);
}
/**
* Type-safe way to bind a listener for a JpegApp0MarkerEvent.
* @param {function(JpegApp0MarkerEvent): void} listener
* @returns {JpegParser} for chaining
*/
onApp0Marker(listener) {
super.addEventListener(JpegParseEventType.APP0_MARKER, listener);
return this;
}
/**
* Type-safe way to bind a listener for a JpegApp0ExtensionEvent.
* @param {function(JpegApp0MarkerEvent): void} listener
* @returns {JpegParser} for chaining
*/
onApp0Extension(listener) {
super.addEventListener(JpegParseEventType.APP0_EXTENSION, listener);
return this;
}
/**
* Type-safe way to bind a listener for a JpegApp1ExifEvent.
* @param {function(JpegApp1ExifEvent): void} listener
* @returns {JpegParser} for chaining
*/
onApp1Exif(listener) {
super.addEventListener(JpegParseEventType.APP1_EXIF, listener);
return this;
}
/**
* Type-safe way to bind a listener for a JpegDefineQuantizationTableEvent.
* @param {function(JpegDefineQuantizationTableEvent): void} listener
* @returns {JpegParser} for chaining
*/
onDefineQuantizationTable(listener) {
super.addEventListener(JpegParseEventType.DEFINE_QUANTIZATION_TABLE, listener);
return this;
}
/**
* Type-safe way to bind a listener for a JpegDefineHuffmanTableEvent.
* @param {function(JpegDefineHuffmanTableEvent): void} listener
* @returns {JpegParser} for chaining
*/
onDefineHuffmanTable(listener) {
super.addEventListener(JpegParseEventType.DEFINE_HUFFMAN_TABLE, listener);
return this;
}
/**
* Type-safe way to bind a listener for a JpegStartOfFrameEvent.
* @param {function(JpegStartOfFrameEvent): void} listener
* @returns {JpegParser} for chaining
*/
onStartOfFrame(listener) {
super.addEventListener(JpegParseEventType.START_OF_FRAME, listener);
return this;
}
/**
* Type-safe way to bind a listener for a JpegStartOfScanEvent.
* @param {function(JpegStartOfScanEvent): void} listener
* @returns {JpegParser} for chaining
*/
onStartOfScan(listener) {
super.addEventListener(JpegParseEventType.START_OF_SCAN, listener);
return this;
}
/** @returns {Promise<void>} A Promise that resolves when the parsing is complete. */
async start() {
const segmentType = getJpegMarker(this.bstream.readBytes(2));
if (segmentType !== JpegSegmentType.SOI) throw `Did not start with a SOI`;
let jpegMarker;
do {
jpegMarker = getJpegMarker(this.bstream.readBytes(2));
if (jpegMarker === JpegSegmentType.APP0) {
this.bstream.setBigEndian();
const length = this.bstream.readNumber(2);
const skipAheadStream = this.bstream.tee().skip(length - 2);
const identifier = this.bstream.readString(4);
if (identifier === 'JFIF') {
if (this.hasApp0MarkerSegment) throw `JFIF found after JFIF`;
if (this.bstream.readNumber(1) !== 0) throw 'No null byte terminator for JFIF';
this.hasApp0MarkerSegment = true;
const majorVer = `${this.bstream.readNumber(1)}.`;
const minorVer = `${this.bstream.readNumber(1)}`.padStart(2, '0');
const densityUnits = this.bstream.readNumber(1);
const xDensity = this.bstream.readNumber(2);
const yDensity = this.bstream.readNumber(2);
const xThumbnail = this.bstream.readNumber(1);
const yThumbnail = this.bstream.readNumber(1);
/** @type {JpegApp0Marker} */
let app0MarkerSegment = {
jfifVersion: `${majorVer}${minorVer}`,
densityUnits,
xDensity,
yDensity,
xThumbnail,
yThumbnail,
thumbnailData: this.bstream.readBytes(3 * xThumbnail * yThumbnail),
};
this.dispatchEvent(new JpegApp0MarkerEvent(app0MarkerSegment));
}
else if (identifier === 'JFXX') {
if (!this.hasApp0MarkerSegment) throw `JFXX found without JFIF`;
if (this.bstream.readNumber(1) !== 0) throw 'No null byte terminator for JFXX';
const thumbnailFormat = this.bstream.readNumber(1);
if (!Object.values(JpegExtensionThumbnailFormat).includes(thumbnailFormat)) {
throw `Bad Extension Thumbnail Format: ${thumbnailFormat}`;
}
// The JFXX segment has length (2), 'JFXX' (4), null byte (1), thumbnail format (1)
const thumbnailData = this.bstream.readBytes(length - 8);
/** @type {JpegApp0Extension} */
let app0ExtensionSegment = {
thumbnailFormat,
thumbnailData,
};
this.dispatchEvent(new JpegApp0ExtensionEvent(app0ExtensionSegment));
}
else {
throw `Bad APP0 identifier: ${identifier}`;
}
this.bstream = skipAheadStream;
} // End of APP0
else if (jpegMarker === JpegSegmentType.APP1) {
this.bstream.setBigEndian();
const length = this.bstream.readNumber(2);
const skipAheadStream = this.bstream.tee().skip(length - 2);
const identifier = this.bstream.readString(4);
if (identifier !== 'Exif') {
// TODO: Handle XMP.
// console.log(identifier + this.bstream.readString(length - 2 - 4));
this.bstream = skipAheadStream;
continue;
}
if (this.bstream.readNumber(2) !== 0) throw `No null byte termination`;
const lookAheadStream = this.bstream.tee();
const tiffByteAlign = this.bstream.readString(2);
if (tiffByteAlign === 'II') {
this.bstream.setLittleEndian();
lookAheadStream.setLittleEndian();
} else if (tiffByteAlign === 'MM') {
this.bstream.setBigEndian();
lookAheadStream.setBigEndian();
} else {
throw `Invalid TIFF byte align symbol: ${tiffByteAlign}`;
}
const tiffMarker = this.bstream.readNumber(2);
if (tiffMarker !== 0x002A) {
throw `Invalid marker, not 0x002a: 0x${tiffMarker.toString(16)}`;
}
/** @type {Map<number, ExifValue} */
const exifValueMap = new Map();
// The offset includes the tiffByteAlign (2), marker (2), and the offset field itself (4).
const ifdOffset = this.bstream.readNumber(4) - 8;
let ifdStream = this.bstream.tee();
let nextIfdOffset;
while (true) {
nextIfdOffset = this.readExifIfd(ifdStream, lookAheadStream, exifValueMap);
// No more IFDs, so stop the loop.
if (nextIfdOffset === 0) break;
// Else, we have another IFD to read, point the stream at it.
ifdStream = lookAheadStream.tee().skip(nextIfdOffset);
}
this.dispatchEvent(new JpegApp1ExifEvent(exifValueMap));
this.bstream = skipAheadStream;
} // End of APP1
else if (jpegMarker === JpegSegmentType.DQT) {
this.bstream.setBigEndian();
const length = this.bstream.readNumber(2);
const dqtLength = length - 2;
let ptr = 0;
while (ptr < dqtLength) {
// https://gist.github.com/FranckFreiburger/d8e7445245221c5cf38e69a88f22eeeb#file-getjpegquality-js-L76
const firstByte = this.bstream.readNumber(1);
// Lower 4 bits are the component index.
const tableNumber = (firstByte & 0xF);
// Upper 4 bits are the precision (0=byte, 1=word).
const precision = ((firstByte & 0xF0) >> 4);
if (precision !== 0 && precision !== 1) throw `Weird value for DQT precision: ${precision}`;
const valSize = precision === 0 ? 1 : 2;
const tableValues = new Array(64);
for (let v = 0; v < 64; ++v) {
tableValues[v] = this.bstream.readNumber(valSize);
}
/** @type {JpegDefineQuantizationTable} */
const table = {
tableNumber,
precision,
tableValues,
};
this.dispatchEvent(new JpegDefineQuantizationTableEvent(table));
ptr += (1 + valSize * 64);
}
} // End of DQT
else if (jpegMarker === JpegSegmentType.DHT) {
this.bstream.setBigEndian();
const length = this.bstream.readNumber(2);
let ptr = 2;
while (ptr < length) {
const firstByte = this.bstream.readNumber(1);
const tableNumber = (firstByte & 0xF);
const tableType = ((firstByte & 0xF0) >> 4);
if (tableNumber > 3) throw `Weird DHT table number = ${tableNumber}`;
if (tableType !== 0 && tableType !== 1) throw `Weird DHT table type = ${tableType}`;
const numberOfSymbols = Array.from(this.bstream.readBytes(16));
let numCodes = 0;
for (let symbolLength = 1; symbolLength <= 16; ++symbolLength) {
const numSymbolsAtLength = numberOfSymbols[symbolLength - 1];
numCodes += numSymbolsAtLength;
}
if (numCodes > 256) throw `Bad # of DHT codes: ${numCodes}`;
const symbols = Array.from(this.bstream.readBytes(numCodes));
/** @type {JpegDefineHuffmanTable} */
const table = {
tableNumber,
tableType,
numberOfSymbols,
symbols,
};
this.dispatchEvent(new JpegDefineHuffmanTableEvent(table));
ptr += (1 + 16 + numCodes);
}
if (ptr !== length) throw `Bad DHT ptr: ${ptr}!`;
} // End of DHT
else if (jpegMarker === JpegSegmentType.SOF0
|| jpegMarker === JpegSegmentType.SOF1
|| jpegMarker === JpegSegmentType.SOF2) {
this.bstream.setBigEndian();
const length = this.bstream.readNumber(2);
const dctType = (jpegMarker - JpegSegmentType.SOF0);
if (![0, 1, 2].includes(dctType)) throw `Weird DCT type: ${dctType}`;
const dataPrecision = this.bstream.readNumber(1);
const imageHeight = this.bstream.readNumber(2);
const imageWidth = this.bstream.readNumber(2);
const numberOfComponents = this.bstream.readNumber(1);
const componentDetails = [];
for (let c = 0; c < numberOfComponents; ++c) {
const componentId = this.bstream.readNumber(1);
const nextByte = this.bstream.readNumber(1);
const verticalSamplingFactor = (nextByte & 0xF);
const horizontalSamplingFactor = ((nextByte & 0xF0) >> 4);
const quantizationTableNumber = this.bstream.readNumber(1);
componentDetails.push({
componentId,
verticalSamplingFactor,
horizontalSamplingFactor,
quantizationTableNumber,
});
}
/** @type {JpegStartOfFrame} */
const sof = {
dctType,
dataPrecision,
imageHeight,
imageWidth,
numberOfComponents,
componentDetails,
};
this.dispatchEvent(new JpegStartOfFrameEvent(sof));
} // End of SOF0, SOF1, SOF2
else if (jpegMarker === JpegSegmentType.SOS) {
this.bstream.setBigEndian();
const length = this.bstream.readNumber(2);
// console.log(`Inside SOS with length = ${length}`);
if (length !== 12) throw `Bad length in SOS header: ${length}`;
/** @type {JpegStartOfScan} */
const sos = {
componentsInScan: this.bstream.readNumber(1),
componentSelectorY: this.bstream.readNumber(1),
huffmanTableSelectorY: this.bstream.readNumber(1),
componentSelectorCb: this.bstream.readNumber(1),
huffmanTableSelectorCb: this.bstream.readNumber(1),
componentSelectorCr: this.bstream.readNumber(1),
huffmanTableSelectorCr: this.bstream.readNumber(1),
scanStartPositionInBlock: this.bstream.readNumber(1),
scanEndPositionInBlock: this.bstream.readNumber(1),
successiveApproximationBitPosition: this.bstream.readNumber(1),
};
const rawImageDataStream = this.bstream.tee();
let numBytes = 0;
// Immediately after SOS header is the compressed image data until the EOI marker is seen.
// Seek until we find the EOI marker.
while (true) {
if (this.bstream.readNumber(1) === 0xFF &&
this.bstream.peekNumber(1) === JpegSegmentType.EOI) {
jpegMarker = this.bstream.readNumber(1);
break;
} else {
numBytes++;
}
}
// NOTE: The below will have the null bytes after every 0xFF value.
sos.rawImageData = rawImageDataStream.readBytes(numBytes);
this.dispatchEvent(new JpegStartOfScanEvent(sos));
} // End of SOS
else {
this.bstream.setBigEndian();
const length = this.bstream.peekNumber(2);
if (DEBUG) console.log(`Unsupported JPEG marker 0xff${jpegMarker.toString(16)} with length ${length}`);
this.bstream.skip(length);
}
} while (jpegMarker !== JpegSegmentType.EOI);
}
/**
* Reads an Image File Directory from stream.
* @param {ByteStream} stream The stream to extract the Exif value descriptor.
* @param {ByteStream} lookAheadStream The lookahead stream if the offset is used.
* @param {Map<number, ExifValue} exifValueMap This map to add the Exif values.
* @returns {number} The next IFD offset.
*/
readExifIfd(stream, lookAheadStream, exifValueMap) {
let exifOffsetStream;
const numDirectoryEntries = stream.readNumber(2);
for (let entry = 0; entry < numDirectoryEntries; ++entry) {
const exifValue = getExifValue(stream, lookAheadStream, DEBUG);
const exifTagNumber = exifValue.tagNumber;
exifValueMap.set(exifTagNumber, exifValue);
if (exifValue.tagNumber === ExifTagNumber.EXIF_OFFSET) {
exifOffsetStream = lookAheadStream.tee().skip(exifValue.numericalValue);
}
} // Loop over Directory Entries.
if (exifOffsetStream) {
this.readExifIfd(exifOffsetStream, lookAheadStream, exifValueMap);
}
const nextIfdOffset = stream.readNumber(4);
return nextIfdOffset;
}
}