Add Extract Quantisation Tables operation

This commit is contained in:
Andy Wang 2020-04-02 01:02:49 +01:00
parent f7be8d720b
commit e156f620b9
3 changed files with 167 additions and 0 deletions

View file

@ -379,6 +379,7 @@
"Remove EXIF",
"Extract EXIF",
"Extract RGBA",
"Extract Quantisation Tables",
"View Bit Plane",
"Randomize Colour Palette",
"Extract LSB"

View file

@ -0,0 +1,68 @@
/**
* Parses a JPEG quantisation table.
*
* @param {Uint8Array} rawTable
* @param {boolean} doublePrec - true if 16-bit precision, false if 8-bit
* @returns {number[][]}
*
*/
export function parseQTable(rawTable, doublePrec = false) {
// The raw table is flattened in zig-zag order, similar to enumerating a countable set.
// This lookup table tells the index in the natural order for each element in the zig-zag order
const unZigZagIndex = [
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];
const ret = [
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]];
let i = 0;
let elemCount = 0;
while (i < rawTable.length) {
const row = ~~(unZigZagIndex[elemCount] / 8); // floor
const col = unZigZagIndex[elemCount] % 8;
if (doublePrec) {
ret[row][col] = rawTable[i++] << 8 + rawTable[i++];
} else {
ret[row][col] = rawTable[i++];
}
elemCount++;
}
return ret;
}
/**
* Finds the index of first appearance of a JPEG marker
*
* @param {Uint8Array} image
* @param {number} markerByte - doesn't include 0xFF
* @param {number} fromIndex - the index to start the search at
* @returns {number}
*
*/
export function indexOfMarker(image, markerByte, fromIndex = 0) {
let ptr = fromIndex;
let markerIndex = 0;
do {
// look for the byte that changes across markers first, as these are less common than 0xFF
markerIndex = image.indexOf(markerByte, ptr);
if (markerIndex === 0) return -1; // marker byte must follow 0xFF
if (image[markerIndex-1] === 0xFF) {
return markerIndex - 1;
} else {
ptr = markerIndex + 1;
}
} while (markerIndex !== -1);
return -1;
}

View file

@ -0,0 +1,98 @@
/**
* @author cbeuw [cbeuw.andy@gmail.com]
* @copyright Crown Copyright 2020
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import {parseQTable, indexOfMarker} from "../lib/QuantisationTable.mjs";
/**
* Extract Quantisation Tables operation
*/
class ExtractQuantisationTables extends Operation {
/**
* ExtractQuantisationTables constructor
*/
constructor() {
super();
this.name = "Extract Quantisation Tables";
this.module = "Default";
this.description = "Extracts quantisation tables embedded in a JPEG image.";
this.infoURL = "https://en.wikipedia.org/wiki/Quantization_(image_processing)";
this.inputType = "ArrayBuffer";
this.outputType = "string";
this.args = [];
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const image = new Uint8Array(input);
// maximally 4 tables are allowed in a JPEG file
let ptr = indexOfMarker(image, 0xD8);
if (ptr === -1) throw new OperationError("Malformed image file: Start of Image not found");
let ret = "";
do {
const dqtIndex = indexOfMarker(image, 0xDB, ptr);
if (dqtIndex === -1) {
break;
}
// -2 here because the length bytes include the two bytes used to record length
// -1 to exclude the byte used to record the table's precision and id
const tableLength = (image[dqtIndex+2] << 8) + image[dqtIndex+3] - 2 - 1;
// if the table is 16-bit precision
const doublePrecision = (image[dqtIndex+4] >> 4) === 1;
if (tableLength !== (doublePrecision? 128:64)) throw new OperationError(`Invalid table length ${tableLength} at table definition beginning at ${dqtIndex}`);
const tableId = image[dqtIndex+4] & 0x0F;
if (tableId >= 4) throw new OperationError(`Invalid table identifier ${tableId} at table definition beginning at ${dqtIndex}`);
const table = parseQTable(image.slice(dqtIndex + 5, dqtIndex + 5 + tableLength), doublePrecision);
ret += `Quantisation table at position ${dqtIndex}. ID ${tableId}, ${(doublePrecision? "16":"8")}-bit precision:\n`;
ret += this.prettifyMatrix(table);
ret += "\n";
ptr = dqtIndex + 5 + tableLength + 1;
} while (ptr < image.length);
return ret;
}
/**
* @param {number[][]} mat
* @returns {string}
*/
prettifyMatrix(mat) {
const nRows = mat.length;
const nCols = mat[0].length;
let maxLen = 0;
for (let i = 0; i < nRows; i++) {
for (let j = 0; j < nCols; j++) {
const elemLen = mat[i][j].toString().length;
if (elemLen > maxLen) {
maxLen = elemLen;
}
}
}
let ret = "";
for (let i = 0; i < nRows; i++) {
for (let j = 0; j < nCols; j++) {
ret += mat[i][j].toString().padStart(maxLen) + " ";
}
ret += "\n";
}
return ret;
}
}
export default ExtractQuantisationTables;