diff --git a/apps/Makefile b/apps/Makefile index 8a96b4e..9b947ca 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -17,6 +17,11 @@ js: - rm assets/scripts/main.* for f in $(coffees); do (cat "$${f}"; echo) >> assets/scripts/main.coffee; done coffee --compile assets/scripts/main.coffee + coffee --compile assets/coffee/decoder.coffee + sed '2d' assets/coffee/decoder.js > assets/scripts/tmp.js + head -n -1 assets/scripts/tmp.js > assets/scripts/decoder.js + -rm assets/coffee/decoder.js + -rm assets/scripts/tmp.js -rm assets/scripts/main.coffee clean: diff --git a/apps/assets/coffee/WVNC.coffee b/apps/assets/coffee/WVNC.coffee index 53bb60b..a9fd59c 100644 --- a/apps/assets/coffee/WVNC.coffee +++ b/apps/assets/coffee/WVNC.coffee @@ -7,127 +7,132 @@ class WVNC extends window.classes.BaseObject @canvas = undefined @canvas = ($ @args[1])[0] if @args and @args.length > 1 @buffer = $("")[0] - @counter = 0 + @lastPose = { x: 0, y: 0 } + @scale = 0.8 + @decoder = new Worker('/assets/scripts/decoder.js') + me = @ + @mouseMask = 0 + @decoder.onmessage = (e) -> + me.process e.data init: () -> me = @ @ready() .then () -> $("#stop").click (e) -> me.socket.close() if me.socket $("#connect").click (e) -> - me.counter = 0 me.openSession() - ($ me.canvas).css "cursor","none" - ($ me.canvas).mousemove (e) -> - rect = me.canvas.getBoundingClientRect() - x = Math.floor(e.clientX - rect.left) - y = Math.floor(e.clientY - rect.top) - me.sendPointEvent x, y, 0 + me.initInputEvent() .catch (m, s) -> console.error(m, s) + initInputEvent: () -> + me = @ + + getMousePos = (e) -> + rect = me.canvas.getBoundingClientRect() + pos= + x: Math.floor((e.clientX - rect.left) / me.scale) + y: Math.floor((e.clientY - rect.top) / me.scale) + return pos + + sendMouseLocation = (e) -> + p = getMousePos e + me.sendPointEvent p.x, p.y, me.mouseMask + + return unless me.canvas + ($ me.canvas).css "cursor", "none" + ($ me.canvas).contextmenu (e) -> + e.preventDefault() + return false + + ($ me.canvas).mousemove (e) -> sendMouseLocation e + + ($ me.canvas).mousedown (e) -> + state = 1 << e.button + me.mouseMask = me.mouseMask | state + sendMouseLocation e + #e.preventDefault() + + ($ me.canvas).mouseup (e) -> + state = 1 << e.button + me.mouseMask = me.mouseMask & (~state) + sendMouseLocation e + #e.preventDefault() + + me.canvas.onkeydown = me.canvas.onkeyup = me.canvas.onkeypress = (e) -> + # get the key code + if e.key is "Shift" + code = 16 + else if e.ctrlKey + code = 17 + else if e.altKey + code = 18 + else if e.metaKey + code = 91 + else + code = String.charCodeAt(e.key) + if e.type is "keydown" + me.sendKeyEvent code, 1 + else if e.type is "keyup" + me.sendKeyEvent code, 0 + e.preventDefault() + + # mouse wheel event + hamster = Hamster @canvas + hamster.wheel (event, delta, deltaX, deltaY) -> + p = getMousePos event.originalEvent + if delta > 0 + me.sendPointEvent p.x, p.y, 8 + me.sendPointEvent p.x, p.y, 0 + return + me.sendPointEvent p.x, p.y, 16 + me.sendPointEvent p.x, p.y, 0 + initCanvas: (w, h , d) -> me = @ @depth = d @buffer.width = w @buffer.height = h + @resolution = + w: w, + h: h, + depth: @depth + @decoder.postMessage @resolution ctx = @buffer.getContext('2d') data = ctx.createImageData w, h ctx.putImageData data, 0, 0 - #@callback = () -> - # me.draw() - @draw() - decodeFB: (d) -> - # the zlib is slower than expected - switch d.flag - when 0x0 # raw data - @drawRaw d.x, d.y, d.w, d.h, d.pixels - when 0x1 # jpeg data - @drawJPEG d.x, d.y, d.pixels - when 0x2 # raw compress in zlib format - pixels = pako.inflate(d.pixels) - @drawRaw d.x, d.y, d.w, d.h, pixels - when 0x3 # jpeg compress in zlib format - jpeg = pako.inflate(d.pixels) - @drawJPEG d.x, d.y, jpeg - - drawJPEG: (x, y, data) -> - me = @ - blob = new Blob [data], { type: "image/jpeg" } - reader = new FileReader() - reader.onloadend = () -> - hiddenImage = new Image() - hiddenImage.style.position = "absolute" - hiddenImage.style.left = "-99999px" - document.body.appendChild hiddenImage - hiddenImage.onload = () -> - ctx = me.buffer.getContext '2d' - ctx.drawImage hiddenImage, x, y - document.body.removeChild hiddenImage - me.draw() - hiddenImage.src = reader.result - reader.readAsDataURL blob - - drawRaw: (x, y, w, h, pixels) -> + process: (data) -> + data.pixels = new Uint8ClampedArray data.pixels + data.pixels = data.pixels.subarray 10 if data.flag is 0 and @resolution.depth is 32 ctx = @buffer.getContext('2d') - ctx.globalAlpha = 1.0 - imgData = ctx.createImageData w, h - imgData.data.set @getCanvasImageData(pixels, w, h) - ctx.putImageData imgData, x, y - @counter = @counter + 1 + imgData = ctx.createImageData data.w, data.h + imgData.data.set data.pixels + ctx.putImageData imgData, data.x, data.y + + @draw() if data.x isnt @lastPose.x or data.y > @resolution.h - 10 + @lastPose = { x: data.x, y: data.y } + + + setScale: (n) -> + @scale = n @draw() - #if @counter > 50 - # @draw() - # @couter = 0 - - getCanvasImageData: (pixels, w, h) -> - return pixels if @depth is 32 - step = @depth / 8 - npixels = pixels.length / step - data = new Uint8ClampedArray w * h * 4 - for i in [0..npixels - 1] - value = 0 - value = value | pixels[i * step + j] << (j * 8) for j in [0..step - 1] - pixel = @pixelValue value - data[i * 4] = pixel.r - data[i * 4 + 1] = pixel.g - data[i * 4 + 2] = pixel.b - data[i * 4 + 3] = pixel.a - return data draw: () -> if not @socket return - scale = 1.0 - w = @buffer.width * scale - h = @buffer.height * scale + + w = @buffer.width * @scale + h = @buffer.height * @scale @canvas.width = w @canvas.height = h ctx = @canvas.getContext "2d" ctx.save() - ctx.scale scale, scale + ctx.scale @scale, @scale ctx.clearRect 0, 0, w, h ctx.drawImage @buffer, 0, 0 ctx.restore() - pixelValue: (value) -> - pixel = - r: 255 - g: 255 - b: 255 - a: 255 - #console.log("len is" + arr.length) - if @depth is 24 or @depth is 32 - pixel.r = value & 0xFF - pixel.g = (value >> 8) & 0xFF - pixel.b = (value >> 16) & 0xFF - else if @depth is 16 - pixel.r = (value & 0x1F) * (255 / 31) - pixel.g = ((value >> 5) & 0x3F) * (255 / 63) - pixel.b = ((value >> 11) & 0x1F) * (255 / 31) - #console.log pixel - return pixel - openSession: () -> me = @ @socket.close() if @socket @@ -145,20 +150,45 @@ class WVNC extends window.classes.BaseObject console.log "socket closed" initConnection: () -> - vncserver = "192.168.1.8:5900" - @socket.send(@buildCommand 0x01, vncserver) + vncserver = "localhost:5901" + data = new Uint8Array vncserver.length + 5 + data[0] = 16 # bbp + ### + flag: + 0: raw data no compress + 1: jpeg no compress + 2: raw data compressed by zlib + 3: jpeg data compressed by zlib + ### + data[1] = 2 + data[2] = 50 # jpeg quality + ## rate in milisecond + rate = 30 + data[3] = rate & 0xFF + data[4] = (rate >> 8) & 0xFF - sendPointEvent: (x,y,mask) -> + data.set (new TextEncoder()).encode(vncserver), 5 + @socket.send(@buildCommand 0x01, data) + + sendPointEvent: (x, y, mask) -> return unless @socket data = new Uint8Array 5 data[0] = x & 0xFF data[1] = x >> 8 data[2] = y & 0xFF data[3] = y >> 8 - data[4] = 0 + data[4] = mask #console.log x,y @socket.send( @buildCommand 0x05, data ) + sendKeyEvent: (code, v) -> + return unless @socket + data = new Uint8Array 2 + data[0] = code + data[1] = v + console.log String.fromCharCode(code), v + @socket.send( @buildCommand 0x06, data ) + buildCommand: (hex, o) -> data = undefined switch typeof o @@ -187,7 +217,7 @@ class WVNC extends window.classes.BaseObject console.log "Error", dec.decode(data) when 0x81 console.log "Request for password" - pass = "sang" + pass = "!x$@n9" @socket.send (@buildCommand 0x02, pass) when 0x82 console.log "Request for login" @@ -208,22 +238,15 @@ class WVNC extends window.classes.BaseObject @socket.send(@buildCommand 0x04, 1) when 0x84 #console.log "update" - d = {} - d.x = data[1] | (data[2]<<8) - d.y = data[3] | (data[4]<<8) - d.w = data[5] | (data[6]<<8) - d.h = data[7] | (data[8]<<8) - d.flag = data[9] - #console.log zlib - d.pixels = data.subarray 10 - @decodeFB d + @decoder.postMessage data.buffer, [data.buffer] + #@decodeFB d # ack #@socket.send(@buildCommand 0x04, 1) else console.log cmd WVNC.dependencies = [ - "/assets/scripts/pako.min.js" + "/assets/scripts/hamster.js" ] makeclass "WVNC", WVNC \ No newline at end of file diff --git a/apps/assets/coffee/decoder.coffee b/apps/assets/coffee/decoder.coffee new file mode 100644 index 0000000..978f4ee --- /dev/null +++ b/apps/assets/coffee/decoder.coffee @@ -0,0 +1,84 @@ +#zlib library +importScripts('pako.min.js') +# jpeg library +importScripts('jpeg-decoder.js') +resolution = undefined +pixelValue = (value, depth) -> + pixel = + r: 255 + g: 255 + b: 255 + a: 255 + #console.log("len is" + arr.length) + if depth is 24 or depth is 32 + pixel.r = value & 0xFF + pixel.g = (value >> 8) & 0xFF + pixel.b = (value >> 16) & 0xFF + else if depth is 16 + pixel.r = (value & 0x1F) * (255 / 31) + pixel.g = ((value >> 5) & 0x3F) * (255 / 63) + pixel.b = ((value >> 11) & 0x1F) * (255 / 31) + #console.log pixel + return pixel + +getImageData = (d) -> + return d.pixels if resolution.depth is 32 + step = resolution.depth / 8 + npixels = d.pixels.length / step + data = new Uint8ClampedArray d.w * d.h * 4 + for i in [0..npixels - 1] + value = 0 + value = value | d.pixels[i * step + j] << (j * 8) for j in [0..step - 1] + pixel = pixelValue value, resolution.depth + data[i * 4] = pixel.r + data[i * 4 + 1] = pixel.g + data[i * 4 + 2] = pixel.b + data[i * 4 + 3] = pixel.a + return data + +decodeRaw = (d) -> + d.pixels = getImageData d + return d + +decodeJPEG = (d) -> + raw = decode d.pixels, { useTArray: true, colorTransform: true } + d.pixels = raw.data + return d + ### + blob = new Blob [d.pixels], { type: "image/jpeg" } + reader = new FileReader() + reader.onloadend = () -> + d.pixels = reader.result + postMessage d + reader.readAsDataURL blob + ### + +update = (msg) -> + d = {} + data = new Uint8Array msg + d.x = data[1] | (data[2]<<8) + d.y = data[3] | (data[4]<<8) + d.w = data[5] | (data[6]<<8) + d.h = data[7] | (data[8]<<8) + d.flag = data[9] + d.pixels = data.subarray 10 + # the zlib is slower than expected + switch d.flag + when 0x0 # raw data + raw = decodeRaw d + when 0x1 # jpeg data + raw = decodeJPEG(d) + when 0x2 # raw compress in zlib format + d.pixels = pako.inflate(d.pixels) + raw = decodeRaw d + when 0x3 # jpeg compress in zlib format + d.pixels = pako.inflate(d.pixels) + raw = decodeJPEG(d) + return unless raw + raw.pixels = raw.pixels.buffer + # fill the rectangle + postMessage raw, [raw.pixels] + +onmessage = (e) -> + return resolution = e.data if e.data.depth + update e.data \ No newline at end of file diff --git a/apps/assets/scripts/decoder.js b/apps/assets/scripts/decoder.js new file mode 100644 index 0000000..94248eb --- /dev/null +++ b/apps/assets/scripts/decoder.js @@ -0,0 +1,114 @@ +// Generated by CoffeeScript 1.9.3 + var decodeJPEG, decodeRaw, getImageData, onmessage, pixelValue, resolution, update; + + importScripts('pako.min.js'); + + importScripts('jpeg-decoder.js'); + + resolution = void 0; + + pixelValue = function(value, depth) { + var pixel; + pixel = { + r: 255, + g: 255, + b: 255, + a: 255 + }; + if (depth === 24 || depth === 32) { + pixel.r = value & 0xFF; + pixel.g = (value >> 8) & 0xFF; + pixel.b = (value >> 16) & 0xFF; + } else if (depth === 16) { + pixel.r = (value & 0x1F) * (255 / 31); + pixel.g = ((value >> 5) & 0x3F) * (255 / 63); + pixel.b = ((value >> 11) & 0x1F) * (255 / 31); + } + return pixel; + }; + + getImageData = function(d) { + var data, i, j, k, l, npixels, pixel, ref, ref1, step, value; + if (resolution.depth === 32) { + return d.pixels; + } + step = resolution.depth / 8; + npixels = d.pixels.length / step; + data = new Uint8ClampedArray(d.w * d.h * 4); + for (i = k = 0, ref = npixels - 1; 0 <= ref ? k <= ref : k >= ref; i = 0 <= ref ? ++k : --k) { + value = 0; + for (j = l = 0, ref1 = step - 1; 0 <= ref1 ? l <= ref1 : l >= ref1; j = 0 <= ref1 ? ++l : --l) { + value = value | d.pixels[i * step + j] << (j * 8); + } + pixel = pixelValue(value, resolution.depth); + data[i * 4] = pixel.r; + data[i * 4 + 1] = pixel.g; + data[i * 4 + 2] = pixel.b; + data[i * 4 + 3] = pixel.a; + } + return data; + }; + + decodeRaw = function(d) { + d.pixels = getImageData(d); + return d; + }; + + decodeJPEG = function(d) { + var raw; + raw = decode(d.pixels, { + useTArray: true, + colorTransform: true + }); + d.pixels = raw.data; + return d; + + /* + blob = new Blob [d.pixels], { type: "image/jpeg" } + reader = new FileReader() + reader.onloadend = () -> + d.pixels = reader.result + postMessage d + reader.readAsDataURL blob + */ + }; + + update = function(msg) { + var d, data, raw; + d = {}; + data = new Uint8Array(msg); + d.x = data[1] | (data[2] << 8); + d.y = data[3] | (data[4] << 8); + d.w = data[5] | (data[6] << 8); + d.h = data[7] | (data[8] << 8); + d.flag = data[9]; + d.pixels = data.subarray(10); + switch (d.flag) { + case 0x0: + raw = decodeRaw(d); + break; + case 0x1: + raw = decodeJPEG(d); + break; + case 0x2: + d.pixels = pako.inflate(d.pixels); + raw = decodeRaw(d); + break; + case 0x3: + d.pixels = pako.inflate(d.pixels); + raw = decodeJPEG(d); + } + if (!raw) { + return; + } + raw.pixels = raw.pixels.buffer; + return postMessage(raw, [raw.pixels]); + }; + + onmessage = function(e) { + if (e.data.depth) { + return resolution = e.data; + } + return update(e.data); + }; + diff --git a/apps/assets/scripts/hamster.js b/apps/assets/scripts/hamster.js new file mode 100644 index 0000000..ac99dc8 --- /dev/null +++ b/apps/assets/scripts/hamster.js @@ -0,0 +1,327 @@ +/* + * Hamster.js v1.1.2 + * (c) 2013 Monospaced http://monospaced.com + * License: MIT + */ + +(function(window, document){ +'use strict'; + +/** + * Hamster + * use this to create instances + * @returns {Hamster.Instance} + * @constructor + */ +var Hamster = function(element) { + return new Hamster.Instance(element); +}; + +// default event name +Hamster.SUPPORT = 'wheel'; + +// default DOM methods +Hamster.ADD_EVENT = 'addEventListener'; +Hamster.REMOVE_EVENT = 'removeEventListener'; +Hamster.PREFIX = ''; + +// until browser inconsistencies have been fixed... +Hamster.READY = false; + +Hamster.Instance = function(element){ + if (!Hamster.READY) { + // fix browser inconsistencies + Hamster.normalise.browser(); + + // Hamster is ready...! + Hamster.READY = true; + } + + this.element = element; + + // store attached event handlers + this.handlers = []; + + // return instance + return this; +}; + +/** + * create new hamster instance + * all methods should return the instance itself, so it is chainable. + * @param {HTMLElement} element + * @returns {Hamster.Instance} + * @constructor + */ +Hamster.Instance.prototype = { + /** + * bind events to the instance + * @param {Function} handler + * @param {Boolean} useCapture + * @returns {Hamster.Instance} + */ + wheel: function onEvent(handler, useCapture){ + Hamster.event.add(this, Hamster.SUPPORT, handler, useCapture); + + // handle MozMousePixelScroll in older Firefox + if (Hamster.SUPPORT === 'DOMMouseScroll') { + Hamster.event.add(this, 'MozMousePixelScroll', handler, useCapture); + } + + return this; + }, + + /** + * unbind events to the instance + * @param {Function} handler + * @param {Boolean} useCapture + * @returns {Hamster.Instance} + */ + unwheel: function offEvent(handler, useCapture){ + // if no handler argument, + // unbind the last bound handler (if exists) + if (handler === undefined && (handler = this.handlers.slice(-1)[0])) { + handler = handler.original; + } + + Hamster.event.remove(this, Hamster.SUPPORT, handler, useCapture); + + // handle MozMousePixelScroll in older Firefox + if (Hamster.SUPPORT === 'DOMMouseScroll') { + Hamster.event.remove(this, 'MozMousePixelScroll', handler, useCapture); + } + + return this; + } +}; + +Hamster.event = { + /** + * cross-browser 'addWheelListener' + * @param {Instance} hamster + * @param {String} eventName + * @param {Function} handler + * @param {Boolean} useCapture + */ + add: function add(hamster, eventName, handler, useCapture){ + // store the original handler + var originalHandler = handler; + + // redefine the handler + handler = function(originalEvent){ + + if (!originalEvent) { + originalEvent = window.event; + } + + // create a normalised event object, + // and normalise "deltas" of the mouse wheel + var event = Hamster.normalise.event(originalEvent), + delta = Hamster.normalise.delta(originalEvent); + + // fire the original handler with normalised arguments + return originalHandler(event, delta[0], delta[1], delta[2]); + + }; + + // cross-browser addEventListener + hamster.element[Hamster.ADD_EVENT](Hamster.PREFIX + eventName, handler, useCapture || false); + + // store original and normalised handlers on the instance + hamster.handlers.push({ + original: originalHandler, + normalised: handler + }); + }, + + /** + * removeWheelListener + * @param {Instance} hamster + * @param {String} eventName + * @param {Function} handler + * @param {Boolean} useCapture + */ + remove: function remove(hamster, eventName, handler, useCapture){ + // find the normalised handler on the instance + var originalHandler = handler, + lookup = {}, + handlers; + for (var i = 0, len = hamster.handlers.length; i < len; ++i) { + lookup[hamster.handlers[i].original] = hamster.handlers[i]; + } + handlers = lookup[originalHandler]; + handler = handlers.normalised; + + // cross-browser removeEventListener + hamster.element[Hamster.REMOVE_EVENT](Hamster.PREFIX + eventName, handler, useCapture || false); + + // remove original and normalised handlers from the instance + for (var h in hamster.handlers) { + if (hamster.handlers[h] == handlers) { + hamster.handlers.splice(h, 1); + break; + } + } + } +}; + +/** + * these hold the lowest deltas, + * used to normalise the delta values + * @type {Number} + */ +var lowestDelta, + lowestDeltaXY; + +Hamster.normalise = { + /** + * fix browser inconsistencies + */ + browser: function normaliseBrowser(){ + // detect deprecated wheel events + if (!('onwheel' in document || document.documentMode >= 9)) { + Hamster.SUPPORT = document.onmousewheel !== undefined ? + 'mousewheel' : // webkit and IE < 9 support at least "mousewheel" + 'DOMMouseScroll'; // assume remaining browsers are older Firefox + } + + // detect deprecated event model + if (!window.addEventListener) { + // assume IE < 9 + Hamster.ADD_EVENT = 'attachEvent'; + Hamster.REMOVE_EVENT = 'detachEvent'; + Hamster.PREFIX = 'on'; + } + + }, + + /** + * create a normalised event object + * @param {Function} originalEvent + * @returns {Object} event + */ + event: function normaliseEvent(originalEvent){ + var event = { + // keep a reference to the original event object + originalEvent: originalEvent, + target: originalEvent.target || originalEvent.srcElement, + type: 'wheel', + deltaMode: originalEvent.type === 'MozMousePixelScroll' ? 0 : 1, + deltaX: 0, + deltaZ: 0, + preventDefault: function(){ + if (originalEvent.preventDefault) { + originalEvent.preventDefault(); + } else { + originalEvent.returnValue = false; + } + }, + stopPropagation: function(){ + if (originalEvent.stopPropagation) { + originalEvent.stopPropagation(); + } else { + originalEvent.cancelBubble = false; + } + } + }; + + // calculate deltaY (and deltaX) according to the event + + // 'mousewheel' + if (originalEvent.wheelDelta) { + event.deltaY = - 1/40 * originalEvent.wheelDelta; + } + // webkit + if (originalEvent.wheelDeltaX) { + event.deltaX = - 1/40 * originalEvent.wheelDeltaX; + } + + // 'DomMouseScroll' + if (originalEvent.detail) { + event.deltaY = originalEvent.detail; + } + + return event; + }, + + /** + * normalise 'deltas' of the mouse wheel + * @param {Function} originalEvent + * @returns {Array} deltas + */ + delta: function normaliseDelta(originalEvent){ + var delta = 0, + deltaX = 0, + deltaY = 0, + absDelta = 0, + absDeltaXY = 0, + fn; + + // normalise deltas according to the event + + // 'wheel' event + if (originalEvent.deltaY) { + deltaY = originalEvent.deltaY * -1; + delta = deltaY; + } + if (originalEvent.deltaX) { + deltaX = originalEvent.deltaX; + delta = deltaX * -1; + } + + // 'mousewheel' event + if (originalEvent.wheelDelta) { + delta = originalEvent.wheelDelta; + } + // webkit + if (originalEvent.wheelDeltaY) { + deltaY = originalEvent.wheelDeltaY; + } + if (originalEvent.wheelDeltaX) { + deltaX = originalEvent.wheelDeltaX * -1; + } + + // 'DomMouseScroll' event + if (originalEvent.detail) { + delta = originalEvent.detail * -1; + } + + // Don't return NaN + if (delta === 0) { + return [0, 0, 0]; + } + + // look for lowest delta to normalize the delta values + absDelta = Math.abs(delta); + if (!lowestDelta || absDelta < lowestDelta) { + lowestDelta = absDelta; + } + absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX)); + if (!lowestDeltaXY || absDeltaXY < lowestDeltaXY) { + lowestDeltaXY = absDeltaXY; + } + + // convert deltas to whole numbers + fn = delta > 0 ? 'floor' : 'ceil'; + delta = Math[fn](delta / lowestDelta); + deltaX = Math[fn](deltaX / lowestDeltaXY); + deltaY = Math[fn](deltaY / lowestDeltaXY); + + return [delta, deltaX, deltaY]; + } +}; + +if (typeof window.define === 'function' && window.define.amd) { + // AMD + window.define('hamster', [], function(){ + return Hamster; + }); +} else if (typeof exports === 'object') { + // CommonJS + module.exports = Hamster; +} else { + // Browser global + window.Hamster = Hamster; +} + +})(window, window.document); diff --git a/apps/assets/scripts/jpeg-decoder.js b/apps/assets/scripts/jpeg-decoder.js new file mode 100644 index 0000000..98c259c --- /dev/null +++ b/apps/assets/scripts/jpeg-decoder.js @@ -0,0 +1,1018 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* + Copyright 2011 notmasteryet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// - The JPEG specification can be found in the ITU CCITT Recommendation T.81 +// (www.w3.org/Graphics/JPEG/itu-t81.pdf) +// - The JFIF specification can be found in the JPEG File Interchange Format +// (www.w3.org/Graphics/JPEG/jfif3.pdf) +// - The Adobe Application-Specific JPEG markers in the Supporting the DCT Filters +// in PostScript Level 2, Technical Note #5116 +// (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf) + +var JpegImage = (function jpegImage() { + "use strict"; + var dctZigZag = new Int32Array([ + 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 dctCos1 = 4017 // cos(pi/16) + var dctSin1 = 799 // sin(pi/16) + var dctCos3 = 3406 // cos(3*pi/16) + var dctSin3 = 2276 // sin(3*pi/16) + var dctCos6 = 1567 // cos(6*pi/16) + var dctSin6 = 3784 // sin(6*pi/16) + var dctSqrt2 = 5793 // sqrt(2) + var dctSqrt1d2 = 2896 // sqrt(2) / 2 + + function constructor() { + } + + function buildHuffmanTable(codeLengths, values) { + var k = 0, code = [], i, j, length = 16; + while (length > 0 && !codeLengths[length - 1]) + length--; + code.push({children: [], index: 0}); + var p = code[0], q; + for (i = 0; i < length; i++) { + for (j = 0; j < codeLengths[i]; j++) { + p = code.pop(); + p.children[p.index] = values[k]; + while (p.index > 0) { + p = code.pop(); + } + p.index++; + code.push(p); + while (code.length <= i) { + code.push(q = {children: [], index: 0}); + p.children[p.index] = q.children; + p = q; + } + k++; + } + if (i + 1 < length) { + // p here points to last code + code.push(q = {children: [], index: 0}); + p.children[p.index] = q.children; + p = q; + } + } + return code[0].children; + } + + function decodeScan(data, offset, + frame, components, resetInterval, + spectralStart, spectralEnd, + successivePrev, successive) { + var precision = frame.precision; + var samplesPerLine = frame.samplesPerLine; + var scanLines = frame.scanLines; + var mcusPerLine = frame.mcusPerLine; + var progressive = frame.progressive; + var maxH = frame.maxH, maxV = frame.maxV; + + var startOffset = offset, bitsData = 0, bitsCount = 0; + function readBit() { + if (bitsCount > 0) { + bitsCount--; + return (bitsData >> bitsCount) & 1; + } + bitsData = data[offset++]; + if (bitsData == 0xFF) { + var nextByte = data[offset++]; + if (nextByte) { + throw new Error("unexpected marker: " + ((bitsData << 8) | nextByte).toString(16)); + } + // unstuff 0 + } + bitsCount = 7; + return bitsData >>> 7; + } + function decodeHuffman(tree) { + var node = tree, bit; + while ((bit = readBit()) !== null) { + node = node[bit]; + if (typeof node === 'number') + return node; + if (typeof node !== 'object') + throw new Error("invalid huffman sequence"); + } + return null; + } + function receive(length) { + var n = 0; + while (length > 0) { + var bit = readBit(); + if (bit === null) return; + n = (n << 1) | bit; + length--; + } + return n; + } + function receiveAndExtend(length) { + var n = receive(length); + if (n >= 1 << (length - 1)) + return n; + return n + (-1 << length) + 1; + } + function decodeBaseline(component, zz) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : receiveAndExtend(t); + zz[0]= (component.pred += diff); + var k = 1; + while (k < 64) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) + break; + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + zz[z] = receiveAndExtend(s); + k++; + } + } + function decodeDCFirst(component, zz) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : (receiveAndExtend(t) << successive); + zz[0] = (component.pred += diff); + } + function decodeDCSuccessive(component, zz) { + zz[0] |= readBit() << successive; + } + var eobrun = 0; + function decodeACFirst(component, zz) { + if (eobrun > 0) { + eobrun--; + return; + } + var k = spectralStart, e = spectralEnd; + while (k <= e) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r) - 1; + break; + } + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + zz[z] = receiveAndExtend(s) * (1 << successive); + k++; + } + } + var successiveACState = 0, successiveACNextValue; + function decodeACSuccessive(component, zz) { + var k = spectralStart, e = spectralEnd, r = 0; + while (k <= e) { + var z = dctZigZag[k]; + var direction = zz[z] < 0 ? -1 : 1; + switch (successiveACState) { + case 0: // initial state + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r); + successiveACState = 4; + } else { + r = 16; + successiveACState = 1; + } + } else { + if (s !== 1) + throw new Error("invalid ACn encoding"); + successiveACNextValue = receiveAndExtend(s); + successiveACState = r ? 2 : 3; + } + continue; + case 1: // skipping r zero items + case 2: + if (zz[z]) + zz[z] += (readBit() << successive) * direction; + else { + r--; + if (r === 0) + successiveACState = successiveACState == 2 ? 3 : 0; + } + break; + case 3: // set value for a zero item + if (zz[z]) + zz[z] += (readBit() << successive) * direction; + else { + zz[z] = successiveACNextValue << successive; + successiveACState = 0; + } + break; + case 4: // eob + if (zz[z]) + zz[z] += (readBit() << successive) * direction; + break; + } + k++; + } + if (successiveACState === 4) { + eobrun--; + if (eobrun === 0) + successiveACState = 0; + } + } + function decodeMcu(component, decode, mcu, row, col) { + var mcuRow = (mcu / mcusPerLine) | 0; + var mcuCol = mcu % mcusPerLine; + var blockRow = mcuRow * component.v + row; + var blockCol = mcuCol * component.h + col; + decode(component, component.blocks[blockRow][blockCol]); + } + function decodeBlock(component, decode, mcu) { + var blockRow = (mcu / component.blocksPerLine) | 0; + var blockCol = mcu % component.blocksPerLine; + decode(component, component.blocks[blockRow][blockCol]); + } + + var componentsLength = components.length; + var component, i, j, k, n; + var decodeFn; + if (progressive) { + if (spectralStart === 0) + decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive; + else + decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive; + } else { + decodeFn = decodeBaseline; + } + + var mcu = 0, marker; + var mcuExpected; + if (componentsLength == 1) { + mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn; + } else { + mcuExpected = mcusPerLine * frame.mcusPerColumn; + } + if (!resetInterval) resetInterval = mcuExpected; + + var h, v; + while (mcu < mcuExpected) { + // reset interval stuff + for (i = 0; i < componentsLength; i++) + components[i].pred = 0; + eobrun = 0; + + if (componentsLength == 1) { + component = components[0]; + for (n = 0; n < resetInterval; n++) { + decodeBlock(component, decodeFn, mcu); + mcu++; + } + } else { + for (n = 0; n < resetInterval; n++) { + for (i = 0; i < componentsLength; i++) { + component = components[i]; + h = component.h; + v = component.v; + for (j = 0; j < v; j++) { + for (k = 0; k < h; k++) { + decodeMcu(component, decodeFn, mcu, j, k); + } + } + } + mcu++; + + // If we've reached our expected MCU's, stop decoding + if (mcu === mcuExpected) break; + } + } + + // find marker + bitsCount = 0; + marker = (data[offset] << 8) | data[offset + 1]; + if (marker < 0xFF00) { + throw new Error("marker was not found"); + } + + if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx + offset += 2; + } + else + break; + } + + return offset - startOffset; + } + + function buildComponentData(frame, component) { + var lines = []; + var blocksPerLine = component.blocksPerLine; + var blocksPerColumn = component.blocksPerColumn; + var samplesPerLine = blocksPerLine << 3; + var R = new Int32Array(64), r = new Uint8Array(64); + + // A port of poppler's IDCT method which in turn is taken from: + // Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, + // "Practical Fast 1-D DCT Algorithms with 11 Multiplications", + // IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, + // 988-991. + function quantizeAndInverse(zz, dataOut, dataIn) { + var qt = component.quantizationTable; + var v0, v1, v2, v3, v4, v5, v6, v7, t; + var p = dataIn; + var i; + + // dequant + for (i = 0; i < 64; i++) + p[i] = zz[i] * qt[i]; + + // inverse DCT on rows + for (i = 0; i < 8; ++i) { + var row = 8 * i; + + // check for all-zero AC coefficients + if (p[1 + row] == 0 && p[2 + row] == 0 && p[3 + row] == 0 && + p[4 + row] == 0 && p[5 + row] == 0 && p[6 + row] == 0 && + p[7 + row] == 0) { + t = (dctSqrt2 * p[0 + row] + 512) >> 10; + p[0 + row] = t; + p[1 + row] = t; + p[2 + row] = t; + p[3 + row] = t; + p[4 + row] = t; + p[5 + row] = t; + p[6 + row] = t; + p[7 + row] = t; + continue; + } + + // stage 4 + v0 = (dctSqrt2 * p[0 + row] + 128) >> 8; + v1 = (dctSqrt2 * p[4 + row] + 128) >> 8; + v2 = p[2 + row]; + v3 = p[6 + row]; + v4 = (dctSqrt1d2 * (p[1 + row] - p[7 + row]) + 128) >> 8; + v7 = (dctSqrt1d2 * (p[1 + row] + p[7 + row]) + 128) >> 8; + v5 = p[3 + row] << 4; + v6 = p[5 + row] << 4; + + // stage 3 + t = (v0 - v1+ 1) >> 1; + v0 = (v0 + v1 + 1) >> 1; + v1 = t; + t = (v2 * dctSin6 + v3 * dctCos6 + 128) >> 8; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 128) >> 8; + v3 = t; + t = (v4 - v6 + 1) >> 1; + v4 = (v4 + v6 + 1) >> 1; + v6 = t; + t = (v7 + v5 + 1) >> 1; + v5 = (v7 - v5 + 1) >> 1; + v7 = t; + + // stage 2 + t = (v0 - v3 + 1) >> 1; + v0 = (v0 + v3 + 1) >> 1; + v3 = t; + t = (v1 - v2 + 1) >> 1; + v1 = (v1 + v2 + 1) >> 1; + v2 = t; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p[0 + row] = v0 + v7; + p[7 + row] = v0 - v7; + p[1 + row] = v1 + v6; + p[6 + row] = v1 - v6; + p[2 + row] = v2 + v5; + p[5 + row] = v2 - v5; + p[3 + row] = v3 + v4; + p[4 + row] = v3 - v4; + } + + // inverse DCT on columns + for (i = 0; i < 8; ++i) { + var col = i; + + // check for all-zero AC coefficients + if (p[1*8 + col] == 0 && p[2*8 + col] == 0 && p[3*8 + col] == 0 && + p[4*8 + col] == 0 && p[5*8 + col] == 0 && p[6*8 + col] == 0 && + p[7*8 + col] == 0) { + t = (dctSqrt2 * dataIn[i+0] + 8192) >> 14; + p[0*8 + col] = t; + p[1*8 + col] = t; + p[2*8 + col] = t; + p[3*8 + col] = t; + p[4*8 + col] = t; + p[5*8 + col] = t; + p[6*8 + col] = t; + p[7*8 + col] = t; + continue; + } + + // stage 4 + v0 = (dctSqrt2 * p[0*8 + col] + 2048) >> 12; + v1 = (dctSqrt2 * p[4*8 + col] + 2048) >> 12; + v2 = p[2*8 + col]; + v3 = p[6*8 + col]; + v4 = (dctSqrt1d2 * (p[1*8 + col] - p[7*8 + col]) + 2048) >> 12; + v7 = (dctSqrt1d2 * (p[1*8 + col] + p[7*8 + col]) + 2048) >> 12; + v5 = p[3*8 + col]; + v6 = p[5*8 + col]; + + // stage 3 + t = (v0 - v1 + 1) >> 1; + v0 = (v0 + v1 + 1) >> 1; + v1 = t; + t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12; + v3 = t; + t = (v4 - v6 + 1) >> 1; + v4 = (v4 + v6 + 1) >> 1; + v6 = t; + t = (v7 + v5 + 1) >> 1; + v5 = (v7 - v5 + 1) >> 1; + v7 = t; + + // stage 2 + t = (v0 - v3 + 1) >> 1; + v0 = (v0 + v3 + 1) >> 1; + v3 = t; + t = (v1 - v2 + 1) >> 1; + v1 = (v1 + v2 + 1) >> 1; + v2 = t; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p[0*8 + col] = v0 + v7; + p[7*8 + col] = v0 - v7; + p[1*8 + col] = v1 + v6; + p[6*8 + col] = v1 - v6; + p[2*8 + col] = v2 + v5; + p[5*8 + col] = v2 - v5; + p[3*8 + col] = v3 + v4; + p[4*8 + col] = v3 - v4; + } + + // convert to 8-bit integers + for (i = 0; i < 64; ++i) { + var sample = 128 + ((p[i] + 8) >> 4); + dataOut[i] = sample < 0 ? 0 : sample > 0xFF ? 0xFF : sample; + } + } + + var i, j; + for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) { + var scanLine = blockRow << 3; + for (i = 0; i < 8; i++) + lines.push(new Uint8Array(samplesPerLine)); + for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) { + quantizeAndInverse(component.blocks[blockRow][blockCol], r, R); + + var offset = 0, sample = blockCol << 3; + for (j = 0; j < 8; j++) { + var line = lines[scanLine + j]; + for (i = 0; i < 8; i++) + line[sample + i] = r[offset++]; + } + } + } + return lines; + } + + function clampTo8bit(a) { + return a < 0 ? 0 : a > 255 ? 255 : a; + } + + constructor.prototype = { + load: function load(path) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", path, true); + xhr.responseType = "arraybuffer"; + xhr.onload = (function() { + // TODO catch parse error + var data = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer); + this.parse(data); + if (this.onload) + this.onload(); + }).bind(this); + xhr.send(null); + }, + parse: function parse(data) { + var offset = 0, length = data.length; + function readUint16() { + var value = (data[offset] << 8) | data[offset + 1]; + offset += 2; + return value; + } + function readDataBlock() { + var length = readUint16(); + var array = data.subarray(offset, offset + length - 2); + offset += array.length; + return array; + } + function prepareComponents(frame) { + var maxH = 0, maxV = 0; + var component, componentId; + for (componentId in frame.components) { + if (frame.components.hasOwnProperty(componentId)) { + component = frame.components[componentId]; + if (maxH < component.h) maxH = component.h; + if (maxV < component.v) maxV = component.v; + } + } + var mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / maxH); + var mcusPerColumn = Math.ceil(frame.scanLines / 8 / maxV); + for (componentId in frame.components) { + if (frame.components.hasOwnProperty(componentId)) { + component = frame.components[componentId]; + var blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * component.h / maxH); + var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * component.v / maxV); + var blocksPerLineForMcu = mcusPerLine * component.h; + var blocksPerColumnForMcu = mcusPerColumn * component.v; + var blocks = []; + for (var i = 0; i < blocksPerColumnForMcu; i++) { + var row = []; + for (var j = 0; j < blocksPerLineForMcu; j++) + row.push(new Int32Array(64)); + blocks.push(row); + } + component.blocksPerLine = blocksPerLine; + component.blocksPerColumn = blocksPerColumn; + component.blocks = blocks; + } + } + frame.maxH = maxH; + frame.maxV = maxV; + frame.mcusPerLine = mcusPerLine; + frame.mcusPerColumn = mcusPerColumn; + } + var jfif = null; + var adobe = null; + var pixels = null; + var frame, resetInterval; + var quantizationTables = [], frames = []; + var huffmanTablesAC = [], huffmanTablesDC = []; + var fileMarker = readUint16(); + if (fileMarker != 0xFFD8) { // SOI (Start of Image) + throw new Error("SOI not found"); + } + + fileMarker = readUint16(); + while (fileMarker != 0xFFD9) { // EOI (End of image) + var i, j, l; + switch(fileMarker) { + case 0xFF00: break; + case 0xFFE0: // APP0 (Application Specific) + case 0xFFE1: // APP1 + case 0xFFE2: // APP2 + case 0xFFE3: // APP3 + case 0xFFE4: // APP4 + case 0xFFE5: // APP5 + case 0xFFE6: // APP6 + case 0xFFE7: // APP7 + case 0xFFE8: // APP8 + case 0xFFE9: // APP9 + case 0xFFEA: // APP10 + case 0xFFEB: // APP11 + case 0xFFEC: // APP12 + case 0xFFED: // APP13 + case 0xFFEE: // APP14 + case 0xFFEF: // APP15 + case 0xFFFE: // COM (Comment) + var appData = readDataBlock(); + + if (fileMarker === 0xFFE0) { + if (appData[0] === 0x4A && appData[1] === 0x46 && appData[2] === 0x49 && + appData[3] === 0x46 && appData[4] === 0) { // 'JFIF\x00' + jfif = { + version: { major: appData[5], minor: appData[6] }, + densityUnits: appData[7], + xDensity: (appData[8] << 8) | appData[9], + yDensity: (appData[10] << 8) | appData[11], + thumbWidth: appData[12], + thumbHeight: appData[13], + thumbData: appData.subarray(14, 14 + 3 * appData[12] * appData[13]) + }; + } + } + // TODO APP1 - Exif + if (fileMarker === 0xFFEE) { + if (appData[0] === 0x41 && appData[1] === 0x64 && appData[2] === 0x6F && + appData[3] === 0x62 && appData[4] === 0x65 && appData[5] === 0) { // 'Adobe\x00' + adobe = { + version: appData[6], + flags0: (appData[7] << 8) | appData[8], + flags1: (appData[9] << 8) | appData[10], + transformCode: appData[11] + }; + } + } + break; + + case 0xFFDB: // DQT (Define Quantization Tables) + var quantizationTablesLength = readUint16(); + var quantizationTablesEnd = quantizationTablesLength + offset - 2; + while (offset < quantizationTablesEnd) { + var quantizationTableSpec = data[offset++]; + var tableData = new Int32Array(64); + if ((quantizationTableSpec >> 4) === 0) { // 8 bit values + for (j = 0; j < 64; j++) { + var z = dctZigZag[j]; + tableData[z] = data[offset++]; + } + } else if ((quantizationTableSpec >> 4) === 1) { //16 bit + for (j = 0; j < 64; j++) { + var z = dctZigZag[j]; + tableData[z] = readUint16(); + } + } else + throw new Error("DQT: invalid table spec"); + quantizationTables[quantizationTableSpec & 15] = tableData; + } + break; + + case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT) + case 0xFFC1: // SOF1 (Start of Frame, Extended DCT) + case 0xFFC2: // SOF2 (Start of Frame, Progressive DCT) + readUint16(); // skip data length + frame = {}; + frame.extended = (fileMarker === 0xFFC1); + frame.progressive = (fileMarker === 0xFFC2); + frame.precision = data[offset++]; + frame.scanLines = readUint16(); + frame.samplesPerLine = readUint16(); + frame.components = {}; + frame.componentsOrder = []; + var componentsCount = data[offset++], componentId; + var maxH = 0, maxV = 0; + for (i = 0; i < componentsCount; i++) { + componentId = data[offset]; + var h = data[offset + 1] >> 4; + var v = data[offset + 1] & 15; + var qId = data[offset + 2]; + frame.componentsOrder.push(componentId); + frame.components[componentId] = { + h: h, + v: v, + quantizationIdx: qId + }; + offset += 3; + } + prepareComponents(frame); + frames.push(frame); + break; + + case 0xFFC4: // DHT (Define Huffman Tables) + var huffmanLength = readUint16(); + for (i = 2; i < huffmanLength;) { + var huffmanTableSpec = data[offset++]; + var codeLengths = new Uint8Array(16); + var codeLengthSum = 0; + for (j = 0; j < 16; j++, offset++) + codeLengthSum += (codeLengths[j] = data[offset]); + var huffmanValues = new Uint8Array(codeLengthSum); + for (j = 0; j < codeLengthSum; j++, offset++) + huffmanValues[j] = data[offset]; + i += 17 + codeLengthSum; + + ((huffmanTableSpec >> 4) === 0 ? + huffmanTablesDC : huffmanTablesAC)[huffmanTableSpec & 15] = + buildHuffmanTable(codeLengths, huffmanValues); + } + break; + + case 0xFFDD: // DRI (Define Restart Interval) + readUint16(); // skip data length + resetInterval = readUint16(); + break; + + case 0xFFDA: // SOS (Start of Scan) + var scanLength = readUint16(); + var selectorsCount = data[offset++]; + var components = [], component; + for (i = 0; i < selectorsCount; i++) { + component = frame.components[data[offset++]]; + var tableSpec = data[offset++]; + component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4]; + component.huffmanTableAC = huffmanTablesAC[tableSpec & 15]; + components.push(component); + } + var spectralStart = data[offset++]; + var spectralEnd = data[offset++]; + var successiveApproximation = data[offset++]; + var processed = decodeScan(data, offset, + frame, components, resetInterval, + spectralStart, spectralEnd, + successiveApproximation >> 4, successiveApproximation & 15); + offset += processed; + break; + + case 0xFFFF: // Fill bytes + if (data[offset] !== 0xFF) { // Avoid skipping a valid marker. + offset--; + } + break; + + default: + if (data[offset - 3] == 0xFF && + data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) { + // could be incorrect encoding -- last 0xFF byte of the previous + // block was eaten by the encoder + offset -= 3; + break; + } + throw new Error("unknown JPEG marker " + fileMarker.toString(16)); + } + fileMarker = readUint16(); + } + if (frames.length != 1) + throw new Error("only single frame JPEGs supported"); + + // set each frame's components quantization table + for (var i = 0; i < frames.length; i++) { + var cp = frames[i].components; + for (var j in cp) { + cp[j].quantizationTable = quantizationTables[cp[j].quantizationIdx]; + delete cp[j].quantizationIdx; + } + } + + this.width = frame.samplesPerLine; + this.height = frame.scanLines; + this.jfif = jfif; + this.adobe = adobe; + this.components = []; + for (var i = 0; i < frame.componentsOrder.length; i++) { + var component = frame.components[frame.componentsOrder[i]]; + this.components.push({ + lines: buildComponentData(frame, component), + scaleX: component.h / frame.maxH, + scaleY: component.v / frame.maxV + }); + } + }, + getData: function getData(width, height) { + var scaleX = this.width / width, scaleY = this.height / height; + + var component1, component2, component3, component4; + var component1Line, component2Line, component3Line, component4Line; + var x, y; + var offset = 0; + var Y, Cb, Cr, K, C, M, Ye, R, G, B; + var colorTransform; + var dataLength = width * height * this.components.length; + var data = new Uint8Array(dataLength); + switch (this.components.length) { + case 1: + component1 = this.components[0]; + for (y = 0; y < height; y++) { + component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)]; + for (x = 0; x < width; x++) { + Y = component1Line[0 | (x * component1.scaleX * scaleX)]; + + data[offset++] = Y; + } + } + break; + case 2: + // PDF might compress two component data in custom colorspace + component1 = this.components[0]; + component2 = this.components[1]; + for (y = 0; y < height; y++) { + component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)]; + component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)]; + for (x = 0; x < width; x++) { + Y = component1Line[0 | (x * component1.scaleX * scaleX)]; + data[offset++] = Y; + Y = component2Line[0 | (x * component2.scaleX * scaleX)]; + data[offset++] = Y; + } + } + break; + case 3: + // The default transform for three components is true + colorTransform = true; + // The adobe transform marker overrides any previous setting + if (this.adobe && this.adobe.transformCode) + colorTransform = true; + else if (typeof this.colorTransform !== 'undefined') + colorTransform = !!this.colorTransform; + + component1 = this.components[0]; + component2 = this.components[1]; + component3 = this.components[2]; + for (y = 0; y < height; y++) { + component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)]; + component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)]; + component3Line = component3.lines[0 | (y * component3.scaleY * scaleY)]; + for (x = 0; x < width; x++) { + if (!colorTransform) { + R = component1Line[0 | (x * component1.scaleX * scaleX)]; + G = component2Line[0 | (x * component2.scaleX * scaleX)]; + B = component3Line[0 | (x * component3.scaleX * scaleX)]; + } else { + Y = component1Line[0 | (x * component1.scaleX * scaleX)]; + Cb = component2Line[0 | (x * component2.scaleX * scaleX)]; + Cr = component3Line[0 | (x * component3.scaleX * scaleX)]; + + R = clampTo8bit(Y + 1.402 * (Cr - 128)); + G = clampTo8bit(Y - 0.3441363 * (Cb - 128) - 0.71413636 * (Cr - 128)); + B = clampTo8bit(Y + 1.772 * (Cb - 128)); + } + + data[offset++] = R; + data[offset++] = G; + data[offset++] = B; + } + } + break; + case 4: + if (!this.adobe) + throw 'Unsupported color mode (4 components)'; + // The default transform for four components is false + colorTransform = false; + // The adobe transform marker overrides any previous setting + if (this.adobe && this.adobe.transformCode) + colorTransform = true; + else if (typeof this.colorTransform !== 'undefined') + colorTransform = !!this.colorTransform; + + component1 = this.components[0]; + component2 = this.components[1]; + component3 = this.components[2]; + component4 = this.components[3]; + for (y = 0; y < height; y++) { + component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)]; + component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)]; + component3Line = component3.lines[0 | (y * component3.scaleY * scaleY)]; + component4Line = component4.lines[0 | (y * component4.scaleY * scaleY)]; + for (x = 0; x < width; x++) { + if (!colorTransform) { + C = component1Line[0 | (x * component1.scaleX * scaleX)]; + M = component2Line[0 | (x * component2.scaleX * scaleX)]; + Ye = component3Line[0 | (x * component3.scaleX * scaleX)]; + K = component4Line[0 | (x * component4.scaleX * scaleX)]; + } else { + Y = component1Line[0 | (x * component1.scaleX * scaleX)]; + Cb = component2Line[0 | (x * component2.scaleX * scaleX)]; + Cr = component3Line[0 | (x * component3.scaleX * scaleX)]; + K = component4Line[0 | (x * component4.scaleX * scaleX)]; + + C = 255 - clampTo8bit(Y + 1.402 * (Cr - 128)); + M = 255 - clampTo8bit(Y - 0.3441363 * (Cb - 128) - 0.71413636 * (Cr - 128)); + Ye = 255 - clampTo8bit(Y + 1.772 * (Cb - 128)); + } + data[offset++] = 255-C; + data[offset++] = 255-M; + data[offset++] = 255-Ye; + data[offset++] = 255-K; + } + } + break; + default: + throw 'Unsupported color mode'; + } + return data; + }, + copyToImageData: function copyToImageData(imageData) { + var width = imageData.width, height = imageData.height; + var imageDataArray = imageData.data; + var data = this.getData(width, height); + var i = 0, j = 0, x, y; + var Y, K, C, M, R, G, B; + switch (this.components.length) { + case 1: + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + Y = data[i++]; + + imageDataArray[j++] = Y; + imageDataArray[j++] = Y; + imageDataArray[j++] = Y; + imageDataArray[j++] = 255; + } + } + break; + case 3: + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + R = data[i++]; + G = data[i++]; + B = data[i++]; + + imageDataArray[j++] = R; + imageDataArray[j++] = G; + imageDataArray[j++] = B; + imageDataArray[j++] = 255; + } + } + break; + case 4: + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + C = data[i++]; + M = data[i++]; + Y = data[i++]; + K = data[i++]; + + R = 255 - clampTo8bit(C * (1 - K / 255) + K); + G = 255 - clampTo8bit(M * (1 - K / 255) + K); + B = 255 - clampTo8bit(Y * (1 - K / 255) + K); + + imageDataArray[j++] = R; + imageDataArray[j++] = G; + imageDataArray[j++] = B; + imageDataArray[j++] = 255; + } + } + break; + default: + throw 'Unsupported color mode'; + } + } + }; + + return constructor; +})(); +//module.exports = decode; + +function decode(jpegData, opts) { + var defaultOpts = { + useTArray: false, + colorTransform: true + }; + if (opts) { + if (typeof opts === 'object') { + opts = { + useTArray: (typeof opts.useTArray === 'undefined' ? + defaultOpts.useTArray : opts.useTArray), + colorTransform: (typeof opts.colorTransform === 'undefined' ? + defaultOpts.colorTransform : opts.colorTransform) + }; + } else { + // backwards compatiblity, before 0.3.5, we only had the useTArray param + opts = defaultOpts; + opts.useTArray = true; + } + } else { + opts = defaultOpts; + } + + var arr = new Uint8Array(jpegData); + var decoder = new JpegImage(); + decoder.parse(arr); + decoder.colorTransform = opts.colorTransform; + + var image = { + width: decoder.width, + height: decoder.height, + data: opts.useTArray ? + new Uint8Array(decoder.width * decoder.height * 4) : + new Buffer(decoder.width * decoder.height * 4) + }; + + decoder.copyToImageData(image); + + return image; +} diff --git a/apps/assets/scripts/main.js b/apps/assets/scripts/main.js index 837e89e..76bee10 100644 --- a/apps/assets/scripts/main.js +++ b/apps/assets/scripts/main.js @@ -1,4 +1,4 @@ -// Generated by CoffeeScript 1.12.7 +// Generated by CoffeeScript 1.9.3 (function() { var APIManager, BaseObject, MarkOn, WVNC, require, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, @@ -164,6 +164,7 @@ extend(WVNC, superClass); function WVNC(args) { + var me; this.args = args; WVNC.__super__.constructor.call(this, "WVNC"); this.socket = void 0; @@ -176,7 +177,17 @@ this.canvas = ($(this.args[1]))[0]; } this.buffer = $("")[0]; - this.counter = 0; + this.lastPose = { + x: 0, + y: 0 + }; + this.scale = 0.8; + this.decoder = new Worker('/assets/scripts/decoder.js'); + me = this; + this.mouseMask = 0; + this.decoder.onmessage = function(e) { + return me.process(e.data); + }; } WVNC.prototype.init = function() { @@ -189,146 +200,146 @@ } }); $("#connect").click(function(e) { - me.counter = 0; return me.openSession(); }); - ($(me.canvas)).css("cursor", "none"); - return ($(me.canvas)).mousemove(function(e) { - var rect, x, y; - rect = me.canvas.getBoundingClientRect(); - x = Math.floor(e.clientX - rect.left); - y = Math.floor(e.clientY - rect.top); - return me.sendPointEvent(x, y, 0); - }); + return me.initInputEvent(); })["catch"](function(m, s) { return console.error(m, s); }); }; + WVNC.prototype.initInputEvent = function() { + var getMousePos, hamster, me, sendMouseLocation; + me = this; + getMousePos = function(e) { + var pos, rect; + rect = me.canvas.getBoundingClientRect(); + pos = { + x: Math.floor((e.clientX - rect.left) / me.scale), + y: Math.floor((e.clientY - rect.top) / me.scale) + }; + return pos; + }; + sendMouseLocation = function(e) { + var p; + p = getMousePos(e); + return me.sendPointEvent(p.x, p.y, me.mouseMask); + }; + if (!me.canvas) { + return; + } + ($(me.canvas)).css("cursor", "none"); + ($(me.canvas)).contextmenu(function(e) { + e.preventDefault(); + return false; + }); + ($(me.canvas)).mousemove(function(e) { + return sendMouseLocation(e); + }); + ($(me.canvas)).mousedown(function(e) { + var state; + state = 1 << e.button; + me.mouseMask = me.mouseMask | state; + return sendMouseLocation(e); + }); + ($(me.canvas)).mouseup(function(e) { + var state; + state = 1 << e.button; + me.mouseMask = me.mouseMask & (~state); + return sendMouseLocation(e); + }); + me.canvas.onkeydown = me.canvas.onkeyup = me.canvas.onkeypress = function(e) { + var code; + if (e.key === "Shift") { + code = 16; + } else if (e.ctrlKey) { + code = 17; + } else if (e.altKey) { + code = 18; + } else if (e.metaKey) { + code = 91; + } else { + code = String.charCodeAt(e.key); + } + if (e.type === "keydown") { + me.sendKeyEvent(code, 1); + } else if (e.type === "keyup") { + me.sendKeyEvent(code, 0); + } + return e.preventDefault(); + }; + hamster = Hamster(this.canvas); + return hamster.wheel(function(event, delta, deltaX, deltaY) { + var p; + p = getMousePos(event.originalEvent); + if (delta > 0) { + me.sendPointEvent(p.x, p.y, 8); + me.sendPointEvent(p.x, p.y, 0); + return; + } + me.sendPointEvent(p.x, p.y, 16); + return me.sendPointEvent(p.x, p.y, 0); + }); + }; + WVNC.prototype.initCanvas = function(w, h, d) { var ctx, data, me; me = this; this.depth = d; this.buffer.width = w; this.buffer.height = h; + this.resolution = { + w: w, + h: h, + depth: this.depth + }; + this.decoder.postMessage(this.resolution); ctx = this.buffer.getContext('2d'); data = ctx.createImageData(w, h); - ctx.putImageData(data, 0, 0); - return this.draw(); + return ctx.putImageData(data, 0, 0); }; - WVNC.prototype.decodeFB = function(d) { - var jpeg, pixels; - switch (d.flag) { - case 0x0: - return this.drawRaw(d.x, d.y, d.w, d.h, d.pixels); - case 0x1: - return this.drawJPEG(d.x, d.y, d.pixels); - case 0x2: - pixels = pako.inflate(d.pixels); - return this.drawRaw(d.x, d.y, d.w, d.h, pixels); - case 0x3: - jpeg = pako.inflate(d.pixels); - return this.drawJPEG(d.x, d.y, jpeg); - } - }; - - WVNC.prototype.drawJPEG = function(x, y, data) { - var blob, me, reader; - me = this; - blob = new Blob([data], { - type: "image/jpeg" - }); - reader = new FileReader(); - reader.onloadend = function() { - var hiddenImage; - hiddenImage = new Image(); - hiddenImage.style.position = "absolute"; - hiddenImage.style.left = "-99999px"; - document.body.appendChild(hiddenImage); - hiddenImage.onload = function() { - var ctx; - ctx = me.buffer.getContext('2d'); - ctx.drawImage(hiddenImage, x, y); - document.body.removeChild(hiddenImage); - return me.draw(); - }; - return hiddenImage.src = reader.result; - }; - return reader.readAsDataURL(blob); - }; - - WVNC.prototype.drawRaw = function(x, y, w, h, pixels) { + WVNC.prototype.process = function(data) { var ctx, imgData; + data.pixels = new Uint8ClampedArray(data.pixels); + if (data.flag === 0 && this.resolution.depth === 32) { + data.pixels = data.pixels.subarray(10); + } ctx = this.buffer.getContext('2d'); - ctx.globalAlpha = 1.0; - imgData = ctx.createImageData(w, h); - imgData.data.set(this.getCanvasImageData(pixels, w, h)); - ctx.putImageData(imgData, x, y); - this.counter = this.counter + 1; - return this.draw(); + imgData = ctx.createImageData(data.w, data.h); + imgData.data.set(data.pixels); + ctx.putImageData(imgData, data.x, data.y); + if (data.x !== this.lastPose.x || data.y > this.resolution.h - 10) { + this.draw(); + } + return this.lastPose = { + x: data.x, + y: data.y + }; }; - WVNC.prototype.getCanvasImageData = function(pixels, w, h) { - var data, i, j, k, npixels, p, pixel, ref, ref1, step, value; - if (this.depth === 32) { - return pixels; - } - step = this.depth / 8; - npixels = pixels.length / step; - data = new Uint8ClampedArray(w * h * 4); - for (i = k = 0, ref = npixels - 1; 0 <= ref ? k <= ref : k >= ref; i = 0 <= ref ? ++k : --k) { - value = 0; - for (j = p = 0, ref1 = step - 1; 0 <= ref1 ? p <= ref1 : p >= ref1; j = 0 <= ref1 ? ++p : --p) { - value = value | pixels[i * step + j] << (j * 8); - } - pixel = this.pixelValue(value); - data[i * 4] = pixel.r; - data[i * 4 + 1] = pixel.g; - data[i * 4 + 2] = pixel.b; - data[i * 4 + 3] = pixel.a; - } - return data; + WVNC.prototype.setScale = function(n) { + this.scale = n; + return this.draw(); }; WVNC.prototype.draw = function() { - var ctx, h, scale, w; + var ctx, h, w; if (!this.socket) { return; } - scale = 1.0; - w = this.buffer.width * scale; - h = this.buffer.height * scale; + w = this.buffer.width * this.scale; + h = this.buffer.height * this.scale; this.canvas.width = w; this.canvas.height = h; ctx = this.canvas.getContext("2d"); ctx.save(); - ctx.scale(scale, scale); + ctx.scale(this.scale, this.scale); ctx.clearRect(0, 0, w, h); ctx.drawImage(this.buffer, 0, 0); return ctx.restore(); }; - WVNC.prototype.pixelValue = function(value) { - var pixel; - pixel = { - r: 255, - g: 255, - b: 255, - a: 255 - }; - if (this.depth === 24 || this.depth === 32) { - pixel.r = value & 0xFF; - pixel.g = (value >> 8) & 0xFF; - pixel.b = (value >> 16) & 0xFF; - } else if (this.depth === 16) { - pixel.r = (value & 0x1F) * (255 / 31); - pixel.g = ((value >> 5) & 0x3F) * (255 / 63); - pixel.b = ((value >> 11) & 0x1F) * (255 / 31); - } - return pixel; - }; - WVNC.prototype.openSession = function() { var me; me = this; @@ -354,9 +365,25 @@ }; WVNC.prototype.initConnection = function() { - var vncserver; - vncserver = "192.168.1.8:5900"; - return this.socket.send(this.buildCommand(0x01, vncserver)); + var data, rate, vncserver; + vncserver = "localhost:5901"; + data = new Uint8Array(vncserver.length + 5); + data[0] = 16; + + /* + flag: + 0: raw data no compress + 1: jpeg no compress + 2: raw data compressed by zlib + 3: jpeg data compressed by zlib + */ + data[1] = 2; + data[2] = 50; + rate = 30; + data[3] = rate & 0xFF; + data[4] = (rate >> 8) & 0xFF; + data.set((new TextEncoder()).encode(vncserver), 5); + return this.socket.send(this.buildCommand(0x01, data)); }; WVNC.prototype.sendPointEvent = function(x, y, mask) { @@ -369,10 +396,22 @@ data[1] = x >> 8; data[2] = y & 0xFF; data[3] = y >> 8; - data[4] = 0; + data[4] = mask; return this.socket.send(this.buildCommand(0x05, data)); }; + WVNC.prototype.sendKeyEvent = function(code, v) { + var data; + if (!this.socket) { + return; + } + data = new Uint8Array(2); + data[0] = code; + data[1] = v; + console.log(String.fromCharCode(code), v); + return this.socket.send(this.buildCommand(0x06, data)); + }; + WVNC.prototype.buildCommand = function(hex, o) { var cmd, data; data = void 0; @@ -395,7 +434,7 @@ }; WVNC.prototype.consume = function(e) { - var arr, cmd, d, data, dec, depth, h, pass, user, w; + var arr, cmd, data, dec, depth, h, pass, user, w; data = new Uint8Array(e.data); cmd = data[0]; switch (cmd) { @@ -405,7 +444,7 @@ return console.log("Error", dec.decode(data)); case 0x81: console.log("Request for password"); - pass = "sang"; + pass = "!x$@n9"; return this.socket.send(this.buildCommand(0x02, pass)); case 0x82: console.log("Request for login"); @@ -424,14 +463,7 @@ this.initCanvas(w, h, depth); return this.socket.send(this.buildCommand(0x04, 1)); case 0x84: - d = {}; - d.x = data[1] | (data[2] << 8); - d.y = data[3] | (data[4] << 8); - d.w = data[5] | (data[6] << 8); - d.h = data[7] | (data[8] << 8); - d.flag = data[9]; - d.pixels = data.subarray(10); - return this.decodeFB(d); + return this.decoder.postMessage(data.buffer, [data.buffer]); default: return console.log(cmd); } @@ -441,7 +473,7 @@ })(window.classes.BaseObject); - WVNC.dependencies = ["/assets/scripts/pako.min.js"]; + WVNC.dependencies = ["/assets/scripts/hamster.js"]; makeclass("WVNC", WVNC); diff --git a/apps/views/default/wvnc/index.ls b/apps/views/default/wvnc/index.ls index 4a26ba5..66dcd9d 100644 --- a/apps/views/default/wvnc/index.ls +++ b/apps/views/default/wvnc/index.ls @@ -1,3 +1,3 @@

VNC screen here

ConnectSTOP

- \ No newline at end of file + \ No newline at end of file