/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ // - The JPEG specification can be found in the ITU CCITT Recommendation T.81 // (www.w3.org/Graphics/JPEG/itu-t81.pdf) // - The JFIF specification can be found in the JPEG File Interchange Format // (www.w3.org/Graphics/JPEG/jfif3.pdf) // - The Adobe Application-Specific JPEG markers in the Supporting the DCT Filters // in PostScript Level 2, Technical Note #5116 // (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf) var JpegImage = (function() { function constructor() { } var iDCTTables = (function() { var cosTables = [], i, j; for (i = 0; i < 8; i++) { cosTables.push(new Float32Array(8)); for (j = 0; j < 8; j++) cosTables[i][j] = Math.cos((2 * i + 1) * j * Math.PI / 16) * (j > 0 ? 1 : 1/Math.sqrt(2)); } var x, y, u, v; var tables = []; for (y = 0; y < 8; y++) { var cosTable_y = cosTables[y]; for (x = 0; x < 8; x++) { var cosTable_x = cosTables[x]; var table = new Float32Array(64); i = 0; for (v = 0; v < 8; v++) { for (u = 0; u < 8; u++) table[i++] = cosTable_x[u] * cosTable_y[v]; } tables.push(table); } } return tables; })(); function buildHuffmanTable(codeLengths, values) { var k = 0, code = [], i, j, length = 16; while (length > 0 && !codeLengths[length - 1]) length--; code.push({children: [], index: 0}); var p = code[0], q; for (i = 0; i < length; i++) { for (j = 0; j < codeLengths[i]; j++) { p = code.pop(); p.children[p.index] = values[k]; while (p.index > 0) { p = code.pop(); } p.index++; code.push(p); while (code.length <= i) { code.push(q = {children: [], index: 0}); p.children[p.index] = q.children; p = q; } k++; } if (i + 1 < length) { // p here points to last code code.push(q = {children: [], index: 0}); p.children[p.index] = q.children; p = q; } } return code[0].children; } function decodeScan(data, offset, frame, components, resetInterval, spectralStart, spectralEnd, successivePrev, successive) { var precision = frame.precision; var samplesPerLine = frame.samplesPerLine; var scanLines = frame.scanLines; var progressive = frame.progressive; var maxH = frame.maxH, maxV = frame.maxV; var startOffset = offset, bitsData = 0, bitsCount = 0; function readBit() { if (bitsCount > 0) { bitsCount--; return (bitsData >> bitsCount) & 1; } bitsData = data[offset++]; if (bitsData == 0xFF) { var nextByte = data[offset++]; if (nextByte) { throw "unexpected marker: " + ((bitsData << 8) | nextByte).toString(16); } // unstuff 0 } bitsCount = 7; return bitsData >>> 7; } function decodeHuffman(tree) { var node = tree, bit; while ((bit = readBit()) !== null) { node = node[bit]; if (typeof node === 'number') return node; if (typeof node !== 'object') throw "invalid huffman sequence"; } return null; } function receive(length) { var n = 0; while (length > 0) { var bit = readBit(); if (bit === null) return; n = (n << 1) | bit; length--; } return n; } function receiveAndExtend(length) { var n = receive(length); if (n >= 1 << (length - 1)) return n; return n + (-1 << length) + 1; } function decodeBaseline(component) { var zz = new Int32Array(64); var t = decodeHuffman(component.huffmanTableDC); var diff = t === 0 ? 0 : receiveAndExtend(t); zz[0]= (component.pred += diff); var k = 1; while (k < 64) { var rs = decodeHuffman(component.huffmanTableAC); var s = rs & 15, r = rs >> 4; if (s === 0) { if (r != 15) break; k += 16; continue; } k += r; zz[k] = receiveAndExtend(s); k++; } return zz; } function quantizeAndInverse(zz, qt) { var R = new Int32Array([ zz[0] * qt[0], zz[1] * qt[1], zz[5] * qt[5], zz[6] * qt[6], zz[14] * qt[14], zz[15] * qt[15], zz[27] * qt[27], zz[28] * qt[28], zz[2] * qt[2], zz[4] * qt[4], zz[7] * qt[7], zz[13] * qt[13], zz[16] * qt[16], zz[26] * qt[26], zz[29] * qt[29], zz[42] * qt[42], zz[3] * qt[3], zz[8] * qt[8], zz[12] * qt[12], zz[17] * qt[17], zz[25] * qt[25], zz[30] * qt[30], zz[41] * qt[41], zz[43] * qt[43], zz[9] * qt[9], zz[11] * qt[11], zz[18] * qt[18], zz[24] * qt[24], zz[31] * qt[31], zz[40] * qt[40], zz[44] * qt[44], zz[53] * qt[53], zz[10] * qt[10], zz[19] * qt[19], zz[23] * qt[23], zz[32] * qt[32], zz[39] * qt[39], zz[45] * qt[45], zz[52] * qt[52], zz[54] * qt[54], zz[20] * qt[20], zz[22] * qt[22], zz[33] * qt[33], zz[38] * qt[38], zz[46] * qt[46], zz[51] * qt[51], zz[55] * qt[55], zz[60] * qt[60], zz[21] * qt[21], zz[34] * qt[34], zz[37] * qt[37], zz[47] * qt[47], zz[50] * qt[50], zz[56] * qt[56], zz[59] * qt[59], zz[61] * qt[61], zz[35] * qt[35], zz[36] * qt[36], zz[48] * qt[48], zz[49] * qt[49], zz[57] * qt[57], zz[58] * qt[58], zz[62] * qt[62], zz[63] * qt[63]]); var i, j, y, x, u, v; var r = new Uint8Array(64), ri; for (i = 0; i < 64; i++) { var sum = 0; var table = iDCTTables[i]; for (j = 0; j < 64; j++) sum += table[j] * R[j]; // TODO loosing precision? var sample = 128 + ((sum / 4) >> (precision - 8)); // clamping r[i] = sample < 0 ? 0 : sample > 0xFF ? 0xFF : sample; } return r; } function storeMcu(component, r, mcu, row, col) { var mcuRow = (mcu / component.mcusPerLine) | 0; var mcuCol = mcu % component.mcusPerLine; var blockRow = mcuRow * component.v + row; var blockCol = mcuCol * component.h + col; var scanLine = blockRow << 3, sample = blockCol << 3; var lines = component.lines; while (scanLine + 8 > lines.length) { lines.push(new Uint8Array(component.blocksPerLine << 3)); } var i, j, offset = 0; for (j = 0; j < 8; j++) { var line = lines[scanLine + j]; for (i = 0; i < 8; i++) line[sample + i] = r[offset++]; } } function storeBlock(component, r, mcu) { var blockRow = (mcu / component.mcusPerLine) | 0; var blockCol = mcu % component.mcusPerLine; var scanLine = blockRow << 3, sample = blockCol << 3; var lines = component.lines; while (scanLine + 8 > lines.length) { lines.push(new Uint8Array(component.blocksPerLine << 3)); } var i, j, offset = 0; for (j = 0; j < 8; j++) { var line = lines[scanLine + j]; for (i = 0; i < 8; i++) line[sample + i] = r[offset++]; } } var componentsLength = components.length; var component, i, j, k, n; if (progressive) { throw "not implemented: progressive"; } else { for (i = 0; i < componentsLength; i++) { component = components[i]; component.blocksPerLine = (samplesPerLine * component.h / maxH + 7) >> 3; component.mcusPerLine = ((component.blocksPerLine + component.h - 1) / component.h) | 0; component.decode = decodeBaseline; } } var mcu = 0, marker; var mcuExpected = (0|((((samplesPerLine + 7) >> 3) + maxH - 1) / maxH)) * (0|((((scanLines + 7) >> 3) + maxV - 1) / maxV)); if (!resetInterval) resetInterval = mcuExpected; var zz, r; while (mcu < mcuExpected) { if (componentsLength == 1) { component = components[0]; for (n = 0; n < resetInterval; n++) { zz = component.decode(component); r = quantizeAndInverse(zz, component.quantizationTable); storeBlock(component, r, mcu); mcu++; } } else { for (n = 0; n < resetInterval; n++) { for (i = 0; i < componentsLength; i++) { component = components[i]; var h = component.h, v = component.v; for (j = 0; j < v; j++) { for (k = 0; k < h; k++) { zz = component.decode(component); r = quantizeAndInverse(zz, component.quantizationTable); storeMcu(component, r, mcu, j, k); } } } mcu++; } } // find marker bitsCount = 0; marker = (data[offset] << 8) | data[offset + 1]; if (marker <= 0xFF00) { throw "marker was not found"; } if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx offset += 2; for (i = 0; i < componentsLength; i++) components[i].pred = 0; } else break; } return offset - startOffset; } constructor.prototype = { load: function(path) { var xhr = new XMLHttpRequest(); xhr.open("GET", path, true); xhr.responseType = "arraybuffer"; xhr.onload = (function() { // TODO catch parse error var data = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer); this.parse(data); if (this.onload) this.onload(); }).bind(this); xhr.send(null); }, parse: function(data) { var offset = 0, length = data.length; function readUint16() { var value = (data[offset] << 8) | data[offset + 1]; offset += 2; return value; } function readDataBlock() { var length = readUint16(); //var array = data.subarray(offset, offset + length - 2); var array = data.slice(offset, offset + length - 2); offset += array.length; return array; } var jfif = null; var adobe = null; var pixels = null; var frame, resetInterval; var quantizationTables = [], frames = []; var huffmanTablesAC = [], huffmanTablesDC = []; var fileMarker = readUint16(); if (fileMarker != 0xFFD8) { // SOI (Start of Image) throw "SOI not found"; } fileMarker = readUint16(); while (fileMarker != 0xFFD9) { // EOI (End of image) var i, j, l; switch(fileMarker) { case 0xFFE0: // APP0 (Application Specific) case 0xFFE1: // APP1 case 0xFFE2: // APP2 case 0xFFE3: // APP3 case 0xFFE4: // APP4 case 0xFFE5: // APP5 case 0xFFE6: // APP6 case 0xFFE7: // APP7 case 0xFFE8: // APP8 case 0xFFE9: // APP9 case 0xFFEA: // APP10 case 0xFFEB: // APP11 case 0xFFEC: // APP12 case 0xFFED: // APP13 case 0xFFEE: // APP14 case 0xFFEF: // APP15 case 0xFFFE: // COM (Comment) var appData = readDataBlock(); if (fileMarker === 0xFFE0) { if (appData[0] === 0x4A && appData[1] === 0x46 && appData[2] === 0x49 && appData[3] === 0x46 && appData[4] === 0) { // 'JFIF\x00' jfif = { version: { major: appData[5], minor: appData[6] }, densityUnits: appData[7], xDensity: (appData[8] << 8) | appData[9], yDensity: (appData[10] << 8) | appData[11], thumbWidth: appData[12], thumbHeight: appData[13], //thumbData: appData.subarray(14, 14 + 3 * appData[12] * appData[13]) thumbData: appData.slice(14, 14 + 3 * appData[12] * appData[13]) }; } } // TODO APP1 - Exif if (fileMarker === 0xFFEE) { if (appData[0] === 0x41 && appData[1] === 0x64 && appData[2] === 0x6F && appData[3] === 0x62 && appData[4] === 65 && appData[5] === 0) { // 'Adobe\x00' adobe = { version: appData[6], flags0: (appData[7] << 8) | appData[8], flags1: (appData[9] << 8) | appData[10], transformCode: appData[11] }; } } break; case 0xFFDB: // DQT (Define Quantization Tables) var quantizationTableCount = Math.floor((readUint16() - 2) / 65); for (i = 0; i < quantizationTableCount; i++) { var quantizationTableSpec = data[offset++]; var tableData = new Int32Array(64); if ((quantizationTableSpec >> 4) === 0) { // 8 bit values for (j = 0; j < 64; j++) tableData[j] = data[offset++]; } else if ((quantizationTableSpec >> 4) === 1) { //16 bit tableData[j] = readUint16(); } else throw "DQT: invalid table spec"; quantizationTables[quantizationTableSpec & 15] = tableData; } break; case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT) case 0xFFC2: // SOF2 (Start of Frame, Progressive DCT) readUint16(); // skip data length frame = {}; frame.progressive = (fileMarker === 0xFFC2); frame.precision = data[offset++]; frame.scanLines = readUint16(); frame.samplesPerLine = readUint16(); frame.components = []; var componentsCount = data[offset++]; var maxH = 0, maxV = 0; for (i = 0; i < componentsCount; i++) { var componentId = data[offset]; var h = data[offset + 1] >> 4; var v = data[offset + 1] & 15; var qId = data[offset + 2]; frame.components[componentId] = { h: h, v: v, quantizationTable: quantizationTables[qId], pred: 0, lines: [] }; offset += 3; if (maxH < h) maxH = h; if (maxV < v) maxV = v; } frame.maxH = maxH; frame.maxV = maxV; frames.push(frame); break; case 0xFFC4: // DHT (Define Huffman Tables) var huffmanLength = readUint16(); for (i = 2; i < huffmanLength;) { var huffmanTableSpec = data[offset++]; var codeLengths = new Uint8Array(16); var codeLengthSum = 0; for (j = 0; j < 16; j++, offset++) codeLengthSum += (codeLengths[j] = data[offset]); var huffmanValues = new Uint8Array(codeLengthSum); for (j = 0; j < codeLengthSum; j++, offset++) huffmanValues[j] = data[offset]; i += 17 + codeLengthSum; ((huffmanTableSpec >> 4) === 0 ? huffmanTablesDC : huffmanTablesAC)[huffmanTableSpec & 15] = buildHuffmanTable(codeLengths, huffmanValues); } break; case 0xFFDD: // DRI (Define Restart Interval) readUint16(); // skip data length resetInterval = readUint16(); break; case 0xFFDA: // SOS (Start of Scan) var scanLength = readUint16(); var selectorsCount = data[offset++]; var components = [], component; for (i = 0; i < selectorsCount; i++) { component = frame.components[data[offset++]]; var tableSpec = data[offset++]; component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4]; component.huffmanTableAC = huffmanTablesAC[tableSpec & 15]; components.push(component); } var spectralStart = data[offset++]; var spectralEnd = data[offset++]; var successiveApproximation = data[offset++]; var processed = decodeScan(data, offset, frame, components, resetInterval, spectralStart, spectralEnd, successiveApproximation >> 4, successiveApproximation & 15); offset += processed; break; default: throw "unknown JPEG marker " + fileMarker.toString(16); } fileMarker = readUint16(); } if (frames.length != 1) throw "only single frame JPEGs supported"; this.width = frame.samplesPerLine; this.height = frame.scanLines; this.jfif = jfif; this.adobe = adobe; this.components = []; for (var id in frame.components) { if (frame.components.hasOwnProperty(id)) { this.components.push({ lines: frame.components[id].lines, scaleX: frame.components[id].h / frame.maxH, scaleY: frame.components[id].v / frame.maxV }); } } }, copyToImageData: function(imageData) { var width = imageData.width, height = imageData.height; var scaleX = this.width / width, scaleY = this.height / height; var component1, component2, component3, component4; var component1Line, component2Line, component3Line, component4Line; var x, y; var offset = 0, data = imageData.data; var Y, Cb, Cr, K, C, M, Ye; switch (this.components.length) { case 1: component1 = this.components[0]; for (y = 0; y < height; y++) { component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)]; for (x = 0; x < width; x++) { Y = component1Line[0 | (x * component1.scaleX * scaleX)]; data[offset++] = Y; data[offset++] = Y; data[offset++] = Y; data[offset++] = 255; } } break; case 3: component1 = this.components[0]; component2 = this.components[1]; component3 = this.components[2]; for (y = 0; y < height; y++) { component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)]; component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)]; component3Line = component3.lines[0 | (y * component3.scaleY * scaleY)]; for (x = 0; x < width; x++) { Y = component1Line[0 | (x * component1.scaleX * scaleX)]; Cb = component2Line[0 | (x * component2.scaleX * scaleX)]; Cr = component3Line[0 | (x * component3.scaleX * scaleX)]; data[offset++] = Y + 1.402 * (Cr - 128); data[offset++] = Y - 0.3441363 * (Cb - 128) - 0.71413636 * (Cr - 128); data[offset++] = Y + 1.772 * (Cb - 128); data[offset++] = 255; } } break; case 4: component1 = this.components[0]; component2 = this.components[1]; component3 = this.components[2]; component4 = this.components[3]; for (y = 0; y < height; y++) { component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)]; component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)]; component3Line = component3.lines[0 | (y * component3.scaleY * scaleY)]; component4Line = component4.lines[0 | (y * component4.scaleY * scaleY)]; for (x = 0; x < width; x++) { Y = component1Line[0 | (x * component1.scaleX * scaleX)]; Cb = component2Line[0 | (x * component2.scaleX * scaleX)]; Cr = component3Line[0 | (x * component3.scaleX * scaleX)]; K = component4Line[0 | (x * component4.scaleX * scaleX)]; C = 255 - (Y + 1.402 * (Cr - 128)); M = 255 - (Y - 0.3441363 * (Cb - 128) - 0.71413636 * (Cr - 128)); Ye = 255 - (Y + 1.772 * (Cb - 128)); data[offset++] = 255 - Math.min(255, C * (1 - K / 255) + K); data[offset++] = 255 - Math.min(255, M * (1 - K / 255) + K); data[offset++] = 255 - Math.min(255, Ye * (1 - K / 255) + K); data[offset++] = 255; } } break; } } }; return constructor; })(); var Buffer = require('buffer').Buffer; var fs = require('fs'); module.exports.readJpeg = function(path) { var jpgData = fs.readFileSync(path); var j = new JpegImage(); j.parse(jpgData); var imageData = {}; imageData.width = j.width; imageData.height = j.height; imageData.data = new Buffer(j.width*j.height*4); j.copyToImageData(imageData); return imageData; }