diff --git a/README.md b/README.md index 59a72b103..be0f4989c 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,9 @@ use a WebSockets to TCP socket proxy. There is a python proxy included * UI and Icons : Chris Gordon * Original Logo : Michael Sersen * tight encoding : Michael Tinglof (Mercuri.ca) - + * pixel format conversion and ATEN iKVM "HARMON" (0x59) support : [Alexander Clouter](http://www.digriz.org.uk/) + * ATEN iKVM "AST2100" (0x57) support : [Kevin Kelley](https://github.com/kelleyk) + * Included libraries: * as3crypto : Henri Torgemane (code.google.com/p/as3crypto) * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net) diff --git a/ast2100-notes.txt b/ast2100-notes.txt new file mode 100644 index 000000000..ac9524eed --- /dev/null +++ b/ast2100-notes.txt @@ -0,0 +1,70 @@ +======================================================================================================================== +======================================================================================================================== + +These are notes to accompany the ATEN iKVM "AST2100" (0x59) encoding implementation that this branch contains. + +This implementation is the product of clean-room reverse engineering (that is, I am not and have never been subject to +nondisclosure agreements, nor have I had access to proprietary information, related to the subject matter of this +project). + +(c) Copyright 2015-2016 Kevin Kelley + +======================================================================================================================== +======================================================================================================================== + +- three major parts of implementation + +- Text/console quality in 4:4:4 will ALSO be higher because that's the only mode where VQ blocks happen, and VQ blocks + give very high quality for blocks containing only a few colors. + +- Currently unchecked, but we expect to get a "video settings changed" message from the Ast2100Decoder after the first + framebuffer update message that we send it following a settings change command. + +======================================================================================================================== +======================================================================================================================== + +- Much more extensive notes on the reverse engineering process, the iKVM protocol, the 0x59 video encoding, and the rest + of this adventure are forthcoming! There is also a Python implementation of many of these ideas that is + better-documented (and where clarity has not been sacrificed for performance). Stay tuned! + +- Current problems / limitations (aka "TODOs") + + - Especially on lower quality settings, you will notice that the picture is not as clear as what the ATEN iKVM client + will show you (using the same settings). I'm aware of this issue and intend to fix it. + + - The code could stand to be much better-tested. + + - The JavaScript files related to the AST2100 decoder are loaded even when noVNC does not use the decoder. It would + be nice to lazy-load them only when they are necessary. + + - Lots of globals (functions, constants, etc.) are exposed. Some quick refactoring could tuck the majority of them + away to avoid cluttering the namespace. + + - There is not any UI associated with sending messages to control power or to change picture mode/quality settings. + +- Profiling + + - For some reason, when I use blitImageData() (with the noVNC render queue disabled), that function shows up as the + "heaviest" function in Chrome's CPU profiler, even though the function is doing nothing other than evaluating a + branch condition or two and then calling _rgbxImageData(). When I call _rgbxImageData() directly, then + putImageData() (the Canvas method that's actually doing the heavy lifting) is correctly shown as the "heaviest" + function. + + - Profiler oddness aside, putImageData() is overwhelmingly the dominant cost; it seems to occupy 75-85% of the CPU + time that noVNC uses. There are plenty of places that we could get small performance improvements in Ast2100Decoder + (and elsewhere in noVNC) but they seem unlikely to have a worthwhile impact, given that fact. + +- About the implementation + + - One large, remaining inefficiency is the several times that image data is copied around before being blitted. The + Ast2100Decoder class generates as output 256-element arrays (representing 64 pixels as (R,G,B,A) 4-tuples). This is + exactly what winds up in the ImageData object that is eventually passed to putImageData(); we could just have the + decoder write its output directly into those arrays if we wanted. + +- Performance questions + + - Is it faster to call putImageData() fewer times with larger buffers? We could collect groups of blocks (or even an + entire frame) and then call putImageData() once. (Of course, this would require redrawing unchanged regions every + frame, too.) + + - Can we use WebGL instead of Canvas? diff --git a/include/ast2100.js b/include/ast2100.js new file mode 100644 index 000000000..0a6453293 --- /dev/null +++ b/include/ast2100.js @@ -0,0 +1,488 @@ +"use strict"; +/*global DCTSIZE, DCTSIZE2, JpegHuffmanTable, BitStream, AST2100IDCT, AAN_IDCT_SCALING_FACTORS, ZIGZAG_ORDER */ +/*global ATEN_QT_LUMA, ATEN_QT_CHROMA, TABLE_CLASS_AC, TABLE_CLASS_DC */ +/*global BITS_AC_LUMA, BITS_AC_CHROMA, BITS_DC_LUMA, BITS_DC_CHROMA, HUFFVAL_AC_LUMA, HUFFVAL_AC_CHROMA, HUFFVAL_DC_LUMA, HUFFVAL_DC_CHROMA */ +/*global inRangeIncl, isEmpty */ +/*global fmt_u8a, fmt_u8, fmt_u16 */ + +/* (c) Copyright 2015-2016 Kevin Kelley . */ + + +var verboseDebug = false; +var verboseMcuCount = false; +var traceUpdates = false; +var verboseStats = false; +var verboseVideoSettings = true; + + +// @KK: This is not properly part of the AST2100 decoder, but it's very related. This function is just a little helper +// to make sending these messages easier. Change the verboseVideoSettings variable above to get console output showing +// you when the server changes what it is sending (i.e. in response to this message). +var atenChangeVideoSettings = function (lumaQt, chromaQt, subsamplingMode) { + //pick correct object to get the rfb websock from: in UI if enabled, in global rfb if using vnc_auto + var sock = typeof UI !== "undefined" ? UI.rfb._sock : rfb._sock; + RFB.messages.atenChangeVideoSettings(sock, lumaQt, chromaQt, subsamplingMode); +}; + + +var Ast2100Decoder; + + +(function () { + "use strict"; + + Ast2100Decoder = function (defaults) { + + this._blitCallback = defaults.blitCallback; + this._videoSettingsChangedCallback = defaults.videoSettingsChangedCallback; + this._frame_width = defaults.width; + this._frame_height = defaults.height; + + if (!this._frame_width || !this._frame_height) + throw 'Missing required parameter: width, height'; + + // Either 444u or 422u (though the "422" mode is really 4:2:0, where chroma is subsampled by a factor of two in + // each direction).. Applies only to JPEG-ish blocks, not to VQ blocks; but VQ blocks seem to only appear in + // 4:4:4 color mode. + this.subsamplingMode = -1; + + this.quantTables = [new Int32Array(64), new Int32Array(64)]; + this._loadedQuantTables = [-1, -1]; + + this.huffTables = [ + [new JpegHuffmanTable({bits: BITS_DC_LUMA, huffval: HUFFVAL_DC_LUMA}), + new JpegHuffmanTable({bits: BITS_DC_CHROMA, huffval: HUFFVAL_DC_CHROMA})], + [new JpegHuffmanTable({bits: BITS_AC_LUMA, huffval: HUFFVAL_AC_LUMA}), + new JpegHuffmanTable({bits: BITS_AC_CHROMA, huffval: HUFFVAL_AC_CHROMA})], + ]; + + this._scan_components = [ + {huffTableSelectorDC: 0, huffTableSelectorAC: 0}, // Y + {huffTableSelectorDC: 1, huffTableSelectorAC: 1}, // Cb + {huffTableSelectorDC: 1, huffTableSelectorAC: 1} // Cr + ]; + this._scan_prev_dc = [0, 0, 0]; + + this._mcuPosX = 0; + this._mcuPosY = 0; + + this._initializeVq(); + + // Allocate memory here once and then re-use it. + // These buffers store data after entropy decoding (Huffman, zigzag) and before we dequant and apply the IDCT. + this._tmpBufY = [ + new Int16Array(DCTSIZE2), + new Int16Array(DCTSIZE2), + new Int16Array(DCTSIZE2), + new Int16Array(DCTSIZE2)]; + this._tmpBufCb = new Int16Array(DCTSIZE2); + this._tmpBufCr = new Int16Array(DCTSIZE2); + // These buffers store IDCT output. + this._componentBufY = [ + new Uint8Array(DCTSIZE2), + new Uint8Array(DCTSIZE2), + new Uint8Array(DCTSIZE2), + new Uint8Array(DCTSIZE2)]; + this._componentBufCb = new Uint8Array(DCTSIZE2); + this._componentBufCr = new Uint8Array(DCTSIZE2); + // The final RGB output goes here; noVNC expects each group of four elements to represent one RGBA pixel. + // this._outputBuf = new Uint8Array(DCTSIZE2 * 4); + this._outputBuf = new Uint8Array(DCTSIZE2 * 4); + + }; + + Ast2100Decoder.prototype = { + + // TODO: Could/should remove this indirection. + _idct: function (quant_table, data_unit, dstBuf) { + if (!quant_table) + throw 'Required argument missing: quant_table'; + if (!data_unit) + throw 'Required argument missing: data_unit'; + if (!dstBuf) + throw 'Required argument missing: dstBuf'; + + return AST2100IDCT.idct_fixed_aan(quant_table, data_unit, dstBuf); + }, + + _getMcuSize: function () { + return {444: 8, 422: 16}[this.subsamplingMode]; + }, + + // Bakes in C(u)*C(v) and the cosine terms (from the IDCT formula). + _loadQuantTable: function (slot, srcTable) { + for (var y = 0; y < 8; ++y) { + for (var x = 0; x < 8; ++x) { + this.quantTables[slot][y*8+x] = ~~(srcTable[y*8+x] * AAN_IDCT_SCALING_FACTORS[x] * AAN_IDCT_SCALING_FACTORS[y] * 65536.0); + } + } + }, + + _initializeVq: function () { + // These colors are in YCbCr, so they're just black, white, and two shades of grey. + this._vqCodewordLookup = [0, 1, 2, 3]; + this._vqCodebook = [ + [0x00, 0x80, 0x80], + [0xFF, 0x80, 0x80], + [0x80, 0x80, 0x80], + [0xC0, 0x80, 0x80] + ]; + }, + + // Update our position variables to point at the next MCU. + _advancePosition: function () { + var mcuSize = this._getMcuSize(); + var widthInMcus = ~~(this._frame_width / mcuSize); + if (this._frame_width % mcuSize != 0) + widthInMcus += 1; + var heightInMcus = ~~(this._frame_height / mcuSize); + if (this._frame_height % mcuSize != 0) + heightInMcus += 1; + + this._mcuPosX += 1; + if (this._mcuPosX >= widthInMcus) { + this._mcuPosX = 0; + this._mcuPosY += 1; + } + if (this._mcuPosY >= heightInMcus) { + this._mcuPosY = 0; + } + }, + + // Change the frame's size. + setSize: function (width, height) { + if (this._frame_width != width || this._frame_height != height) + Util.Debug('Ast2100Decoder: frame height changed to '+width+'x'+height); + this._frame_width = width; + this._frame_height = height; + }, + + // Each quant table selector is between 0x0 (lowest quality) and 0xB (highest quality). The ATEN client shows a + // single quality slider, which changes both values in tandem. The server sends all three values with each + // FramebufferUpdate message, so these values are updated with every call to decode(). They will be -1 before + // the first frame is decoded. + getVideoSettings: function () { + return { + quantTableSelectorLuma: this._loadedQuantTables[0], + quantTableSelectorChroma: this._loadedQuantTables[1], + subsamplingMode: this.subsamplingMode + }; + }, + + decode: function (data) { + + var mcuIdx = 0; + if (verboseStats) { + var blockTypeCounter = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // length 16 + } + + // Reset state that must be reset between frames. + this._scan_prev_dc = [0, 0, 0]; + this._mcuPosX = 0; + this._mcuPosY = 0; + + // First four bytes. + var quantTableSelectorLuma = data[0]; // 0 <= x <= 0xB + var quantTableSelectorChroma = data[1]; // 0 <= x <= 0xB + var subsamplingMode = (data[2] << 8) | data[3]; // 422u or 444u + + var changedSettings = false; + if (this.subsamplingMode != subsamplingMode) { + if (verboseVideoSettings) + console.log('decode(): new subsampling mode: '+subsamplingMode); + this.subsamplingMode = subsamplingMode; + changedSettings = true; + } + if (quantTableSelectorLuma != this._loadedQuantTables[0]) { + if (!inRangeIncl(quantTableSelectorLuma, 0, 0xB)) + throw 'Out-of-range selector for luma quant table: ' + quantTableSelectorLuma.toString(16); + if (verboseVideoSettings) + console.log('decode(): loading new luma quant table: '+fmt_u8(quantTableSelectorLuma)); + this._loadQuantTable(0, ATEN_QT_LUMA[quantTableSelectorLuma]); + this._loadedQuantTables[0] = quantTableSelectorLuma; + changedSettings = true; + } + if (quantTableSelectorChroma != this._loadedQuantTables[1]) { + if (!inRangeIncl(quantTableSelectorChroma, 0, 0xB)) + throw 'Out-of-range selector for chroma quant table: ' + quantTableSelectorChroma.toString(16); + if (verboseVideoSettings) + console.log('decode(): loading new chroma quant table: '+fmt_u8(quantTableSelectorChroma)); + this._loadQuantTable(1, ATEN_QT_CHROMA[quantTableSelectorChroma]); + this._loadedQuantTables[1] = quantTableSelectorChroma; + changedSettings = true; + } + + if (this.subsamplingMode != 422 && this.subsamplingMode != 444) + throw 'Unexpected value for subsamplingMode: 0x' + fmt_u16(this.subsamplingMode); + + if (changedSettings && this._videoSettingsChangedCallback) + this._videoSettingsChangedCallback(this.getVideoSettings()); + + // The remainder of the stream is byte-swapped in four-byte chunks. BitStream takes care of this. + this._stream = new BitStream({data: data}); + this._stream.skip(16); + this._stream.skip(16); // do this in two parts because bits must be < 32; thanks JavaScript! + + while (true) { + var controlFlag = this._stream.read(4); // uint4 + + if (verboseStats) + ++blockTypeCounter[controlFlag]; + + if (verboseMcuCount) { + console.log('MCU #' + mcuIdx + '- Control flag: ' + controlFlag.toString(16)); + console.log(' stream pos = ' + this._stream.getPos()); + } + + if (controlFlag == 0 || controlFlag == 4 || controlFlag == 8 || controlFlag == 0xC) { + // JPEG-ish (DCT-compressed) data. + + if (controlFlag == 8 || controlFlag == 0xC) { + this._mcuPosX = this._stream.read(8); // uint8 + this._mcuPosY = this._stream.read(8); // uint8 + if (traceUpdates) + console.log("decode(): read new MCU pos: (0x"+fmt_u8(this._mcuPosX)+",0x"+fmt_u8(this._mcuPosY)+")"); + } + + if (controlFlag == 4 || controlFlag == 0xC) { + // Haven't seen traffic where this feature is used yet. + throw 'Unexpected control flag: alternate quant table'; + } + + // Since we always have 4:2:2 chroma subsampling on, we'll read 6 blocks (4 Y, 1 Cr, 1 Cb) and + // produce a block of 16x16 pixels. + this._parseMcu(); + + } else if (inRangeIncl(controlFlag, 5, 7) || inRangeIncl(controlFlag, 0xD, 0xF)) { + // VQ-compressed data. + + if (controlFlag >= 0xD) { + this._mcuPosX = this._stream.read(8); // uint8 + this._mcuPosY = this._stream.read(8); // uint8 + if (traceUpdates) + console.log("decode(): read new MCU pos: (0x"+fmt_u8(this._mcuPosX)+",0x"+fmt_u8(this._mcuPosY)+")"); + } + + var codewordSize = (controlFlag & 7) - 5; // 0 <= codewordSize <= 2 + this._parseVqBlock(codewordSize); + + } else if (controlFlag == 9) { + // Done with frame! + break; + } else { + throw 'Unexpected control flag: unknown value 0x'+fmt_u8(controlFlag); + } + + ++mcuIdx; + } + + if (traceUpdates) + console.log("decode(): finished after "+mcuIdx+" blocks"); // one of which will have been the 0x9 "end-of-frame" block + + if (verboseStats) { + var counts = {}; + for (var i = 0; i < 16; ++i) + if (i != 9 && blockTypeCounter[i] > 0) + counts[i] = blockTypeCounter[i]; + if (!isEmpty(counts)) + console.log(counts); + } + + }, + + // - Always 8x8, since chroma subsampling does not apply to VQ-compressed data. (As a consequence, VQ blocks only seem to + // appear when DCT chroma subsampling is disabled; that is, in "444" mode.) + // - Reads 64 * codewordSize bits from the input stream. + _parseVqBlock: function (codewordSize) { + + var mcuSize = this._getMcuSize(); + if (mcuSize != 8) + throw 'Unexpected MCU size for VQ block!'; + if (!inRangeIncl(codewordSize, 0, 2)) + throw 'Out-of-range codewordSize!'; + + var i; + + var y_buf = this._componentBufY[0]; + var cb_buf = this._componentBufCb; + var cr_buf = this._componentBufCr; + + var that = this; + var setColor = function (j, codeword) { + var color = that._vqCodebook[that._vqCodewordLookup[codeword]]; + y_buf[j] = color[0]; + cb_buf[j] = color[1]; + cr_buf[j] = color[2]; + }; + + // Read new codebook data. (That is: new colors for each of our codewords (the values we'll read from the + // input data) to map to.) + for (i = 0; i < (1 << codewordSize); ++i) { + // Read 1b flag and 2b codebook slot number. if flag is set, read 24b RGB value and set colors[slot #]. + // Regardless (?), set the ith codeowrd to map to this slot. + var hasNewColor = this._stream.read(1); + var codebookSlotIdx = this._stream.read(2); + if (hasNewColor) { + var color = [this._stream.read(8), this._stream.read(8), this._stream.read(8)]; // Y, Cb, Cr + // console.log(' - loaded VQ codebook color; codeword='+i+' slot='+codebookSlotIdx+' color='+fmt_u8a(color)); + this._vqCodebook[codebookSlotIdx] = color; + } + this._vqCodewordLookup[i] = codebookSlotIdx; + } + + // Read a block of image data. + if (codewordSize == 0) { + // Act as though we've got a single-entry codebook. + for (i = 0; i < 64; ++i) + setColor(i, 0); + } else { + for (i = 0; i < 64; ++i) + setColor(i, this._stream.read(codewordSize)); + } + + // Perform colorspace conversion and copy into destination image buffer. + for (var j = 0; j < 64; ++j) + this._ycbcrToRgb(this._outputBuf, j, this._componentBufY[0][j], this._componentBufCb[j], this._componentBufCr[j]); + + this._blitCallback(8 * this._mcuPosX, 8 * this._mcuPosY, 8, 8, this._outputBuf); + + this._advancePosition(); + }, + + _parseMcu: function () { + + var qtLuma = this.quantTables[0]; + var qtChroma = this.quantTables[1]; + + this._parseDataUnit(0, this._tmpBufY[0]); + this._idct(qtLuma, this._tmpBufY[0], this._componentBufY[0]); + if (this.subsamplingMode != 444) { + this._parseDataUnit(0, this._tmpBufY[1]); + this._idct(qtLuma, this._tmpBufY[1], this._componentBufY[1]); + this._parseDataUnit(0, this._tmpBufY[2]); + this._idct(qtLuma, this._tmpBufY[2], this._componentBufY[2]); + this._parseDataUnit(0, this._tmpBufY[3]); + this._idct(qtLuma, this._tmpBufY[3], this._componentBufY[3]); + } + this._parseDataUnit(1, this._tmpBufCb); + this._idct(qtChroma, this._tmpBufCb, this._componentBufCb); + this._parseDataUnit(2, this._tmpBufCr); + this._idct(qtChroma, this._tmpBufCr, this._componentBufCr); + + if (this.subsamplingMode != 444) { + // 4:2:0 subsampling (x2 in each direction), or what ATEN calls "422" (even though it's not). + for (var dy = 0; dy < 2; ++ dy) { + for (var dx = 0; dx < 2; ++dx) { + // for each of the four blocks in this MCU + var componentBufY = this._componentBufY[dx*2+dy]; + for (var y = 0; y < 8; ++y) { + for (var x = 0; x < 8; ++x) { + var hy = ~~((8*dx+y)/2); + var hx = ~~((8*dy+x)/2); + this._ycbcrToRgb(this._outputBuf, y*8+x, componentBufY[y*8+x], this._componentBufCb[hy*8+hx], this._componentBufCr[hy*8+hx]); + } + } + this._blitCallback(16 * this._mcuPosX + 8 * dy, 16 * this._mcuPosY + 8 * dx, 8, 8, this._outputBuf); + } + } + } else { + // No subsampling. + for (var j = 0; j < 64; ++j) + this._ycbcrToRgb(this._outputBuf, j, this._componentBufY[0][j], this._componentBufCb[j], this._componentBufCr[j]); + this._blitCallback(8 * this._mcuPosX, 8 * this._mcuPosY, 8, 8, this._outputBuf); + } + + this._advancePosition(); + }, + + _parseDataUnit: function (componentIdx, buf) { + var scanComponent = this._scan_components[componentIdx]; + var dc_hufftable = this.huffTables[TABLE_CLASS_DC][scanComponent.huffTableSelectorDC]; + var ac_hufftable = this.huffTables[TABLE_CLASS_AC][scanComponent.huffTableSelectorAC]; + + var setValue = function (i, val) { + buf[ZIGZAG_ORDER[i]] = val; + }; + + // First element is the DC component, followed by 63 AC components. The DC component is encoded slightly + // differently than the AC components: it is stored as the delta between the value and the last DC component + // from the same component. + var dc_delta = this._readEncodedValueDC(dc_hufftable); + this._scan_prev_dc[componentIdx] += dc_delta; + buf[0] = this._scan_prev_dc[componentIdx]; + + // Read the AC components. + for (var i = 1; i < 64;) { + var x = ac_hufftable.readCode(this._stream); + + // @KK: Renamed r, s to runlen, size. + // See ITU T.81 p89 (e.g. Fig F.1). + // r is runlength of zeroes; if s==0, r==0 means EOB and r==15 means ZRL. + // s is number of bits required to represent the amplitude that follows the codeword. + var runlen = x >>> 4; + var size = x & 0x0F; + + if (size == 0) { + if (runlen == 0) { // special EOB (end-of-block) codeword; fill remainder with zeroes + while (i < 64) { + setValue(i, 0); + ++i; + } + break; + } else if (runlen == 0xF) { // special ZLE (zero-length-encode) codeword; emit sixteen zeroes + for (var j = 0; j < 16; ++j) + setValue(i + j, 0); + i += 16; + continue; + } + } + + // Emit runlen zero entries. + for (var j = 0; j < runlen; ++j) + setValue(i + j, 0); + i += runlen; + + setValue(i, this._readEncodedValueAC(size)); // category=size + i += 1; + } + + return buf; + }, + + _readEncodedValueDC: function (huffTable) { + var category = huffTable.readCode(this._stream); + return this._readEncodedValueAC(category); + }, + + _readEncodedValueAC: function (category) { + if (category == 0) + return 0; + + var value; // sint32 + var val_sign = this._stream.read(1); + + if (val_sign == 0) { // Negative (unlike a normal "signed int" representation). + value = -(1 << category) + 1; + } else { + value = (1 << category - 1); + } + + if (category > 1) { + var more_bits = this._stream.read(category - 1); // uint + value += more_bits; + } + + return value; + }, + + _ycbcrToRgb: function (outputBuf, outputOffset, y, cb, cr) { + outputOffset *= 4; + outputBuf[outputOffset + 0] = clamp(YUVTORGB_Y_TABLE[y] + YUVTORGB_CR_R_TABLE[cr]); + outputBuf[outputOffset + 1] = clamp(YUVTORGB_Y_TABLE[y] + YUVTORGB_CR_G_TABLE[cr] + YUVTORGB_CB_G_TABLE[cb]); + outputBuf[outputOffset + 2] = clamp(YUVTORGB_Y_TABLE[y] + YUVTORGB_CB_B_TABLE[cb]); + outputBuf[outputOffset + 3] = 0xFF; // noVNC expects alpha + } + + }; + +})(); diff --git a/include/ast2100const.js b/include/ast2100const.js new file mode 100644 index 000000000..0ae46d0f9 --- /dev/null +++ b/include/ast2100const.js @@ -0,0 +1,337 @@ +/* (c) Copyright 2015-2016 Kevin Kelley . */ + + +// JPEG zigzag tables +var ZIGZAG_ORDER = [ + 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, +]; +var DEZIGZAG_ORDER = [ + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63, +]; + + +// Huffman tables +var BITS_DC_LUMA = [0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]; +var HUFFVAL_DC_LUMA = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; +var BITS_DC_CHROMA = [0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]; +var HUFFVAL_DC_CHROMA = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; +var BITS_AC_LUMA = [0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d]; +var HUFFVAL_AC_LUMA = [ + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, + 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, + 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, + 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, + 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, + 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, + 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, + 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, + 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, + 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa +]; +var BITS_AC_CHROMA = [0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77]; +var HUFFVAL_AC_CHROMA = [ + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, + 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, + 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, + 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, + 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, + 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, + 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, + 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, + 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, + 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, + 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, + 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, + 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa +]; + + +// Quant tables +var ATEN_QT_LUMA = [ + [ + 0x14, 0x0D, 0x0C, 0x14, 0x1E, 0x32, 0x3F, 0x4C, + 0x0F, 0x0F, 0x11, 0x17, 0x20, 0x48, 0x4B, 0x44, + 0x11, 0x10, 0x14, 0x1E, 0x32, 0x47, 0x56, 0x46, + 0x11, 0x15, 0x1B, 0x24, 0x3F, 0x6C, 0x64, 0x4D, + 0x16, 0x1B, 0x2E, 0x46, 0x55, 0x88, 0x80, 0x60, + 0x1E, 0x2B, 0x44, 0x50, 0x65, 0x82, 0x8D, 0x73, + 0x3D, 0x50, 0x61, 0x6C, 0x80, 0x97, 0x96, 0x7E, + 0x5A, 0x73, 0x76, 0x7A, 0x8C, 0x7D, 0x80, 0x7B, + ], [ + 0x11, 0x0C, 0x0A, 0x11, 0x1A, 0x2B, 0x37, 0x42, + 0x0D, 0x0D, 0x0F, 0x14, 0x1C, 0x3F, 0x41, 0x3C, + 0x0F, 0x0E, 0x11, 0x1A, 0x2B, 0x3E, 0x4B, 0x3D, + 0x0F, 0x12, 0x18, 0x1F, 0x37, 0x5F, 0x57, 0x43, + 0x13, 0x18, 0x28, 0x3D, 0x4A, 0x77, 0x70, 0x54, + 0x1A, 0x26, 0x3C, 0x46, 0x58, 0x71, 0x7B, 0x64, + 0x35, 0x46, 0x55, 0x5F, 0x70, 0x84, 0x83, 0x6E, + 0x4E, 0x64, 0x67, 0x6B, 0x7A, 0x6D, 0x70, 0x6C, + ], [ + 0x0E, 9, 9, 0x0E, 0x15, 0x24, 0x2E, 0x37, + 0x0A, 0x0A, 0x0C, 0x11, 0x17, 0x34, 0x36, 0x31, + 0x0C, 0x0B, 0x0E, 0x15, 0x24, 0x33, 0x3E, 0x32, + 0x0C, 0x0F, 0x13, 0x1A, 0x2E, 0x4E, 0x48, 0x38, + 0x10, 0x13, 0x21, 0x32, 0x3D, 0x62, 0x5D, 0x45, + 0x15, 0x1F, 0x31, 0x3A, 0x49, 0x5E, 0x66, 0x53, + 0x2C, 0x3A, 0x46, 0x4E, 0x5D, 0x6D, 0x6C, 0x5B, + 0x41, 0x53, 0x56, 0x58, 0x65, 0x5A, 0x5D, 0x59, + ], [ + 0x0B, 7, 7, 0x0B, 0x11, 0x1C, 0x24, 0x2B, + 8, 8, 0x0A, 0x0D, 0x12, 0x29, 0x2B, 0x27, + 0x0A, 9, 0x0B, 0x11, 0x1C, 0x28, 0x31, 0x28, + 0x0A, 0x0C, 0x0F, 0x14, 0x24, 0x3E, 0x39, 0x2C, + 0x0C, 0x0F, 0x1A, 0x28, 0x30, 0x4E, 0x4A, 0x37, + 0x11, 0x19, 0x27, 0x2E, 0x3A, 0x4A, 0x51, 0x42, + 0x23, 0x2E, 0x38, 0x3E, 0x4A, 0x56, 0x56, 0x48, + 0x33, 0x42, 0x44, 0x46, 0x50, 0x47, 0x4A, 0x47, + ], [ + 9, 6, 5, 9, 0x0D, 0x16, 0x1C, 0x22, + 6, 6, 7, 0x0A, 0x0E, 0x20, 0x21, 0x1E, + 7, 7, 9, 0x0D, 0x16, 0x20, 0x26, 0x1F, + 7, 9, 0x0C, 0x10, 0x1C, 0x30, 0x2D, 0x22, + 0x0A, 0x0C, 0x14, 0x1F, 0x26, 0x3D, 0x39, 0x2B, + 0x0D, 0x13, 0x1E, 0x24, 0x2D, 0x3A, 0x3F, 0x33, + 0x1B, 0x24, 0x2B, 0x30, 0x39, 0x44, 0x43, 0x38, + 0x28, 0x33, 0x35, 0x37, 0x3F, 0x38, 0x39, 0x37, + ], [ + 6, 4, 3, 6, 9, 0x0F, 0x13, 0x16, + 4, 4, 5, 7, 9, 0x15, 0x16, 0x14, + 5, 4, 6, 9, 0x0F, 0x15, 0x19, 0x15, + 5, 6, 8, 0x0A, 0x13, 0x20, 0x1E, 0x17, + 6, 8, 0x0D, 0x15, 0x19, 0x28, 0x26, 0x1C, + 9, 0x0D, 0x14, 0x18, 0x1E, 0x27, 0x2A, 0x22, + 0x12, 0x18, 0x1D, 0x20, 0x26, 0x2D, 0x2D, 0x25, + 0x1B, 0x22, 0x23, 0x24, 0x2A, 0x25, 0x26, 0x25, + ], [ + 3, 2, 1, 3, 4, 7, 9, 0x0B, + 2, 2, 2, 3, 4, 0x0A, 0x0B, 0x0A, + 2, 2, 3, 4, 7, 0x0A, 0x0C, 0x0A, + 2, 3, 4, 5, 9, 0x10, 0x0F, 0x0B, + 3, 4, 6, 0x0A, 0x0C, 0x14, 0x13, 0x0E, + 4, 6, 0x0A, 0x0C, 0x0F, 0x13, 0x15, 0x11, + 9, 0x0C, 0x0E, 0x10, 0x13, 0x16, 0x16, 0x12, + 0x0D, 0x11, 0x11, 0x12, 0x15, 0x12, 0x13, 0x12, + ], [ + 2, 1, 1, 2, 3, 5, 6, 7, + 1, 1, 1, 2, 3, 7, 7, 6, + 1, 1, 2, 3, 5, 7, 8, 7, + 1, 2, 2, 3, 6, 0x0A, 0x0A, 7, + 2, 2, 4, 7, 8, 0x0D, 0x0C, 9, + 3, 4, 6, 8, 0x0A, 0x0D, 0x0E, 0x0B, + 6, 8, 9, 0x0A, 0x0C, 0x0F, 0x0F, 0x0C, + 9, 0x0B, 0x0B, 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, + ], [ + 2, 1, 1, 2, 3, 5, 6, 7, + 1, 1, 1, 2, 3, 7, 7, 6, + 1, 1, 2, 3, 5, 7, 8, 7, + 1, 2, 2, 3, 6, 0x0A, 0x0A, 7, + 2, 2, 4, 7, 8, 0x0D, 0x0C, 9, + 3, 4, 6, 8, 0x0A, 0x0D, 0x0E, 0x0B, + 6, 8, 9, 0x0A, 0x0C, 0x0F, 0x0F, 0x0C, + 9, 0x0B, 0x0B, 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, + ], [ + 1, 1, 1, 1, 2, 3, 4, 5, + 1, 1, 1, 1, 2, 5, 5, 5, + 1, 1, 1, 2, 3, 5, 6, 5, + 1, 1, 2, 2, 4, 8, 7, 5, + 1, 2, 3, 5, 6, 0x0A, 9, 7, + 2, 3, 5, 6, 7, 9, 0x0A, 8, + 4, 6, 7, 8, 9, 0x0B, 0x0B, 9, + 6, 8, 8, 9, 0x0A, 9, 9, 9, + ], [ + 1, 1, 1, 1, 1, 2, 3, 3, + 1, 1, 1, 1, 1, 3, 3, 3, + 1, 1, 1, 1, 2, 3, 4, 3, + 1, 1, 1, 1, 3, 5, 5, 3, + 1, 1, 2, 3, 4, 6, 6, 4, + 1, 2, 3, 4, 5, 6, 7, 5, + 3, 4, 4, 5, 6, 7, 7, 6, + 4, 5, 5, 6, 7, 6, 6, 6, + ], [ + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 2, 1, + 1, 1, 1, 1, 1, 2, 2, 1, + 1, 1, 1, 1, 2, 3, 3, 2, + 1, 1, 1, 2, 2, 3, 3, 2, + 1, 2, 2, 2, 3, 3, 3, 3, + 2, 2, 2, 3, 3, 3, 3, 3, + ] +]; + +var ATEN_QT_CHROMA = [ + [ + 0x1F, 0x21, 0x2D, 0x58, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + 0x21, 0x27, 0x30, 0x7B, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + 0x2D, 0x30, 0x69, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + 0x58, 0x7B, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, 0x0B9, + ], [ + 0x1B, 0x1D, 0x27, 0x4C, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + 0x1D, 0x22, 0x2A, 0x6B, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + 0x27, 0x2A, 0x5B, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + 0x4C, 0x6B, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, 0x0A0, + ], [ + 0x16, 0x18, 0x20, 0x3F, 0x85, 0x85, 0x85, 0x85, + 0x18, 0x1C, 0x22, 0x58, 0x85, 0x85, 0x85, 0x85, + 0x20, 0x22, 0x4B, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x3F, 0x58, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + ], [ + 0x12, 0x13, 0x1A, 0x33, 0x6C, 0x6C, 0x6C, 0x6C, + 0x13, 0x16, 0x1C, 0x48, 0x6C, 0x6C, 0x6C, 0x6C, + 0x1A, 0x1C, 0x3D, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, + 0x33, 0x48, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, + 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, + 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, + 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, + 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, + ], [ + 0x0D, 0x0E, 0x13, 0x26, 0x50, 0x50, 0x50, 0x50, + 0x0E, 0x11, 0x15, 0x35, 0x50, 0x50, 0x50, 0x50, + 0x13, 0x15, 0x2D, 0x50, 0x50, 0x50, 0x50, 0x50, + 0x26, 0x35, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, + 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, + 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, + 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, + 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, + ], [ + 9, 0x0A, 0x0D, 0x1A, 0x37, 0x37, 0x37, 0x37, + 0x0A, 0x0B, 0x0E, 0x25, 0x37, 0x37, 0x37, 0x37, + 0x0D, 0x0E, 0x1F, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x1A, 0x25, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + ], [ + 4, 5, 6, 0x0D, 0x1B, 0x1B, 0x1B, 0x1B, + 5, 5, 7, 0x12, 0x1B, 0x1B, 0x1B, 0x1B, + 6, 7, 0x0F, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, + 0x0D, 0x12, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, + 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, + 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, + 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, + 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, 0x1B, + ], [ + 3, 3, 4, 8, 0x12, 0x12, 0x12, 0x12, + 3, 3, 4, 0x0C, 0x12, 0x12, 0x12, 0x12, + 4, 4, 0x0A, 0x12, 0x12, 0x12, 0x12, 0x12, + 8, 0x0C, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + ], [ + 2, 2, 3, 7, 0x0F, 0x0F, 0x0F, 0x0F, + 2, 3, 4, 0x0A, 0x0F, 0x0F, 0x0F, 0x0F, + 3, 4, 8, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + 7, 0x0A, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + ], [ + 2, 2, 3, 5, 0x0C, 0x0C, 0x0C, 0x0C, + 2, 2, 3, 8, 0x0C, 0x0C, 0x0C, 0x0C, + 3, 3, 7, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 5, 8, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + ], [ + 1, 1, 2, 4, 9, 9, 9, 9, + 1, 1, 2, 6, 9, 9, 9, 9, + 2, 2, 5, 9, 9, 9, 9, 9, + 4, 6, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, + ], [ + 1, 1, 1, 2, 6, 6, 6, 6, + 1, 1, 1, 4, 6, 6, 6, 6, + 1, 1, 3, 6, 6, 6, 6, 6, + 2, 4, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + ] +]; + +/* + Ref.: libjpeg's jddctmgr.c: + "For float AA&N IDCT method, multipliers are equal to quantization + coefficients scaled by scalefactor[row]*scalefactor[col], where + scalefactor[0] = 1 + scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 +*/ +var AAN_IDCT_SCALING_FACTORS = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379, +]; + + +var YUVTORGB_CR_G_TABLE = new Int32Array(256); +var YUVTORGB_CR_R_TABLE = new Int32Array(256); +var YUVTORGB_CB_G_TABLE = new Int32Array(256); +var YUVTORGB_CB_B_TABLE = new Int32Array(256); +var YUVTORGB_Y_TABLE = new Int32Array(256); +for (var i = 0; i < 256; ++i) { + YUVTORGB_CR_G_TABLE[i] = (0x688000 - i * 0xD000) >> 16; + YUVTORGB_CR_R_TABLE[i] = (0xFF340000 + i * 0x19900) >> 16; + YUVTORGB_CB_G_TABLE[i] = (0x328000 - i * 0x6400) >> 16; + YUVTORGB_CB_B_TABLE[i] = (0xFEFE8000 + i * 0x20400) >> 16; + YUVTORGB_Y_TABLE[i] = (0xFFEDE040 + i * 0x129FC) >> 16; +} + + +var TABLE_CLASS_DC = 0; +var TABLE_CLASS_AC = 1; +var DCTSIZE = 8; +var DCTSIZE2 = DCTSIZE * DCTSIZE; diff --git a/include/ast2100idct.js b/include/ast2100idct.js new file mode 100644 index 000000000..dbdc626f3 --- /dev/null +++ b/include/ast2100idct.js @@ -0,0 +1,204 @@ +/* (c) Copyright 2015-2016 Kevin Kelley . */ + + +var AST2100IDCT; + + +// N.B.: These values assume CONST_BITS == 8 +var FIX_1_082392200 = 277; // fix(1.082392200) +var FIX_1_414213562 = 362; // fix(1.414213562) +var FIX_1_847759065 = 473; // fix(1.847759065) +var FIX_2_613125930 = 669; // fix(2.613125930) + + +var MAXJSAMPLE = 255; // largest value of a sample; 8-bit, so 2**8-1 +var CONST_BITS = 8; +var PASS1_BITS = 0; + +// This is like clamp() except that it also adds the MAXJSAMPLE/2 offset. +var range_limit = function (x) { + x += 128; // XXX: This is baked into the range limit table in the ATEN stuff and in libjpeg; see commpent above prepare_range_limit_table() in jdmaster.c. + return Math.max(0, Math.min(255, x)); +}; + +// Convert float to fixed-point. +var fix = function (x) { + return ~~(x * (1 << CONST_BITS) + 0.5); +}; + +var fixed_dequant = function (scaled_quant_table, buf, i) { + // N.B.: The data in buf is unscaled; the data in scaled_quant_table has been scaled by 1<<16. + return fixed_mul(scaled_quant_table[i], buf[i]); +}; + +var descale = function (x, n) { + // console.log([x, x>>n, (x>>n)+128]); + return x >> n; +}; + +// if not accurate rounding mode... +var idescale = descale; +// else use better implementation + +// @KK: This isn't a libjpeg parameter; it just means that ATEN downshifts all the way back to "normal ints" at the end of pass 1. +var END_PASS1_DESCALE_BITS = CONST_BITS; + +var fixed_mul = function (a, b) { + return descale(a * b, CONST_BITS); +}; + + +(function () { + "use strict"; + + AST2100IDCT = { + + // Uses the 16/16 integer representation that ATEN and libjpeg's jidctfst.c ("fast, not-so-accurate integer + // IDCT") do. Note that, for performance, this routine *also* incorporates the dequantization step, which is + // why it takes scaled_quant_table as an argument. (This argument does not actually contain "just" a scaled + // quant table; some constants have been pre-multiplied into it. See the function that loads quant tables for + // more details.) + idct_fixed_aan: function (scaled_quant_table, buf, dstBuf) { + + // ATEN rounds things off early, at a cost to precision: the int32 values in 'workspace' are not scaled at all. + var workspace = new Int32Array(64); + + for (var x = 0; x < 8; ++x) + AST2100IDCT._aan_idct_col(scaled_quant_table, buf, workspace, x); + + for (var y = 0; y < 8; ++y) + AST2100IDCT._aan_idct_row(scaled_quant_table, dstBuf, workspace, y); + + return dstBuf; + }, + + // Columns; aka "Pass 1". + _aan_idct_col: function(scaled_quant_table, buf, workspace, x) { + + var dequant = function (idx) { return fixed_dequant(scaled_quant_table, buf, idx); }; + var mul = fixed_mul; + + var y; + + var all_ac_zero = true; + for (y = 1; y < 8; ++y) { + if (buf[8 * y + x] != 0) { + all_ac_zero = false; + break; + } + } + if (all_ac_zero) { + var raw_dcval = buf[8 * 0 + x]; + var quant_val = scaled_quant_table[8 * 0 + x]; + var dcval = idescale(dequant(8 * 0 + x), END_PASS1_DESCALE_BITS); // in total, >> 16 + for (y = 0; y < 8; ++y) + workspace[8 * y + x] = dcval; + return; + } + + // Even part. + var tmp0 = dequant(8 * 0 + x); + var tmp1 = dequant(8 * 2 + x); + var tmp2 = dequant(8 * 4 + x); + var tmp3 = dequant(8 * 6 + x); + + var tmp10 = tmp0 + tmp2; // Phase 3 + var tmp11 = tmp0 - tmp2; + + var tmp13 = tmp1 + tmp3; // Phases 5-3 + var tmp12 = mul((tmp1 - tmp3), FIX_1_414213562) - tmp13; // 2 * c4 + + tmp0 = tmp10 + tmp13; + tmp3 = tmp10 - tmp13; + tmp1 = tmp11 + tmp12; + tmp2 = tmp11 - tmp12; + + // Odd part. + var tmp4 = dequant(8 * 1 + x); + var tmp5 = dequant(8 * 3 + x); + var tmp6 = dequant(8 * 5 + x); + var tmp7 = dequant(8 * 7 + x); + + var z13 = tmp6 + tmp5; // Phase 6 + var z10 = tmp6 - tmp5; + var z11 = tmp4 + tmp7; + var z12 = tmp4 - tmp7; + + tmp7 = z11 + z13; // Phase 5 + tmp11 = mul((z11 - z13), FIX_1_414213562); // 2 * c4 + + var z5 = mul((z10 + z12), FIX_1_847759065); // 2 * c2 + tmp10 = mul(FIX_1_082392200, z12) - z5; // 2 * (c2-c6) + tmp12 = mul(-FIX_2_613125930, z10) + z5; + + tmp6 = tmp12 - tmp7; + tmp5 = tmp11 - tmp6; + tmp4 = tmp10 + tmp5; + + workspace[x + 8 * 0] = idescale(tmp0 + tmp7, END_PASS1_DESCALE_BITS); + workspace[x + 8 * 7] = idescale(tmp0 - tmp7, END_PASS1_DESCALE_BITS); + workspace[x + 8 * 1] = idescale(tmp1 + tmp6, END_PASS1_DESCALE_BITS); + workspace[x + 8 * 6] = idescale(tmp1 - tmp6, END_PASS1_DESCALE_BITS); + workspace[x + 8 * 2] = idescale(tmp2 + tmp5, END_PASS1_DESCALE_BITS); + workspace[x + 8 * 5] = idescale(tmp2 - tmp5, END_PASS1_DESCALE_BITS); + workspace[x + 8 * 4] = idescale(tmp3 + tmp4, END_PASS1_DESCALE_BITS); + workspace[x + 8 * 3] = idescale(tmp3 - tmp4, END_PASS1_DESCALE_BITS); + + }, + + // Rows; aka "Pass 2". + _aan_idct_row: function(scaled_quant_table, buf, workspace, y) { + + var wsptr = function (x) { return workspace[8 * y + x]; }; + var mul = fixed_mul; + + // Even part. + var tmp10 = wsptr(0) + wsptr(4); + var tmp11 = wsptr(0) - wsptr(4); + + var tmp13 = wsptr(2) + wsptr(6); + var tmp12 = mul((wsptr(2) - wsptr(6)), FIX_1_414213562) - tmp13; + + var tmp0 = tmp10 + tmp13; + var tmp3 = tmp10 - tmp13; + var tmp1 = tmp11 + tmp12; + var tmp2 = tmp11 - tmp12; + + // Odd part. + var z13 = wsptr(5) + wsptr(3); + var z10 = wsptr(5) - wsptr(3); + var z11 = wsptr(1) + wsptr(7); + var z12 = wsptr(1) - wsptr(7); + + var tmp7 = z11 + z13; + tmp11 = mul((z11 - z13), FIX_1_414213562); + + var z5 = mul((z10 + z12), FIX_1_847759065); // 2 * c2 + tmp10 = mul(FIX_1_082392200, z12) - z5; // 2 * (c2-c6) + tmp12 = mul(-FIX_2_613125930, z10) + z5; + + var tmp6 = tmp12 - tmp7; + var tmp5 = tmp11 - tmp6; + var tmp4 = tmp10 + tmp5; + + var set_out = function (x, val) { + // Shift right by PASS1_BITS bits to convert back to a normal int, and then by another 3 to divide by 8. + val = idescale(val, PASS1_BITS + 3); + val = range_limit(val); // This also applies the +128 offset. + buf[y * 8 + x] = val; + }; + + // Final output stage: scale down by a factor of 8 and range-limit + set_out(0, tmp0 + tmp7); + set_out(7, tmp0 - tmp7); + set_out(1, tmp1 + tmp6); + set_out(6, tmp0 - tmp6); + set_out(2, tmp2 + tmp5); + set_out(5, tmp2 - tmp5); + set_out(4, tmp3 + tmp4); + set_out(3, tmp3 - tmp4); + } + + }; + +})(); diff --git a/include/ast2100util.js b/include/ast2100util.js new file mode 100644 index 000000000..8d8852f05 --- /dev/null +++ b/include/ast2100util.js @@ -0,0 +1,286 @@ +/* (c) Copyright 2015-2016 Kevin Kelley . */ + + + +function fmt_rgb_buf (n, buf) { + var s = ""; + for (var y = 0; y < n; ++y) { + for (var x = 0; x < n; ++x) { + var offset = ((y*n)+x)*4; + s += fmt_u8(buf[offset+0]) + fmt_u8(buf[offset+1]) + fmt_u8(buf[offset+2]) + fmt_u8(buf[offset+3]) + ' '; + } + s += '\n'; + } + return s; +} + +var fmt_u8 = function (x) { + var pad = '00'; + var s = x.toString(16); + return pad.substring(0, pad.length - s.length) + s; +}; +var fmt_u16 = function (x) { + var pad = '0000'; + var s = x.toString(16); + return pad.substring(0, pad.length - s.length) + s; +}; +var fmt_u32 = function (x) { // cheesy way to ger around the sign bit + return fmt_u16(x >>> 16) + fmt_u16(x & 0xFFFF); +}; + +var fmt_s8 = function (x) { + if (x < 0) + x += (1 << 8); + return fmt_u8(x); +}; + +var fmt_s16 = function (x) { + if (x < 0) + x += (1 << 16); + return fmt_u16(x); +}; + +var fmt_s32 = function (x) { + if (x < 0) + x += (1 << 32); + return fmt_u32(x); +}; + +var fmt_array = function(f, xx) { + xx = Array.from(xx); // if xx is a typed array, the strings fmt_u8 returns will be silently coerced back to numbers + return '[' + xx.map(f).join(', ') + ']'; +}; + +/* +var hexlify_u8a = function (xx) { + xx = Array.from(xx); // if xx is a typed array, the strings fmt_u8 returns will be silently coerced back to numbers + return xx.map(fmt_u8).join(''); +}; +*/ + +var fmt_u8a = function (xx) { return fmt_array(fmt_u8, xx); }; +var fmt_u16a = function (xx) { return fmt_array(fmt_u16, xx); }; +var fmt_u32a = function (xx) { return fmt_array(fmt_u32, xx); }; +var fmt_s8a = function (xx) { return fmt_array(fmt_s8, xx); }; +var fmt_s16a = function (xx) { return fmt_array(fmt_s16, xx); }; +var fmt_s32a = function (xx) { return fmt_array(fmt_s32, xx); }; + + +var inRangeIncl = function (x, a, b) { + return (x >= a && x <= b); +}; +var inRange = function (x, a, b) { + return (x >= a && x < b); +}; + +var swap32 = function (val) { + return ((val & 0xFF) << 24) + | ((val & 0xFF00) << 8) + | ((val >> 8) & 0xFF00) + | ((val >> 24) & 0xFF); +}; +/* +var swap16 = function (val) { + return ((val & 0xFF) << 8) + | ((val >> 8) & 0xFF); +}; + */ + +var arrayEq = function (a, b) { + if (a.length != b.length) + return false; + for (var i = 0; i < a.length; ++i) + if (a[i] != b[i]) + return false; + return true; +}; + +var isEmpty = function (obj) { + for(var prop in obj) + if(obj.hasOwnProperty(prop)) + return false; + return true; +}; + +var clamp = function (x) { + x = ~~x; + if (x <= 0) + return 0; + if (x >= 255) + return 255; + return x; +}; + + +var BitStream; +var JpegHuffmanTable; + + +(function () { + "use strict"; + + BitStream = function (defaults) { + + this._interpretMarkers = false; // ATEN scan data does not treat 0xFF bytes specially + this._eoi = false; // "end-of-image" encountered? + + this._data = defaults.data; // uint8[] + if (this._data === undefined) + throw 'BitStream constructor requires argument "data"!'; + + this._nextDword = 0; // uint8 + + // Fill the readbuf and reservoir with the first two dwords in the data buffer. + this._reservoir = 0; + this._bitsInReservoir = 0; + this._refill(); + this._readbuf = this._reservoir; + this._reservoir = 0; + this._bitsInReservoir = 0; + this._refill(); + }; + + BitStream.prototype = { + getPos: function () { + return (this._nextDword * 32) - (32 + this._bitsInReservoir); + }, + skip: function (bits) { + while (bits > 0) { + var n = Math.min(32, bits); + this.read(n); + bits -= n; + } + }, + read: function (bits) { + if (bits >= 32) // due to the fact that the bitwise operators have this limitation + throw 'Number of bits must be less than 32.'; + + // this.checkInvariants(); + + var that = this; + var moveFromReservoir = function (n) { + that._readbuf = (that._readbuf << n) | (that._reservoir >>> (32 - n)); + that._reservoir <<= n; + that._bitsInReservoir -= n; + }; + + var retval = this._readbuf >>> (32 - bits); // same as this.peek(bits) + + if (bits > this._bitsInReservoir) { + bits -= this._bitsInReservoir; + moveFromReservoir(this._bitsInReservoir); + this._refill(); + } + moveFromReservoir(bits); + + return retval; + }, + _refill: function () { + // this.checkInvariants(); + + // N.B.: Must use >>> for unsigned shift; >> is sra. + if (this._bitsInReservoir != 0) + throw 'Oops: in _refill(), bitsInReservoir='+this._bitsInReservoir; + for (var i = 0; i < 4; ++i) { + var x = this._data[(4 * this._nextDword) + i]; + if (x === undefined) + throw 'BitStream overran available data!'; + // TODO: if interpretMarkers ... + this._reservoir = (this._reservoir << 8) | x; + this._bitsInReservoir += 8; + } + this._nextDword += 1; + + this._reservoir = swap32(this._reservoir); + + // this.checkInvariants(); + }, + peek: function (bits) { + if (bits >= 32) // due to the fact that the bitwise operators have this limitation + throw 'Number of bits must be less than 32.'; + + // this.checkInvariants(); + return this._readbuf >>> (32 - bits); + }, + showDebug: function () { + console.log('stream readbuf='+fmt_u32(this._readbuf)+' reservoir='+fmt_u32(this._reservoir)+' bitsLeft='+this._bitsInReservoir+'d nextDword='+fmt_u16(this._nextDword)); + }, + checkInvariants: function () { + var err = null; + + if (this._bitsInReservoir < 0 || this._bitsInReservoir > 32) + err = 'Oops: BitStream invariant violated: bitsInReservoir='+this._bitsInReservoir; + + if (err) { + console.log(err); + this.showDebug(); + throw err; + } + } + + }; + + + JpegHuffmanTable = function (defaults) { + if (!defaults) + defaults = {}; + if (!defaults.bits || !defaults.huffval) + throw 'Arguments bits, huffval are required.'; + + this._huffsize = new Uint8Array(1<<16); + this._huffval_lookup = new Uint8Array(1<<16); + this._bits = new Uint8Array(17); // 1 to 16, plus a useless 0th element to make indexing nice + + this._buildTables(defaults.bits, defaults.huffval); + }; + + JpegHuffmanTable.prototype = { + + _buildTables: function (bits, huffval) { + // First, copy BITS. + for (var i = 0; i < 17; ++i) { + this._bits[i] = bits[i]; + + // This follows from the fact that the all-ones codeword of each length is reserved as a prefix for longer + // codewords, which implies that there may not be more than (2**i)-1 codewords of length i bits. + if ((1 << i) <= bits[i]) + throw 'Oops: bad BITS.'; + } + + // Calculate Huffman codewords. The best explanation for this is a graphical one. Refer to 'Binary Tree + // Expansin of DHT' figure here: http://www.impulseadventure.com/photo/jpeg-huffman-coding.html. + var next_codeword = 0; + var codeword_qty = 0; + var codeword_idx = 0; // index into HUFFVAL + for (var code_len = 1; code_len < 17; ++code_len) { + for (var i = 0; i < this._bits[code_len]; ++i) { + for (var j = 0; j < (1 << (16 - code_len)); ++j) { + this._huffsize[next_codeword + j] = code_len; + this._huffval_lookup[next_codeword + j] = huffval[codeword_idx]; + } + next_codeword += (1 << (16 - code_len)); + codeword_idx += 1; + } + } + if (codeword_idx != huffval.length) + throw 'Oops: all codewords should be used!'; + }, + + // Reads the next code from the stream. Consumes up to 16 bits. Returns the corresponding codeword (the value + // that the code represents, which is always exactly one byte). + readCode: function (stream) { + // Some number of bits, 16 < x <= 0, on the least-significant end of this 16b value are not relevant, depoending + // on how long the code actually is. However, we've built our lookup tables so that, regardless of what value + // the irrelevant bits have, we will get the same result. + var fixedlenCode = stream.peek(16); // uint16 + + var codeLen = this._huffsize[fixedlenCode]; + stream.skip(codeLen); + var codeword = this._huffval_lookup[fixedlenCode]; + return codeword; + } + + }; + +})(); + diff --git a/include/base.css b/include/base.css index 2769357e2..5596aece4 100644 --- a/include/base.css +++ b/include/base.css @@ -113,6 +113,53 @@ html { float:right; } +/* Settings slider: currently only used by AST2100 code, but could be reused. Horribly messy, like most of the rest of the UI widgetry. */ +#noVNC_settings_menu .slider-end, +#noVNC_settings_menu .slider-front { + display: inline-block; + vertical-align: top; + font-size: 0.8em; +} +#noVNC_settings_menu .slider-bar { + margin: 0 10px; +} +#noVNC_settings_menu .slider-area { + margin-top: 2px; /* Just to keep the slider from getting too close to the label above. */ +} +#noVNC_settings_menu .slider-front { + text-align: right; +} +#noVNC_settings_menu .slider-label { + width: 110px; + display: inline-block; + vertical-align: top; +} + +/* Settings slider: video quality (quantization table selector) in AST2100 mode. */ +#noVNC_settings_menu #noVNC_ast2100_quality { + height: 20px; + position: relative; + cursor: pointer; + display: inline-block; +} +#noVNC_settings_menu #noVNC_ast2100_quality:before { + content: ""; + display: block; + position: absolute; + top: 9px; + left: 0; + width: 100%; + height: 2px; + background-color: black; +} +#noVNC_settings_menu #noVNC_ast2100_quality span { + display: block; + height: inherit; + position: relative; + z-index: 2; + background-color: gray; +} + /* Do not set width/height for VNC_screen or VNC_canvas or incorrect * scaling will occur. Canvas resizes to remote VNC settings */ #noVNC_screen { diff --git a/include/base64.js b/include/base64.js index 651fbadc9..7011fc1c2 100644 --- a/include/base64.js +++ b/include/base64.js @@ -103,7 +103,7 @@ var Base64 = { // If there are any bits left, the base64 string was corrupted if (leftbits) { - err = new Error('Corrupted base64 string'); + var err = new Error('Corrupted base64 string'); err.name = 'Base64-Error'; throw err; } diff --git a/include/black.css b/include/black.css index 7d940c5af..aa92ce393 100644 --- a/include/black.css +++ b/include/black.css @@ -69,3 +69,7 @@ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#9dd53a', endColorstr='#7cbc0a',GradientType=0 ); /* IE6-9 */ background: linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* W3C */ } + +#noVNC_settings_menu .ast2100-quality-slider:before { + background-color: white; +} diff --git a/include/blue.css b/include/blue.css index b2a0adcc9..b3bbadc77 100644 --- a/include/blue.css +++ b/include/blue.css @@ -62,3 +62,6 @@ background-color:#04073d; } +#noVNC_settings_menu .ast2100-quality-slider:before { + background-color:white; +} diff --git a/include/display.js b/include/display.js index a492817dd..47a466944 100644 --- a/include/display.js +++ b/include/display.js @@ -15,6 +15,9 @@ var Display; (function () { "use strict"; + // Should be false only for PhantomJS 1.x. + var SUPPORTS_UINT8CLAMPEDARRAY = (window.Uint8ClampedArray !== undefined); + var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; try { new ImageData(new Uint8ClampedArray(1), 1, 1); @@ -469,13 +472,16 @@ var Display; // else: No-op -- already done by setSubTile }, - blitImage: function (x, y, width, height, arr, offset, from_queue) { + // NB(kelleyk): You *cannot* call this during a test in RGBX-order true-color mode, or PhantomJS dies with a Uint8ClampeddArray error. + // @KK: If you know that your data will always be RGB (that is, isRgb==true and this._true_color==true), then + // you should be calling blitRgbxImage() instead of blitImage() to avoid the extra branches. + blitImage: function (x, y, width, height, arr, offset, isRgb, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { // NB(directxman12): it's technically more performant here to use preallocated arrays, // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, // this probably isn't getting called *nearly* as much var new_arr = new Uint8Array(width * height * 4); - new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); // @KK: Why are we allocating *two* new arrays here? this.renderQ_push({ 'type': 'blit', 'data': new_arr, @@ -483,9 +489,14 @@ var Display; 'y': y, 'width': width, 'height': height, + 'isRgb': isRgb, // @KK: ??? }); } else if (this._true_color) { - this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + if (isRgb) { + this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } } else { this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); } @@ -505,15 +516,21 @@ var Display; 'y': y, 'width': width, 'height': height, + 'isRgb': isRgb, }); } else if (this._true_color) { - this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + if (isRgb) { + this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } } else { - // probably wrong? this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); } }, + // this is different from the above in that it assumes the data is always rgbx format, instead + // of dealing with the possibility of cmap data blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { // NB(directxman12): it's technically more performant here to use preallocated arrays, @@ -714,12 +731,26 @@ var Display; _rgbxImageData: function (x, y, vx, vy, width, height, arr, offset) { // NB(directxman12): arr must be an Type Array view var img; - if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { - img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); - } else { - img = this._drawCtx.createImageData(width, height); - img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); - } + if (SUPPORTS_UINT8CLAMPEDARRAY) { + if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { + img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); + } else { + img = this._drawCtx.createImageData(width, height); + img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); + } + } else { + // NB(kelleyk): Normally, this function uses Uint8ClampedArray (which isn't implemented by PhantomJS + // 1.x); this path is here only for test-case support. A longer-term solution would be to upgrade to + // PhantomJS 2.x. + img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < (width * height * 4); i += 4, j += 4) { + data[i] = arr[j]; + data[i+1] = arr[j+1]; + data[i+2] = arr[j+2]; + data[i+3] = arr[j+3]; + } + } this._drawCtx.putImageData(img, x - vx, y - vy); }, @@ -749,10 +780,7 @@ var Display; this.fillRect(a.x, a.y, a.width, a.height, a.color, true); break; case 'blit': - this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'blitRgb': - this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, a.rgb, true); break; case 'blitRgbx': this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); diff --git a/include/keysym.js b/include/keysym.js index 58b107c05..2cc268896 100644 --- a/include/keysym.js +++ b/include/keysym.js @@ -376,3 +376,80 @@ XK_udiaeresis = 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIA XK_yacute = 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ XK_thorn = 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ XK_ydiaeresis = 0x00ff; /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ + +var XK2HID = {}; + +// F{1..12} +for (var i = XK_F1; i <= XK_F12; i++) { + XK2HID[i] = 0x3a + (i - XK_F1); +} +// A-Za-z +for (var i = XK_A; i <= XK_Z; i++) { + XK2HID[i] = 0x04 + (i - XK_A); + XK2HID[i + (XK_a - XK_A)] = 0x04 + (i - XK_A); +} +// 1-9 +for (var i = XK_1; i <= XK_9; i++) { + XK2HID[i] = 0x1e + (i - XK_1); +} + +XK2HID[XK_0] = 0x27; +XK2HID[XK_Return] = 0x28; +XK2HID[XK_Escape] = 0x29; +XK2HID[XK_BackSpace] = 0x2a; +XK2HID[XK_Tab] = 0x2b; +XK2HID[XK_space] = 0x2c; +XK2HID[XK_minus] = 0x2d; +XK2HID[XK_equal] = 0x2e; +XK2HID[XK_bracketleft] = 0x2f; +XK2HID[XK_bracketright] = 0x30; +XK2HID[XK_backslash] = 0x31; +XK2HID[XK_semicolon] = 0x33; +XK2HID[XK_apostrophe] = 0x34; +XK2HID[XK_grave] = 0x35; +XK2HID[XK_comma] = 0x36; +XK2HID[XK_period] = 0x37; +XK2HID[XK_slash] = 0x38; + +XK2HID[XK_Print] = 0x46; +XK2HID[XK_Scroll_Lock] = 0x47; +XK2HID[XK_Pause] = 0x48; +XK2HID[XK_Insert] = 0x49; +XK2HID[XK_Home] = 0x4a; +XK2HID[XK_Page_Up] = 0x4b; +XK2HID[XK_Delete] = 0x4c; +XK2HID[XK_End] = 0x4d; +XK2HID[XK_Page_Down] = 0x4e; +XK2HID[XK_Right] = 0x4f; +XK2HID[XK_Left] = 0x50; +XK2HID[XK_Down] = 0x51; +XK2HID[XK_Up] = 0x52; + +XK2HID[XK_Control_L] = 0xe0; +XK2HID[XK_Control_R] = XK2HID[XK_Control_L]; +XK2HID[XK_Shift_L] = 0xe1; +XK2HID[XK_Shift_R] = XK2HID[XK_Shift_L]; +XK2HID[XK_Alt_L] = 0xe2; +XK2HID[XK_Alt_R] = XK2HID[XK_Alt_L]; +XK2HID[XK_Super_L] = 0xe3; +XK2HID[XK_Super_R] = XK2HID[XK_Super_L]; + +XK2HID[XK_Caps_Lock] = 0x39; + +// locale hardcoded hack +XK2HID[XK_less] = XK2HID[XK_comma]; +XK2HID[XK_greater] = XK2HID[XK_period]; +XK2HID[XK_exclam] = XK2HID[XK_1]; +XK2HID[XK_at] = XK2HID[XK_2]; +XK2HID[XK_numbersign] = XK2HID[XK_3]; +XK2HID[XK_dollar] = XK2HID[XK_4]; +XK2HID[XK_percent] = XK2HID[XK_5]; +XK2HID[XK_asciicircum] = XK2HID[XK_6]; +XK2HID[XK_ampersand] = XK2HID[XK_7]; +XK2HID[XK_asterisk] = XK2HID[XK_8]; +XK2HID[XK_parenleft] = XK2HID[XK_9]; +XK2HID[XK_parenright] = XK2HID[XK_0]; +XK2HID[XK_underscore] = XK2HID[XK_minus]; +XK2HID[XK_bar] = XK2HID[XK_backslash]; +XK2HID[XK_quotedbl] = XK2HID[XK_apostrophe]; +XK2HID[XK_asciitilde] = XK2HID[XK_grave]; diff --git a/include/rfb.js b/include/rfb.js index 48fa5a8c5..bda21fb15 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -11,7 +11,7 @@ */ /*jslint white: false, browser: true */ -/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ +/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, Ast2100Decoder, arrayEq */ var RFB; @@ -33,6 +33,8 @@ var RFB; this._rfb_auth_scheme = ''; this._rfb_tightvnc = false; + this._rfb_atenikvm = false; + this._rfb_openvnc = false; this._rfb_xvp_ver = 0; // In preference order @@ -43,6 +45,14 @@ var RFB; ['HEXTILE', 0x05 ], ['RRE', 0x02 ], ['RAW', 0x00 ], + + // ATEN iKVM encodings + ['ATEN_HERMON', 0x59 ], + ['ATEN_ASTJPEG', 0x58 ], + ['ATEN_AST2100', 0x57 ], + ['ATEN_YARKON', 0x60 ], + ['ATEN_PILOT3', 0x61 ], + ['DesktopSize', -223 ], ['Cursor', -239 ], @@ -57,6 +67,16 @@ var RFB; ['ExtendedDesktopSize', -308 ] ]; + //dell uses openVNC, which doesn't send us FBU if we ask for COPYRECT, TIGHT, or TIGHT_PNG first + // during SetEncodings message to server. + this._openvnc_encodings = [ + 'HEXTILE', + 'RRE', + 'RAW', + //use all enabled pseudo encodings + 'DesktopSize', 'Cursor', 'JPEG_quality_med', 'compress_hi','last_rect','xvp','ExtendedDesktopSize' + ]; + this._encHandlers = {}; this._encNames = {}; this._encStats = {}; @@ -75,6 +95,8 @@ var RFB; subrects: 0, // RRE lines: 0, // RAW tiles: 0, // HEXTILE + aten_len: -1, // ATEN + aten_type: -1, // ATEN bytes: 0, x: 0, y: 0, @@ -86,14 +108,14 @@ var RFB; zlib: [] // TIGHT zlib streams }; - this._fb_Bpp = 4; - this._fb_depth = 3; + this._pixelFormat = {}; this._fb_width = 0; this._fb_height = 0; this._fb_name = ""; this._destBuff = null; - this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + this._paletteRawBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + this._paletteConvertedBuff = new Uint8Array(1024); // 256 * 4 (max palette size * rgbx bytes-per-pixel) this._rre_chunk_sz = 100; @@ -126,15 +148,18 @@ var RFB; 'target': 'null', // VNC display rendering Canvas object 'focusContainer': document, // DOM element that captures keyboard input 'encrypt': false, // Use TLS/SSL/wss encryption - 'true_color': true, // Request true color pixel data + 'convertColor': false, // Client will not honor request for native color 'local_cursor': false, // Request locally rendered cursor 'shared': true, // Request shared mode 'view_only': false, // Disable client mouse/keyboard + 'aten_password_sep': ':', // Separator for ATEN iKVM password fields 'xvp_password_sep': '@', // Separator for XVP password fields 'disconnectTimeout': 3, // Time (s) to wait for disconnection 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection 'repeaterID': '', // [UltraVNC] RepeaterID to connect to 'viewportDrag': false, // Move the viewport on mouse drags + 'ast2100_quality': -1, // If set, use this quality upon connection to a server using the AST2100 video encoding. Ranges from 0 (lowest) to 0xB (highest) quality. + 'ast2100_subsamplingMode': -1, // If set, use this subsampling mode upon connection to a serve rusing the AST2100 video encoding. The value may either be 444 or 422 (which is really 4:2:0 subsampling). // Callback functions 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change @@ -146,6 +171,7 @@ var RFB; 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection + 'ast2100_onVideoSettingsChanged': function () { } }); // main setup @@ -364,9 +390,12 @@ var RFB; this._FBU.lines = 0; // RAW this._FBU.tiles = 0; // HEXTILE this._FBU.zlibs = []; // TIGHT zlib encoders + this._FBU.aten_len = -1; // ATEN + this._FBU.aten_type = -1; // ATEN this._mouse_buttonMask = 0; this._mouse_arr = []; this._rfb_tightvnc = false; + this._rfb_atenikvm = false; // Clear the per connection encoding stats var i; @@ -662,6 +691,7 @@ var RFB; case "003.008": case "004.000": // Intel AMT KVM case "004.001": // RealVNC 4.6 + case "055.008": // SuperMicro KVM this._rfb_version = 3.8; break; default: @@ -687,6 +717,10 @@ var RFB; var cversion = "00" + parseInt(this._rfb_version, 10) + ".00" + ((this._rfb_version * 10) % 10); + // FIXME! We need this for some stupid SuperMicro KVMs + if (sversion == "055.008") { + cversion = sversion; + } this._sock.send_string("RFB " + cversion + "\n"); this._updateState('Security', 'Sent ProtocolVersion: ' + cversion); }, @@ -706,6 +740,7 @@ var RFB; this._rfb_auth_scheme = 0; var types = this._sock.rQshiftBytes(num_types); Util.Debug("Server security types: " + types); + this._rfb_server_supported_security_types = Array.from(types); // @KK: Deliberately copy-constructed, since the underlying array is reused. for (var i = 0; i < types.length; i++) { if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) { this._rfb_auth_scheme = types[i]; @@ -728,6 +763,37 @@ var RFB; }, // authentication + _negotiate_aten_auth: function () { + var aten_sep = this._aten_password_sep; + var aten_auth = this._rfb_password.split(aten_sep); + if (aten_auth.length < 2) { + this._updateState('password', 'ATEN iKVM credentials required (user' + aten_sep + + 'password) -- got only ' + this._rfb_password); + this._onPasswordRequired(this); + return false; + } + + this._rfb_atenikvm = true; + + if (this._rfb_tightvnc) { + // @KK: N.B.: We've already "skipped" the four bytes that we read into numTunnels. + this._rfb_tightvnc = false; + } else { + this._sock.rQskipBytes(4); + } + + this._sock.rQskipBytes(16); + + var username = aten_auth[0]; + username += new Array(24 - username.length+1).join("\x00"); + var password = aten_auth.slice(1).join(aten_sep); + password += new Array(24 - password.length+1).join("\x00"); + + this._sock.send_string(username + password); + this._updateState("SecurityResult"); + return true; + }, + _negotiate_xvp_auth: function () { var xvp_sep = this._xvp_password_sep; var xvp_auth = this._rfb_password.split(xvp_sep); @@ -768,6 +834,7 @@ var RFB; }, _negotiate_tight_tunnels: function (numTunnels) { + // @KK: For a full of known tunnel types, see: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#tight-security-type var clientSupportedTunnelTypes = { 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } }; @@ -794,9 +861,21 @@ var RFB; }, _negotiate_tight_auth: function () { + var numTunnels; // NB(directxman12): this is only in scope within the following block, + // or if equal to zero (necessary for ATEN iKVM support) if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation if (this._sock.rQwait("num tunnels", 4)) { return false; } - var numTunnels = this._sock.rQshift32(); + numTunnels = this._sock.rQshift32(); + // @KK: I can only find about a half-dozen known tunnel types, so TightVNC should be sending a + // relatively small number here, whereas the ATEN servers send four bytes that, when interpreted as a + // u32, represent a very large number. (The condition I've chosen below just checks that the first byte + // is nonzero.) Further, TightVNC servers seem to be support both 0x02 and 0x10 security types, whereas + // ATEN iKVM servers advertise only support for 0x10. (Are there *other* VNC servers that only support + // 0x10?) + if (this._rfb_version === 3.8 && arrayEq(this._rfb_server_supported_security_types, [0x10]) && (numTunnels <= 0 || numTunnels > 0x1000000)) { + Util.Info('Detected ATEN iKVM server (using heuristic #0 -- older Winbond/Nuvoton or Renesas BMC?).'); + return this._negotiate_aten_auth(); + } if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } this._rfb_tightvnc = true; @@ -812,6 +891,13 @@ var RFB; var subAuthCount = this._sock.rQshift32(); if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } + // Newer X10 Supermicro motherboards get here. @KK: If we had trouble with this heuristic matching non-ATEN + // servers, we could also add the "only security type 0x10 is supported" condition from above. + if (this._rfb_version === 3.8 && numTunnels === 0 && (subAuthCount === 0 || (subAuthCount & 0xFFFF) == 0x0100)) { + Util.Info('Detected ATEN iKVM server (using heuristic #1 -- newer AST2400 BMC?).'); + return this._negotiate_aten_auth(); + } + var clientSupportedTypes = { 'STDVNOAUTH__': 1, 'STDVVNCAUTH_': 2 @@ -898,24 +984,25 @@ var RFB; _negotiate_server_init: function () { if (this._sock.rQwait("server initialization", 24)) { return false; } + if (this._rfb_atenikvm && this._sock.rQwait("ATEN server initialization", 36)) { return false; } /* Screen size */ - this._fb_width = this._sock.rQshift16(); - this._fb_height = this._sock.rQshift16(); - this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); + this._fb_width = this._sock.rQshift16(); + this._fb_height = this._sock.rQshift16(); + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); /* PIXEL_FORMAT */ - var bpp = this._sock.rQshift8(); - var depth = this._sock.rQshift8(); - var big_endian = this._sock.rQshift8(); - var true_color = this._sock.rQshift8(); - - var red_max = this._sock.rQshift16(); - var green_max = this._sock.rQshift16(); - var blue_max = this._sock.rQshift16(); - var red_shift = this._sock.rQshift8(); - var green_shift = this._sock.rQshift8(); - var blue_shift = this._sock.rQshift8(); + this._pixelFormat.bpp = this._sock.rQshift8(); + this._pixelFormat.depth = this._sock.rQshift8(); + this._pixelFormat.big_endian = (this._sock.rQshift8() !== 0) ? true : false; + this._pixelFormat.true_color = (this._sock.rQshift8() !== 0) ? true : false; + + this._pixelFormat.red_max = this._sock.rQshift16(); + this._pixelFormat.green_max = this._sock.rQshift16(); + this._pixelFormat.blue_max = this._sock.rQshift16(); + this._pixelFormat.red_shift = this._sock.rQshift8(); + this._pixelFormat.green_shift = this._sock.rQshift8(); + this._pixelFormat.blue_shift = this._sock.rQshift8(); this._sock.rQskipBytes(3); // padding // NB(directxman12): we don't want to call any callbacks or print messages until @@ -926,6 +1013,14 @@ var RFB; if (this._sock.rQwait('server init name', name_length, 24)) { return false; } this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); + if (this._rfb_atenikvm) { + this._sock.rQskipBytes(8); // unknown + this._sock.rQskip8(); // IKVMVideoEnable + this._sock.rQskip8(); // IKVMKMEnable + this._sock.rQskip8(); // IKVMKickEnable + this._sock.rQskip8(); // VUSBEnable + } + if (this._rfb_tightvnc) { if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } // In TightVNC mode, ServerInit message is extended @@ -953,52 +1048,144 @@ var RFB; // NB(directxman12): these are down here so that we don't run them multiple times // if we backtrack Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + - ", bpp: " + bpp + ", depth: " + depth + - ", big_endian: " + big_endian + - ", true_color: " + true_color + - ", red_max: " + red_max + - ", green_max: " + green_max + - ", blue_max: " + blue_max + - ", red_shift: " + red_shift + - ", green_shift: " + green_shift + - ", blue_shift: " + blue_shift); - - if (big_endian !== 0) { - Util.Warn("Server native endian is not little endian"); - } - - if (red_shift !== 16) { - Util.Warn("Server native red-shift is not 16"); - } - - if (blue_shift !== 0) { - Util.Warn("Server native blue-shift is not 0"); - } + ", bpp: " + this._pixelFormat.bpp + ", depth: " + this._pixelFormat.depth + + ", big_endian: " + this._pixelFormat.big_endian + + ", true_color: " + this._pixelFormat.true_color + + ", red_max: " + this._pixelFormat.red_max + + ", green_max: " + this._pixelFormat.green_max + + ", blue_max: " + this._pixelFormat.blue_max + + ", red_shift: " + this._pixelFormat.red_shift + + ", green_shift: " + this._pixelFormat.green_shift + + ", blue_shift: " + this._pixelFormat.blue_shift); // we're past the point where we could backtrack, so it's safe to call this this._onDesktopName(this, this._fb_name); - if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { - Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); - this._true_color = false; - } - - this._display.set_true_color(this._true_color); + if (this._fb_name === "Intel(r) AMT KVM") { + Util.Warn("Intel AMT KVM only supports 8/16 bit depths, using server pixel format"); + this._convertColor = true; + } + else if(this._fb_name === "OpenVNC") { + Util.Warn("OpenVNC will not respond to TIGHT or COPYRECT encodings; will not be sent to server."); + this._rfb_openvnc = true; + } + + // ATEN 'wisdom' from chicken-aten-ikvm:lens/lens.rb + // tested against the following Supermicro motherboards + // (use 'dmidecode -s baseboard-product-name' for model): + // - X7SPA-F + // - X8DTL + // - X8SIE-F + // - X9SCL/X9SCM + // - X9SCM-F + // - X9DRD-iF + // - X9SRE/X9SRE-3F/X9SRi/X9SRi-3F + // - X9DRL-3F/X9DRL-6F + // - X10SLD + // + // Supported using the ATEN "AST2100" encoding (0x57 / 87): + // - X10SL7-F + // - X10SLD-F + // - X10SLM-F + // - X10SLE + // + // Simply does not work: + // Hermon (WPMC450) [hangs at login]: + // - X7SB3-F + // - X8DTU-F + // - X8STi-3F + // Peppercon (Raritan/Kira100) [connection refused]: + // - X7SBi + // + // Thanks to Brian Rak and Erik Smit for testing + if (this._rfb_atenikvm) { + // we do not know the resolution till the first fbupdate so go large + // although, not necessary, saves a pointless full screen refresh + this._fb_width = RFB.ATEN_INIT_WIDTH; + this._fb_height = RFB.ATEN_INIT_HEIGHT; + + // TODO: @KK: This message (and this block of code) is part of the original ATEN "HERMON" (0x59) + // support. The "AST2100" (0x57) encoding delivers RGB888 color. I suppose that we should update this + // somehow? What effect does it have? + // lies about what it supports + Util.Warn("ATEN iKVM lies and only does 15 bit depth with RGB555"); + this._convertColor = true; + this._pixelFormat.bpp = 16; + this._pixelFormat.depth = 15; + this._pixelFormat.red_max = (1 << 5) - 1; + this._pixelFormat.green_max = (1 << 5) - 1; + this._pixelFormat.blue_max = (1 << 5) - 1; + this._pixelFormat.red_shift = 10; + this._pixelFormat.green_shift = 5; + this._pixelFormat.blue_shift = 0; + + // changes to protocol format + RFB.messages.keyEvent = RFB.messages.atenKeyEvent; + RFB.messages.pointerEvent = RFB.messages.atenPointerEvent; + } + + if (this._convertColor) + this._display.set_true_color(this._pixelFormat.true_color); this._display.resize(this._fb_width, this._fb_height); this._onFBResize(this, this._fb_width, this._fb_height); this._keyboard.grab(); this._mouse.grab(); - if (this._true_color) { - this._fb_Bpp = 4; - this._fb_depth = 3; - } else { - this._fb_Bpp = 1; - this._fb_depth = 1; + // only send if not native, and we think the server will honor the conversion + if (!this._convertColor) { + if (this._pixelFormat.big_endian !== false || + this._pixelFormat.red_max !== 255 || + this._pixelFormat.green_max !== 255 || + this._pixelFormat.blue_max !== 255 || + this._pixelFormat.red_shift !== 16 || + this._pixelFormat.green_shift !== 8 || + this._pixelFormat.blue_shift !== 0 || + !(this._pixelFormat.bpp === 32 && + this._pixelFormat.depth === 24 && + this._pixelFormat.true_color === true) || + !(this._pixelFormat.bpp === 8 && + this._pixelFormat.depth === 8 && + this._pixelFormat.true_color === false)) { + this._pixelFormat.big_endian = false; + this._pixelFormat.red_max = 255; + this._pixelFormat.green_max = 255; + this._pixelFormat.blue_max = 255; + this._pixelFormat.red_shift = 16; + this._pixelFormat.green_shift = 8; + this._pixelFormat.blue_shift = 0; + if (this._pixelFormat.true_color) { + this._pixelFormat.bpp = 32; + this._pixelFormat.depth = 24; + } else { + this._pixelFormat.bpp = 8; + this._pixelFormat.depth = 8; + } + RFB.messages.pixelFormat(this._sock, this._pixelFormat); + } else { + Util.Warn("Server pixel format matches our preferred native, disabling color conversion"); + this._convertColor = false; + } + } + + this._pixelFormat.Bpp = this._pixelFormat.bpp / 8; + this._pixelFormat.Bdepth = Math.ceil(this._pixelFormat.depth / 8); + + if (this._pixelFormat.bpp < this._pixelFormat.depth) { + return this._fail('server claims greater depth than bpp'); + } + + var max_depth = Math.ceil(Math.log(this._pixelFormat.red_max)/Math.LN2) + + Math.ceil(Math.log(this._pixelFormat.green_max)/Math.LN2) + + Math.ceil(Math.log(this._pixelFormat.blue_max)/Math.LN2); + + if (this._pixelFormat.true_color && this._pixelFormat.depth > max_depth) { + return this._fail('server claims greater depth than sum of RGB maximums'); } - RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); - RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); + RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._pixelFormat._true_color, + //If using openVNC, use the encodings that the server likes as a filter; else all allowed (empty filter) + this._rfb_openvnc ? this._openvnc_encodings : [] + ); RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); @@ -1101,6 +1288,42 @@ var RFB; msg_type = this._sock.rQshift8(); } + if (this._rfb_atenikvm) { + switch (msg_type) { + case 4: // Front Ground Event + Util.Debug("ATEN iKVM Front Ground Event"); + this._sock.rQskipBytes(20); + return true; + + case 22: // Keep Alive Event + Util.Debug("ATEN iKVM Keep Alive Event"); + this._sock.rQskipBytes(1); + return true; + + case 51: // Video Get Info + Util.Debug("ATEN iKVM Video Get Info"); + this._sock.rQskipBytes(4); + return true; + + case 55: // Mouse Get Info + Util.Debug("ATEN iKVM Mouse Get Info"); + this._sock.rQskipBytes(2); + return true; + + case 57: // Session Message + Util.Debug("ATEN iKVM Session Message"); + this._sock.rQskipBytes(4); // u32 + this._sock.rQskipBytes(4); // u32 + this._sock.rQskipBytes(256); + return true; + + case 60: // Get Viewer Lang + Util.Debug("ATEN iKVM Get Viewer Lang"); + this._sock.rQskipBytes(8); + return true; + } + } + switch (msg_type) { case 0: // FramebufferUpdate var ret = this._framebufferUpdate(); @@ -1174,11 +1397,20 @@ var RFB; this._FBU.encoding); return false; } + + // @KK: We should probably modify this condition so that this can only happen when we are using the + // ATEN_HERMON encoding, and not other ATEN encodings. -- + // ATEN uses 0x00 even when it is meant to be 0x59 + if (this._rfb_atenikvm && this._FBU.encoding === 0x00) { + this._FBU.encoding = 0x59; + } } this._timing.last_fbu = (new Date()).getTime(); + // @KK: This is where the encoding handler is invoked. var handler = this._encHandlers[this._FBU.encoding]; + /* try { //ret = this._encHandlers[this._FBU.encoding](); ret = handler(); @@ -1186,6 +1418,8 @@ var RFB; console.log("missed " + this._FBU.encoding + ": " + handler); ret = this._encHandlers[this._FBU.encoding](); } + */ + ret = handler(); now = (new Date()).getTime(); this._timing.cur_fbu += (now - this._timing.last_fbu); @@ -1232,21 +1466,107 @@ var RFB; return true; // We finished this FBU }, + + // like _convert_color, but always outputs bgr, and for only one pixel + _convert_one_color: function (arr, offset, Bpp) { + if (Bpp === undefined) { + Bpp = this._pixelFormat.Bpp; + } + + if (offset === undefined) { + offset = 0; + } + + if (!this._convertColor || + // HACK? Xtightvnc needs this and I have no idea why + (this._FBU.encoding === 0x07 && this._pixelFormat.depth === 24)) { + if (Bpp === 4) { + return [arr[offset + 0], arr[offset + 1], arr[offset + 2], arr[offset + 3]]; + } else if (Bpp === 3) { + return [arr[offset + 2], arr[offset + 1], arr[offset + 0]]; + } else { + Util.Error('convert color disabled, but Bpp is not 3 or 4!'); + } + } + + var bgr = new Array(3); + + var redMult = 256/(this._pixelFormat.red_max + 1); + var greenMult = 256/(this._pixelFormat.red_max + 1); + var blueMult = 256/(this._pixelFormat.blue_max + 1); + + var pix = 0; + for (var k = 0; k < Bpp; k++) { + if (this._pixelFormat.big_endian) { + pix = (pix << 8) | arr[k + offset]; + } else { + pix = (arr[k + offset] << (k*8)) | pix; + } + } + + bgr[2] = ((pix >>> this._pixelFormat.red_shift) & this._pixelFormat.red_max) * redMult; + bgr[1] = ((pix >>> this._pixelFormat.green_shift) & this._pixelFormat.green_max) * greenMult; + bgr[0] = ((pix >>> this._pixelFormat.blue_shift) & this._pixelFormat.blue_max) * blueMult; + + return bgr; + }, + + // takes a byte stream in the pixel format, and outputs rgbx into the output buffer + _convert_color: function (out_arr, in_arr, Bpp) { + if (Bpp === undefined) { + Bpp = this._pixelFormat.Bpp; + } + + if (!this._convertColor || + // HACK? Xtightvnc needs this and I have no idea why + (this._FBU.encoding === 0x07 && this._pixelFormat.depth === 24)) { + if (Bpp !== 4 && Bpp !== 3) { + Util.Error('convert color disabled, but Bpp is not 3 or 4!'); + } else { + out_arr.set(in_arr); + return; + } + } + + var redMult = 256/(this._pixelFormat.red_max + 1); + var greenMult = 256/(this._pixelFormat.red_max + 1); + var blueMult = 256/(this._pixelFormat.blue_max + 1); + + for (var i = 0, j = 0; i < in_arr.length; i += Bpp, j += 4) { + var pix = 0; + + for (var k = 0; k < Bpp; k++) { + if (this._pixelFormat.big_endian) { + pix = (pix << 8) | in_arr[i + k]; + } else { + pix = (in_arr[i + k] << (k*8)) | pix; + } + } + + out_arr[j] = ((pix >>> this._pixelFormat.red_shift) & this._pixelFormat.red_max) * redMult; + out_arr[j + 1] = ((pix >>> this._pixelFormat.green_shift) & this._pixelFormat.green_max) * greenMult; + out_arr[j + 2] = ((pix >>> this._pixelFormat.blue_shift) & this._pixelFormat.blue_max) * blueMult; + out_arr[j + 3] = 255; + } + }, }; Util.make_properties(RFB, [ ['target', 'wo', 'dom'], // VNC display rendering Canvas object ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption - ['true_color', 'rw', 'bool'], // Request true color pixel data + ['convertColor', 'rw', 'bool'], // Client will not honor request for native color ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor ['shared', 'rw', 'bool'], // Request shared mode ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard + ['aten_password_sep', 'rw', 'str'], // Separator for ATEN iKVM password fields ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags + ['ast2100_quality', 'rw','int'], // 0-100, force quality mode for ATEN AST2100 server + ['ast2100_subsamplingMode', 'rw', 'int'], // Use advanced subampling fot ATEN AST2100 server // Callback functions ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change @@ -1258,6 +1578,7 @@ var RFB; ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection + ['ast2100_onVideoSettingsChanged', 'rw', 'func'], // onVideoSettingsChanged(videoSettings): AST2100 video quality settings changed in latest FBU ]); RFB.prototype.set_local_cursor = function (cursor) { @@ -1278,6 +1599,11 @@ var RFB; RFB.prototype.get_keyboard = function () { return this._keyboard; }; RFB.prototype.get_mouse = function () { return this._mouse; }; + // ATEN iKVM doesn't send the resolution until the first refresh, so start big + // to avoid a repaint + RFB.ATEN_INIT_WIDTH = 10000; + RFB.ATEN_INIT_HEIGHT = 10000; + // Class Methods RFB.messages = { keyEvent: function (sock, keysym, down) { @@ -1298,6 +1624,90 @@ var RFB; sock._sQlen += 8; }, + atenKeyEvent: function (sock, keysym, down) { + var ks = XK2HID[keysym]; + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 4; + buff[offset + 1] = 0; + buff[offset + 2] = down; + + buff[offset + 3] = 0; + buff[offset + 4] = 0; + + buff[offset + 5] = (ks >> 24); + buff[offset + 6] = (ks >> 16); + buff[offset + 7] = (ks >> 8); + buff[offset + 8] = ks; + + buff[offset + 9] = 0; + buff[offset + 10] = 0; + buff[offset + 11] = 0; + buff[offset + 12] = 0; + + buff[offset + 13] = 0; + buff[offset + 14] = 0; + buff[offset + 15] = 0; + buff[offset + 16] = 0; + + buff[offset + 17] = 0; + + sock._sQlen += 18; + }, + + atenPointerEvent: function (sock, x, y, mask) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 5; + buff[offset + 1] = 0; + buff[offset + 2] = mask; + + buff[offset + 3] = x >> 8; + buff[offset + 4] = x; + + buff[offset + 5] = y >> 8; + buff[offset + 6] = y; + + buff[offset + 7] = 0; + buff[offset + 8] = 0; + buff[offset + 9] = 0; + buff[offset + 10] = 0; + + buff[offset + 11] = 0; + buff[offset + 12] = 0; + buff[offset + 13] = 0; + buff[offset + 14] = 0; + + buff[offset + 15] = 0; + buff[offset + 16] = 0; + + buff[offset + 17] = 0; + + sock._sQlen += 18; + }, + + atenChangeVideoSettings: function (sock, lumaQt, chromaQt, subsamplingMode) { + if (!inRangeIncl(lumaQt, 0, 0xB)) + throw 'Bad value: must have 0 <= lumaQt <= 0xB'; + if (!inRangeIncl(chromaQt, 0, 0xB)) + throw 'Bad value: must have 0 <= chromaQt <= 0xB'; + if (subsamplingMode != 422 && subsamplingMode != 444) + throw 'Bad value: subsamplingMode must be one of 444, 422'; + + var buf = sock._sQ; + var offset = sock._sQlen; + + buf[offset] = 0x32; + buf[offset + 1] = lumaQt; + buf[offset + 2] = chromaQt; + buf[offset + 3] = subsamplingMode >>> 8; + buf[offset + 4] = subsamplingMode; + + sock._sQlen += 5; + }, + pointerEvent: function (sock, x, y, mask) { var buff = sock._sQ; var offset = sock._sQlen; @@ -1340,7 +1750,7 @@ var RFB; sock._sQlen += 8 + n; }, - pixelFormat: function (sock, bpp, depth, true_color) { + pixelFormat: function (sock, pf) { var buff = sock._sQ; var offset = sock._sQlen; @@ -1350,23 +1760,23 @@ var RFB; buff[offset + 2] = 0; // padding buff[offset + 3] = 0; // padding - buff[offset + 4] = bpp * 8; // bits-per-pixel - buff[offset + 5] = depth * 8; // depth - buff[offset + 6] = 0; // little-endian - buff[offset + 7] = true_color ? 1 : 0; // true-color + buff[offset + 4] = pf.bpp; // bits-per-pixel + buff[offset + 5] = pf.depth; // depth + buff[offset + 6] = pf.big_endian ? 1 : 0; // big-endian + buff[offset + 7] = pf.true_color ? 1 : 0; // true-color - buff[offset + 8] = 0; // red-max - buff[offset + 9] = 255; // red-max + buff[offset + 8] = (pf.red_max >> 8) & 0xFF; // red-max + buff[offset + 9] = pf.red_max & 0xFF; // red-max - buff[offset + 10] = 0; // green-max - buff[offset + 11] = 255; // green-max + buff[offset + 10] = (pf.green_max >> 8) & 0xFF; // green-max + buff[offset + 11] = pf.green_max & 0xFF; // green-max - buff[offset + 12] = 0; // blue-max - buff[offset + 13] = 255; // blue-max + buff[offset + 12] = (pf.blue_max >> 8) & 0xFF; // blue-max + buff[offset + 13] = (pf.blue_max) & 0xFF; // blue-max - buff[offset + 14] = 16; // red-shift - buff[offset + 15] = 8; // green-shift - buff[offset + 16] = 0; // blue-shift + buff[offset + 14] = pf.red_shift; // red-shift + buff[offset + 15] = pf.green_shift; // green-shift + buff[offset + 16] = pf.blue_shift; // blue-shift buff[offset + 17] = 0; // padding buff[offset + 18] = 0; // padding @@ -1375,7 +1785,7 @@ var RFB; sock._sQlen += 20; }, - clientEncodings: function (sock, encodings, local_cursor, true_color) { + clientEncodings: function (sock, encodings, local_cursor, true_color, filter_encodings) { var buff = sock._sQ; var offset = sock._sQlen; @@ -1388,6 +1798,9 @@ var RFB; for (i = 0; i < encodings.length; i++) { if (encodings[i][0] === "Cursor" && !local_cursor) { Util.Debug("Skipping Cursor pseudo-encoding"); + } else if(filter_encodings.length > 0 && filter_encodings.indexOf(encodings[i][0]) == -1){ + //use filter if any are present; else use all - used for OpenVNC + Util.Debug("Skipping " + encodings[i][0] + ", is not in filtered list."); } else if (encodings[i][0] === "TIGHT" && !true_color) { // TODO: remove this when we have tight+non-true-color Util.Warn("Skipping tight as it is only supported with true color"); @@ -1474,19 +1887,21 @@ var RFB; this._FBU.lines = this._FBU.height; } - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line + this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // at least a line if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); var curr_height = Math.min(this._FBU.lines, - Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); - this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, - curr_height, this._sock.get_rQ(), - this._sock.get_rQi()); - this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); + Math.floor(this._sock.rQlen() / (this._FBU.width * this._pixelFormat.Bpp))); + + // NB(directxman12): renderQ_push automatically clones the data is we have to push + // to the render queue + this._convert_color(this._destBuff, this._sock.rQshiftBytes(curr_height * this._FBU.width * this._pixelFormat.Bpp)); + this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, curr_height, this._destBuff, 0, this._convertColor || this._pixelFormat.Bpp === 3, false); + this._FBU.lines -= curr_height; if (this._FBU.lines > 0) { - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // At least another line } else { this._FBU.rects--; this._FBU.bytes = 0; @@ -1510,15 +1925,15 @@ var RFB; RRE: function () { var color; if (this._FBU.subrects === 0) { - this._FBU.bytes = 4 + this._fb_Bpp; - if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.bytes = 4 + this._pixelFormat.Bpp; + if (this._sock.rQwait("RRE", 4 + this._pixelFormat.Bpp)) { return false; } this._FBU.subrects = this._sock.rQshift32(); - color = this._sock.rQshiftBytes(this._fb_Bpp); // Background + color = this._convert_one_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)); // Background this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); } - while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { - color = this._sock.rQshiftBytes(this._fb_Bpp); + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._pixelFormat.Bpp + 8)) { + color = this._convert_one_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)); var x = this._sock.rQshift16(); var y = this._sock.rQshift16(); var width = this._sock.rQshift16(); @@ -1529,7 +1944,7 @@ var RFB; if (this._FBU.subrects > 0) { var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); - this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + this._FBU.bytes = (this._pixelFormat.Bpp + 8) * chunk; } else { this._FBU.rects--; this._FBU.bytes = 0; @@ -1569,20 +1984,20 @@ var RFB; // Figure out how much we are expecting if (subencoding & 0x01) { // Raw - this._FBU.bytes += w * h * this._fb_Bpp; + this._FBU.bytes += w * h * this._pixelFormat.Bpp; } else { if (subencoding & 0x02) { // Background - this._FBU.bytes += this._fb_Bpp; + this._FBU.bytes += this._pixelFormat.Bpp; } if (subencoding & 0x04) { // Foreground - this._FBU.bytes += this._fb_Bpp; + this._FBU.bytes += this._pixelFormat.Bpp; } if (subencoding & 0x08) { // AnySubrects this._FBU.bytes++; // Since we aren't shifting it off if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek if (subencoding & 0x10) { // SubrectsColoured - this._FBU.bytes += subrects * (this._fb_Bpp + 2); + this._FBU.bytes += subrects * (this._pixelFormat.Bpp + 2); } else { this._FBU.bytes += subrects * 2; } @@ -1602,26 +2017,19 @@ var RFB; this._display.fillRect(x, y, w, h, this._FBU.background); } } else if (this._FBU.subencoding & 0x01) { // Raw - this._display.blitImage(x, y, w, h, rQ, rQi); + // NB(directxman12): renderQ_push automatically clones the data is we have to push + // to the render queue + this._convert_color(this._destBuff, new Uint8Array(rQ.buffer, rQi, this._FBU.bytes - 1)); + this._display.blitImage(x, y, w, h, this._destBuff, 0, this._convertColor || this._pixelFormat.Bpp === 3, false); rQi += this._FBU.bytes - 1; } else { if (this._FBU.subencoding & 0x02) { // Background - if (this._fb_Bpp == 1) { - this._FBU.background = rQ[rQi]; - } else { - // fb_Bpp is 4 - this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + this._FBU.background = this._convert_one_color(rQ, rQi); + rQi += this._pixelFormat.Bpp; } if (this._FBU.subencoding & 0x04) { // Foreground - if (this._fb_Bpp == 1) { - this._FBU.foreground = rQ[rQi]; - } else { - // this._fb_Bpp is 4 - this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + this._FBU.foreground = this._convert_one_color(rQ, rQi); + rQi += this._pixelFormat.Bpp; } this._display.startTile(x, y, w, h, this._FBU.background); @@ -1632,13 +2040,8 @@ var RFB; for (var s = 0; s < subrects; s++) { var color; if (this._FBU.subencoding & 0x10) { // SubrectsColoured - if (this._fb_Bpp === 1) { - color = rQ[rQi]; - } else { - // _fb_Bpp is 4 - color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - } - rQi += this._fb_Bpp; + color = this._convert_one_color(rQ, rQi); + rQi += this._pixelFormat.Bpp; } else { color = this._FBU.foreground; } @@ -1685,7 +2088,7 @@ var RFB; }, display_tight: function (isTightPNG) { - if (this._fb_depth === 1) { + if (this._pixelFormat.Bdepth === 1) { this._fail("Tight protocol handler only implements true color mode"); } @@ -1805,7 +2208,7 @@ var RFB; var handlePalette = function () { var numColors = rQ[rQi + 2] + 1; - var paletteSize = numColors * this._fb_depth; + var paletteSize = numColors * this._pixelFormat.Bdepth; this._FBU.bytes += paletteSize; if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } @@ -1839,8 +2242,8 @@ var RFB; // Shift ctl, filter id, num colors, palette entries, and clength off this._sock.rQskipBytes(3); - //var palette = this._sock.rQshiftBytes(paletteSize); - this._sock.rQshiftTo(this._paletteBuff, paletteSize); + this._sock.rQshiftTo(this._paletteRawBuff, paletteSize); + this._convert_color(this._paletteConvertedBuff, this._paletteRawBuff, this._pixelFormat.Bdepth); this._sock.rQskipBytes(cl_header); if (raw) { @@ -1852,10 +2255,10 @@ var RFB; // Convert indexed (palette based) image data to RGB var rgbx; if (numColors == 2) { - rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); + rgbx = indexedToRGBX2Color(data, this._paletteConvertedBuff, this._FBU.width, this._FBU.height); this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); } else { - rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); + rgbx = indexedToRGBX(data, this._paletteConvertedBuff, this._FBU.width, this._FBU.height); this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); } @@ -1865,7 +2268,7 @@ var RFB; var handleCopy = function () { var raw = false; - var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + var uncompressedSize = this._FBU.width * this._FBU.height * this._pixelFormat.Bdepth; if (uncompressedSize < 12) { raw = true; cl_header = 0; @@ -1898,7 +2301,8 @@ var RFB; data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); } - this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); + this._convert_color(this._destBuff, data, this._pixelFormat.Bdepth); + this._display.blitImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, this._destBuff, 0, this._convertColor || this._pixelFormat.Bpp === 3, false); return true; }.bind(this); @@ -1926,7 +2330,7 @@ var RFB; switch (cmode) { // fill use fb_depth because TPIXELs drop the padding byte case "fill": // TPIXEL - this._FBU.bytes += this._fb_depth; + this._FBU.bytes += this._pixelFormat.Bdepth; break; case "jpeg": // max clength this._FBU.bytes += 3; @@ -1947,8 +2351,9 @@ var RFB; switch (cmode) { case "fill": // skip ctl byte - this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); - this._sock.rQskipBytes(4); + var color = this._convert_one_color(rQ, rQi + 1, this._pixelFormat.Bdepth); + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color, false); + this._sock.rQskipBytes(this._pixelFormat.Bdepth + 1); break; case "png": case "jpeg": @@ -2099,7 +2504,7 @@ var RFB; var w = this._FBU.width; var h = this._FBU.height; - var pixelslength = w * h * this._fb_Bpp; + var pixelslength = w * h * this._pixelFormat.Bpp; var masklength = Math.floor((w + 7) / 8) * h; this._FBU.bytes = pixelslength + masklength; @@ -2122,6 +2527,173 @@ var RFB; compress_lo: function () { Util.Error("Server sent compress level pseudo-encoding"); + }, + + ATEN_HERMON: function () { + if (this._FBU.aten_len === -1) { + this._FBU.bytes = 8; + if (this._sock.rQwait("ATEN_HERMON", this._FBU.bytes)) { return false; } + this._FBU.bytes = 0; + this._sock.rQskipBytes(4); // @KK: This is the "mysteryFlag". + this._FBU.aten_len = this._sock.rQshift32(); + + if (this._FBU.width === 64896 && this._FBU.height === 65056) { + Util.Info("ATEN iKVM screen is probably off"); + if (this._FBU.aten_len !== 10 && this._FBU.aten_len !== 0) { + Util.Debug(">> ATEN iKVM screen off (aten_len="+this._FBU.aten_len+")"); + this._fail('expected aten_len to be 10 when screen is off'); + } + this._FBU.aten_len = 0; + return true; + } + if (this._fb_width !== this._FBU.width && this._fb_height !== this._FBU.height) { + Util.Debug(">> ATEN_HERMON resize desktop"); + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._onFBResize(this, this._fb_width, this._fb_height); + this._display.resize(this._fb_width, this._fb_height); + Util.Debug("<< ATEN_HERMON resize desktop"); + } + } + + if (this._FBU.aten_type === -1) { + this._FBU.bytes = 10; + if (this._sock.rQwait("ATEN_HERMON", this._FBU.bytes)) { return false; } + this._FBU.bytes = 0; + this._FBU.aten_type = this._sock.rQshift8(); + this._sock.rQskip8(); + + this._sock.rQskipBytes(4); // number of subrects + if (this._FBU.aten_len !== this._sock.rQshift32()) { + return this._fail('ATEN_HERMON RAW len mis-match'); + } + this._FBU.aten_len -= 10; + } + + while (this._FBU.aten_len > 0) { + switch (this._FBU.aten_type) { + case 0: // Subrects + this._FBU.bytes = 6 + (16 * 16 * this._pixelFormat.Bpp); // at least a subrect + if (this._sock.rQwait("ATEN_HERMON", this._FBU.bytes)) { return false; } + var a = this._sock.rQshift16(); + var b = this._sock.rQshift16(); + var y = this._sock.rQshift8(); + var x = this._sock.rQshift8(); + this._convert_color(this._destBuff, this._sock.rQshiftBytes(this._FBU.bytes - 6)); + this._display.blitImage(x * 16, y * 16, 16, 16, this._destBuff, 0, true, false); + this._FBU.aten_len -= this._FBU.bytes; + this._FBU.bytes = 0; + break; + case 1: // RAW + var olines = (this._FBU.lines === 0) ? this._FBU.height : this._FBU.lines; + this._encHandlers.RAW(); + this._FBU.aten_len -= (olines - this._FBU.lines) * this._FBU.width * this._pixelFormat.Bpp; + if (this._FBU.bytes > 0) return false; + break; + default: + return this._fail('unknown ATEN_HERMON type: '+this._FBU.aten_type); + } + } + + if (this._FBU.aten_len < 0) { + this._fail('aten_len dropped below zero'); + } + + if (this._FBU.aten_type === 0) { + this._FBU.rects--; + } + + this._FBU.aten_len = -1; + this._FBU.aten_type = -1; + + return true; + }, + + ATEN_AST2100: function () { + + if (this._FBU.aten_len === -1) { + this._FBU.bytes = 8; + if (this._sock.rQwait("ATEN_AST2100", this._FBU.bytes)) { return false; } + + // @KK: I think that the mysteryFlag is 0 when in "text mode" (at the BIOS, without X started, etc.) and + // 1 when running X. Perhaps it's something to do with what mode the "video card" is being used in? + var mysteryFlag = this._sock.rQshift32(); + // if (mysteryFlag != 0) + // console.log('Nonzero mysteryFlag (='+mysteryFlag+')! When does this occur?'); + this._FBU.aten_len = this._sock.rQshift32(); + } + + // Actually read the data. + if (this._FBU.aten_len !== 0) { + this._FBU.bytes = this._FBU.aten_len; + if (this._sock.rQwait("ATEN_AST2100", this._FBU.bytes)) { return false; } + var data = this._sock.rQshiftBytes(this._FBU.aten_len); + } + + // Without this, the code in _framebufferUpdate() will keep looping instead of realizing that it's finished + // and sending out a FramebufferUpdateRequest. It's important to make sure this code is called as soon as + // we have read any data we are going to read: in particular, it must be called BEFORE we might return true + // ("we're done"). Otherwise... infinite loop! + this._FBU.rects -= 1; + if (this._FBU.rects != 0) + throw 'Unexpected number of rects in FramebufferUpdate message; should always be 1!'; + + // @KK: N.B.: It's also very important for the way that this function works that aten_len wind up -1 before + // we ever return true; otherwise we'll fail to parse the two extra, ATEN-specific header fields (see above) + // the next time we get a FramebufferUpdate message. + this._FBU.aten_len = -1; + + if (this._FBU.width === 64896 && this._FBU.height === 65056) { // I expect that these are -640 and -480 (as int16s), as in the other ATEN encodings. + Util.Debug('Ast2100Decoder: screen is off.'); + if (this._FBU.aten_len !== 0) + Util.Warn('Ast2100Decoder: warning: framebuffer dimensions indicate that screen is off but data length is nonzero.'); + return true; + } else if (this._FBU.aten_len === 0) { + // This seems to happen when the display is off (e.g. when you tell the machine to restart). + // TODO: Is there a way to tell noVNC that the display is "off"? Should we show a black screen, or otherwise indicate to the user that this is what is going on? + Util.Warn('Ast2100Decoder: warning: data length is zero, but framebuffer dimensions are not -640x-480 (which is typically given to indicate that the screen is off).'); + return true; + } + + if (!this._aten_ast2100_dec) { + var _rfb = this; + var display = this._display; + this._aten_ast2100_dec = new Ast2100Decoder({ + width: this._FBU.width, + height: this._FBU.height, + blitCallback: function (x, y, width, height, buf) { + // Last arguments here are offset, from_queue. 'from_queue' means 'should this block be + // rendered from the queue?', not 'is this block being rendered from the queue?'. It causes the + // block to be enqueued instead of being blitted right away via a call to _rgbxImageData(). + display.blitRgbxImage(x, y, width, height, buf, 0, true); + + // Last argument is offset. + // display._rgbxImageData(x, y, display._viewportLoc.x, display._viewportLoc.y, width, height, buf, 0); + }, + videoSettingsChangedCallback: function (settings) { + _rfb._ast2100_onVideoSettingsChanged(settings); + } + }); + } + + // @KK: Copied this block from ATEN_HERMON above. + if (this._fb_width !== this._FBU.width && this._fb_height !== this._FBU.height) { + Util.Debug(">> ATEN_AST2100 resize desktop"); + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._onFBResize(this, this._fb_width, this._fb_height); + this._display.resize(this._fb_width, this._fb_height); + Util.Debug("<< ATEN_AST2100 resize desktop"); + + this._aten_ast2100_dec.setSize(this._FBU.width, this._FBU.height); + } + + // // This can be useful for dumping data for testing purposes. + // console.log(hexlify_u8a(data)); + // throw 'oops'; + + this._aten_ast2100_dec.decode(data); + return true; } }; })(); diff --git a/include/ui.js b/include/ui.js index cfdedb3ae..bb0fa8a1b 100644 --- a/include/ui.js +++ b/include/ui.js @@ -19,6 +19,7 @@ var UI; window.onscriptsload = function () { UI.load(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", + "ast2100.js", "ast2100idct.js", "ast2100util.js", "ast2100const.js", "rfb.js", "keysym.js", "inflator.js"]); UI = { @@ -45,6 +46,12 @@ var UI; altDown: false, altGrDown: false, + // True if we are connected to an ATEN iKVM server speaking the AST2100 video encoding. + // This variable tracks whether the extra UI elements used to configure video settings + // for the AST2100 encoding have been shown yet; it's set on the first FramebufferUpdate + // message that we receive. + _ast2100_videoSettingsInitialized: false, + // Setup rfb object, load settings from browser storage, then call // UI.init to setup the UI/menus load: function (callback) { @@ -95,7 +102,7 @@ var UI; UI.initSetting('port', port); UI.initSetting('password', ''); UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('true_color', true); + UI.initSetting('convertColor', false); UI.initSetting('cursor', !UI.isTouchDevice); UI.initSetting('resize', 'off'); UI.initSetting('shared', true); @@ -190,7 +197,8 @@ var UI; 'onClipboard': UI.clipReceive, 'onFBUComplete': UI.FBUComplete, 'onFBResize': UI.updateViewDrag, - 'onDesktopName': UI.updateDocumentTitle}); + 'onDesktopName': UI.updateDocumentTitle, + 'ast2100_onVideoSettingsChanged': UI.ast2100_handleVideoSettingsChanged}); return true; } catch (exc) { UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); @@ -237,7 +245,7 @@ var UI; $D("noVNC_clipboard_clear_button").onclick = UI.clipClear; $D("noVNC_settings_menu").onmouseover = UI.displayBlur; - $D("noVNC_settings_menu").onmouseover = UI.displayFocus; + $D("noVNC_settings_menu").onmouseout = UI.displayFocus; $D("noVNC_apply").onclick = UI.settingsApply; $D("noVNC_connect_button").onclick = UI.connect; @@ -547,7 +555,7 @@ var UI; UI.closeSettingsMenu(); } else { UI.updateSetting('encrypt'); - UI.updateSetting('true_color'); + UI.updateSetting('convertColor'); if (Util.browserSupportsCursorURIs()) { UI.updateSetting('cursor'); } else { @@ -599,7 +607,7 @@ var UI; settingsApply: function() { //Util.Debug(">> settingsApply"); UI.saveSetting('encrypt'); - UI.saveSetting('true_color'); + UI.saveSetting('convertColor'); if (Util.browserSupportsCursorURIs()) { UI.saveSetting('cursor'); } @@ -618,6 +626,12 @@ var UI; UI.saveSetting('stylesheet'); UI.saveSetting('logging'); + if (UI._ast2100_videoSettingsInitialized) { + var videoSettings = UI.ast2100_getConfiguredSettings(); + if (videoSettings != UI._ast2100_serverVideoSettings) + atenChangeVideoSettings(videoSettings.quantTableSelectorLuma, videoSettings.quantTableSelectorChroma, videoSettings.subsamplingMode); + } + // Settings with immediate (non-connected related) effect WebUtil.selectStylesheet(UI.getSetting('stylesheet')); WebUtil.init_logging(UI.getSetting('logging')); @@ -626,8 +640,6 @@ var UI; //Util.Debug("<< settingsApply"); }, - - setPassword: function() { UI.rfb.sendPassword($D('noVNC_password').value); //Reset connect button. @@ -688,6 +700,7 @@ var UI; case 'disconnected': $D('noVNC_logo').style.display = "block"; $D('noVNC_container').style.display = "none"; + UI.ast2100_reset(); /* falls through */ case 'loaded': klass = "noVNC_status_normal"; @@ -720,7 +733,7 @@ var UI; //Util.Debug(">> updateVisualState"); $D('noVNC_encrypt').disabled = connected; - $D('noVNC_true_color').disabled = connected; + $D('noVNC_convertColor').disabled = connected; if (Util.browserSupportsCursorURIs()) { $D('noVNC_cursor').disabled = connected; } else { @@ -805,6 +818,153 @@ var UI; document.title = name + " - noVNC"; }, + ast2100_handleVideoSettingsChanged: function (settings) { + if (!UI._ast2100_videoSettingsInitialized) + UI.ast2100_setDefaultSettings(settings); + UI.ast2100_updateVideoSettings(settings); + }, + + // Called the first time we receive a FramebufferUpdate object. Responsible for telling the server about any configured default settings. + ast2100_setDefaultSettings: function (settings) { + // Convert the settings that noVNC has been configured to set for all AST2100 servers to the familiar videoSettings format. + var defaultQuality = parseInt(UI.ast2100_quality); + var defaultSettings = { + quantTableSelectorLuma: defaultQuality, + quantTableSelectorChroma: defaultQuality, + subsamplingMode: parseInt(UI.ast2100_subsamplingMode) + }; + + // If defaults were not given or were invalid, stick with what the server is already using. + if (!(defaultSettings.subsamplingMode == 422 || defaultSettings.subsamplingMode == 444)) + defaultSettings.subsamplingMode = settings.subsamplingMode; + if (!inRangeIncl(defaultQuality, 0x0, 0xB)) { + defaultSettings.quantTableSelectorLuma = settings.quantTableSelectorLuma; + defaultSettings.quantTableSelectorChroma = settings.quantTableSelectorChroma; + } + + if (defaultSettings != settings) + atenChangeVideoSettings(defaultSettings.quantTableSelectorLuma, defaultSettings.quantTableSelectorChroma, defaultSettings.subsamplingMode); + }, + + // Should be called at init time and after disconnects. This is sort of the opposite of _init(). + ast2100_reset: function() { + UI.ast2100_setSettingsVisible(false); + UI._ast2100_videoSettingsInitialized = false; + UI._ast2100_serverVideoSettings = undefined; + }, + + // Updates the UI to reflect values received from the server. + ast2100_updateVideoSettings: function (videoSettings) { + Util.Info("AST2100 video settings changed:"); + Util.Info(videoSettings); + + // We use this to tell if the user changed anything when they apply settings. + UI._ast2100_serverVideoSettings = videoSettings; + + // First run: tell UI to show video quality controls, now that we know we are on a machine + // that supports them, and we know their current values. + if (!UI._ast2100_videoSettingsInitialized) { + UI.slider("noVNC_ast2100_quality", {minVal: 0x0, maxVal: 0xB}); // XXX: initial value is ignored + this.ast2100_setSettingsVisible(true); + UI._ast2100_videoSettingsInitialized = true; + } + + // Average the two quant table selectors as a poor way of dealing with the fact that they can, technically, + // be different. + var quality = ~~((videoSettings.quantTableSelectorLuma + videoSettings.quantTableSelectorChroma) / 2); + $D("noVNC_ast2100_quality").setValue(quality); + + // Either 444 or 422 (which is really 4:2:0). + $D("noVNC_ast2100_subsampling").value = videoSettings.subsamplingMode; + }, + + // Returns the current state of the UI. + ast2100_getConfiguredSettings: function () { + var quality = $D("noVNC_ast2100_quality").value; + return { + quantTableSelectorLuma: quality, + quantTableSelectorChroma: quality, + subsamplingMode: parseInt($D("noVNC_ast2100_subsampling").value) + }; + }, + + ast2100_setSettingsVisible: function (visible) { + var propertyValue; + if (visible) { + propertyValue = 'block'; + Util.Info('Showing AST2100 UI.'); + } else { + Util.Info('Hiding AST2100 UI.'); + propertyValue = 'none'; + } + + $D("noVNC_ast2100_settings").style.display = propertyValue; + }, + + // Create a slider widget. Based on code borrowed from: XXX: TODO: + slider: function(id, defaults) { + var slider = $D(id), + btn = slider.children[0], + btnWidth = 8, + sldWidth = 120, + down = false, + minVal = defaults.minVal, + maxVal = defaults.maxVal, + defaultValue = defaults.value, + snapToInt = true, + snapDuringDrag = true, + rect; + + if (minVal === undefined || minVal === null) + minVal = 0; + if (maxVal === undefined || maxVal === null) + maxVal = 100; + if (defaultValue === undefined || defaultValue === null) + defaultValue = minVal; + + slider.style.width = sldWidth + 'px'; + btn.style.width = btnWidth + 'px'; + btn.style.left = -btnWidth + 'px'; + btn.style.marginLeft = (btnWidth / 2) + 'px'; + + slider.addEventListener("mousedown", function(e) { + rect = this.getBoundingClientRect(); + down = true; + updatePosition(e); // important to catch clicks that don't become drags + return false; + }); + + document.addEventListener("mousemove", function(e) { + if (down) + updatePosition(e); + }); + + document.addEventListener("mouseup", function() { + down = false; + }); + + function updatePosition(e) { + // Get value based on mouse position. + var rawValue = minVal + Math.round(((e.pageX - rect.left) / rect.width) * (maxVal - minVal)); + if (snapDuringDrag) + rawValue = ~~rawValue; + + // Clamp to range. + rawValue = Math.max(minVal, Math.min(maxVal, rawValue)); + + // Update displayed position of draggable UI element. + slider.setValue(rawValue, true); + } + + slider.setValue = function (value, internal) { + slider.value = value; + btn.style.left = (((value - minVal) / (maxVal - minVal) * sldWidth) - btnWidth) + 'px'; // XXX: @KK: check my math here + }; + + slider.setValue(defaultValue, true); // slider.value = defaults.value; + + }, + clipReceive: function(rfb, text) { Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); $D('noVNC_clipboard_text').value = text; @@ -833,7 +993,7 @@ var UI; if (!UI.initRFB()) return; UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_convertColor(UI.getSetting('convertColor')); UI.rfb.set_local_cursor(UI.getSetting('cursor')); UI.rfb.set_shared(UI.getSetting('shared')); UI.rfb.set_view_only(UI.getSetting('view_only')); @@ -862,14 +1022,12 @@ var UI; displayBlur: function() { if (!UI.rfb) return; - UI.rfb.get_keyboard().set_focused(false); UI.rfb.get_mouse().set_focused(false); }, displayFocus: function() { if (!UI.rfb) return; - UI.rfb.get_keyboard().set_focused(true); UI.rfb.get_mouse().set_focused(true); }, diff --git a/include/util.js b/include/util.js index ed0e3cdea..a3eb54b58 100644 --- a/include/util.js +++ b/include/util.js @@ -23,6 +23,12 @@ var addFunc = function (cl, name, func) { } }; +var addClassFunc = function (cl, name, func) { + if (!cl[name]) { + Object.defineProperty(cl, name, { enumerable: false, value: func }); + } +}; + addFunc(Array, 'push8', function (num) { "use strict"; this.push(num & 0xFF); @@ -42,6 +48,24 @@ addFunc(Array, 'push32', function (num) { num & 0xFF); }); +// @KK: There's probably a better way to do this, but TypedArray isn't directly accessible. +// @KK: PhantomJS 1.x does not support Uint8ClampedArray or Float64Array, so those are left out. +[Array, Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array].forEach( + function (cls) { + var thatCls = cls; + addClassFunc(cls, 'from', function (arrayLike, mapFn, thisArg) { + if (typeof(mapFn) !== 'undefined' || typeof(thisArg) !== 'undefined') + throw new Error("This version of Array.from() does not support mapFn or thisArg arguments."); + + // var result = new arrayLike.constructor(arrayLike.length); + var result = new thatCls(arrayLike.length); + for (var i = 0; i < arrayLike.length; ++i) + result[i] = arrayLike[i]; + + return result; + }); + }); + // IE does not support map (even in IE9) //This prototype is provided by the Mozilla foundation and //is distributed under the MIT license. diff --git a/karma.conf.js b/karma.conf.js index 870b8551c..fca1091c9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -122,6 +122,10 @@ module.exports = function(config) { 'include/des.js', 'include/display.js', 'include/inflator.js', + 'include/ast2100const.js', + 'include/ast2100util.js', + 'include/ast2100idct.js', + 'include/ast2100.js', 'tests/test.*.js' ], diff --git a/tests/frame4.hex b/tests/frame4.hex new file mode 100644 index 000000000..bd2d11f94 --- /dev/null +++ b/tests/frame4.hex @@ -0,0 +1 @@ +050501a6a2a89a0f8a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a020075e800a0695c064f7d6f750fdf5169aa4bb3c2dac8b2d26486760a0103cc08653889150efa4ccd381dfc835d345b568fbed5f44b7c5767c67e517ebc92733b4107700039c57f4dbe6f886f7add3b35afbb53db111b326385e146e4c361ac7755700a6743e719c81912fe5b3557f86f3b7d8ef05fed336f824ffb6d9fed3706da3a9b8dd9396bff6d80e238e7c2a3ef01d5630b9f95670bd69bb32ff5314f9a85944f96071cb72b639e18dc06d7c4f5406dd4007eafddfe1e4fff1e2cd565fbb5649e3492f949e1510624048a7c8f0119c27fab660aff6d47cf11feeb7de64db069bfedb3fdc640fb67b331bb67edbf4d561ce73c28c4f7ad6db5efae8fa7344d7e6cd553e4863cc351a50a34813d96599771c281007acf3877ba320ee01fecd2d9b27aecada65fa2bb3a33f68bf2e3e59cdb09f28003c89156ccf33d7624fcf7bfaef0df04fb1ce13edb67deb49ff6dbb3db6f0cdb74361bce73d6fe0500c5716a00bf9b6e7f8f367f0f96d7b2fddaa74f1ac9eaa4f028321202c5fcc7800c03ff4533be361d3580f29250df4d1bc1f28cbc258569a085349c71f90278360b261c8c4a2a5f6ec691ee8a427cd3d456fb3df5784a33ecc75640436ec8991555aa1c18d8638c73192712ffadf75e37b66bd535f111bf81cdcd29c4d6d59259bdb4950493e12092688b7712390000a8cf38c2a3cf71d5630b9f95670bd69bb32ff5314f9a85944f96071cb72b639e18dc06d7c4f54061d4001eef489eb7ac68272cc6b1a646d89a06d2dcd5fb81fc646080a77c43acffad998efcb71d0947f8af2b9937c13efdb6cff61b03eda7cdc6ecf6b5ff369d719cf39cb0f05f5aff1bea7f17f6ff328bfcfffadff17ff79b0380d6f69735fbb4bfda07d1bed33fc98bfde5e7f6ea3fefd3dfcc7aafb771b43f57a7fd41e27f3f3bf69b47fbf8f8dfbfbcda37bbddfd3dbe73ff0074aa794051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100a8931e00834447f8b48b17e2dab6ace8973cdae9aff9d1567104b9fb4fced9901c39782c7c8b7fd665f60f1bebe1a586d281687bb2bbbe60441a498aa074bb0fe01873368223311c37d41c40bba210df34b5d5be4f3d9ed20cfbb155d0901bf26645952a0706f658e35cc609e297e83d094c471479e9940621ba8afd4743ee95b6b971e5e77107041c07361f4301e01ea71ef0547d6effa4fe367ba6d13ef2c5f3ee98b15466b7b138e719671ef769fcb761d4002cef489e46ac6827d2c6b1a681d89a0680dcd5fbacfc64608ea77c437cabb999b7578b4253fa44d630ccbaa4c825d3ad2f8825da700ac8964b2a07b918869113b0f09fe5ff1bea7f17f6ff328bfcfffadff17ff79e0180d699bef0204603f8a52b75add4da6eae694b949de1aaae808bc0a0600482dd495e00cf3849073cd556db3fe9a9cd9e699f8f7cb1bf3b666cb4d96decbc79c61995631a3fcee3daa67835ba76c1129792fe69c9acd8b1ca9e3f59acacf904e67d9073c07c1c96569cca2d0af1ad1359df5eeb924ee94cb7c23096682397205bbe201ce4c229464e2ca900906318bcad2be3f041f4d1d357db76437db9e3dc5a48fb3fca374d8a2a17d5e5a38d187a4ef2e45d25070eba24fc271f20fc6765ff8ef0c7c77fdeb6ff3cdb2feffaaf5ff97666e7dcbe730d00aafdb66816bf836a5f1189267ac2c3dba67d1ac42de6693ccd83b14943b5cc0a27678e812979ba02ae78e9156f7ba2be6dbaf6883eda8b853ab7c39bc95817ac0824df152a7b9791b493c47acf38f6974dfbb37fd907d1bed3ffceb77de6e3f6ea3fefd3d9cb6aafbb73f3860e00c107d1474f5f6ddb0ff5e58e716b21edff28df3428aa5c54978f3662e839c993affa1b3861dbb4805931e9ab56e972bcaf9a742f4b5d4b9a8dc8231244aa7016183906e8e58adcead325e1ffff00e13f2efb77843e3efef3b5fde7d97b79d77ffbcab7333fe7f69ddf9b55eddbdd128882addcde36eaac47fff2caa28b6525cd63b16d180202cc0af2cb4772038026855487ae9290c665f0daf756f7fd1d95a6b6342bac802c2b4d6668a71030c08c50839358e15d3dd78cbde2bf3ed737c43788ee9d9ac2dda9ede10d99b1aa7023f2a130d6bb0c3885b39af30ce4bbf401001e3bf8079768b6ac8c7dabe978f9aece82fca2fc7224e7767c0fe000293cfac25d3db6f05f79b660b839fb5210f3a45946f96479c071bb32e489c16dfe335c0ff86f3b128ef05f57336f827dfb6d9fed3706da4f9b8dd9ed6bff6d3ae238e739ed48f88f7f5de1bf09f639c27db6cfbc683fedb766b7df18b7e96c369ce7acfd0b008ae3d4007e37ddfe1e6dff1e2caf65fbb54f9e3492d549e1516424048af98f0119068ab9667c21beefecab7d77453ca5696a63ab9e7a37e419f62a55a021ecb1cc8a8c130e0cd77bc6b9d6014019fac283f80de09766d4b5521bbbb9a6ad5176866bba022e2e838211a8762779013ce32409c2837800e09766fab5521b0db9a6add476866bbb022e2e518211a8ba27790183e3240976bc4d013ce0716d53ff1a5dbb6c894b499fb46456fc5865cfc82c56d60e02f33ee539603e146f284e2e785cdbd2bf46d7155be252f3272d99353f56d90f328b958f83c0bc53790e98dc07008ab6530f78a4af0ef19f993dc9d96bb349646c33d8fc42ac0a3a0c27b956f33a80107d346fd5b61d7c5feef8f416d2fe50f24d13b7ca45f58f682386a2933c79f9d281839ec98278dded758847d4a02d65e9342db5a9a65dfe23cbd20d3456ae22998da204a0e39c253f724ddc9fe992f0c27f80f07997fd3b6c1f1fffbfdafef399bdbcebce7de5dbf69f73fbba3900a80080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a00d5a10380a471193cf6bdd53d7f47a5a92dcd0a6b20cb4a9319da29040c302394e0245638e93335e376f00f76d16c593dfb56d32ff25d9d19f945f9f148ceed041ec001e415ff35f9be21bee974efd4bcee4e6d476fc88c15851b910f85b1de55c0299c0d9c67206748f86fd55de1bfedf639c27fb6cfbc093fedb77db7df1868e96c3666e7acfdb7008ae39c0a8fbe07578f2d7c579e2d586ecebed4c43c6916513e591e70dcae8c7962701b5d13d703b45103f8bc76fb7b3efd7bb05697edd79179d248e627854718901028f13d066409ffad9a2bfcb71d3e47f8aff69937c1a7fdb6cff61b03ed9dcdc6ec9cb5ff365a719cf3a210dfb7b5d5bebb3d9ed234fbb1554f901bf20c45952ad006f658665cc6090700e83de3dfe9ca38837fb04b66cbeab1b79a7e89efeaccd82fca8f97726e27c8000e20475b31cff7db91f0dfffbac27f13ec7384fb6c9f79d07eda6fcc6ebf316fd3d96c39cf59fb160014c7a801fc6ebbfd3ddafe3d585ecbf66b9f3c6924ab93c2a3c8480814f31e03320cfe17cdf8db74d400cb4b427d366d04cb30f29614a48116d270c6e50be0d92c9872302aa97db91947bb2b0af14d535bedf5d4e329cfb01f5b020db921655654a97060608f33ce659c49fcb7de78ddd8ae57d7c447ff063637a6105b574b66f5d256124c868048a22ddc49e40001a03ee30a8f3ec7578f2d7c579e2d586ecebed4c43c6916513e591e70dcae8c7962701b5c13d70386510378bc2379deb1a29db01bc79a1a626b1a487257ef07f29381019ef20db1fcb7663af0df76241ce1bfae67de04fbf6db3edb6f0cb49f361bb3dbd6fedb74c571ce73c1c27f69fc6fa8ff5fd8ffcb2ff2ffeb7fc7ffdd6f0e005ad85fd6ecd0fe6a1f47fb4eff242ff6979fdbabffbd4f7f33eabddec6d1fe5c9df60789ffffecd86f1fede3e37ffff26adfec76f7f5f8cefd00d0a9e60045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501a34e7a000f121de1d32e5e886bdbb2a25df268a7bfe6475bc611e4ee3c39674373e4e0b1f02dfe5996d93f6cad87971a4907a2edcaeefa821069242980d2ed3e8063ccd9098ec470df507300ee8a427cd3d456fb3df5784a33ecc75640436ec8991555aa1c18d8638c731927885fa2f724301d51e7a5531a86e82af61f0db957d8e6c6959cc71d10701cd87c0d05807b9e7ac053f6b9fd93fbdbec9946fbc817cebb63c65199ddc6e39c679c78dca7f1de865103b0bc23791ab1a29d481bc79a07626b1a017257efb1f293813a9ef20df1ade666df5e2d0a4ee91359c230eb9223974cb7be209668c229205b2ca91ce46318464ec1c27f96fc6fa8ff5fd8ffcb2ff2ffeb7fc7ffdd7806005a66fac2831b0de097add4b5526bbbb9a62e517686a8ba022e0183821109762779013ce3241ef0545b6effa4a7367ba67d3ef2c5feee98b1d166b7b1f3e71967548d69fc388f6b9be2d7e8da054b5c4afaa425b362c72a7bfe66b1b2e61098f741cf01f3715a5a712ab528c4b74f647d7bac4b3aa532dd0ac358a28d5c806cf98272900ba71839b1a403408e61f3b6ae8cc107d1474f5f6ddb0ff5e58e716b21edff28df3428aa5c54978f3662e839c99374951c38e992f09f7f80f09f97fd3bc21f1fff79dafef36cbdbcebbf7de5db999f73fbce3600a8f6d8a259fc0faa7d45259ae8090f6f9bf66a10b798a6f1340fc5260dd5312b9c9c3906a6e4e90ab8e2a657bced8bfab6e9da23fa682d16eadc0c6f26635db022907e57a8ec5c46d24e13eb3de3d85f36edcffe651f47fb4eff38dff6998fdbabffbd4f672fa8bdeececd1b3a00071f441f3e7db56d3fd4973bc4ad85b4fda37cd3a1a872515e3eda88a0e7244fbeea6fe0866dd30266c5a4af58a5cbf1be6ad2bd2c752d6934228f4813a9c25963e418a0972b72ab4c9784fffe0384ffbbecdf11fbf8f8cfd5f69f67ece55dffee2bdfceff9cdb777e6f56b56f774b2009b6727bdaa8b31effcb2b8b2c9695348cc5b6610908302bca2f1fc90e009a14531dba4a431a97c16adf5bddf677549ad9d2acb000b2ac3499a19d42c30033420e4e628574f55c33f48afffa5edf10df23ba776a0a77a7b6873764c6aac28dc886c258ef33e014ce68ce3390ecd207007aece01f5fa2d9b233f6ada6e3e5bb3a09f28bf2c8919cdbf33d8003a7f0e80b75f5d8c27de5d982e1e6ec4b41cc936618e593e501c7edca902706b7f8cf703de1bfed4839c27f5dcfbc09f6edb77db6df18683f6c3666b7acfdb7e98ae39ce7b623e13fff7585ff26d8e708f6d93ef3a0fdb4df98dd7e63dfa6b3d9739eb3f62c00288e5103f8dd76fb7bb4fd7bb0bc97edd73e79d2485627854791901028e63d0664182be69af185f8beb3adf6dd15f194a6a98fad7aeadc9067d8aa548186b0c7322b324e38305cef19e75b070065e90b0fe234805f9a52d74a6dede69ab644d919aeea0ab8b80c0a46a0d89de405f08c93240b0fe201805f9ae9d74a6d34e69ab652d919aeed0ab8b8440a46a0ea9de4050c8c9324d8f13605f082c7b54dfd6b74edb1252e257fd29259f363953d20b358593808ccfb95e780f953bca138bbe0716d49ff1a5d566c894bcf9fb464d6fc58653ec82c563e0e02f34ee53960711f0028db4e3de093be3ac47d66f62467afcd2690b1cd60f20bb12aea309ce459cdeb0041f4d1bc57db76f07db9e3d35a48fb43ca374ddc2a17d53fa38d188a4ef2e4e54b070e7a250be275b5d7211e5283b694a7d3b4d4a49a76f98c2c4b37d058b98a64368a12838e7396ffc835717fa64bc208ff01c2e75df6efb37d7cfcff6afbcf67f6f2ae3bf7956fda7fceede8e600a002008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa2280054870e0090c665f0daf756f7fd1d95a6b6342bac802c2b4d6668a71030c08c50839358e1a5cfd48cd8c13fd844b365f5ec5b4dbfcb777566e417e5c72339b7137b00079057fcd7e4fa86f8a6d1bd53f3b83bb51dbc213356156e443e14c67a5701a77036739e819c23e1bf557585ffb6d8e708ffd93ef326fdb4dff6dd7e63a0a6b3d9989eb3f6df00288e73293cfa1e5d3db6f05f79b660b839fb5210f3a45946f96479c071bb32e489c16d774d5c0fd1460de0f2daedeffbf4efc1595db65f45e64923989f141e614042a0c6f7189024fcb76aaef0df76fb1ce1bfdb67de049ff6db3edb6f0cb474361bb373d6fedb6ac571ce8a427cdfd456fbeef5784ad3ecc7563d436ec8331555aa4018d863997319271c00a0f78c7da72be30efec12e9a2dabc7df6afa25beab3363bf283f5ec9b99d200338801c6fc53cdf6d47c27ffeeb0aff4db0cf11edb37de640fb69bf31bbfdc6bf4d67b3e73c67ed5900501ca306f0bbedf6f768faf760792edbaf7df3a491ac4f0a8f22202150cc7b0cc830f85f34e36dd351032f2f09f5d8b4112cc3c85b5290065a48c219972f8267b360c9c1a8a4f7e5661cefae28c4344d6db5d5538fa73cc37e6c0a34e486965951a5c281813dcf38977126f1df7ae17563bb5c5d131ffd1bd8dc9b426c5d2e99d54b58493019002289b6732791030780fa8c293cfa1c5d3db6f05f79b660b839fb5210f3a45946f96479c071bb32e489c16d714d5c0f1b460de0f28ee479c48a76c26d1c6b6a88ad6920c85dbd1fca4f060678ca37c4f0df9ae9c27fdb917384ffba9f7913ecda6ffb6cbf31d07ed96ccc6e59fb6fd315c739cf070bffa5f3bfa1fe7f61ff2fbfc8ffaffd1dff77bf390068607f59b343fbab7d1eed3bfd93bcd85f7c6eaffef73efdcdaaf77a1b47fb7375d91f24feffb363bf7db48f8ffffdcbab7fb3dbddd7e33bf70040a79a00144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114051445510051140500455100401405001451004051050014450040511400144551405114058f3ae9013e4874844ebb7821ae6dcb8a75c9a39dff9a1f6d194790bbf2e49c0dcd9183c7c1b7f8675866ffb0b71e5e6a261d88b628bbeb0b40a491a4034ab7fb018e3167243812c37d43cd01bb2b0af14d535bedf5d4e329cfb01f5b020db921655654a97060608f33ce659c217e89de90c074449f974e6919a2abd87e34e45e609b1b57711e7740c17160f3351400ee7aea014fdae7f64fec6fb3671bed235f3bef8e194665761b8f739e71e0719fc6791b460dc2f28ee46ac48a76206d1c6b1f88ad6906c85dbdc4ca4f06e978ca37c4b79a9b7d7bb5283aa54f640ac3ac4b8d5c32ddf98258a20ba7806cb1a472908e611839070bff59f3bfa1fe7f61ff2fbfc8ffaffd1dff77e21900689ae90b0f6d34805fb652d74aaeede69ab844d919a0ea0ab8050c0a4624d89de405f08c937ac0536db9fd939edbec99f6fbc817fbbb63c64699ddc6ce9c679c5137a6f1e33cae6d8a5fa36b172d7129e99396cc8a1fabecf999c5ca9a4160de073c07ccc76a69c5a9d5a210df3e91f5edb32ee994c9742b0c6289367202b2e50bca412e9c61e4c4920e003986cddbba32071f441f3e7db56d3fd4973bc4ad85b4fda37cd3a1a872515e3eda88a0e7244fd25572e0a64bc27fff01c27f5df6ef087d7cfce76afbcfb3f6f2aefff7956f677fceed3bdb00a0da618b66f13ca8f6159768a2273ebc6ddaa841dc629bc6d33c169b3454c7ac7072e7189892a72be08a9b5ef1b62deadba66b8fe8a3b558a87332bc998c77c18a40f95da1b27319493b4facf78c607fd9b43ffb977d1eed3bfde37cdb673c6eaffef73e9dbda0f6ba3b346fe8001d7c107df8f4d5b6fe505fee13b716d2f58ff24d86a2ca4579f96823839e933cf8aabf811ab64d0b9b1593be62952ec7f9aa49f7b1d4b5a4d1883c224ea40a678e9163805faec8ad335d12fef80f10feefb27f47ede3e33f57db7f9eb39777fdb9af7c3bfe736edff8bd59d5bddd2d8124d8caed6aa3ce7afc2faf2cb15856d23016db862720c0ac28bf7c24390068524f75e82a0f695c06aa7d6f75dadf5169644bb3c201c8b2d26586760a0e03cc0838388915d3d573cdd32bfeeb797d437c8ee8dea92bdc9dda1fde9019ab0a37221b0a63bdce805338a039cf40b04b1f00eab1837f7e8966cbccd8b79a8f97efea27c82fca2047726eccf7000e9fc2a32fd6d5630bf595670b859bb32f07314f9a63944f96061cb72b409e18dce13fc3f585ffb623e708ff753ef326d8b4dff6d97e63a0fdb3d998ddb3f6dfa6288e739edb8e84fffcd715fe9b609f23db67fbcc81f6d37e6376fb8d7f9bce66ce79cedab300a038460de077daedefd1f4efc1f25db65ffbe64923599f141e454042a098f7189061ae986bc614e2fbceb6da7757c7539aa63fb6eaa972439e61a852051ac01ecbaccb38e1c071bd679c6f1d0094a62f3c88d1007e694a5d2bb5b69b6bda126567b8aa2be0e23028188160779217c0334e922f3c8807007e69a65d2bb5d19b6bda4a6567b8b62be0e212281881aa77921730334e9260c5db14c00b1ed736f4afd1b5c596b894fc494b66cd8f55f683cc6265e32030ef549e03e64df186e2ed82c7b525fd6b7459b1252e3d7fd29259f36395fb20b358f93808cc3895e780c77d00a06f3bf5804cfaea10f499d9939dbd369b40c63683cb2fc4aaa8c370926635af0307d147f35f6ddbc1f5e58e4f6b21ed0f28df3471aa5c54ff8f36622839c993972d1d38e8942c88d7d65e87784b0dda529f4ed352906adae532b22cdd4063e52a92d9284a0d3ace59ff23d7c4ff992e0923fc07089f77d9bfcff6f1f1feabed3f9dd9cbbbefdc57be6aff39b7a29b03800a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200531d3a00431a97c16adf5bddf677549ad9d2acb000b2ac3499a19d42c30033420e4e6285973e53336307ff6012cd96d5b16f35fd2fdfd599905f941f8ee4dc4eef011c405ef15f93eb1be29b44f74ecde1eed476f086cc5855b811f95018eb5d069cc2d9cd7906728e84ff56d715fedb609f23fc67fbcc9bf6d37edb76fb8d819bce666379ceda7f00a038cea7f0e87b75f5d8c27de5d982e1e6ec4b41cc936618e593e501c7edca902706b7df35713d471b3580cb6bb7bfedd3bf076475d97e1499278d627e52788601098119df634091f0dfaabac27fdbec7384ff6c9f79137eda6ffb6ebf31d0d3d96ccccf59fb6fab15c7392b0af17d535bedbbd4e3294db01f5bf50db921cf5654a90260608f65ce659c700380de33f49dae8c3bf807bb68b6ac1e7dabe997f9aece8cfca2fc7824e776820fe00072bd15f37cb71d09fff8af2bfc37c13e47b6cff69903eda7fdc6ecf61bff369dcd9cf39cb5660140718d1ac0efb5dbdfa3e9df83e5ba6cbff6cc9346b23f293c8a80844031ef3120c3e07fd18cb74d470dbcbc24d461d346b00d236f49401a682109675cbe0a9ecd822407a392df979b71bebba210d234b5d5554f3d9ef20cfbb12ad0901b58664595090706f63de35cc69ac47feb84d78ded73754d7cf56f60736d0ab175b864562f6225c164008824dace9d440e1c00ea33a7f0e87375f5d8c27de5d982e1e6ec4b41cc936618e593e501c7edca902706b7c735713d6d183580cb3b92e7112bda09b471aca920b6a6812077f57e2b3f1918e329df10c27f6ba60aff6d47cf11feeb7de64db069bfedb3fdc640fb67b331bb67edbf4d561ce73c1f2cfc97ccff86fafe85fdbffd22ffbff577fcdffee600a081fd65cd0fedaff679b4eff44ff2627ff3b9bdfadcfbf437a9deeb6d1fedcfd5667f90f8fecf8efdf6d13e3efff72fafffcd6e775e8fefdc01009d6a00501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551143eeaa407f820d1113aede285bab62d2bd5258f76fe6b7eb4641c41eecb93733635470e1e06dfe29f6199fdc3de7a78a9987420daa2ecae2f039146920d28ddee0738c69c90e0480cf70d3507efae28c4344d6db5d5538fa73cc37e6c0a34e486965951a5c281813dcf38977185f8257a4102d3117f5e3aa56588ae62f9d1907b816d6e5cc779dc0107c781cdd55000b8e9a9073c699fdb3fb1bfcd9e6cb48f7cecbc3b661995d96d3fce79c680c77d1ae76d183509cb3b92a9112bda81b471ac7e20b6a6182077f5102b3f19a6e329df10df6a6ef5edd5a2e9943e912b0cb32e3672c974e50b62892e9c02b2c492ca41398661e41f2cfc67ccff86fafe85fdbffd22ffbff577fcdf886700a069a62f3cb5d1007eda4a5d2bb8b69b6be212656781aa2be0173028189260779215c0334eea014fb5e7f64f7a6fb367daed235fecef8e191b65761b3b739e7146de98c68ff0b8b6297f8dae5db6c4a5a44f5a322b7eacb2e764162b6b0781791ff21c301faba515a7578b427cfa44d6b7ccbaa45325d3ad308825dac80ac8962f2a07b9708691134b3800e418346febca1d7c107df8f4d5b6fe505fee13b716d2f58ff24d86a2ca4579f96823839e933c4957c981992e09fffc0708ff77d9bf23f6f1f19fabed3fcfd9cbbbfedc57be9dff39b7ef6f03806a842d9ac5f0a0da575fa2899ef9f0b669a006718b6d1a4ff3596cd2501eb3c2c99e63604a9eae802b6e7ac5dbb6a86f9bad3da28fd662a1cec9f06632de052b02e47785cace6524ed3eb1de3381fd65d3ffec5ff679b4eff48ff36d9ff2b8bdfadcfb74f680daebeed1bca10376f041f4e3d357dbfb437db94ddc5a48d53fca37188a2a17e4e5a38d0e7a4ef2e0abfe066ad8362d6f564cfa8b55ba1ce6ab26ddc452d7924523f2883a912a9c3a468e017fb922b7cf7449f8e13f40f8bccbfe1db68f8fff5f6dff79cc5edef5e7bef2edfbcfb97de2f76655f776b70491602bb7a88d3aebf3bfbcb2c6625949c2586c1b9c8000b3a1fcf291e400a0493cd5a1ab3da47119a9f6bdd56b7f47a5932dcd0a0420cb4a9419da29380c3023e3e024564f57cf354daff8afe6f50df13ba27ba7ac70776a7c784366ae2adc886c288cf539034ee180e63c03c12e7d00abc70efefa259a2d3363df6a3f5ebeab9d20bf28801cc9b930df03387c0a8fbe58578f2dd4579e2d166ecebe1ec43c698c513e591b70dcae0379627084ff0cd715fedb8e9f23fcd7fbcc9b60d37edb67fb8d81f6ce666376ceda7f9ba338ce796f3b12fef05f57f86f827d8e6d9fed3306da4ffb8dd9ed37ff6d3a9b38e7396bcd0280e21b3580df6bb7bf47d3bf07cb75d97eed99278d647e52781401098162df634086bb62ae195188ef3bda6adf5d1e4f699afdd8aaa7c80d7986a24a1568037b2cb32ee38403c6f59e71be75005099bef0204603f8a52b75add4da6eae694b949de1aaae808bc0a0600482dd495e00cf3849bef0201e03f8a59975add4466eae692b949de1daae808b4ba06004aadd495ec0cf384982146f53002e785cdbd2bf46d7155be252f3272d99353f56d90f328b958f83c0bc53790e9836c51b8ab50b1ed794f4afd166c596b8f6fc494b65cd8f55ef83cc62e6e32030e2549e031ef70180bcedd40332e9ab43d267664f76f6da6c0219db0c2ebf10aba00ec3499bd5bc0e1f441fcd7db56d07d4973b3ead85b43fa37cd3c4a87251fd3eda88a1e7244f5eb774e0a051b2205e597b1de22d35684b7f3a4d4b43aa6997c8c8b274018d95ab4966a32837e83867fc8f5c13fc67ba248ef01f207fde65ff3cdbc7c7faafb6ff76662fefbe735ff9aafde7dc8a6e0e002800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a024f75e8000f695c06aa7d6f75dadf5169644bb3c201c8b2d26586760a0e03cc08383889155dfa4ccd8f1dfc834b345b56c6bed5f4bc7c5767417e517e3992733bbe0770007ac57f4daf6f886f11dd3b3585bb53dbc31b326355e146e44361ac7719700a6735e719c83b12fe5b5f57f86f827d8ef09fed336fda4ffb6dd9ed37066d3a9b8de7396bff0180e2389fc2a3efd6d5630bf595670b859bb32f07314f9a63944f96061cb72b409e18dc7ed7c4f51e6dd4002cafddfeb54fff1e92d565fb51649e348af949e119062404667c8f0147c27fabeb0aff6db0cf11feb37de64dfb69bfedbbfdc6404d67b3313c67edbfad561ce7ae28c4f74d6db5ef538fa734c37e6cd534e4863c5951a50a81813d96389771c20e007acfd277ba32ece01feca2d9b27af6ada65fe5bb3a33f28bf2e3919cdb093d8003c8f756ccf3df7624fce1bfaef0de04fb1cdb3edb670cb49ff61bb3db6ffedb743671ce73d69b0500c5366a00bfd76e7f8fa77f0f96eab2fdda324f1ac9fca4f028031202c5bec7800c80ff4533df361d35f2f29250854d1bc1348cbc250269a085269c71f92a78360b911c8c4a7c5f6ec6fbee8a424ad3d456563df578c833ecc7aa40436e63991555271c18d8f78c73196b12ffad115e37b6cdd535f1d5bf81cdb429c4d6e19259bd8b95049300209268387712397100a8cf9fc2a3cfd6d5630bf595670b859bb32f07314f9a63944f96061cb72b409e18dc1ed7c4f5b761d4002cef489e46ac6827d2c6b1a681d89a0680dcd5fbacfc64608ea77c4309ffad992bfcb71d3e47f8aff69937c1a7fdb6cff61b03ed9dcdc6ec9cb5ff365a719cf37fb0f05f32ff1beafa17f6fff78bfcffd6dff17ffb9b038007f697353fb4bfdae5d1bed33fc98bfdcce7f6ea71efd3dfa77aafb77fb43f579bfd41e2f83f3bf6da47fbf8fddfbfbcff37bbdd793dbe731d0074aa748ae725f411adfb8a61d085b4c138c644fa2f2bad14ff33779fdf2685015c4606ed3650c257edba59781a7a6dd216f5838eadac61daf7154ba491b89bd808b96df7db197e7d25d3daf2386e85f8e6ec6d485c138e48a7f457347482b9ca0cb90c4242e2fac0769c320e008a00a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a533c2fe98f68dda70c832ea40dc63156d27f59a1a5f89f21fbfc36690ce032ba68b7812abe6ad735c2d3d01393b6a8cf746c656dd3beaf18228dc40dc446c85dbbdfced8eb2b996e96c771f3c43767d743e29a28443aa56fa2a113745566c8bd101212cf07b6e364710050d400455194501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014070045519de279497d44eb3e621874216d308eb191fecb0a2bc5ff0cdde7b749610097d141bb0d543ac497aef8a483367f51d36e2f88d90f3c98b874d0f972378c1ccddd47c8108cc57f0c96116cc1ebde5b6d5eb71df12a3eb37f88a73561cbc8d22cd85d029779f0749fe8cfc9838bb803c76383af6d001abed361cd3a97ebadd5b0bdd168da56b5cf9364651b5f595109b85fdcf03e901e2b8e73a5fac0e7bd4e84bfaca23df22cc5e04edd27ab6cb82aba202dec00c9d69c1ed40af1ad9659df5e2d924ee913b7c230eb6823974c5bbe2096e4c229204e2ca91c936318461685f85689ac6faf7549a7f4a65b61984bb4914b902d5f100e72e1142327965400c8310c0b0fe243805f9ae9d74a6d34e69ab652d919aeed0ab8b8440a46a0ea9de4050c8c9324d89f3b05f0fe96ea03c8f73a113bb18af6b2b1148382749fac24e3aae850b7b0037843737ac1e3daa6fe35ba76d81297923f69c9acf9b1ca9e9059acac1c04e67dca73c07cad96569c5e2d0af1e91359df30eb924e974cb7c22096682329205bbea91ce4c218464e2ca10090633e9de279217d44ebb16218740a6d308e0c91fecb492bc5ffd1dde7b754610097ae41bb0d9ef055bb7d169e866b9bb445c5a0632b6e98f67dee126924c626364274dbfd769b5f5fc9bbb63c8e4421be397d1b12d7a023d229ee150d9d78ae324327839090a23eb01da28c03800a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280af1bca403a2759f4e0cba903e18c75831ff658536e27f8648f3dba49580cbe8eedd06aa30e24bd7a0d2411b1da869377cc4ec87bf4c5cba177cb91b1e8ee66ee8640846463f06cb23b6e0f5e2ad36af088e7815efd93fc4db9ab0659f6916ecd381cb3c64ba4ff42ee4c1457881e3b1e7d73600dcdfe9b0c19dcb750d6ad8de66346dabd6e749b2688dafacda04dcafb2781fc8a815c7396e7de0738f27c2df52d11ef95e6270275693553696155d90ee7680645c4e0fea16f8564b6b6faf1685a7f489ac61987549914ba65b5f104bb4e114902d96540e72310c2327427cabc9d6b7578ba453fa44ad30ccbadac825d3962f8825b9700ac8134b2a07e418869107f121002fcdf485a5361ac04d5ba96b0cd776735c5ca2ec23507505f20206054912ec4e9d0278c64bf581cf7b9d087f58457be4588ac19dba4f56d9715574415bd80192a1393da8716d53bc1a5dbbe0894b49ffb464566c5865cf9f2c56d6fc02f33ec839603e0e4b2b4ee51685f85689ac6faf7549a7f4a65b61984bb4914b902d5f100e72e1142327965400c8310c4ef1bc503ea2759f310cba903618c75848ff658595e27f86eef3dba43080cbe8a0dd06aaf8aa5dd70b4f434f4ddaa23ed0b195b54cfbbe6289341237131b2177ed7e3b63afaf64ba5b1ec7cd10df9c5d0d896ba211e994be8a864ed0579921f74148483c1fd88e93c6014051001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405005ed20140ba4fa7785d481fd163ac1806b2421b8c3f43a4ff6dd24af16574f7f9035518c0a56bd06ea08d0ef1b41b3ee9f6c35fd42edd0b62dc0d0f26733774be0423234783e51132f07af11f9b57045bbc8af7561fe26d47d8b2cfec0bf6694d651eb234277a97c0e0223cddf1d873f21b00eec074d8e06be5ba86ef6c6fb3ceb6556b352459349a5756edf3ee57d9c60f645402e31c37bcf0b9c78ae16fa93e8f7caf13b813ab682a1b4b312e48f7c94032ae8a07750b3baba535a7578b427cfa44d6b7ccbaa45325d3ad308825dac80ac8962f2a07b9708691134bbed5e418dbab4521297d22eb18665dd2e492e95617c4126d380564cb2595835c0cc3c889f810007266fac2831b0de097add4b5526bbbb9a62e517686a8ba022e0183821109762779013ce324fac0e74e4e84bfa5a23df2bdc5e04eac27ab6c2c2aba20ddec00c9b89c1ed42db629ded0ae5df0b8a5a47f8d322bb6c4b2e74f5a2b6b7eac791f6416301f078115a7f21c427caba5d6b7578ba453fa44ad30ccbadac825d3962f8825b9700ac8134b2a07e4188691785e2800d1ba4fa7065d481f8c63ac18ffb2421bf13f43a4f96dd24ac06574f76e035518d5ae6bd0a7a1277c6d519f85d8cada267d5f31e81a891ba68d90bb44bf9db1895732dd768fe3e6d76fceae2dc4355188744adf864327e888cc907b4524249eab6cc7c92000a0a80f8aa228e3288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008ae900a028a7533c2fa48f68dd560c832ea10dc63121d27f5969a5f89fbafbfc362a0ce0323568b7814687f8d20d9f74d0e12f6adaee0531fb860713971b3a5fee9191a3b9f2081982bdf88fc12b822d78c57babcdf1b6235ed967f60ffbb4266c0f599a05bd4be032119eee13ec3979700077e0786cf0b50d5dc3773ab759e772aab51ab62c1a4ddbabf67992ab6ce32b322a01f78e1bde07dc63c571b7541ff8bed789f08955b4478da518dca4fb649519574517ba851d20d29ad3834521bed522ebdbab5dd2297de9561866126de49264cb17c4835c3805c88925956a720cc3d5a210df3e91f5edb32ee994c9742b0c6289367202b2e50bca412e9c61e4c492080039867de1417c06f04b33ea5aa98ddd5cd356283bc3b55d01179741c10854bb93bc809e719204e073a700c2df527d1ef95e27702756d1553696625d90ee9380645c150fea1676146f684e2e785cdbd2bf46d7155be252f3272d99353f56d90f328b958f83c0bc53790e98bed5d28adbab4521297d22eb18665dd2e492e95617c4126d380564cb2595835c0cc3c8892f140072dda7533c2ea48f6831560c8359a10dc69f21d27f3669a5f832bafbfc812a0ce0d73568b7d013be6aa8cfc2d3656d93b6af18746cc40dd3bec85d228dced8c446996ebbdf71f3eb2b67d796c79a28c437a56f43e21374443ac8bda2a112cf5566e364101250d407b65194710045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445004551145014450145511400144501005114005045010045140050140100455100501445299e977447b4eed3864117d206e3182be9bfacd052fccf107d7e9bb4067019ddb4db4015437ce91a4f3a68a31735ed868298fdf083894bf79d2f77c3c8d1dc0d840cc1c8fcc76079c116bc5ebdd5e615db11afe233fb87785a13b6ec2ccd827d257099874ff789de9c3cb8083b703cf6f8da0680e13b1d36ac73b9ae5a0ddbdb8da66dd5fb3c4916b6f195559580fb550def0319b1e238c7aa0f7ceeeb44f85b2ada23df520ceec47db2cac6aba20bd2c20e908ccde941dd10df6a69f5edd5a2e9943e912b0cb32e3672c974e50b62892e9c02b2c492ca41398661e451886f35c8faf66a97744a9fba158659441bb964d9f205b120174e01726249e5801cc330f0203e04f8a599beadd44603ae692b759de1da6e808b4b946004aaae495ec0a0384982ddb95300cf6fa93ef07caf13e113ab688f1b4b31b848f7c92a32ae8a2e750b3b403734a7073cae6d8a5fa36b172d7129e99396cc8a1fabecf999c5ca9a4160de073c07ccc76a69c5a9d5a210df3e91f5edb32ee994c9742b0c6289367202b2e50bca412e9c61e4c4920a003986d3299e17d247b4ee2b864117d006e31810e9bfacb452fccfdd7d7e9b150670191ab4db40095fb5eb67e169e8b6495bd40c3ab6b28669df572e9146e26c6223e4b7dd6f67f9f5954c6bcbe3b814e29bb3b721714d3a229dd25ed1d009e72a33e432080989ea03db71ca3800280080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200cf4b3a005af7e914a00be923718c15c35f566883fe6788f4bf4d5a29b88cee3e6da00a03be740dda1db4d1219a76c327cc7ef88bc4a57b4197bbe1c168ee86ce866064e463b03c420b5e2ffe6af38a608857f1defd43bced095bf69966c13eadb8cc4396fb44ef121e5c84a7381e7b4e6d03c01d9d0e1b7cb95cd7f086ed6dd6d3b66aad9e248b46f8caaa7dc0fd2adbf7818c4a719ce386073ef75822fc2dd5ed91ef750677621559656329d105e93e0748c655f4a06e616fb5b4e6f66a51884a9fc8fa86599774b964ba1505b1441b4e01d9f249e52017c3307262c4b79a1c7d7bb5283aa54f640ac3ac4b8d5c32ddf98258a20ba7806cb1a472908e611839101f0240d24c5f786aa301fcb495ba56706d37d7c525cace025557c02f60503024c1ee242980679c541ff8dcd789f0b755b447bea518dc89fb64958d574517a4851d20199ad383bad736c51bd1b50b1eb894f4af4b66c59655f6fc496265cd8f30ef83cc03e6e320b4e2549e51886fb5c8faf66a97744a9fba158659441bb964d9f205b120174e01726249e5801cc33014cf0b05235af7e9c3a00be983718c15f45f566829fe67883ebf4d5a03b88ceeda6da00aafda750df034f484a42deab31d5b59dbb4ef2b06482371c3b1117297eeb73336fa4aa6dbe571dcfcf1cdd9b590b8260a914ee9db68e8041d951972af8484c47381ed38191c0014f540511465144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040251d0014fb748ae785f411adc68a61d02bb4c1383344fa2f26ad14ff46779fdf5085015cba06ed36dae8105fbbe1930e3ffc454dd2bd2066ddf060e27743e7cb30327234581e2143af17ff317945b005ab786fb521de76c42dfbccfe609fd684e6214bb3a277095c2ec2d37d8f3d270f01e00e1c870dbeb6ae6bf84ef636eb5c5bb556c39245a36965d53e4f7e956d7c404625e0ce71c3fb9f7bac38fe96ea03c8f73a113bb18af6b2b1148382749fac24e3aae850b7b0035a5a737ab528c4b74f647d7bac4b3aa532dd0ac358a28d5c806cf98272900ba71839b1a45b4d8e61bd5a14e2d227b2be61d6259d2e996e85412cd1465340b67c5239c885308c9c580f0120c7a62f3c88d1007e694a5d2bb5b69b6bda126567b8aa2be0e23028188160779217c0334e920f7cee1444f85baada23dfeb0ceec42ab2cac652a20bd27d0e908cabe941ddc29be20dcdda058f6b4afad7e8b3624b5c7bfea425b2e6c72af74166b1f3711098712acf01c4b75a5a7d7bb5283aa54f640ac3ac4b8d5c32ddf98258a20ba7806cb1a472908e611839e7850240adfb748ad085f41138c68a612f2bb4c1ff3344fadf26ad145c46779f36508501edba06ed1a7ac25716f55978adac6dd2f715838e91b861da08b94ba4db199bd825d36df7386e7e7de6ecdaf25c1385f8a7f46d4874828e480cb9573442e2b9ca769c0c42008afac0288a320ea22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a0280e008aa23ac5f392fa88d67dc530e842da601c6322fd9715568aff19bbcf6f93c2002ea383761ba874882f5df049076dfea2a6dd5e10b31f783071e9a1f3e56e19399abb8f9021188bff182c22d882d7bcb7dabc6f3be2557d66ff104f6bc29690a559b0bb042ef3e1e93ed19e93071770078ec7065fdb00357ca7c39b752ed75aab617ba2d1b4ad6a9f27c9ca36beb2a31270bfb8e17d203d561ce74bf581cf7b9d087f58457be4588ac19dba4f56d9715574415bd80192ad393da814e25b2db2bebd5a259dd2276e8561d6d1462e99b67c412cc88553409c58523926c7308c2d0af1ad1359df5eeb924ee94cb7c23096682397205bbe201ce4c229464e2ca900906318171ec48700bf34d3ae95da68cd356da5b2335cdb15707189148c40d53bc90b18192749b03e770ae0fc2dd50791ef7522776215ed6563290605e93e5948c655d1a06e6107f186e6f482c7b54dfd6b74edb1252e257fd29259f363953d20b358593808ccfb95e780f95b2dad38bd5a14e2d227b2be61d6259d2e996e85412cd1465340b67c5239c885308c9c58420120c77d3ac5f342fa88d663c530e815da601c1922fd9793568affa3bbcf6fa8c2002e5d83761b3de1ab76fa2c3c0dd636698b8a41c756dc30edfbdc25d2488c4d6c84e9b6fbed37bfbe92766d791c89427c73fa3624ae4147a453dc2b1a3af15c65864e062121457d603b4519070014005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114e279490744eb3e9d1874217d308eb162fecb0a6dc5ff0c91e7b7492b0097d1ddbb0d5461c497ae41a483363a51d36ef888d90f7f98b8742ff972373c1ccdddd0c8108c8c7f0c96476cc1ebc55b6d5e111df12adeb37f88b73561cb3ed22cd8a7029779c8749fe85dc9838bf003c763cfaf6d00b8bed361833a97eb1ad5b0bdcd68da56adcf9364d11b5f59b509b85f65f03e90512b8e73dcfac0e71e4e84bfa5a23df2bdc5e04eac27ab6c2c2aba20ddec00c9b89c1ed42df1ad96d6df5e2d0a4ee91359c230eb9223974cb7be209668c229205b2ca91ce46318464e85f85693ac6faf1649a7f4895b619875b4914ba62d5f104b72e114902796540ec8310c230fe243005f9ae90b4a6d34809ab652d719aeede6b8b844d946a0ea0ae4050c0a9324d89d3b05f08c96ea039ff73a11feb18af6c8b114833b749facb2e3aae882b7b0032443737a50e3daa67835ba76c1129792fe69c9acd8b1ca9e3f59acacf904e67d9073c07c1c96569cca2d0af1ad1359df5eeb924ee94cb7c23096682397205bbe201ce4c229464e2ca9009063189de279a17d44eb3e621874216d308eb191fecb0a2bc5ff0cdde7b749610097d141bb0d54f055bbae169e869e9bb4457da0632b6b98f67dc51269246e263642eedbfd76c65f5fc974b63c8e9b21be39bb1b12d74423d2297d150d9da0ae3243ee839090783eb01d278c0380a200288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a00bca40380759f4ef1ba903ea2c758310c658536187f8648ffdba495e2cbe8eef306aa30804bd7a0dd411b1de269377cd2ec87bfa85cba17c4b91b1e4ce66ee87c0846468e06cb2364e0f5e23f36af08b67815efad3fc4db8eb0659fd916ecd39acb3c64694ff42e81c14578bae3b1e7e43600dc81e9b0c1d7cb750ddfd8de669d6dabd66a49b26834afacdae7dcafb28d1fc8a804c7396e78e0738f15c2df527d1ef95e27702756d1553696625d90ee9380645c150fea1676564b6b4eaf1685f8f489ac6f987549a74ba65b61104bb49114902d5f540e72e10c2327967cabc931b7578b4253fa44d630ccbaa4c825d3ad2f8825da700ac8964b2a07b918869113f12100e4cdf48507361ac02f5ba96ba5d776734d5ca2ec0c5075055c0206052312ec4ef20278c649f581cf9d9d087f4b457be47b8ac19d584f56d958557441bad8019271393da85b6d53bca15dbbe0714b49ff1a64566c8965cf9fb456d6fc58f33ec82c603e0e022b4ee53985f8564bac6faf1649a7f4895b619875b4914ba62d5f104b72e114902796540ec8310c23f1bc5000a2759f4e0cba903e18c75831ff658536e27f8648f3dba49580cbe8eedd06aa30aa5dd7a04f434ff8daa23e0bb195b54dfbbe62d03412374c1b2177897e3b6313af64baed1ec7cdafdf9c5d5b896ba210e994be0d864ed0119921f78a48483c57d88e93410140511f144551c6511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014d20140514fa7785e481fd1baac18065d421b8c6343a4ffb2d24af13f74f7f96d5518c0656bd06e038d0ef1a51b3ee9a0c35fd4b4dd0b62f60d0f262e3774bedc23234773e51132047af11f8357045bf08af7569be26d47bcb2cfec1ff6694dd81eb2340b7a97c065223cdd27d873f2e000eec0f1d8e06b1bba86ef746fb3cee5556b356c59349ab656edf32457d9c657645402ee1c37bc0fb9c78ae36fa93ef07caf13e113ab688f1b4b31b848f7c92a32ae8a2e750b3b40a535a7078b427cab44d6b757baa453fad3ad30cc25dac825c8962f8807b9700a91134b2ad5e41886ab4521be7d22ebdb665dd22992e95618c4126de40564cb1795835c38c3c889251000720cfac283f80de09766d4b5521bbbb9a6ad5176866bba022e2e838211a8762779013ce32409c0e74e0184bfa5fa3df2bd4ee04eaca2ab6c2cc5ba20dd2700c9b82a1ed42dec29ded09c5df0b8b6a47f8dae2bb6c4a5e74f5a326b7eacb21f64162b1f078179a7f21c307caba515b7578b4253fa44d630ccbaa4c825d3ad2f8825da700ac8964b2a07b9188691135e2800e4ba4fa7785d481fd163ac1806b2421b8c3f43a4ff6dd24af16574f7f9035518c0ae6bd06ea1277cd5519f85a7cada266d5f31e8d8891ba67d90bb441a9db1898d32dd76bfe3e6d757ceae2d8f3551886f4adf86c427e88874907b4543249eabccc7c92024a0a80f6ca228e3008a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a533c2fe98f68dda70c832ea40dc63156d27f59a1a5f89f21fbfc36690ce032ba68b7812a87f8d2359f74d0462f6ada0d0531fbe1071397ee3a5fee8691a3b91b08198291f88fc1f2822d78bd7babcd2bb6235ec567f60ff1b4266cd9599a05fb4be0320f9eee13bd3979701177e078ecf0b50d00c3773a6c59e7725db51ab6b71a4ddbaaf679922c6ce32bab2a01f7ab1bde073263c5718e541ff8dcd789f0b755b447bea518dc89fb64958d574517a4851d20199ad383ba21bed5d2ebdbab45d2297d225618665d6de492e9cb17c4125c38056489259583720cc3c8a210df6a91f5edd52ee9943e742b0cb3893672c9b2e50b62412e9c02e4c492ca00398661e1417c08f04b337d5aa98d065cd356ea3bc3b5dd01179728c108545d93bc8041719204bb73a7009edf527de0f95e27c22756d11e3696627090ee9355645c155dea1676806f684e0f785cdb14bf46d72e5be252d2272d99153f56d9f3328b953583c0bc0f790e988fd5d28a53ab4521be7d22ebdb665dd22992e95618c4126de40564cb1795835c38c3c889251400720ca7533c2fa48f68dd560c832ea10dc63121d27f5969a5f89fbafbfc362a0ce0323568b78113be6ad7cfc2d3d06d93b6a818746c650dd3beaf5d228dc4d8c446c86ebbdfcef3eb2b99d796c77128c437676f43e29a74443aa5bda2a113cf5566c864101212d407b6e394710050010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445019e977400b4eed3294117d247e3182b86bfacd006fccf10e97e9bb4527019dd7ddb4015067ce91ab43a68a34335ed864f98fdf017894bf7822f77c383d1dc0d9d0cc1c8c8c760798416bc5efcd5e615c111afe2bdfb8778db13b6ec33cd827d5a7099872cf789de253cb8084f703cf69cda06803b3b1d36f873b9aee10ddbdbaca66dd55a3c49168df19555fb80fb55b6ef031995e238c70d0f7ceeb144f85baada23dfeb0ceec42ab2cac652a20bd27d0e908cabe941ddc2df6a69cdedd5a210943e91f50cb32ee972c9742b0b6289369c02b2e592ca412e8661e4c4886f3539faf66a51744a9fc8158659971bb964baf205b144174e01d96249e5201cc33072203e0480a599bef0d44603f8692b75ade1da6eae8b4b949d04aaae805ec0a0604982dd495300cf38a93ef0b9af13e16fab688f7c4b31b813f7c92a1bae8a2e480b3b403234a70775ae6d8a37a36b173c7129e95f96cc8a2dabecf993c5ca9a1f60de079907ccc74169c5a93ca210df6a91f5edd52ee9943e742b0cb3893672c9b2e50b62412e9c02e4c492ca00398661299e170a47b4eed3864117d206e3182be9bfacd052fccf107d7e9bb4067019ddb4db40155fb5eb1ae169e809495bd4673ab6b2b669df570c9146e2866223e42edd6f676cf5954cb7cbe3b8f9e29bb36b21714d14229dd2b7d1d0093a2a33e45e080989e703db7132380028ea80a228ca288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa200804b3a0028f7e914cf0be9235a8c15c3a0566883716788f45f4d5a29fe8cee3ebfa00a03b8740dda6db4d121be76c3271d7ef88b9aa57b41ccbbe1c1c4ee86ce976064e468b03c42865e2ffe63f38a600b57f1de6a43bced885bf699fdc13ead09cc43966644ef12b85c84a7fb1e7b4e1e03c01d380e1b7c6d5cd7f09ded6dd6b9b66aad86248b46d3caaa7d9efd2adbf8818c4ac09ce386f73ef75871fc2dd50791ef7522776215ed6563290605e93e5948c655d1a06e6107b5b4e6f46a51886f9fc8faf65997744a64ba1586b1441bb901d9f205e520174e30726249b79a1cc37bb528c4a54f647dc3ac4b3a5c32dd0a8258a28da7806cf9a472900b611839b11f02408e4c5f7810a301fcd295ba566a6d37d7b425cace705557c0c560503002c1ee242f80679c241ff8dc2989f0b754b447bed718dc895564958da54517a4fb1d201957d383ba8536c51b9ab50b1ed794f4afd166c596b8f6fc494b65cd8f55ef83cc62e6e32030e2549e03886fb5b4faf66a51744a9fc8158659971bb964baf205b144174e01d96249e5201cc33072cf0b05805af7e914a00be923718c15c35f566883fe6788f4bf4d5a29b88cee3e6da00a03da750dda34f484af2deab3f05b59dba4ef2b061d2371c3b411729748b73336b14aa6dbee71dcfcfacdd9b5e5b8260af14ee9db90e8041d911972af6884c47395ed3819840014f5815114651c4551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040511d001445748ae725f411adfb8a61d085b4c138c644fa2f2bad14ff33779fdf2685015c4606ed3650e8105fbae1930edafc454dbbbd20663ff060e2d243e7cbdd327234771e21433017ff315845b005af786fb579de76c4abfbccfe219fd6842d214bb36077095ce6c2d37da23d270f2ee00e1c8f0dbeb6016bf84e8736eb5caeb556c3f645a3695bd53e4f92956d7c654625e07e71c3fb407bac38ce96ea039ff73a11feb18af6c8b114833b749facb2e3aae882b7b003245a737a5028c4b75a647d7bb54b3aa54fdd0ac3aca28d5c326cf98258900ba78039b1a4724d8e61185a14e25b27b2bebdd6259dd2996e85612cd1462e40b67c4139c885538c9c58520120c7302f3c880f007e69a65d2bb5d19b6bda4a6567b8b62be0e212281881aa77921730334e92607cee14c0f85baa0f23dfeb44eec42adacac6520c0bd27db2908caba241ddc20ee20dcde9058f6b9bfad7e8da624b5c4afea425b3e6c72a7b4166b1b2711098f72acf01f3b75a5a717bb528c4a54f647dc3ac4b3a5c32dd0a8258a28da7806cf9a472900b611839b18502408efb748ae785f411adc68a61d02bb4c1383344fa2f26ad14ff46779fdf5085015cba06ed367ac257edf559781aac6dd21615838eadb861daf7b94ba491199bd808d36df7db6e7e7d25ecdaf2381385f8e6f46d485c828e48a7b9573474e2b9ca0c9c0c42428afac0768a320e002800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228c5f3920e88d67d3a30e842fa601c63c5fd9715da8aff1922cf6f9356002ea3bb761ba8c2882f5d8349076d74a2a6ddf010b31ffe3071e95ef3e56e78399abba190211819ff182c8fd882d78bb7dabc223be255bc66ff106f6bc2967da559b04f042ef390e93ed1bb930717e1078ec79e5fdb00707ca7c306752ed735ab617b9bd1b4ad5a9f27c9a236beb26a1270bfcae17d20a3561ce7b8f581cf3d9d087f4b457be47b8ac19d584f56d958557441bad8019271393da85be25b2dadbebd5a149dd227b28561d625462e996e7c412cd1855340b6585239c8c7308c9c0af1ad2659df5e2d924ee913b7c230eb6823974c5bbe2096e4c229204e2ca91c906318461ec48700bf34d31795da6800356da5ae335cdbcd707189b28c40d515c90b18142749b03b770ae0192dd5073eef7522fc6215ed9163290677e93e5965c655d1056e61074886e6f4a0c7b54df16b74ed82252e25fdd29259b163953d7fb35859f308ccfb20e780f9382dad38955a14e25b27b2bebdd6259dd2996e85612cd1462e40b67c4139c885538c9c58520120c7303ac5f342fa88d67dc530e842da601c6322fd9715568aff19bbcf6f93c2002ea383761ba8e1ab765d2c3c0d3d36698bfa41c756d630edfb8a25d248dc4d6c84dcb6fbed8cbfbe92e96d791c37427c73763624ae8947a453fa2b1a3a415c6586dc062121f17d603b4e190700450050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140079490700eb3e9de274217d448eb16218cb0a6d30ff0c91feb7492bc597d1dde70d54610097ae41bb83363ac4d36ef8a4d90f7f51b8742f8872373c98cdddd0f9108c8c1c0c9647c8c1ebc57f6d5e116cf12ade5b7f88b71d61cb3eb32cd8a7359779c8d29fe85d02838bf074c763cfc96d00b803d36183af97eb1abeb0bdcd3ada56add59364d1685f59b5cfb85f651b3e9051098e73dcf0c0e71e2b84bfa5fa3df2bd4ee04eaca2ab6c2cc5ba20dd2700c9b82a1ed42decad96d69c5e2d0af1e91359df30eb924e974cb7c22096682329205bbea91ce4c218464e2cf85693636faf1685a7f489ac61987549914ba65b5f104bb4e114902d96540e72310c2327e24300c89ae90b0f6d34805fb652d74aaeede69ab844d919a0ea0ab8050c0a4624d89de405f08c93ea039f3b3a11fe968af6c8f714833bb19facb2b1aae88274b00324e3737a50b7daa67843ba76c1e39792fe35c9acd812ca9e3f69acacf9b1e67d9059c07c1c04569cca730af1ad9659df5e2d924ee913b7c230eb6823974c5bbe2096e4c229204e2ca91c90631846e279a10044eb3e9d1874217d308eb162fecb0a6dc5ff0c91e7b7492b0097d1ddbb0d546155bbae419e869ef0b4457d16632b6b9bf67dc5a069246e983642ee12fd76c6265fc974db3c8e9b5fbe39bbb612d74421d2297d1b0d9da0233243ee15909078aeb01d27830380a23e288aa28ca2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a40380a29f4ef1bc903ea27558310cba853618c78648ff65a495e27fe8eef3dbaa3080cbd7a0dd061b1de24b377cd24187bfa869ba17c4ec1b1e4c5c6ee87cb946468ee6cb236408f5e23f06af08b6e015efad36c4db8e78659fd93fecd39ab03c646916f42e81cb4578ba4fb1e7e4c100dc81e3b0c1d736750ddfe9de669dcbabd66ad8b268346dacdae749afb28dafc8a804dc396e781f738f15c7df527de0f95e27c22756d11e3696627090ee9355645c155dea1676804b6b4e0f1685f85689ac6faf7549a7f4a65b61984bb4914b902d5f100e72e11423279654abc9310c578b427cfa44d6b7ccbaa45325d3ad308825dac80ac8962f2a07b9708691134b2100e418f48507f11ac02fcda96ba53676734d5ba2ec0cd775055c5c06052350ec4ef20278c6491281cf9d02087f4bf57be47b9dc19d584556d9588a7441ba4f019271553da85bd853bca139bbe0716d49ff1a5d566c894bcf9fb464d6fc58653ec82c563e0e02f34ee53960f8564b2b6faf1685a7f489ac61987549914ba65b5f104bb4e114902d96540e72310c2327bc5000c8759f4ef1ba903ea2c758310c658536187f8648ffdba495e2cbe8eef306aa30805dd7a0dd434ff8aaa23e0b4f95b54ddabe62d0b112374cfb217789343b63131b64baed7ec7cdafaf9c5d5b1e6ba210df94be0d894ed011e921f78a86483c57998e93414840511fd84551c60114050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144501405114a7785ed21fd1ba4f18065d481b8c63aca4ffb2424af13f43f7f96dd218c06574d06e03550ef1a56b3ee9a08d5fd4b41b0b62f6c30f262edd74bedc0d2347733711320423f11f83e5045bf07af7569b576d47bc8acfec1fe2694dd8b2b2340bf697c0651e3cdd277a73f2e022eec0f1d8e06b1b0086ef74d8b3cee5ba6b356c6f349ab655edf32459d9c657565402ee5737bc0f64c78ae31ca93ef0b9af13e16fab688f7c4b31b813f7c92a1bae8a2e480b3b403235a70775427caba5d6b7578ba453fa44ad30ccbadac825d3962f8825b9700ac8134b2a07e41886914521bed522ebdbab5dd2297de9561866126de49264cb17c4835c3805c889259500720cc3c283f810e09766fab5521b0db9a6add476866bbb022e2e518211a8ba27790183e3240976e74e013cbfa5fac0f2bd4e844eaca23d6c2cc5e020dd27abc9b82abad42dec00ded09c1ef0b8b6297f8dae5db6c4a5a44f5a322b7eacb2e764162b6b0781791ff21c301faba515a7578b427cfa44d6b7ccbaa45325d3ad308825dac80ac8962f2a07b9708691134b2800e4184fa7785e481fd1baac18065d421b8c6343a4ffb2d24af13f74f7f96d5518c0656bd06e03277cd5ae9f85a7a1da266d5131e8d8ca1ba67d5fbb441a89b1898d90dd76bf9de6d75732ae2d8fe351886fcedf86c435e888744a7b4543279eabcc90c9202424a80f6cc728e300a002008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a023c2fe90068dda753832ea48fc631560c7f59a10df89f21d2fc3669a5e032bafbb7812a0cf8d2356874d046876ada0d9f31fbe12f1397ee055fee8607a3b91b3a198291918fc1f2082d78bdf8abcd2b82235ec57bf60ff1b6266cd9679a05fbb4e0320f59ee13bd4b7970119ee078ec39b50d0077773a6cf0e7725dc31ab6b7594ddbaab579922c1ae32babf601f7ab6cde07322ac5718e1b1ff8dc6389f0b754b447bed718dc895564958da54517a4fb1d201957d383ba85bed5d29adbab4521297d22eb18665dd2e492e95617c4126d380564cb2595835c0cc3c88910df6a72f5edd5a2e9943e912b0cb32e3672c974e50b62892e9c02b2c492ca41398661e4417c08004b337de1a98d06f0d356ea5ac3b5dd5c1797283b08545d01bc8041c19204bb93a7009e71527de0735e27c2df56d11ef996627027ee9355365c155d9016768064684e0fea5cdb146f46d72e78e252d2bf2d99155b56d9f3278b95353fc0bc0f320e988f83d28a53794521bed522ebdbab5dd2297de9561866126de49264cb17c4835c3805c889259500720cc3533c2f148f68dda70c832ea40dc63156d27f59a1a5f89f21fbfc36690ce032ba68b7812abe6ad735c2d3d01393b6a8cf746c656dd3beaf18228dc40dc446c85dbbdfced8eb2b996e96c771f3c43767d743e29a28443aa56fa2a113745566c8bd101212cf07b6e364710050d40045519450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010097740050eed3299e17d247b4182b8641acd006e3cf10e9bf9bb452fc19dd7d7e40150670e91ab4db68a3437ced864f3afdf017354bf7829877c38389dc0d9d2fc1c8c8d16079840cbc5efcc7e615c116afe2bdd58778db11b6ec33fb827d5a1399872ccd89de2570b8084ff73cf69c3c06803b701d36f8dab9aee13bdbdbac736dd55a0d49168da69555fb3cfb55b6f10319958038c70def7ceeb1e2f85baa0f23dfeb44eec42adacac6520c0bd27db2908caba241ddc20e6a69cde9d5a210df3e91f5edb32ee994c9742b0c6289367202b2e50bca412e9c61e4c4926f353986f66a51884a9fc8fa86599774b964ba1505b1441b4e01d9f249e52017c33072623e04801c99bef0204603f8a52b75add4da6eae694b949de1aaae808bc0a0600482dd495e00cf38493ef0b95313e16fa9688f7caf31b813abc92a1b4b8a2e48f73b4032aea707750b6d8a37346b173cae29e95fa3cc8a2d71ecf99396ca9a1fabde0799c5ccc74160c5a93c0710df6a69f5edd5a2e9943e912b0cb32e3672c974e50b62892e9c02b2c492ca41398661e49e170a00b4eed3294117d247e3182b86bfacd006fccf10e97e9bb4527019dd7ddb401506b5eb1ab469e8095f5bd467e1b6b2b649df570c3a46e2866923e42e916f676c62954cb7dde3b8f9f59bb36bcb714d14e29dd2b721d0093a2233e45ed10989e72adb7132080028ea03a228ca388aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a23a00288ae914cf4be9235af715c3a00b6883718c88f45f565a29fe67ee3ebf4d0a03b88c0dda6da0d121be74c3271db4f88b9a767b41cc7ee1c1c4a586ce97bb64e468ee3c4286602ffe63b08a600b5ef1de6af3bced8857f699fd433ead095b439666c1ef12b8cc84a7fb447b4e1e5cc01d381e1b7c6d03d7f09d0e6dd6b95c6aad86ed8b46d3b6aa7d9e242adbf8ca8c4ac0fde386f781f758719c2dd5073eef7522fc6215ed9163290677e93e5965c655d1056e610748b4e6f4a051886fb5c8faf66a97744a9fba158659441bb964d9f205b120174e01726249e59a1cc330b528c4b74f647d7bac4b3aa532dd0ac358a28d5c806cf98272900ba71839b1a402408e615f78101f01fcd24cba566aa337d7b495cace706d57c0c52550300255ee242f60679c24c1f8dc2980f0b7541f47bed789dc8955b4958da51817a4fb642019574583ba851dc51b9ad30b1ed736f4afd1b5c596b894fc494b66cd8f55f683cc6265e32030ef549e03e66fb5b4e2f66a51884a9fc8fa86599774b964ba1505b1441b4e01d9f249e52017c33072620b05801cf7e914cf0be9235a8c15c3a0566883716788f45f4d5a29fe8cee3ebfa00a03b8750dda6df484afdaeab3f03459dba42d2b061d5b71c3b4ef729748233336b111a6dbeeb7dcfcfa4ad9b5e571260af1cde9db90b8041d914e72af68e8c47395193819848414f581ed14651c00510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445514051140514455100511405004551004014050014510040510500144500405114001445518ae7251d11adfb7461d085f4c138c68afa2f2bb414ff33449fdf26ad015c4677ed365085105fba06930edae8454dbbe120663ffc60e2d2bde7cbddf07234774321433032ff31581eb005af176fb5794576c4ab78ccfe21ded6842dfb4bb3609f095ce621d37da277270f2ec20e1c8f3dbeb601e0f84e870deb5cae6b56c3f636a3695bb53e4f92456d7c65d525e07e95c3fb4046ac38ce71ea039f7b3a11fe968af6c8f714833bb19facb2b1aae88274b00324e3737a50b7c4b75a5a7d7bb5283aa54f640ac3ac4b8d5c32ddf98258a20ba7806cb1a472908e61183914e25b4db2bebd5a259dd2276e8561d6d1462e99b67c412cc88553409c58523920c7308c3c880f017e69a62f2bb5d1006bda4a5d67b8b69be0e212651881aa2b921730284e926077ee14c0335baa0f7cdfeb44f8c42ada23c6520ceed27db2ca8caba20bddc20e900dcde9418f6b9be2d7e8da054b5c4afaa425b362c72a7bfe66b1b2e61098f741cf01f3715a5a712ab528c4b74f647d7bac4b3aa532dd0ac358a28d5c806cf98272900ba71839b1a402408e61748ae785f411adfb8a61d085b4c138c644fa2f2bad14ff33779fdf2685015c4606ed3650c257edba59781a7a6dd216f5838eadac61daf7154ba491b89bd808b96df7db197e7d25d3daf2386e85f8e6ec6d485c138e48a7f457347482b9ca0cb90c4242e2fac0769c320e008a00a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800f3920e00d67d3ac5e842fa881c63c5309715da60ff1922fd6f93568a2ea3bbcf1ba8c2002f5d8376076d7488a6ddf049b31ffea271e95e10e56e78309abba1f321181939182c8f9082d78bffdabc22d8e255bcb7ff106f3bc2967d6659b04f6b2ef390a53ed1bb040717e1e98ec79e93db007007a7c3065f2ed7357c617b9b75b4ad5aab27c9a2d1beb26a9f70bfca367d20a3121ce7b8e181cf3d56087f4bf57be47b9dc19d584556d9588a7441ba4f019271553da85bd85b2dad39bd5a14e2d227b2be61d6259d2e996e85412cd1465340b67c5239c885308c9c58f1ad26c7df5e2d0a4ee91359c230eb9223974cb7be209668c229205b2ca91ce46318464ec487009034d3171eda6800bf6da5ae955cdbcd357189b23340d515700b18148c49b03bc90ae01927d5073e777522fc2d15ed91ef290677623e59656355d105e9610748c6e6f4a06eb54df18674ed82c72e25fd6b9259b125953d7fd25859f363ccfb20b380f93808ad3895e714e25b2db2bebd5a259dd2276e8561d6d1462e99b67c412cc88553409c58523920c7308cc5f3420188d67d3a30e842fa601c63c5fd9715da8aff1922cf6f9356002ea3bb761ba8c2ab765d833c0d3de1698bfa2cc756d636edfb8a41d248dc306c84dc25fbed8c4dbe92e9b6791c37bf7c73766d24ae8942a453fa361a3a41476586dc2b2121f15c603b4e060700457d50144519455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050450100451400501401004551005014450045511450144501455114001445010051140050490700453e9de279217d44ebb16218740a6d308e0c91fecb492bc5ffd1dde7b754610097ae41bb0d363ac4976ef8a4830f7f51d3742f88d9373c98b8ddd0f9728c8c1ccd9647c810ebc57f0c5e116cc12ade5b6d88b71df1cb3eb37fd8a7356179c8d22ce85d02978bf0749f63cfc98300b803c76183af6deb1abed3bdcd3a9756add5b064d168da59b5cf935f651b5f905109b873dcf03ee71e2b8ebfa5fac0f2bd4e844eaca23d6c2cc5e020dd27abc9b82abad42dec0096d69c1e2d0af1ad1359df5eeb924ee94cb7c23096682397205bbe201ce4c229464e2ca956936318af1685f8f489ac6f987549a74ba65b61104bb49114902d5f540e72e10c2327964300c831e90b0fe234805f9a52d74a6dede69ab644d919aeea0ab8b80c0a46a0d89de405f08c9324039f3b0511fe96eaf6c8f73a833bb18aacb2b114e882749f0324e3aa7a50b7b0a678437376c1e3da92fe35baacd812979e3f69c9acf9b1ca7d9059ac7c1c04e69cca73c0f1ad9656df5e2d0a4ee91359c230eb9223974cb7be209668c229205b2ca91ce46318464e79a10090eb3e9de274217d448eb16218cb0a6d30ff0c91feb7492bc597d1dde70d546100bbae41bb869ef055457d169e2b6b9bb47dc5a063246e98f642ee126976c62636c974dbfd8e9b5f5f39bbb63cd74421be297d1b129da023d243ee150d9078ae321d27839080a23eb08aa28c03280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0080a22800288aa280a2280a288aa200a2280a008aa20080280a0028a20080a20a00288a0380a2284ef1bca43ea2759f310cba903618c75848ff658595e27f86eef3dba43080cbe8a0dd06aa1de24bd77cd2411bbfa8693717c4ec871e4c5cbae87cb91b468ee66e23640846e23f06cb08b6e0f5efad36afdb8e78159fd93fc4d39ab065646916ec2e81cb3c78ba4ff4e7e4c145dc81e3b1c1d736000ddfe9b0669dcb75d66ad8de68346dabdae749b2b28dafaca804dcaf6e781fc88f15c739527de0735e27c2df56d11ef996627027ee9355365c155d90167680646b4e0fea85f8564bac6faf1649a7f4895b619875b4914ba62d5f104b72e114902796540ec9310c238b427cab44d6b757baa453fad3ad30cc25dac825c8962f8807b9700a91134b2a00e418868507f121c02fcdf46ba5361a734d5ba9ec0cd776055c5ca2052350754ef20206c64912eccf9d02787f4bf581e47b9d089d58457bd9588ac141ba4f5692715574a85bd801bca1393de0716d53ff1a5dbb6c894b499fb46456fc5865cfc82c56d60e02f33ee539603e564b2b4eaf1685f8f489ac6f987549a74ba65b61104bb49114902d5f540e72e10c2327965000c8319f4ef1bc903ea27558310cba853618c78648ff65a495e27fe8eef3dbaa3080cbd7a0dd064ff8aa5d3e0b4f43b54ddaa262d0b195374cfbbe7789341263131b21baed7e3bcdafaf645d5b1ec7a210df9cbe0d896bd011e994f78a864e3c57992193414848511fd88e51c6014005001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405144551005114050045510040140500145100405105001445004051140014455140511405785ed201d1ba4fa7065d481f8c63ac18ffb2421bf13f43a4f96dd24ac06574f76e035518f1a56bd0e9a08d0ed4b41b3e62f6c35f262edd0bbedc0d0f47733774320423231f83e5115bf07af1569b570447bc8af7ec1fe26d4dd8b2cf340bf669c0651eb2dd277a97f2e0223cc0f1d8736b1b00eeef74d8e0cee5ba86356c6fb39ab6556bf3245934c65756ed02ee57d9bc0f64548ae31c373ef0b9c713e16fa9688f7caf31b813abc92a1b4b8a2e48f73b4032aea707750b7caba535b7578b4253fa44d630ccbaa4c825d3ad2f8825da700ac8964b2a07b91886911321bed5e4ebdbab45d2297d225618665d6de492e9cb17c4125c38056489259583720cc3c883f810009766fac2521b0de0a6add4b5866bbbb92e2e517611a8ba0279018382240976274e013ce3a5fac0e7bd4e84bfaca23df22cc5e04edd27ab6cb82aba202dec00c9d09c1ed4b8b629de8dae5df0c4a5a47f5a322bb6acb2e74f162b6b7e81791f641c301f07a515a7f28b427cab44d6b757baa453fad3ad30cc25dac825c8962f8807b9700a91134b2a00e41886a7785e281fd1ba4f18065d481b8c63aca4ffb2424af13f43f7f96dd218c06574d06e03557cd5ae6b85a7a127266d519fe8d8cadaa67d5f31441a891b898d90bb76bf9db1d75732dd2d8fe3e6886fceae86c4355188744adf454327e8abcc907b2024249e0f6cc7c9e300a0a8008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a0200a22800a08a02008a2800a02802008aa200a0288a008aa228a0288a028aa22800288a02002fe900a0dda7533c2ea48f6831560c8359a10dc69f21d27f3669a5f832bafbfc812a0ce0d23568b7d04687f8da0d9f74fbe12f6a97ee0531ee860713b91b3a5f829191a3c1f2081978bdf88fcd2b822d5ec57bab0ff1b6236cd967f605fbb426320f599a13bd4be070119eee78ec39790d0077e03a6cf0b5725dc377b6b759e7dbaab51a922c1a4d2babf679f7ab6ce307322a01718e1bdef8dc63c5f0b7541f47bed789dc8955b4958da51817a4fb642019574583ba851dd5d29ad3ab4521be7d22ebdb665dd22992e95618c4126de40564cb1795835c38c3c88925df6a720cedd5a210943e91f50cb32ee972c9742b0b6289369c02b2e592ca412e8661e4c47c080039337de1418d06f04b56ea5aa9b5dd5cd397283bc3545d01178041c10804bb93bc009e71927de073a727c2df52d11ef95e6270275693553696155d90ee7680645c4e0fea16db146f68d72e785c52d2bf4699155be2d9f3272d95353f56bc0f328b988f83c08a53790e21bed5d2ebdbab45d2297d225618665d6de492e9cb17c4125c38056489259583720cc3c83c2f140068dda753832ea48fc631560c7f59a10df89f21d2fc3669a5e032bafbb7812a0c6ad73568d3d013beb6a8cfc26c656d93beaf18748dc40dd346c85d22dfced8c42b996ebbc771f3eb3767d796e29a28c43aa56f43a113744466c8bda21212cf55b6e364100050d4074551947114450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004574005014d3299e97d247b4ee2b864117d006e31810e9bfacb452fccfdd7d7e9b150670191ab4db40a3437ce9864f3a68f01735edf78298fdc383894b0d9d2f77c8c8d1dc79840cc15efcc76015c116bce2bdd5e678db11afec33fb877d5a13b6872ccd82de257099084ff789f69c3cb8803b703c36f8da06aee13b1ddbac73b9d55a0ddb168da66d55fb3c4955b6f195199580fbc70def03eeb1e2385baa0f7cdfeb44f8c42ada23c6520ceed27db2ca8caba20bddc20e9069cde941a210df6a91f5edd52ee9943e742b0cb3893672c9b2e50b62412e9c02e4c492ca353986616a51886f9fc8faf65997744a64ba1586b1441bb901d9f205e520174e3072624904801cc3bef0203e03f8a59975add4466eae692b949de1daae808b4ba06004aadd495ec0cf384982f0b95300e16fa93e8f7caf13b813ab682a1b4b312e48f7c94032ae8a07750b3b8a3734a7173cae6de95fa36b8a2d7129f99396cc9a1fabec0799c5cac74160dea93c07ccdf6a69c5edd5a210943e91f50cb32ee972c9742b0b6289369c02b2e592ca412e8661e4c4320e0039e13fec8afed7f85133f9bf805c15ff0f4501007d14005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114501445014551140014450100511400504501004514005014010045510050144500455114000000480000000000000000 diff --git a/tests/test.ast2100.js b/tests/test.ast2100.js new file mode 100644 index 000000000..d1b46b68f --- /dev/null +++ b/tests/test.ast2100.js @@ -0,0 +1,384 @@ +// requires local modules: ast2100, ast2100idct, ast2100const, ast2100util +/* jshint expr: true */ + +var assert = chai.assert; +var expect = chai.expect; + + +// Convert a hex string, optionally containing spaces, into an array of integers representing bytes. +var parseHex = function (s) { + s = s.replace(/\s/g, ''); + if (s.length % 2 != 0) + throw 'Hex data has uneven length!'; + for (var bytes = [], i = 0; i < s.length; i += 2) + bytes.push(parseInt(s.substr(i, 2), 16)); + return bytes; +}; + + +function make_decoder () { + return new Ast2100Decoder({ + width: 0x400, + height: 0x300, + blitCallback: function () {} + }); +} + +// Swap u32 byte order. Mutates input! +function buf_swap32 (data) { + for (var i = 0; i < data.length; i += 4) { + var tmp; + + tmp = data[i+0]; + data[i+0] = data[i+3]; + data[i+3] = tmp; + + tmp = data[i+1]; + data[i+1] = data[i+2]; + data[i+2] = tmp; + } + + return data; +} + + +describe('ATEN_AST2100 video encoding', function() { + "use strict"; + + var dec; + beforeEach(function () { + dec = make_decoder(); + dec._mcuLimit = 10; + }); + + describe('BitStream', function() { + var stream, stream2, stream3; + var data = buf_swap32(parseHex('f0f0 cccc cccc cccc cccc cccc cccc cccc')); + var data2 = parseHex('F0F1F2F3 F4F5F6F7 F8F9FAFB FCFDFEFF'); + var data3 = parseHex('0b0b01bc45fbc5e020040101ffff3f20c0ffffff0000240000000000000000005028140a0000000000000000'); + + beforeEach(function () { + stream = new BitStream({data: data}); + stream2 = new BitStream({data: data2}); + stream3 = new BitStream({data: data3}); + }); + + it('should pass simple sanity checks', function () { + expect(stream.read(4)).to.equal(0xF); + expect(stream.read(4)).to.equal(0x0); + expect(stream.read(4)).to.equal(0xF); + expect(stream.read(4)).to.equal(0x0); + }); + + it('should pass simple sanity checks (part II)', function () { + var i; + for (i = 0; i < 4; ++i) + expect(stream.read(1)).to.equal(1); + for (i = 0; i < 4; ++i) + expect(stream.read(1)).to.equal(0); + }); + + it('should be able to properly refill the buffer', function () { + expect(stream.read(4)).to.equal(0xF); + expect(stream.read(4)).to.equal(0x0); + expect(stream.read(4)).to.equal(0xF); + expect(stream.read(8)).to.equal(0x0C); + }); + + it('should properly swap byte order', function () { + expect(stream2.read(8)).to.equal(0xF3); + expect(stream2.read(8)).to.equal(0xF2); + expect(stream2.read(8)).to.equal(0xF1); + expect(stream2.read(8)).to.equal(0xF0); + expect(stream2.read(8)).to.equal(0xF7); + }); + + it('should properly handle skipping the full 32-bit read-buffer (data3)', function () { + // Must do this in two parts so that bits < 32 each time. + stream3.skip(16); + stream3.skip(16); + expect(stream3.read(4)).to.equal(0xE); + }); + + // it('issue repro', function () { + // var data = parseHex('f0f0 cccc cccc cccc cccc cccc cccc cccc'); + // var stream = new BitStream({data: data}); + // expect(stream.read(31)).to.equal(0xF0F0CCCC >>> 1); + // expect(stream.read(8)).to.equal(0x66); + // }); + }); + + describe('quant tables', function() { + it('quant tables are properly loaded and scaled', function () { + // This is luma quant table #4. + var expected = Int32Array.from([ + 0x00090000, 0x0008527e, 0x00068866, 0x000a9537, 0x000d0000, 0x00114908, 0x000f274b, 0x0009616d, + 0x0008527e, 0x000b8b14, 0x000caf8f, 0x00104f53, 0x00136b26, 0x0022df8f, 0x0018c594, 0x000b7b02, + 0x0009255c, 0x000caf8f, 0x000f5d2c, 0x0013f8fd, 0x001cbe90, 0x0020d994, 0x001adebc, 0x000b2cc4, + 0x00083b2b, 0x000eadca, 0x00126faf, 0x00161f78, 0x0020ecad, 0x002c58a1, 0x001ca316, 0x000b07c7, + 0x000a0000, 0x0010a4fc, 0x001a219a, 0x002473bf, 0x00260000, 0x002fed69, 0x001ed922, 0x000bdd19, + 0x000a36ca, 0x0014b4bd, 0x001ecbfa, 0x00214279, 0x00235b34, 0x0023cdea, 0x001ac9de, 0x000b0e2f, + 0x000e9cbf, 0x001b0616, 0x001e67d4, 0x001e8bd4, 0x001ed922, 0x001cea24, 0x00139fb4, 0x00085c96, + 0x000b0935, 0x00138450, 0x00131afd, 0x0011d7e1, 0x001161b4, 0x000c23a7, 0x000882d0, 0x00042fc6 + ]); + + dec._loadQuantTable(0, ATEN_QT_LUMA[4]); + var luma_quant_table = dec.quantTables[0]; + expect(luma_quant_table).to.deep.equal(expected); + }); + }); + + describe('IDCT', function () { + + var outputBuf; + + beforeEach(function () { + outputBuf = new Uint8Array(DCTSIZE2); // equivalent to one of the componentBufs in Ast2100Decoder. + }); + + // TODO: Fix typing: variables should be typed arrays. + it('test case 0 - DC value only', function () { + + // this is the output of the VLC / entropy coding process + // XXX: @KK: Why is this 16-bit? + var dataUnit = [ // new Int16Array( + 0xFF9C,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + + var expected_idct_output = Uint8Array.from([ // new Uint8Array( + 0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF, + 0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF, + 0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF]); + + dec._loadQuantTable(0, ATEN_QT_LUMA[4]); + var luma_quant_table = dec.quantTables[0]; + + AST2100IDCT.idct_fixed_aan(luma_quant_table, dataUnit, outputBuf); + + // console.log(result); + + console.log('expected:'); + console.log(fmt_u8a(expected_idct_output)); + console.log('result:'); + console.log(fmt_u8a(outputBuf)); + + expect(outputBuf).to.deep.equal(expected_idct_output); + + }); + + it('test case 1', function () { + // this is the output of the VLC / entropy coding process + // XXX: @KK: Why is this 16-bit? + var dataUnit = Int16Array.from([ + 0xFFBD,0,0,0,0,0,0,0, + 0xFFC3,0,0,0,0,0,0,0, + 0x26,0,0,0,0,0,0,0, + 0xFFED,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0, + 6,0,0,0,0,0,0,0, + 0xFFFC,0,0,0,0,0,0,0, + 2,0,0,0,0,0,0,0]); + + var expected = Uint8Array.from([ + 0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF, + 0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11, + 0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12, + 0xE,0xE,0xE,0xE,0xE,0xE,0xE,0xE, + 0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12, + 0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF, + 0x9F,0x9F,0x9F,0x9F,0x9F,0x9F,0x9F,0x9F, + 0xA1,0xA1,0xA1,0xA1,0xA1,0xA1,0xA1,0xA1]); + + dec._loadQuantTable(0, ATEN_QT_LUMA[4]); + var luma_quant_table = dec.quantTables[0]; + + AST2100IDCT.idct_fixed_aan(luma_quant_table, dataUnit, outputBuf); + + /* + console.log('expected:'); + console.log(fmt_u8a(expected)); + console.log('result:'); + console.log(fmt_u8a(outputBuf)); + */ + + // XXX: TEMPORARY -- figure out this rounding issue + // expect(result).to.deep.equal(expected); + var maxErr = 0; + for (i = 0; i < 64; ++i) + maxErr = Math.max(maxErr, Math.abs(outputBuf[i] - expected[i])); + if (maxErr > 1) + throw 'Error too high!'; + + }); + + it('test case 2', function () { + // this is the output of the VLC / entropy coding process + // XXX: @KK: Why is this 16-bit? + + var dataUnit = Int16Array.from([ + 0xFF9B, 0, 0, 0, 0, 0, 0, 0, 0xFFA4, 0, 0, 0, 0, 0, 0, 0, 0x35, 0, 0, 0, 0, 0, 0, 0, + 0xFFE6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0xFFFA, + 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]); + + var expected = [ + 0xE, 0xE, 0xE, 0xE, 0xE, 0xE, 0xE, 0xE, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1]; + + // Between IDCT passes, the workspace should look like this: + // [-903, 0, 0, 0, 0, 0, 0, 0, -874, 0, 0, 0, 0, 0, 0, 0, -914, 0, 0, 0, 0, 0, 0, 0, -874, 0, 0, 0, 0, 0, 0, 0, + // -915, 0, 0, 0, 0, 0, 0, 0, -868, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 266, 0, 0, 0, 0, 0, 0, 0] + + + dec._loadQuantTable(0, ATEN_QT_LUMA[5]); + var luma_quant_table = dec.quantTables[0]; + + AST2100IDCT.idct_fixed_aan(luma_quant_table, dataUnit, outputBuf); + + // console.log(result); + + console.log('expected:'); + console.log(fmt_u8a(expected)); + console.log('result:'); + console.log(fmt_u8a(outputBuf)); + + // XXX: TEMPORARY -- figure out this rounding issue + // expect(result).to.deep.equal(expected); + var maxErr = 0; + for (i = 0; i < 64; ++i) + maxErr = Math.max(maxErr, Math.abs(outputBuf[i] - expected[i])); + if (maxErr > 1) + throw 'Error too high!'; + }); + + }); + + describe('VQ', function () { + + it('should successfully load a codebook (colors)', function () { + var data = buf_swap32(parseHex('bc010b0b e0c5fb45 01010420 203fffff ffffffc0 00240000 00000000 00000000 0a142850')); + data = data.slice(4); + + var stream = new BitStream({data: data}); // Strip off quant table selectors and subsampling mode. + var controlFlag = stream.read(4); + expect(controlFlag).to.equal(0xE); + + var xMcuPos = stream.read(8), yMcuPos = stream.read(8); + expect(xMcuPos).to.equal(0xC); + expect(yMcuPos).to.equal(0x5F); + + dec.subsamplingMode = 444; // required or an assertion in the VQ code will fail + dec._stream = stream; + dec._parseVqBlock(1); // codewordSize=1 + expect(dec._vqCodewordLookup).to.deep.equal([1, 0, 2, 3]); + expect(dec._vqCodebook).to.deep.equal([ + [0x10, 0x80, 0x80], + [0xA2, 0x80, 0x80], + [0x80, 0x80, 0x80], + [0xC0, 0x80, 0x80] + ]); + }); + + it('should correctly handle multiple data blocks', function () { + // This data is from a memory dump. It should contain two VQ (0xE-type) blocks and then an 0x9 (end-frame). + var data = buf_swap32(parseHex('bc010b0b e0c5fb45 01010420 203fffff ffffffc0 00240000 00000000 00000000 0a142850' + '00000000 00000000')); + console.log('VQ multiple block data:'); + console.log(fmt_u8a(data)); + dec.decode(data); + + // TODO: improve asserts/etc. beyond just 'finished without error' + }); + + }); + + describe('Full JPEG subsampled MCU example 0', function () { + // Corresponds to whole-mcu-example / main_mcu_test_2() + var data = parseHex('040701a61bff6280f81fbaa2ff4dbc408dfccf405c15ff1f004800f500000000000000002dd4a462237ced2c9c25dca4cef7e6667d6626f3308063c3c7a1b7335badc24aaf0b5e9daf69590fcb96206b59e6f2ccb2bdd386b17dbb72182080d6288a2a1f2f0608a0fe87ae287f132f1023ff33d057c5ff470012403d0000000000000000ec1fb06cdacd2bdf9d79f3cde7f9f1362b7ce914ba521c79524a5bdc212a5ed47c3cbc6c7e072ae3e3c18384785f6ca6d4e22fb1af0c96449d1847a9c7037e966fa3ac8d4990830339463d277f25fc6f30ffe5f4f5cfec9f4ffff8bf00a0f5d358a2ab6e4e6778d929f686bd58ca9c26b8f7338f15105065dd39c718e219a78e'); + + + it('should decode properly', function () { + // DOES NOT APPEAR TO correspond to my notes in 'whole-mcu-example.txt'. + + dec._blitCallback = function (x, y, width, height, buf) { + console.log({x:x, y:y, width:width, height:height, buf:buf}); + console.log(fmt_rgb_buf(width, buf)); + }; + + dec.decode(data); + + }); + + }); + + describe('Full JPEG subsampled MCU example 1', function () { + + /* TODO: this looks VERRRRRY SIMILAR to the above example */ + + var data = parseHex('040701a61bff6280f81fbaa2ff4dbc408dfccf405c15ff1f004800f500000000000000002dd4a462237ced2c9c25dca4cef7e6667d6626f3308063c3c7a1b7335badc24aaf0b5e9daf69590fcb96206b59e6f2ccb2bdd386b17dbb72182080d6288a2a1f2f0608a0fe87ae287f132f1023ff33d057c5ff470012403d0000000000000000ec1fb06cdacd2bdf9d79f3cde7f9f1362b7ce914ba521c79524a5bdc212a5ed47c3cbc6c7e072ae3e3c18384785f6ca6d4e22fb1af0c96449d1847a9c7037e966fa3ac8d4990830339463d277f25fc6f30ffe5f4f5cfec9f4ffff8bf00a0f5d358a2ab6e4e6778d929f686bd58ca9c26b8f7338f15105065dd39c718'); + + /* + it('should load quant tables and set subsamplingMode', function () { + dec.decode(data); + + expect(dec._loadedQuantTables[0]).to.equal(4); + expect(dec._loadedQuantTables[1]).to.equal(7); + expect(dec.subsamplingMode).to.equal(422); + }); + */ + + it('should decode properly', function () { + // DOES NOT APPEAR TO correspond to my notes in 'whole-mcu-example.txt'. + + dec._blitCallback = function (x, y, width, height, buf) { + console.log({x:x, y:y, width:width, height:height, buf:buf}); + console.log(fmt_rgb_buf(width, buf)); + }; + + dec.decode(data); + + }); + }); + + // TODO: On the 'full-frame decode' and 'frame udpate decode' tests, we are NOT actually asserting anything about the code's output yet. + /* + describe('Full frame decode test', function () { + + it('should decode properly', function () { + + throw "Won't work without jQuery (or some other way of loading data)."; + + dec._blitCallback = function (x, y, width, height, buf) { + if (x == 0 && y == 0) { + console.log({x:x, y:y, width:width, height:height, buf:buf}); + console.log(fmt_rgb_buf(width, buf)); + } + }; + + // TODO: This path won't work for other people, and the test case needs to be made to understand that this + // is async; plus, I had to manually add jQuery to the generated HTML. + $.get("/novnc-tests/tests/frame4.hex", function (data) { + data = parseHex(data); + dec.decode(data); + }); + }); + }); + */ + + describe('Frame update decode test', function () { + + it('should decode properly', function () { + var data = parseHex('050501a69a3f6080008aa2a8000000900000000000000000'); + console.log(data); + + // this data should NOT be solid-white! + + dec._blitCallback = function (x, y, width, height, buf) { + console.log({x:x, y:y, width:width, height:height, buf:buf}); + console.log(fmt_rgb_buf(width, buf)); + }; + + dec.decode(data); + }); + }); + +}); diff --git a/tests/test.display.js b/tests/test.display.js index 32a92e221..80bf8a8e6 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -324,18 +324,12 @@ describe('Display/Canvas Helper', function () { data[i * 4 + 2] = checked_data[i * 4]; data[i * 4 + 3] = checked_data[i * 4 + 3]; } - display.blitImage(0, 0, 4, 4, data, 0); + display.blitImage(0, 0, 4, 4, data, 0, false); expect(display).to.have.displayed(checked_data); }); - it('should support drawing RGB blit images with true color via #blitRgbImage', function () { - var data = []; - for (var i = 0; i < 16; i++) { - data[i * 3] = checked_data[i * 4]; - data[i * 3 + 1] = checked_data[i * 4 + 1]; - data[i * 3 + 2] = checked_data[i * 4 + 2]; - } - display.blitRgbImage(0, 0, 4, 4, data, 0); + it('should support drawing RGBX blit images with true color via #blitImage', function () { + display.blitImage(0, 0, 4, 4, checked_data, 0, true); expect(display).to.have.displayed(checked_data); }); @@ -423,18 +417,18 @@ describe('Display/Canvas Helper', function () { expect(display.fillRect).to.have.been.calledOnce; }); - it('should draw a blit image on type "blit"', function () { + it('should draw a blit image on type "blit" with "rgb" set to false', function () { display.blitImage = sinon.spy(); - display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9], rgb: false }); expect(display.blitImage).to.have.been.calledOnce; - expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0, false); }); - it('should draw a blit RGB image on type "blitRgb"', function () { - display.blitRgbImage = sinon.spy(); - display.renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); - expect(display.blitRgbImage).to.have.been.calledOnce; - expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + it('should draw a blit RGBX image on type "blit" with "rgb" set to true', function () { + display.blitImage = sinon.spy(); + display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9, 10], rgb: true }); + expect(display.blitImage).to.have.been.calledOnce; + expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9, 10], 0, true); }); it('should copy a region on type "copy"', function () { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 1b73986f2..e400dce41 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -660,8 +660,17 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_state = 'Security'; }); - function send_security(type, cl) { - cl._sock._websocket._receive_data(new Uint8Array([1, type])); + // @KK: 'types' may be either a single number or an array of numbers. + function send_security(types, cl) { + if (types.constructor !== Array) + types = [types]; + + var data = new Uint8Array(1 + types.length); + data[0] = types.length; + for (var i = 0; i < types.length; ++i) + data[i+1] = types[i]; + + cl._sock._websocket._receive_data(data); } it('should fail on auth scheme 0 (pre 3.7) with the given message', function () { @@ -796,7 +805,11 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock._websocket._open(); client._rfb_state = 'Security'; client._rfb_version = 3.8; - send_security(16, client); + // @KK: Actual TightVNC servers support more than just 16 ("tight"), whereas ATEN iKVM servers + // advertise only 16. This is a large part of how we detect that we are speaking to an iKVM server, + // so if we want to test the client's interaction with a TightVNC server, we need to send more than + // just the one. + send_security([1, 16], client); client._sock._websocket._get_sent_data(); // skip the security reply }); @@ -873,6 +886,52 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_state).to.equal('failed'); }); }); + + describe('ATEN iKVM Authentication Handler', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + client._rfb_version = 3.8; + // @KK: ATEN iKVM server advertise *only* 16 ("tight"), which is how we detect them. + send_security(16, client); + client._sock._websocket._get_sent_data(); // skip the security reply + client._rfb_password = 'test1:test2'; + }); + + var auth = [ + 116, 101, 115, 116, 49, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 116, 101, 115, 116, 50, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0]; + + it('via old style method', function () { + client._sock._websocket._receive_data(new Uint8Array([ + 0xaf, 0xf9, 0x0f, 0xb0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.false; + expect(client._rfb_atenikvm).to.be.true; + expect(client._sock).to.have.sent(auth); + }); + + it('via new style method', function () { + client._sock._websocket._receive_data(new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.false; + expect(client._rfb_atenikvm).to.be.true; + expect(client._sock).to.have.sent(auth); + }); + }); }); describe('SecurityResult', function () { @@ -991,8 +1050,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._fb_height).to.equal(84); }); - // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them - it('should set the framebuffer name and call the callback', function () { client.set_onDesktopName(sinon.spy()); send_server_init({ name: 'some name' }, client); @@ -1022,12 +1079,23 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_state).to.equal('normal'); }); - it('should set the true color mode on the display to the configuration variable', function () { - client.set_true_color(false); - sinon.spy(client._display, 'set_true_color'); - send_server_init({ true_color: 1 }, client); - expect(client._display.set_true_color).to.have.been.calledOnce; - expect(client._display.set_true_color).to.have.been.calledWith(false); + it('should handle an ATEN iKVM server initialization', function () { + RFB.ATEN_INIT_WIDTH = 1; + RFB.ATEN_INIT_HEIGHT = 1; + client._rfb_atenikvm = true; + send_server_init({ true_color: 1, bpp: 32 }, client); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])); + expect(client._pixelFormat.bpp).to.equal(16); + expect(client._pixelFormat.depth).to.equal(15); + expect(client._pixelFormat.red_max).to.equal(31); + expect(client._pixelFormat.green_max).to.equal(31); + expect(client._pixelFormat.blue_max).to.equal(31); + expect(client._pixelFormat.red_shift).to.equal(10); + expect(client._pixelFormat.green_shift).to.equal(5); + expect(client._pixelFormat.blue_shift).to.equal(0); + expect(client._pixelFormat.Bpp).to.equal(2); + expect(client._pixelFormat.Bdepth).to.equal(2); + expect(client._rfb_state).to.equal('normal'); }); it('should call the resize callback and resize the display', function () { @@ -1051,28 +1119,34 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._mouse.grab).to.have.been.calledOnce; }); - it('should set the BPP and depth to 4 and 3 respectively if in true color mode', function () { - client.set_true_color(true); - send_server_init({}, client); - expect(client._fb_Bpp).to.equal(4); - expect(client._fb_depth).to.equal(3); + it('should set the BPP and depth to 4 and 3 respectively if server can send native (true color)', function () { + send_server_init({ true_color: 1, bpp: 8, depth: 8 }, client); + expect(client._pixelFormat.Bpp).to.equal(4); + expect(client._pixelFormat.Bdepth).to.equal(3); }); - it('should set the BPP and depth to 1 and 1 respectively if not in true color mode', function () { - client.set_true_color(false); - send_server_init({}, client); - expect(client._fb_Bpp).to.equal(1); - expect(client._fb_depth).to.equal(1); + it('should set the BPP and depth to 2 and 2 respectively if server cannot send native (true color)', function () { + client.set_convertColor(true); + send_server_init({ true_color: 1, bpp: 16, depth: 15 }, client); + expect(client._pixelFormat.Bpp).to.equal(2); + expect(client._pixelFormat.Bdepth).to.equal(2); + }); + + it('should set the BPP and depth to 1 and 1 respectively if server cannot send native (not true color)', function () { + client.set_convertColor(true); + send_server_init({ true_color: 0, bpp: 8, depth: 8 }, client); + expect(client._pixelFormat.Bpp).to.equal(1); + expect(client._pixelFormat.Bdepth).to.equal(1); }); // TODO(directxman12): test the various options in this configuration matrix it('should reply with the pixel format, client encodings, and initial update request', function () { - client.set_true_color(true); client.set_local_cursor(false); // we skip the cursor encoding var expected = {_sQ: new Uint8Array(34 + 4 * (client._encodings.length - 1)), _sQlen: 0}; - RFB.messages.pixelFormat(expected, 4, 3, true); - RFB.messages.clientEncodings(expected, client._encodings, false, true); + var pf = { bpp: 32, depth: 24, big_endian: false, true_color: true, red_max: 255, green_max: 255, blue_max: 255, red_shift: 16, green_shift: 8, blue_shift: 0 }; + RFB.messages.pixelFormat(expected, pf); + RFB.messages.clientEncodings(expected, client._encodings, false, true, []); var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, dirtyBoxes: [ { x: 0, y: 0, w: 27, h: 32 } ] }; RFB.messages.fbUpdateRequests(expected, expected_cdr, 27, 32); @@ -1112,13 +1186,14 @@ describe('Remote Frame Buffer Protocol Client', function() { client._fb_name = 'some device'; client._fb_width = 640; client._fb_height = 20; + client._pixelFormat.Bpp = 4; }); var target_data_arr = [ - 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + 0xf8, 0x00, 0x00, 255, 0x00, 0xf8, 0x00, 255, 0x00, 0x00, 0xf8, 255, 0x00, 0x00, 0xf8, 255, + 0x00, 0xf8, 0x00, 255, 0xf8, 0x00, 0x00, 255, 0x00, 0x00, 0xf8, 255, 0x00, 0x00, 0xf8, 255, + 0xe8, 0x00, 0xf8, 255, 0x00, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, + 0xe8, 0x00, 0xf8, 255, 0x00, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255 ]; var target_data; @@ -1269,22 +1344,96 @@ describe('Remote Frame Buffer Protocol Client', function() { client._display._fb_height = 4; client._display._viewportLoc.w = 4; client._display._viewportLoc.h = 4; - client._fb_Bpp = 4; + client._pixelFormat.Bpp = 4; + client._destBuff = new Uint8Array(client._fb_width * client._fb_height * 4); }); - it('should handle the RAW encoding', function () { - var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, - { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; - // data is in bgrx - var rects = [ - [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0], - [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]]; - send_fbu_msg(info, rects, client); - expect(client._display).to.have.displayed(target_data); + // warning: the fbupdates *overlap* so you have to send all rects for the numbers + // to even make sense; this means (ironically) no iterative building of your tests + describe('should handle the RAW encoding', function () { + it('should handle 24bit depth (RGBX888) @ 32bpp [native]', function () { + client._convertColor = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.red_shift = 0; + client._pixelFormat.red_max = 255; + client._pixelFormat.green_shift = 8; + client._pixelFormat.green_max = 255; + client._pixelFormat.blue_shift = 16; + client._pixelFormat.blue_max = 255; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0xf8, 0x00, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0xf8, 0x00, 0x00, 0], + [0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0], + [0xe8, 0x00, 0xf8, 0, 0x00, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0], + [0xe8, 0x00, 0xf8, 0, 0x00, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle 24bit depth (BGRX888) @ 32bpp', function () { + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x00, 0x00, 0xf8, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0x00, 0xf8, 0], + [0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0], + [0xf8, 0x00, 0xe8, 0, 0xf8, 0xe8, 0x00, 0, 0xf8, 0xe8, 0xa8, 0, 0xf8, 0xe8, 0xa8, 0], + [0xf8, 0x00, 0xe8, 0, 0xf8, 0xe8, 0x00, 0, 0xf8, 0xe8, 0xa8, 0, 0xf8, 0xe8, 0xa8, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + // for wisdom: perl -e '($w, $r, $g, $b) = @ARGV; $W=2**$w; $nb = $b*($W/256); $ng = $g*($W/256); $nr = $r*($W/256); printf "%f:%f:%f %04x\n", $nr, $ng, $nb, unpack("S", pack("n", ($nr << (2*$w)) | ($ng << (1*$w)) | ($nb << (0*$w))))' 5 0 248 0 + it('should handle 15bit depth (BGR555) @ 16bpp', function () { + client._convertColor = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.Bpp = 2; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x00, 0x7c, 0xe0, 0x03, 0xe0, 0x03, 0x00, 0x7c], + [0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00], + [0x1f, 0x74, 0xbf, 0x03, 0xbf, 0x57, 0xbf, 0x57], + [0x1f, 0x74, 0xbf, 0x03, 0xbf, 0x57, 0xbf, 0x57]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle 15bit depth (BGR555) @ 16bpp big-endian', function () { + client._convertColor = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.Bpp = 2; + client._pixelFormat.big_endian = true; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x7c, 0x00, 0x03, 0xe0, 0x03, 0xe0, 0x7c, 0x00], + [0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f], + [0x74, 0x1f, 0x03, 0xbf, 0x57, 0xbf, 0x57, 0xbf], + [0x74, 0x1f, 0x03, 0xbf, 0x57, 0xbf, 0x57, 0xbf]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); }); it('should handle the COPYRECT encoding', function () { @@ -1319,7 +1468,7 @@ describe('Remote Frame Buffer Protocol Client', function() { rect.push16(2); // width: 2 rect.push16(2); // height: 2 rect.push(0xff); // becomes ff0000ff --> #0000FF color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF color rect.push(0x00); rect.push(0xff); rect.push16(2); // x: 2 @@ -1346,7 +1495,8 @@ describe('Remote Frame Buffer Protocol Client', function() { client._display._fb_height = 4; client._display._viewportLoc.w = 4; client._display._viewportLoc.h = 4; - client._fb_Bpp = 4; + client._pixelFormat.Bpp = 4; + client._destBuff = new Uint8Array(client._fb_width * client._fb_height * 4); }); it('should handle a tile with fg, bg specified, normal subrects', function () { @@ -1490,6 +1640,97 @@ describe('Remote Frame Buffer Protocol Client', function() { // TODO(directxman12): test this }); + describe('the ATEN encoding handler', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + client._rfb_atenikvm = true; + // start large, then go small + client._fb_width = 10000; + client._fb_height = 10000; + client._display._fb_width = 10000; + client._display._fb_height = 10000; + client._display._viewportLoc.w = 10000; + client._display._viewportLoc.h = 10000; + client._convertColor = true; + client._pixelFormat.Bpp = 2; + client._pixelFormat.big_endian = false; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + client._destBuff = new Uint8Array(client._fb_width * client._fb_height * 4); + }); + + var aten_target_data_arr = [0xa8, 0xe8, 0xf8, 0xff]; + var aten_target_data; + + before(function () { + for (var i = 0; i < 10; i++) { + aten_target_data_arr = aten_target_data_arr.concat(aten_target_data_arr); + } + aten_target_data = new Uint8Array(aten_target_data_arr); + }); + + it('should handle subtype subrect encoding', function () { + var info = [{ x: 0, y: 0, width: 32, height: 32, encoding: 0x59 }]; + var rect = []; + + rect.push32(0); // padding + rect.push32(2082); // 10 + 32x32x2Bpp + 6*(num of subrects) + + rect.push8(0); // type + rect.push8(0); // padding + + rect.push32(4); // num of subrects (32/16)*(32/16) + rect.push32(2082); // length (again) + + for (var y = 0; y < 2; y++) { + for (var x = 0; x < 2; x++) { + rect.push16(0); // a + rect.push16(0); // b + rect.push8(y); + rect.push8(x); + + for (var i = 0; i < 16*16; i++) { + rect.push16(0xbf57); + } + } + } + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(aten_target_data); + }); + + it('should handle subtype RAW encoding', function () { + // do not use encoding=0x59 here, as rfb.js should override it + var info = [{ x: 0, y: 0, width: 32, height: 32, encoding: 0x00 }]; + var rect = []; + + rect.push32(0); // padding + rect.push32(2058); // 10 + 32x32x2Bpp + + rect.push8(1); // type + rect.push8(0); // padding + + rect.push32(0); // padding + rect.push32(2058); // length (again) + + for (var i = 0; i < 32*32; i++) { + rect.push16(0xbf57); + } + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(aten_target_data); + }); + }); + it('should handle the DesktopSize pseduo-encoding', function () { client.set_onFBResize(sinon.spy()); sinon.spy(client._display, 'resize'); diff --git a/tests/test.util.js b/tests/test.util.js index 7d6524a3e..623db4a4c 100644 --- a/tests/test.util.js +++ b/tests/test.util.js @@ -4,7 +4,7 @@ var assert = chai.assert; var expect = chai.expect; -describe('Utils', function() { +describe('Utils', function () { "use strict"; describe('Array instance methods', function () { @@ -51,6 +51,44 @@ describe('Utils', function() { }); }); + describe('Array class methods', function () { + + var expectTypedArrayEq = function (cls, arr) { + var other = cls.from(arr); + + expect(other).to.be.instanceof(cls); + + // N.B.: We might be tempted to say 'expect(other).to.deep.equal(arr);', but that would be incorrect in the + // situation where 'arr' is not an instance of 'cls'. + expect(other.length).to.equal(arr.length); + for (var i = 0; i < arr.length; ++i) + expect(other[i]).to.equal(arr[i]); + + // TODO: test deep/shallow copy behavior? + }; + + describe('Array.from', function () { + it('should create a new object with the same type and contents', function () { + var arr = [5, 4, 3]; + expectTypedArrayEq(Array, arr); + }); + }); + + // As a stand-in for all of the TypedArray classes + describe('Int32Array.from', function () { + it('should create a new object with the same type and contents', function () { + var arr = new Int32Array(3); + arr[0] = 5; arr[1] = 4; arr[2] = 3; // want to do this without from(), obviously + expectTypedArrayEq(Int32Array, arr); + }); + + it('should return an Int32Array even if the argument is a different type of array', function () { + var arr = [5, 4, 3]; + expectTypedArrayEq(Int32Array, arr); + }); + }); + }); + describe('logging functions', function () { beforeEach(function () { sinon.spy(console, 'log'); diff --git a/vnc.html b/vnc.html index f64c750cc..b4ef036b8 100644 --- a/vnc.html +++ b/vnc.html @@ -10,9 +10,9 @@ This file is licensed under the 2-Clause BSD license (see LICENSE.txt). Connect parameters are provided in query string: - http://example.com/?host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/?host=HOST&port=PORT&encrypt=1&convertColor=1 or the fragment: - http://example.com/#host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/#host=HOST&port=PORT&encrypt=1&convertColor=1 --> noVNC @@ -158,7 +158,7 @@
  • Encrypt
  • -
  • True Color
  • +
  • Convert Color
  • Local Cursor
  • Clip to Window
  • Shared Mode
  • @@ -188,6 +188,34 @@
    + + + +
@@ -220,6 +248,6 @@

no
VNC

- + diff --git a/vnc_auto.html b/vnc_auto.html index 73174713d..742dfcb0f 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -10,9 +10,9 @@ This file is licensed under the 2-Clause BSD license (see LICENSE.txt). Connect parameters are provided in query string: - http://example.com/?host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/?host=HOST&port=PORT&encrypt=1&convertColor=1 or the fragment: - http://example.com/#host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/#host=HOST&port=PORT&encrypt=1&convertColor=1 --> noVNC @@ -79,6 +79,7 @@ // Load supporting scripts Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", + "ast2100.js", "ast2100idct.js", "ast2100util.js", "ast2100const.js", "inflator.js", "rfb.js", "keysym.js"]); var rfb; @@ -225,11 +226,13 @@ rfb = new RFB({'target': $D('noVNC_canvas'), 'encrypt': WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:")), - 'repeaterID': WebUtil.getConfigVar('repeaterID', ''), - 'true_color': WebUtil.getConfigVar('true_color', true), - 'local_cursor': WebUtil.getConfigVar('cursor', true), - 'shared': WebUtil.getConfigVar('shared', true), - 'view_only': WebUtil.getConfigVar('view_only', false), + 'repeaterID': WebUtil.getQueryVar('repeaterID', ''), + 'true_color': WebUtil.getQueryVar('convertColor', true), + 'local_cursor': WebUtil.getQueryVar('cursor', true), + 'shared': WebUtil.getQueryVar('shared', true), + 'view_only': WebUtil.getQueryVar('view_only', false), + 'ast2100_quality': WebUtil.getQueryVar('ast2100_quality', -1), + 'ast2100_subsamplingMode': WebUtil.getQueryVar('ast2100_subsamplingMode', -1), 'onUpdateState': updateState, 'onXvpInit': xvpInit, 'onPasswordRequired': passwordRequired, @@ -238,7 +241,6 @@ updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); return; // don't continue trying to connect } - rfb.connect(host, port, password, path); };