image-in-browser
Version:
Package for encoding / decoding images, transforming images, applying filters, drawing primitives on images on the client side (no need for server Node.js)
551 lines • 20.1 kB
JavaScript
import { InputBuffer } from '../../common/input-buffer.js';
import { LibError } from '../../error/lib-error.js';
import { JpegComponentData } from './jpeg-component-data.js';
import { JpegAdobe } from './jpeg-adobe.js';
import { JpegComponent } from './jpeg-component.js';
import { JpegFrame } from './jpeg-frame.js';
import { JpegHuffman } from './jpeg-huffman.js';
import { JpegInfo } from './jpeg-info.js';
import { JpegJfif } from './jpeg-jfif.js';
import { JpegQuantize } from './jpeg-quantize.js';
import { JpegScan } from './jpeg-scan.js';
import { ExifData } from '../../exif/exif-data.js';
import { ArrayUtils } from '../../common/array-utils.js';
import { JpegMarker } from './jpeg-marker.js';
import { HuffmanValue } from './huffman-value.js';
import { HuffmanParent } from './huffman-parent.js';
import { IccProfile } from '../../image/icc-profile.js';
import { IccProfileCompression } from '../../image/icc-profile-compression.js';
export class JpegData {
constructor() {
this._exifData = new ExifData();
this._quantizationTables = ArrayUtils.fill(JpegData.numQuantizationTables, undefined);
this._frames = new Array();
this._huffmanTablesAC = [];
this._huffmanTablesDC = [];
this._components = new Array();
}
get input() {
return this._input;
}
get jfif() {
return this._jfif;
}
get adobe() {
return this._adobe;
}
get frame() {
return this._frame;
}
get resetInterval() {
return this._resetInterval;
}
get comment() {
return this._comment;
}
get iccProfile() {
return this._iccProfile;
}
get exifData() {
return this._exifData;
}
get quantizationTables() {
return this._quantizationTables;
}
get frames() {
return this._frames;
}
get huffmanTablesAC() {
return this._huffmanTablesAC;
}
get huffmanTablesDC() {
return this._huffmanTablesDC;
}
get components() {
return this._components;
}
get width() {
return this._frame.samplesPerLine;
}
get height() {
return this._frame.scanLines;
}
readMarkers() {
let marker = this.nextMarker();
if (marker !== JpegMarker.soi) {
throw new LibError('Start Of Image marker not found.');
}
marker = this.nextMarker();
while (marker !== JpegMarker.eoi && !this._input.isEOS) {
const block = this.readBlock();
switch (marker) {
case JpegMarker.app0:
case JpegMarker.app1:
case JpegMarker.app2:
case JpegMarker.app3:
case JpegMarker.app4:
case JpegMarker.app5:
case JpegMarker.app6:
case JpegMarker.app7:
case JpegMarker.app8:
case JpegMarker.app9:
case JpegMarker.app10:
case JpegMarker.app11:
case JpegMarker.app12:
case JpegMarker.app13:
case JpegMarker.app14:
case JpegMarker.app15:
case JpegMarker.com:
this.readAppData(marker, block);
break;
case JpegMarker.dqt:
this.readDQT(block);
break;
case JpegMarker.sof0:
case JpegMarker.sof1:
case JpegMarker.sof2:
this.readFrame(marker, block);
break;
case JpegMarker.sof3:
case JpegMarker.sof5:
case JpegMarker.sof6:
case JpegMarker.sof7:
case JpegMarker.jpg:
case JpegMarker.sof9:
case JpegMarker.sof10:
case JpegMarker.sof11:
case JpegMarker.sof13:
case JpegMarker.sof14:
case JpegMarker.sof15:
throw new LibError(`Unhandled frame type ${marker.toString(16)}`);
case JpegMarker.dht:
this.readDHT(block);
break;
case JpegMarker.dri:
this.readDRI(block);
break;
case JpegMarker.sos:
this.readSOS(block);
break;
case 0xff:
if (this._input.get(0) !== 0xff) {
this._input.skip(-1);
}
break;
default:
if (this._input.get(-3) === 0xff &&
this._input.get(-2) >= 0xc0 &&
this._input.get(-2) <= 0xfe) {
this._input.skip(-3);
break;
}
if (marker !== 0) {
throw new LibError(`Unknown JPEG marker ${marker.toString(16)}`);
}
break;
}
marker = this.nextMarker();
}
}
skipBlock() {
const length = this._input.readUint16();
if (length < 2) {
throw new LibError('Invalid Block');
}
this._input.skip(length - 2);
}
validate(bytes) {
this._input = new InputBuffer({
buffer: bytes,
bigEndian: true,
});
const soiCheck = this._input.peek(2);
if (soiCheck.get(0) !== 0xff || soiCheck.get(1) !== 0xd8) {
return false;
}
let marker = this.nextMarker();
if (marker !== JpegMarker.soi) {
return false;
}
let hasSOF = false;
let hasSOS = false;
marker = this.nextMarker();
while (marker !== JpegMarker.eoi && !this._input.isEOS) {
const sectionByteSize = this._input.readUint16();
if (sectionByteSize < 2) {
break;
}
this._input.skip(sectionByteSize - 2);
switch (marker) {
case JpegMarker.sof0:
case JpegMarker.sof1:
case JpegMarker.sof2:
hasSOF = true;
break;
case JpegMarker.sos:
hasSOS = true;
break;
default:
}
marker = this.nextMarker();
}
return hasSOF && hasSOS;
}
readInfo(bytes) {
this._input = new InputBuffer({
buffer: bytes,
bigEndian: true,
});
let marker = this.nextMarker();
if (marker !== JpegMarker.soi) {
return undefined;
}
const info = new JpegInfo();
let hasSOF = false;
let hasSOS = false;
marker = this.nextMarker();
while (marker !== JpegMarker.eoi && !this._input.isEOS) {
switch (marker) {
case JpegMarker.sof0:
case JpegMarker.sof1:
case JpegMarker.sof2:
hasSOF = true;
this.readFrame(marker, this.readBlock());
break;
case JpegMarker.sos:
hasSOS = true;
this.skipBlock();
break;
default:
this.skipBlock();
break;
}
marker = this.nextMarker();
}
if (this._frame !== undefined) {
info.setSize(this._frame.samplesPerLine, this._frame.scanLines);
info.numComponents = this._frame.components.size;
this._frame = undefined;
}
this.frames.length = 0;
return hasSOF && hasSOS ? info : undefined;
}
read(bytes) {
this._input = new InputBuffer({
buffer: bytes,
bigEndian: true,
});
this.readMarkers();
if (this._frames.length !== 1) {
throw new LibError('Only single frame JPEGs supported');
}
if (this._frame !== undefined) {
for (let i = 0; i < this._frame.componentsOrder.length; ++i) {
const component = this._frame.components.get(this._frame.componentsOrder[i]);
if (component !== undefined) {
this.components.push(new JpegComponentData(component.hSamples, this._frame.maxHSamples, component.vSamples, this._frame.maxVSamples, JpegData.buildComponentData(component)));
}
}
}
}
getImage() {
return JpegQuantize.getImageFromJpeg(this);
}
static buildHuffmanTable(codeLengths, values) {
let k = 0;
const code = new Array();
let length = 16;
while (length > 0 && codeLengths[length - 1] === 0) {
length--;
}
code.push(new JpegHuffman());
let p = code[0];
for (let i = 0; i < length; i++) {
for (let j = 0; j < codeLengths[i]; j++) {
p = code.pop();
if (p.children.length <= p.index) {
p.children.length = p.index + 1;
}
p.children[p.index] = new HuffmanValue(values[k]);
while (p.index > 0) {
p = code.pop();
}
p.incrementIndex();
code.push(p);
while (code.length <= i) {
const q = new JpegHuffman();
code.push(q);
if (p.children.length <= p.index) {
p.children.length = p.index + 1;
}
p.children[p.index] = new HuffmanParent(q.children);
p = q;
}
k++;
}
if (i + 1 < length) {
const q = new JpegHuffman();
code.push(q);
if (p.children.length <= p.index) {
p.children.length = p.index + 1;
}
p.children[p.index] = new HuffmanParent(q.children);
p = q;
}
}
return code[0].children;
}
static buildComponentData(component) {
const blocksPerLine = component.blocksPerLine;
const blocksPerColumn = component.blocksPerColumn;
const samplesPerLine = blocksPerLine << 3;
const R = new Int32Array(64);
const r = new Uint8Array(64);
const lines = ArrayUtils.fill(blocksPerColumn * 8, undefined);
let l = 0;
for (let blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
const scanLine = blockRow << 3;
for (let i = 0; i < 8; i++) {
lines[l++] = new Uint8Array(samplesPerLine);
}
for (let blockCol = 0; blockCol < blocksPerLine; blockCol++) {
JpegQuantize.quantizeAndInverse(component.quantizationTable, component.blocks[blockRow][blockCol], r, R);
let offset = 0;
const sample = blockCol << 3;
for (let j = 0; j < 8; j++) {
const line = lines[scanLine + j];
for (let i = 0; i < 8; i++) {
line[sample + i] = r[offset++];
}
}
}
}
return lines;
}
static toFix(val) {
const fixedPoint = 20;
const one = 1 << fixedPoint;
return Math.trunc(val * one) & 0xffffffff;
}
readBlock() {
const length = this._input.readUint16();
if (length < 2) {
throw new LibError('Invalid Block');
}
return this._input.readRange(length - 2);
}
nextMarker() {
let c = 0;
if (this._input.isEOS) {
return c;
}
do {
do {
c = this._input.read();
} while (c !== 0xff && !this._input.isEOS);
if (this._input.isEOS) {
return c;
}
do {
c = this._input.read();
} while (c === 0xff && !this._input.isEOS);
} while (c === 0 && !this._input.isEOS);
return c;
}
readIccProfile(block) {
const iccProfileSignature = [
0x49, 0x43, 0x43, 0x5f, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x00,
];
for (let i = 0; i < iccProfileSignature.length; i++) {
const b = block.read();
if (b !== iccProfileSignature[i]) {
return;
}
}
const data = block.toUint8Array();
this._iccProfile = new IccProfile('ICC_PROFILE', IccProfileCompression.none, data);
}
readExifData(block) {
const exifSignature = 0x45786966;
const signature = block.readUint32();
if (signature !== exifSignature) {
return;
}
if (block.readUint16() !== 0) {
return;
}
this.exifData.read(block);
}
readAppData(marker, block) {
const appData = block;
if (marker === JpegMarker.app0) {
if (appData.get(0) === 0x4a &&
appData.get(1) === 0x46 &&
appData.get(2) === 0x49 &&
appData.get(3) === 0x46 &&
appData.get(4) === 0) {
const majorVersion = appData.get(5);
const minorVersion = appData.get(6);
const densityUnits = appData.get(7);
const xDensity = (appData.get(8) << 8) | appData.get(9);
const yDensity = (appData.get(10) << 8) | appData.get(11);
const thumbWidth = appData.get(12);
const thumbHeight = appData.get(13);
const thumbSize = 3 * thumbWidth * thumbHeight;
const thumbData = appData.subarray(14 + thumbSize, undefined, 14);
this._jfif = new JpegJfif(thumbWidth, thumbHeight, majorVersion, minorVersion, densityUnits, xDensity, yDensity, thumbData);
}
}
else if (marker === JpegMarker.app1) {
this.readExifData(appData);
}
else if (marker === JpegMarker.app2) {
this.readIccProfile(appData);
}
else if (marker === JpegMarker.app14) {
if (appData.get(0) === 0x41 &&
appData.get(1) === 0x64 &&
appData.get(2) === 0x6f &&
appData.get(3) === 0x62 &&
appData.get(4) === 0x65 &&
appData.get(5) === 0) {
const version = appData.get(6);
const flags0 = (appData.get(7) << 8) | appData.get(8);
const flags1 = (appData.get(9) << 8) | appData.get(10);
const transformCode = appData.get(11);
this._adobe = new JpegAdobe(version, flags0, flags1, transformCode);
}
}
else if (marker === JpegMarker.com) {
try {
this._comment = appData.readStringUtf8();
}
catch (_) {
}
}
}
readDQT(block) {
while (!block.isEOS) {
let n = block.read();
const prec = n >>> 4;
n &= 0x0f;
if (n >= JpegData.numQuantizationTables) {
throw new LibError('Invalid number of quantization tables');
}
if (this._quantizationTables[n] === undefined) {
this._quantizationTables[n] = new Int16Array(64);
}
const tableData = this._quantizationTables[n];
if (tableData !== undefined) {
for (let i = 0; i < JpegData.dctSize2; i++) {
const tmp = prec !== 0 ? block.readUint16() : block.read();
tableData[JpegData.dctZigZag[i]] = tmp;
}
}
}
if (!block.isEOS) {
throw new LibError('Bad length for DQT block');
}
}
readFrame(marker, block) {
if (this._frame !== undefined) {
throw new LibError('Duplicate JPG frame data found.');
}
const extended = marker === JpegMarker.sof1;
const progressive = marker === JpegMarker.sof2;
const precision = block.read();
const scanLines = block.readUint16();
const samplesPerLine = block.readUint16();
const numComponents = block.read();
const components = new Map();
const componentsOrder = new Array();
for (let i = 0; i < numComponents; i++) {
const componentId = block.read();
const x = block.read();
const h = (x >>> 4) & 15;
const v = x & 15;
const qId = block.read();
componentsOrder.push(componentId);
const component = new JpegComponent(h, v, this._quantizationTables, qId);
components.set(componentId, component);
}
this._frame = new JpegFrame(components, componentsOrder, extended, progressive, precision, scanLines, samplesPerLine);
this._frame.prepare();
this.frames.push(this._frame);
}
readDHT(block) {
while (!block.isEOS) {
let index = block.read();
const bits = new Uint8Array(16);
let count = 0;
for (let j = 0; j < 16; j++) {
bits[j] = block.read();
count += bits[j];
}
const huffmanValues = block.readRange(count).toUint8Array();
let ht = [];
if ((index & 0x10) !== 0) {
index -= 0x10;
ht = this._huffmanTablesAC;
}
else {
ht = this._huffmanTablesDC;
}
if (ht.length <= index) {
ht.length = index + 1;
}
ht[index] = JpegData.buildHuffmanTable(bits, huffmanValues);
}
}
readDRI(block) {
this._resetInterval = block.readUint16();
}
readSOS(block) {
const n = block.read();
if (n < 1 || n > JpegData.maxCompsInScan) {
throw new LibError('Invalid SOS block');
}
const components = new Array();
for (let i = 0; i < n; i++) {
const id = block.read();
const c = block.read();
if (!this._frame.components.has(id)) {
throw new LibError('Invalid Component in SOS block');
}
const component = this._frame.components.get(id);
if (component !== undefined) {
const dcTableNumber = (c >>> 4) & 15;
const acTableNumber = c & 15;
if (dcTableNumber < this._huffmanTablesDC.length) {
component.huffmanTableDC = this._huffmanTablesDC[dcTableNumber];
}
if (acTableNumber < this._huffmanTablesAC.length) {
component.huffmanTableAC = this._huffmanTablesAC[acTableNumber];
}
components.push(component);
}
}
const spectralStart = block.read();
const spectralEnd = block.read();
const successiveApproximation = block.read();
const ah = (successiveApproximation >>> 4) & 15;
const al = successiveApproximation & 15;
const scan = new JpegScan(this._input, this._frame, components, spectralStart, spectralEnd, ah, al, this._resetInterval);
scan.decode();
}
}
JpegData.dctZigZag = [
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40,
48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36,
29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61,
54, 47, 55, 62, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63,
];
JpegData.dctSize = 8;
JpegData.dctSize2 = 64;
JpegData.numQuantizationTables = 4;
JpegData.numHuffmanTables = 4;
JpegData.numArithTables = 16;
JpegData.maxCompsInScan = 4;
JpegData.maxSamplingFactor = 4;
//# sourceMappingURL=jpeg-data.js.map