mirror of
https://git.proxmox.com/git/mirror_novnc
synced 2025-04-28 19:59:58 +00:00

Try to be more consistent in how we capitalize things. Both the "Title Case" and "Sentence case" styles are popular, so either would work. Google and Mozilla both prefer "Sentence case", so let's follow them.
394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
/*
|
|
* noVNC: HTML5 VNC client
|
|
* Copyright (C) 2019 The noVNC authors
|
|
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
|
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
|
*
|
|
* See README.md for usage and integration instructions.
|
|
*
|
|
*/
|
|
|
|
import * as Log from '../util/logging.js';
|
|
import Inflator from "../inflator.js";
|
|
|
|
export default class TightDecoder {
|
|
constructor() {
|
|
this._ctl = null;
|
|
this._filter = null;
|
|
this._numColors = 0;
|
|
this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
|
|
this._len = 0;
|
|
|
|
this._zlibs = [];
|
|
for (let i = 0; i < 4; i++) {
|
|
this._zlibs[i] = new Inflator();
|
|
}
|
|
}
|
|
|
|
decodeRect(x, y, width, height, sock, display, depth) {
|
|
if (this._ctl === null) {
|
|
if (sock.rQwait("TIGHT compression-control", 1)) {
|
|
return false;
|
|
}
|
|
|
|
this._ctl = sock.rQshift8();
|
|
|
|
// Reset streams if the server requests it
|
|
for (let i = 0; i < 4; i++) {
|
|
if ((this._ctl >> i) & 1) {
|
|
this._zlibs[i].reset();
|
|
Log.Info("Reset zlib stream " + i);
|
|
}
|
|
}
|
|
|
|
// Figure out filter
|
|
this._ctl = this._ctl >> 4;
|
|
}
|
|
|
|
let ret;
|
|
|
|
if (this._ctl === 0x08) {
|
|
ret = this._fillRect(x, y, width, height,
|
|
sock, display, depth);
|
|
} else if (this._ctl === 0x09) {
|
|
ret = this._jpegRect(x, y, width, height,
|
|
sock, display, depth);
|
|
} else if (this._ctl === 0x0A) {
|
|
ret = this._pngRect(x, y, width, height,
|
|
sock, display, depth);
|
|
} else if ((this._ctl & 0x08) == 0) {
|
|
ret = this._basicRect(this._ctl, x, y, width, height,
|
|
sock, display, depth);
|
|
} else {
|
|
throw new Error("Illegal tight compression received (ctl: " +
|
|
this._ctl + ")");
|
|
}
|
|
|
|
if (ret) {
|
|
this._ctl = null;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
_fillRect(x, y, width, height, sock, display, depth) {
|
|
if (sock.rQwait("TIGHT", 3)) {
|
|
return false;
|
|
}
|
|
|
|
let pixel = sock.rQshiftBytes(3);
|
|
display.fillRect(x, y, width, height, pixel, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
_jpegRect(x, y, width, height, sock, display, depth) {
|
|
let data = this._readData(sock);
|
|
if (data === null) {
|
|
return false;
|
|
}
|
|
|
|
display.imageRect(x, y, width, height, "image/jpeg", data);
|
|
|
|
return true;
|
|
}
|
|
|
|
_pngRect(x, y, width, height, sock, display, depth) {
|
|
throw new Error("PNG received in standard Tight rect");
|
|
}
|
|
|
|
_basicRect(ctl, x, y, width, height, sock, display, depth) {
|
|
if (this._filter === null) {
|
|
if (ctl & 0x4) {
|
|
if (sock.rQwait("TIGHT", 1)) {
|
|
return false;
|
|
}
|
|
|
|
this._filter = sock.rQshift8();
|
|
} else {
|
|
// Implicit CopyFilter
|
|
this._filter = 0;
|
|
}
|
|
}
|
|
|
|
let streamId = ctl & 0x3;
|
|
|
|
let ret;
|
|
|
|
switch (this._filter) {
|
|
case 0: // CopyFilter
|
|
ret = this._copyFilter(streamId, x, y, width, height,
|
|
sock, display, depth);
|
|
break;
|
|
case 1: // PaletteFilter
|
|
ret = this._paletteFilter(streamId, x, y, width, height,
|
|
sock, display, depth);
|
|
break;
|
|
case 2: // GradientFilter
|
|
ret = this._gradientFilter(streamId, x, y, width, height,
|
|
sock, display, depth);
|
|
break;
|
|
default:
|
|
throw new Error("Illegal tight filter received (ctl: " +
|
|
this._filter + ")");
|
|
}
|
|
|
|
if (ret) {
|
|
this._filter = null;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
_copyFilter(streamId, x, y, width, height, sock, display, depth) {
|
|
const uncompressedSize = width * height * 3;
|
|
let data;
|
|
|
|
if (uncompressedSize === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (uncompressedSize < 12) {
|
|
if (sock.rQwait("TIGHT", uncompressedSize)) {
|
|
return false;
|
|
}
|
|
|
|
data = sock.rQshiftBytes(uncompressedSize);
|
|
} else {
|
|
data = this._readData(sock);
|
|
if (data === null) {
|
|
return false;
|
|
}
|
|
|
|
this._zlibs[streamId].setInput(data);
|
|
data = this._zlibs[streamId].inflate(uncompressedSize);
|
|
this._zlibs[streamId].setInput(null);
|
|
}
|
|
|
|
let rgbx = new Uint8Array(width * height * 4);
|
|
for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
|
|
rgbx[i] = data[j];
|
|
rgbx[i + 1] = data[j + 1];
|
|
rgbx[i + 2] = data[j + 2];
|
|
rgbx[i + 3] = 255; // Alpha
|
|
}
|
|
|
|
display.blitImage(x, y, width, height, rgbx, 0, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
_paletteFilter(streamId, x, y, width, height, sock, display, depth) {
|
|
if (this._numColors === 0) {
|
|
if (sock.rQwait("TIGHT palette", 1)) {
|
|
return false;
|
|
}
|
|
|
|
const numColors = sock.rQpeek8() + 1;
|
|
const paletteSize = numColors * 3;
|
|
|
|
if (sock.rQwait("TIGHT palette", 1 + paletteSize)) {
|
|
return false;
|
|
}
|
|
|
|
this._numColors = numColors;
|
|
sock.rQskipBytes(1);
|
|
|
|
sock.rQshiftTo(this._palette, paletteSize);
|
|
}
|
|
|
|
const bpp = (this._numColors <= 2) ? 1 : 8;
|
|
const rowSize = Math.floor((width * bpp + 7) / 8);
|
|
const uncompressedSize = rowSize * height;
|
|
|
|
let data;
|
|
|
|
if (uncompressedSize === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (uncompressedSize < 12) {
|
|
if (sock.rQwait("TIGHT", uncompressedSize)) {
|
|
return false;
|
|
}
|
|
|
|
data = sock.rQshiftBytes(uncompressedSize);
|
|
} else {
|
|
data = this._readData(sock);
|
|
if (data === null) {
|
|
return false;
|
|
}
|
|
|
|
this._zlibs[streamId].setInput(data);
|
|
data = this._zlibs[streamId].inflate(uncompressedSize);
|
|
this._zlibs[streamId].setInput(null);
|
|
}
|
|
|
|
// Convert indexed (palette based) image data to RGB
|
|
if (this._numColors == 2) {
|
|
this._monoRect(x, y, width, height, data, this._palette, display);
|
|
} else {
|
|
this._paletteRect(x, y, width, height, data, this._palette, display);
|
|
}
|
|
|
|
this._numColors = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
_monoRect(x, y, width, height, data, palette, display) {
|
|
// Convert indexed (palette based) image data to RGB
|
|
// TODO: reduce number of calculations inside loop
|
|
const dest = this._getScratchBuffer(width * height * 4);
|
|
const w = Math.floor((width + 7) / 8);
|
|
const w1 = Math.floor(width / 8);
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
let dp, sp, x;
|
|
for (x = 0; x < w1; x++) {
|
|
for (let b = 7; b >= 0; b--) {
|
|
dp = (y * width + x * 8 + 7 - b) * 4;
|
|
sp = (data[y * w + x] >> b & 1) * 3;
|
|
dest[dp] = palette[sp];
|
|
dest[dp + 1] = palette[sp + 1];
|
|
dest[dp + 2] = palette[sp + 2];
|
|
dest[dp + 3] = 255;
|
|
}
|
|
}
|
|
|
|
for (let b = 7; b >= 8 - width % 8; b--) {
|
|
dp = (y * width + x * 8 + 7 - b) * 4;
|
|
sp = (data[y * w + x] >> b & 1) * 3;
|
|
dest[dp] = palette[sp];
|
|
dest[dp + 1] = palette[sp + 1];
|
|
dest[dp + 2] = palette[sp + 2];
|
|
dest[dp + 3] = 255;
|
|
}
|
|
}
|
|
|
|
display.blitImage(x, y, width, height, dest, 0, false);
|
|
}
|
|
|
|
_paletteRect(x, y, width, height, data, palette, display) {
|
|
// Convert indexed (palette based) image data to RGB
|
|
const dest = this._getScratchBuffer(width * height * 4);
|
|
const total = width * height * 4;
|
|
for (let i = 0, j = 0; i < total; i += 4, j++) {
|
|
const sp = data[j] * 3;
|
|
dest[i] = palette[sp];
|
|
dest[i + 1] = palette[sp + 1];
|
|
dest[i + 2] = palette[sp + 2];
|
|
dest[i + 3] = 255;
|
|
}
|
|
|
|
display.blitImage(x, y, width, height, dest, 0, false);
|
|
}
|
|
|
|
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
|
|
// assume the TPIXEL is 3 bytes long
|
|
const uncompressedSize = width * height * 3;
|
|
let data;
|
|
|
|
if (uncompressedSize === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (uncompressedSize < 12) {
|
|
if (sock.rQwait("TIGHT", uncompressedSize)) {
|
|
return false;
|
|
}
|
|
|
|
data = sock.rQshiftBytes(uncompressedSize);
|
|
} else {
|
|
data = this._readData(sock);
|
|
if (data === null) {
|
|
return false;
|
|
}
|
|
|
|
this._zlibs[streamId].setInput(data);
|
|
data = this._zlibs[streamId].inflate(uncompressedSize);
|
|
this._zlibs[streamId].setInput(null);
|
|
}
|
|
|
|
let rgbx = new Uint8Array(4 * width * height);
|
|
|
|
let rgbxIndex = 0, dataIndex = 0;
|
|
let left = new Uint8Array(3);
|
|
for (let x = 0; x < width; x++) {
|
|
for (let c = 0; c < 3; c++) {
|
|
const prediction = left[c];
|
|
const value = data[dataIndex++] + prediction;
|
|
rgbx[rgbxIndex++] = value;
|
|
left[c] = value;
|
|
}
|
|
rgbx[rgbxIndex++] = 255;
|
|
}
|
|
|
|
let upperIndex = 0;
|
|
let upper = new Uint8Array(3),
|
|
upperleft = new Uint8Array(3);
|
|
for (let y = 1; y < height; y++) {
|
|
left.fill(0);
|
|
upperleft.fill(0);
|
|
for (let x = 0; x < width; x++) {
|
|
for (let c = 0; c < 3; c++) {
|
|
upper[c] = rgbx[upperIndex++];
|
|
let prediction = left[c] + upper[c] - upperleft[c];
|
|
if (prediction < 0) {
|
|
prediction = 0;
|
|
} else if (prediction > 255) {
|
|
prediction = 255;
|
|
}
|
|
const value = data[dataIndex++] + prediction;
|
|
rgbx[rgbxIndex++] = value;
|
|
upperleft[c] = upper[c];
|
|
left[c] = value;
|
|
}
|
|
rgbx[rgbxIndex++] = 255;
|
|
upperIndex++;
|
|
}
|
|
}
|
|
|
|
display.blitImage(x, y, width, height, rgbx, 0, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
_readData(sock) {
|
|
if (this._len === 0) {
|
|
if (sock.rQwait("TIGHT", 3)) {
|
|
return null;
|
|
}
|
|
|
|
let byte;
|
|
|
|
byte = sock.rQshift8();
|
|
this._len = byte & 0x7f;
|
|
if (byte & 0x80) {
|
|
byte = sock.rQshift8();
|
|
this._len |= (byte & 0x7f) << 7;
|
|
if (byte & 0x80) {
|
|
byte = sock.rQshift8();
|
|
this._len |= byte << 14;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sock.rQwait("TIGHT", this._len)) {
|
|
return null;
|
|
}
|
|
|
|
let data = sock.rQshiftBytes(this._len, false);
|
|
this._len = 0;
|
|
|
|
return data;
|
|
}
|
|
|
|
_getScratchBuffer(size) {
|
|
if (!this._scratchBuffer || (this._scratchBuffer.length < size)) {
|
|
this._scratchBuffer = new Uint8Array(size);
|
|
}
|
|
return this._scratchBuffer;
|
|
}
|
|
}
|