Reduce kept state in JPEG decoder

We don't have to keep track of this much data between rects, so
restructure things to make it more simple. This allows the JPEG parsing
code to be a pure function which only depends on the input.
This commit is contained in:
Pierre Ossman 2023-05-15 12:57:59 +02:00
parent eb0ad829d2
commit 87143b361e
1 changed files with 118 additions and 113 deletions

View File

@ -11,131 +11,136 @@ export default class JPEGDecoder {
constructor() { constructor() {
// RealVNC will reuse the quantization tables // RealVNC will reuse the quantization tables
// and Huffman tables, so we need to cache them. // and Huffman tables, so we need to cache them.
this._quantTables = [];
this._huffmanTables = [];
this._cachedQuantTables = []; this._cachedQuantTables = [];
this._cachedHuffmanTables = []; this._cachedHuffmanTables = [];
this._jpegLength = 0;
this._segments = []; this._segments = [];
} }
decodeRect(x, y, width, height, sock, display, depth) { decodeRect(x, y, width, height, sock, display, depth) {
// A rect of JPEG encodings is simply a JPEG file // A rect of JPEG encodings is simply a JPEG file
if (!this._parseJPEG(sock.rQslice(0))) { while (true) {
let segment = this._readSegment(sock);
if (segment === null) {
return false; return false;
} }
const data = sock.rQshiftBytes(this._jpegLength); this._segments.push(segment);
if (this._quantTables.length != 0 && this._huffmanTables.length != 0) { // End of image?
// If there are quantization tables and Huffman tables in the JPEG if (segment[1] === 0xD9) {
// image, we can directly render it. break;
display.imageRect(x, y, width, height, "image/jpeg", data); }
return true; }
} else {
// Otherwise we need to insert cached tables. let huffmanTables = [];
let quantTables = [];
for (let segment of this._segments) {
let type = segment[1];
if (type === 0xC4) {
// Huffman tables
huffmanTables.push(segment);
} else if (type === 0xDB) {
// Quantization tables
quantTables.push(segment);
}
}
const sofIndex = this._segments.findIndex( const sofIndex = this._segments.findIndex(
x => x[1] == 0xC0 || x[1] == 0xC2 x => x[1] == 0xC0 || x[1] == 0xC2
); );
if (sofIndex == -1) { if (sofIndex == -1) {
throw new Error("Illegal JPEG image without SOF"); throw new Error("Illegal JPEG image without SOF");
} }
let segments = this._segments.slice(0, sofIndex);
segments = segments.concat(this._quantTables.length ? if (quantTables.length === 0) {
this._quantTables : this._segments.splice(sofIndex+1, 0,
this._cachedQuantTables); ...this._cachedQuantTables);
segments.push(this._segments[sofIndex]);
segments = segments.concat(this._huffmanTables.length ?
this._huffmanTables :
this._cachedHuffmanTables,
this._segments.slice(sofIndex + 1));
let length = 0;
for (let i = 0; i < segments.length; i++) {
length += segments[i].length;
}
const data = new Uint8Array(length);
length = 0;
for (let i = 0; i < segments.length; i++) {
data.set(segments[i], length);
length += segments[i].length;
}
display.imageRect(x, y, width, height, "image/jpeg", data);
return true;
} }
if (huffmanTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedHuffmanTables);
} }
_parseJPEG(buffer) { let length = 0;
if (this._quantTables.length != 0) { for (let segment of this._segments) {
this._cachedQuantTables = this._quantTables; length += segment.length;
} }
if (this._huffmanTables.length != 0) {
this._cachedHuffmanTables = this._huffmanTables; let data = new Uint8Array(length);
length = 0;
for (let segment of this._segments) {
data.set(segment, length);
length += segment.length;
} }
this._quantTables = [];
this._huffmanTables = []; display.imageRect(x, y, width, height, "image/jpeg", data);
if (huffmanTables.length !== 0) {
this._cachedHuffmanTables = huffmanTables;
}
if (quantTables.length !== 0) {
this._cachedQuantTables = quantTables;
}
this._segments = []; this._segments = [];
let i = 0;
let bufferLength = buffer.length;
while (true) {
let j = i;
if (j + 2 > bufferLength) {
return false;
}
if (buffer[j] != 0xFF) {
throw new Error("Illegal JPEG marker received (byte: " +
buffer[j] + ")");
}
const type = buffer[j+1];
j += 2;
if (type == 0xD9) {
this._jpegLength = j;
this._segments.push(buffer.slice(i, j));
return true; return true;
} else if (type == 0xDA) {
// start of scan
let hasFoundEndOfScan = false;
for (let k = j + 3; k + 1 < bufferLength; k++) {
if (buffer[k] == 0xFF && buffer[k+1] != 0x00 &&
!(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) {
j = k;
hasFoundEndOfScan = true;
break;
} }
_readSegment(sock) {
if (sock.rQwait("JPEG", 2)) {
return null;
} }
if (!hasFoundEndOfScan) {
return false; let marker = sock.rQshift8();
if (marker != 0xFF) {
throw new Error("Illegal JPEG marker received (byte: " +
marker + ")");
} }
this._segments.push(buffer.slice(i, j)); let type = sock.rQshift8();
i = j; if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
continue;
} else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
// No length after marker // No length after marker
this._segments.push(buffer.slice(i, j)); return new Uint8Array([marker, type]);
i = j;
continue;
} }
if (j + 2 > bufferLength) {
return false; if (sock.rQwait("JPEG", 2, 2)) {
return null;
} }
const length = (buffer[j] << 8) + buffer[j+1] - 2;
if (length < 0) { let length = sock.rQshift16();
if (length < 2) {
throw new Error("Illegal JPEG length received (length: " + throw new Error("Illegal JPEG length received (length: " +
length + ")"); length + ")");
} }
j += 2;
if (j + length > bufferLength) { if (sock.rQwait("JPEG", length-2, 4)) {
return false; return null;
} }
j += length;
const segment = buffer.slice(i, j); let extra = 0;
if (type == 0xC4) { if (type === 0xDA) {
// Huffman tables // start of scan
this._huffmanTables.push(segment); extra += 2;
} else if (type == 0xDB) { while (true) {
// Quantization tables if (sock.rQwait("JPEG", length-2+extra, 4)) {
this._quantTables.push(segment); return null;
} }
this._segments.push(segment); let data = sock.rQslice(0, length-2+extra);
i = j; if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
extra -= 2;
break;
}
extra++;
} }
} }
let segment = new Uint8Array(2 + length + extra);
segment[0] = marker;
segment[1] = type;
segment[2] = length >> 8;
segment[3] = length;
segment.set(sock.rQshiftBytes(length-2+extra), 4);
return segment;
}
} }