From 0d048acaca845a86eab8949b685a352376a636af Mon Sep 17 00:00:00 2001 From: Kevin Kelley Date: Wed, 11 Jan 2017 23:43:50 -0800 Subject: [PATCH] Implement ATEN AST2100 (0x59) support Tweak heuristic used to identify iKVM servers; it now correctly detects all of the SuperMicro hardware that we've tested it against. Modify test case for TightVNC to avoid triggering iKVM heuristic. --- README.md | 3 +- app/ui.js | 4 +- core/ast2100/README.md | 49 ++++ core/ast2100/ast2100.js | 482 +++++++++++++++++++++++++++++++++++ core/ast2100/ast2100const.js | 337 ++++++++++++++++++++++++ core/ast2100/ast2100idct.js | 214 ++++++++++++++++ core/ast2100/ast2100util.js | 284 +++++++++++++++++++++ core/display.js | 1 + core/rfb.js | 203 +++++++++++++-- karma.conf.js | 4 + tests/frame4.hex | 1 + tests/test.ast2100.js | 384 ++++++++++++++++++++++++++++ tests/test.rfb.js | 34 ++- vnc_auto.html | 4 +- 14 files changed, 1976 insertions(+), 28 deletions(-) create mode 100644 core/ast2100/README.md create mode 100644 core/ast2100/ast2100.js create mode 100644 core/ast2100/ast2100const.js create mode 100644 core/ast2100/ast2100idct.js create mode 100644 core/ast2100/ast2100util.js create mode 100644 tests/frame4.hex create mode 100644 tests/test.ast2100.js diff --git a/README.md b/README.md index c926d360..8fb2088a 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,8 @@ WebSockets to TCP socket proxy. There is a python proxy included * Original Logo : Michael Sersen * tight encoding : Michael Tinglof (Mercuri.ca) * pixel format conversion : [Alexander Clouter](http://www.digriz.org.uk/) - * ATEN iKVM support : [Alexander Clouter](http://www.digriz.org.uk/) + * 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) diff --git a/app/ui.js b/app/ui.js index 52ecd88c..a0feae6a 100644 --- a/app/ui.js +++ b/app/ui.js @@ -70,7 +70,9 @@ var UI; WebUtil.load_scripts( {'core': ["base64.js", "websock.js", "des.js", "input/keysymdef.js", "input/xtscancodes.js", "input/util.js", "input/devices.js", - "display.js", "inflator.js", "rfb.js", "input/keysym.js"]}); + "display.js", "inflator.js", "rfb.js", "input/keysym.js", + "ast2100/ast2100const.js", "ast2100/ast2100util.js", + "ast2100/ast2100idct.js", "ast2100/ast2100.js"]}); window.onscriptsload = function () { UI.load(); }; /* [end skip-as-module] */ diff --git a/core/ast2100/README.md b/core/ast2100/README.md new file mode 100644 index 00000000..aeeb791d --- /dev/null +++ b/core/ast2100/README.md @@ -0,0 +1,49 @@ +# AST2100 (0x57) encoding support + +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-2017 Kevin Kelley + +### 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. Disabling chroma + subsampling will allow the encoder to use VQ when there are few colors (e.g. when you are looking at a terminal); VQ + data doesn't have the same quality issue. + + - 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. + +### 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.) diff --git a/core/ast2100/ast2100.js b/core/ast2100/ast2100.js new file mode 100644 index 00000000..ce40d647 --- /dev/null +++ b/core/ast2100/ast2100.js @@ -0,0 +1,482 @@ +"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-2017 Kevin Kelley . + +This implementation is the product of clean-room reverse engineering (that is, I +am not and have never been subject to enondisclosure agreements, nor have I had +access to proprietary information, related to the subject matter of this +project). +*/ + +var verboseDebug = false; +var verboseMcuCount = false; +var traceUpdates = false; +var verboseStats = false; +var verboseVideoSettings = true; + + +var Ast2100Decoder; + + +(function () { + "use strict"; + + Ast2100Decoder = function (defaults) { + + this._blitCallback = defaults.blitCallback; + 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; + }, + + 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 + if (this.subsamplingMode != subsamplingMode) { + if (verboseVideoSettings) + console.log('decode(): new subsampling mode: '+subsamplingMode); + this.subsamplingMode = subsamplingMode; + } + + // 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! + + 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; + } + 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; + } + if (this.subsamplingMode != 422 && this.subsamplingMode != 444) + throw 'Unexpected value for subsamplingMode: 0x' + fmt_u16(this.subsamplingMode); + + // 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; + } + + // The 0x9 "end-of-frame" block is included in this count. + if (traceUpdates) + console.log("decode(): finished after "+mcuIdx+" blocks"); + + 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 + 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); + + // N.B.(kelleyk): 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 the more common two's-complement + // 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/core/ast2100/ast2100const.js b/core/ast2100/ast2100const.js new file mode 100644 index 00000000..87d94576 --- /dev/null +++ b/core/ast2100/ast2100const.js @@ -0,0 +1,337 @@ +/* (c) Copyright 2015-2017 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/core/ast2100/ast2100idct.js b/core/ast2100/ast2100idct.js new file mode 100644 index 00000000..5677c4f1 --- /dev/null +++ b/core/ast2100/ast2100idct.js @@ -0,0 +1,214 @@ +/* (c) Copyright 2015-2017 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) { + // XXX(kelleyk): This offset is baked into the range limit table in the ATEN + // stuff and in libjpeg; see commpent above prepare_range_limit_table() in + // jdmaster.c. + x += 128; + + 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 + +// N.B.(kelleyk): 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/core/ast2100/ast2100util.js b/core/ast2100/ast2100util.js new file mode 100644 index 00000000..3dea96e7 --- /dev/null +++ b/core/ast2100/ast2100util.js @@ -0,0 +1,284 @@ +/* (c) Copyright 2015-2017 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 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 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) { + + // ATEN scan data does not treat 0xFF bytes specially. + this._interpretMarkers = false; + // True iff we've encountered "end-of-image". + this._eoi = false; + + 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) { + // JavaScript's bitwise operators have this limitation. + if (bits >= 32) + 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); + // 1 to 16, plus a useless 0th element to make indexing nicer. + this._bits = new Uint8Array(17); + + 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/core/display.js b/core/display.js index 5a6df0e6..a61b22f6 100644 --- a/core/display.js +++ b/core/display.js @@ -467,6 +467,7 @@ // 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); + // XXX(kelleyk): Why are we allocating *two* new arrays here? new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); this._renderQ_push({ 'type': 'blit', diff --git a/core/rfb.js b/core/rfb.js index 9c630841..6218127e 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -21,9 +21,11 @@ * import XK2HID from "./input/keysym"; * import XtScancode from "./input/xtscancodes"; * import Inflator from "./inflator.mod"; + * import Ast2100Decoder from "./ast2100/ast2100"; + * import arrayEq from "./ast2100/ast2100util"; */ /*jslint white: false, browser: true */ -/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, KeyTable, Inflator, XtScancode */ +/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, KeyTable, Inflator, XtScancode, Ast2100Decoder, arrayEq */ /* [module] export default */ function RFB(defaults) { "use strict"; @@ -44,6 +46,7 @@ this._rfb_disconnect_reason = ""; this._rfb_tightvnc = false; + this._rfb_atenikvm = false; this._rfb_xvp_ver = 0; // In preference order @@ -54,7 +57,13 @@ ['HEXTILE', 0x05 ], ['RRE', 0x02 ], ['RAW', 0x00 ], - ['ATEN', 0x59 ], + + // ATEN iKVM encodings + ['ATEN_AST2100', 0x57 ], + ['ATEN_ASTJPEG', 0x58 ], + ['ATEN_HERMON', 0x59 ], + ['ATEN_YARKON', 0x60 ], + ['ATEN_PILOT3', 0x61 ], // Psuedo-encoding settings @@ -369,6 +378,13 @@ } }, + // Tell the ATEN iKVM server to change the quantization tables and/or + // type of subsampling that it uses to encode video. + atenChangeVideoSettings: function (lumaQt, chromaQt, subsamplingMode) { + RFB.messages.atenChangeVideoSettings(this._sock, lumaQt, chromaQt, subsamplingMode); + this._sock.flush(); + }, + // Private methods @@ -803,6 +819,8 @@ this._rfb_auth_scheme = 0; var types = this._sock.rQshiftBytes(num_types); Util.Debug("Server security types: " + types); + // N.B.(kelleyk): Deliberately copy-constructed, since the underlying array is reused. + this._rfb_server_supported_security_types = Array.from(types); for (var i = 0; i < types.length; i++) { switch (types[i]) { case 1: // None @@ -841,8 +859,9 @@ var aten_sep = this._aten_password_sep; var aten_auth = this._rfb_password.split(aten_sep); if (aten_auth.length < 2) { - this._onPasswordRequired(this, 'ATEN iKVM credentials required (user' + aten_sep + - 'password) -- got only ' + this._rfb_password); + this._onPasswordRequired( + this, + 'ATEN iKVM credentials required (user' + aten_sep + 'password)'); return false; } @@ -850,6 +869,7 @@ this._convert_color = true; if (this._rfb_tightvnc) { + // N.B.(kelleyk): We've already "skipped" the four bytes that we read into numTunnels. this._rfb_tightvnc = false; } else { this._sock.rQskipBytes(4); @@ -904,6 +924,8 @@ }, _negotiate_tight_tunnels: function (numTunnels) { + // N.B.(kelleyk): 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' } }; @@ -934,15 +956,30 @@ }, _negotiate_tight_auth: function () { - var numTunnels; // NB(directxman12): this is only in scope within the following block, + var numTunnels = 0; // 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; } numTunnels = this._sock.rQshift32(); - if (this._rfb_version === 3.8 && (numTunnels & 0xffff0ff0) >>> 0 === 0xaff90fb0) { return this._negotiate_aten_auth(); } - if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } - this._rfb_tightvnc = true; + + // N.B.(kelleyk): 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; } if (numTunnels > 0) { this._negotiate_tight_tunnels(numTunnels); @@ -960,9 +997,14 @@ if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } - // Newer X10 Supermicro motherboards get here - if (this._rfb_version === 3.8 && numTunnels === 0 && subAuthCount === 0) { - Util.Warn("Newer ATEN iKVM detected, you may get an 'unsupported encoding 87'"); + // Newer X10 Supermicro motherboards get here. + // N.B.(kelleyk): 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(); } @@ -1156,7 +1198,7 @@ // - X9DRL-3F/X9DRL-6F // - X10SLD // - // Not supported (uses encoding 87): + // Supported using the ATEN "AST2100" encoding (0x57 / 87): // - X10SL7-F // - X10SLD-F // - X10SLM-F @@ -1177,7 +1219,10 @@ this._fb_width = 10000; this._fb_height = 10000; - // lies about what it supports + // TODO(kelleyk): 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? Util.Warn("ATEN iKVM lies and only does 15 bit depth with RGB555"); this._convert_color = true; this._pixelFormat.bpp = 16; @@ -1560,6 +1605,9 @@ return false; } + // TODO(kelleyk): 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; @@ -1877,6 +1925,26 @@ 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; @@ -2766,12 +2834,12 @@ Util.Error("Server sent compress level pseudo-encoding"); }, - ATEN: function () { + ATEN_HERMON: function () { if (this._FBU.aten_len === -1) { this._FBU.bytes = 8; - if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; } + if (this._sock.rQwait("ATEN_HERMON", this._FBU.bytes)) { return false; } this._FBU.bytes = 0; - this._sock.rQskipBytes(4); + this._sock.rQskipBytes(4); // N.B.(kelleyk): This is the "mysteryFlag". this._FBU.aten_len = this._sock.rQshift32(); if (this._FBU.width === 64896 && this._FBU.height === 65056) { @@ -2784,25 +2852,25 @@ return true; } if (this._fb_width !== this._FBU.width && this._fb_height !== this._FBU.height) { - Util.Debug(">> ATEN resize desktop"); + 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 resize desktop"); + Util.Debug("<< ATEN_HERMON resize desktop"); } } if (this._FBU.aten_type === -1) { this._FBU.bytes = 10; - if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; } + 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 RAW len mis-match'); + return this._fail('ATEN_HERMON RAW len mis-match'); } this._FBU.aten_len -= 10; } @@ -2811,7 +2879,7 @@ 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", this._FBU.bytes)) { return false; } + 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(); @@ -2828,7 +2896,7 @@ if (this._FBU.bytes > 0) return false; break; default: - return this._fail('unknown ATEN type: '+this._FBU.aten_type); + return this._fail('unknown ATEN_HERMON type: '+this._FBU.aten_type); } } @@ -2843,6 +2911,97 @@ 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; } + + // N.B.(kelleyk): 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!'; + + // N.B.(kelleyk): 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; + + // These are -640 and -480 (as int16s), as in the other ATEN encodings. + if (this._FBU.width === 64896 && this._FBU.height === 65056) { + 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(kelleyk): 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 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); + } + }); + } + + // N.B.(kelleyk): 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._aten_ast2100_dec.decode(data); return true; } }; diff --git a/karma.conf.js b/karma.conf.js index 5f3c20b9..07afe5a0 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -123,6 +123,10 @@ module.exports = function(config) { 'core/des.js', 'core/display.js', 'core/inflator.js', + 'core/ast2100/ast2100const.js', + 'core/ast2100/ast2100util.js', + 'core/ast2100/ast2100idct.js', + 'core/ast2100/ast2100.js', 'tests/test.*.js' ], diff --git a/tests/frame4.hex b/tests/frame4.hex new file mode 100644 index 00000000..bd2d11f9 --- /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 00000000..d1b46b68 --- /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.rfb.js b/tests/test.rfb.js index 3e8a1640..68fe54ec 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -759,8 +759,17 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_init_state = 'Security'; }); - function send_security(type, cl) { - cl._sock._websocket._receive_data(new Uint8Array([1, type])); + // N.B.(kelleyk): '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 () { @@ -906,7 +915,13 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock._websocket._open(); client._rfb_init_state = 'Security'; client._rfb_version = 3.8; - send_security(16, client); + // N.B.(kelleyk): 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 }); @@ -995,6 +1010,8 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock._websocket._open(); client._rfb_init_state = 'Security'; client._rfb_version = 3.8; + // N.B.(kelleyk): 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'; @@ -1018,6 +1035,17 @@ describe('Remote Frame Buffer Protocol Client', function() { 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); + }); }); }); diff --git a/vnc_auto.html b/vnc_auto.html index ca158e10..cfabbe09 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -81,7 +81,9 @@ WebUtil.load_scripts({ 'core': ["base64.js", "websock.js", "des.js", "input/keysymdef.js", "input/xtscancodes.js", "input/util.js", "input/devices.js", - "display.js", "inflator.js", "rfb.js", "input/keysym.js"]}); + "ast2100/ast2100.js", "ast2100/ast2100idct.js", "ast2100/ast2100util.js", "ast2100/ast2100const.js", + "display.js", "inflator.js", "rfb.js", "input/keysym.js"], + 'app': [WebUtil.getLanguageFileLocation(), "webutil.js"]}); var rfb; var resizeTimeout;