Skip to content

Commit

Permalink
Reduce kept state in JPEG decoder
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
CendioOssman committed Jun 4, 2023
1 parent eb0ad82 commit 87143b3
Showing 1 changed file with 111 additions and 106 deletions.
217 changes: 111 additions & 106 deletions core/decoders/jpeg.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,131 +11,136 @@ export default class JPEGDecoder {
constructor() {
// RealVNC will reuse the quantization tables
// and Huffman tables, so we need to cache them.
this._quantTables = [];
this._huffmanTables = [];
this._cachedQuantTables = [];
this._cachedHuffmanTables = [];

this._jpegLength = 0;
this._segments = [];
}

decodeRect(x, y, width, height, sock, display, depth) {
// A rect of JPEG encodings is simply a JPEG file
if (!this._parseJPEG(sock.rQslice(0))) {
return false;
}
const data = sock.rQshiftBytes(this._jpegLength);
if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
// If there are quantization tables and Huffman tables in the JPEG
// image, we can directly render it.
display.imageRect(x, y, width, height, "image/jpeg", data);
return true;
} else {
// Otherwise we need to insert cached tables.
const sofIndex = this._segments.findIndex(
x => x[1] == 0xC0 || x[1] == 0xC2
);
if (sofIndex == -1) {
throw new Error("Illegal JPEG image without SOF");
while (true) {
let segment = this._readSegment(sock);
if (segment === null) {
return false;
}
let segments = this._segments.slice(0, sofIndex);
segments = segments.concat(this._quantTables.length ?
this._quantTables :
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;
this._segments.push(segment);
// End of image?
if (segment[1] === 0xD9) {
break;
}
const data = new Uint8Array(length);
length = 0;
for (let i = 0; i < segments.length; i++) {
data.set(segments[i], length);
length += segments[i].length;
}

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);
}
display.imageRect(x, y, width, height, "image/jpeg", data);
return true;
}
}

_parseJPEG(buffer) {
if (this._quantTables.length != 0) {
this._cachedQuantTables = this._quantTables;
const sofIndex = this._segments.findIndex(
x => x[1] == 0xC0 || x[1] == 0xC2
);
if (sofIndex == -1) {
throw new Error("Illegal JPEG image without SOF");
}

if (quantTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedQuantTables);
}
if (huffmanTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedHuffmanTables);
}

let length = 0;
for (let segment of this._segments) {
length += segment.length;
}

let data = new Uint8Array(length);
length = 0;
for (let segment of this._segments) {
data.set(segment, length);
length += segment.length;
}

display.imageRect(x, y, width, height, "image/jpeg", data);

if (huffmanTables.length !== 0) {
this._cachedHuffmanTables = huffmanTables;
}
if (this._huffmanTables.length != 0) {
this._cachedHuffmanTables = this._huffmanTables;
if (quantTables.length !== 0) {
this._cachedQuantTables = quantTables;
}
this._quantTables = [];
this._huffmanTables = [];

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;
} 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;
}

return true;
}

_readSegment(sock) {
if (sock.rQwait("JPEG", 2)) {
return null;
}

let marker = sock.rQshift8();
if (marker != 0xFF) {
throw new Error("Illegal JPEG marker received (byte: " +
marker + ")");
}
let type = sock.rQshift8();
if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
// No length after marker
return new Uint8Array([marker, type]);
}

if (sock.rQwait("JPEG", 2, 2)) {
return null;
}

let length = sock.rQshift16();
if (length < 2) {
throw new Error("Illegal JPEG length received (length: " +
length + ")");
}

if (sock.rQwait("JPEG", length-2, 4)) {
return null;
}

let extra = 0;
if (type === 0xDA) {
// start of scan
extra += 2;
while (true) {
if (sock.rQwait("JPEG", length-2+extra, 4)) {
return null;
}
if (!hasFoundEndOfScan) {
return false;
let data = sock.rQslice(0, length-2+extra);
if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
extra -= 2;
break;
}
this._segments.push(buffer.slice(i, j));
i = j;
continue;
} else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
// No length after marker
this._segments.push(buffer.slice(i, j));
i = j;
continue;
}
if (j + 2 > bufferLength) {
return false;
}
const length = (buffer[j] << 8) + buffer[j+1] - 2;
if (length < 0) {
throw new Error("Illegal JPEG length received (length: " +
length + ")");
}
j += 2;
if (j + length > bufferLength) {
return false;
extra++;
}
j += length;
const segment = buffer.slice(i, j);
if (type == 0xC4) {
// Huffman tables
this._huffmanTables.push(segment);
} else if (type == 0xDB) {
// Quantization tables
this._quantTables.push(segment);
}
this._segments.push(segment);
i = j;
}

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;
}
}

0 comments on commit 87143b3

Please sign in to comment.