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", "Remove EXIF",
"Extract EXIF", "Extract EXIF",
"Extract RGBA", "Extract RGBA",
"Extract Quantisation Tables",
"View Bit Plane", "View Bit Plane",
"Randomize Colour Palette", "Randomize Colour Palette",
"Extract LSB" "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;