JavaScript 65.5%
Python 17.8%
CSS 13%
HTML 3.7%
1/*2 * noVNC: HTML5 VNC client3 * Copyright (C) 2019 The noVNC authors4 * Licensed under MPL 2.0 (see LICENSE.txt)5 *6 * See README.md for usage and integration instructions.7 */89import * as Log from './util/logging.js';10import Base64 from "./base64.js";11import { toSigned32bit } from './util/int.js';1213export default class Display {14 constructor(target) {15 this._drawCtx = null;1617 this._renderQ = []; // queue drawing actions for in-oder rendering18 this._flushPromise = null;1920 // the full frame buffer (logical canvas) size21 this._fbWidth = 0;22 this._fbHeight = 0;2324 this._prevDrawStyle = "";2526 Log.Debug(">> Display.constructor");2728 // The visible canvas29 this._target = target;3031 if (!this._target) {32 throw new Error("Target must be set");33 }3435 if (typeof this._target === 'string') {36 throw new Error('target must be a DOM element');37 }3839 if (!this._target.getContext) {40 throw new Error("no getContext method");41 }4243 this._targetCtx = this._target.getContext('2d');4445 // the visible canvas viewport (i.e. what actually gets seen)46 this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };4748 // The hidden canvas, where we do the actual rendering49 this._backbuffer = document.createElement('canvas');50 this._drawCtx = this._backbuffer.getContext('2d');5152 this._damageBounds = { left: 0, top: 0,53 right: this._backbuffer.width,54 bottom: this._backbuffer.height };5556 Log.Debug("User Agent: " + navigator.userAgent);5758 Log.Debug("<< Display.constructor");5960 // ===== PROPERTIES =====6162 this._scale = 1.0;63 this._clipViewport = false;64 }6566 // ===== PROPERTIES =====6768 get scale() { return this._scale; }69 set scale(scale) {70 this._rescale(scale);71 }7273 get clipViewport() { return this._clipViewport; }74 set clipViewport(viewport) {75 this._clipViewport = viewport;76 // May need to readjust the viewport dimensions77 const vp = this._viewportLoc;78 this.viewportChangeSize(vp.w, vp.h);79 this.viewportChangePos(0, 0);80 }8182 get width() {83 return this._fbWidth;84 }8586 get height() {87 return this._fbHeight;88 }8990 // ===== PUBLIC METHODS =====9192 viewportChangePos(deltaX, deltaY) {93 const vp = this._viewportLoc;94 deltaX = Math.floor(deltaX);95 deltaY = Math.floor(deltaY);9697 if (!this._clipViewport) {98 deltaX = -vp.w; // clamped later of out of bounds99 deltaY = -vp.h;100 }101102 const vx2 = vp.x + vp.w - 1;103 const vy2 = vp.y + vp.h - 1;104105 // Position change106107 if (deltaX < 0 && vp.x + deltaX < 0) {108 deltaX = -vp.x;109 }110 if (vx2 + deltaX >= this._fbWidth) {111 deltaX -= vx2 + deltaX - this._fbWidth + 1;112 }113114 if (vp.y + deltaY < 0) {115 deltaY = -vp.y;116 }117 if (vy2 + deltaY >= this._fbHeight) {118 deltaY -= (vy2 + deltaY - this._fbHeight + 1);119 }120121 if (deltaX === 0 && deltaY === 0) {122 return;123 }124 Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);125126 vp.x += deltaX;127 vp.y += deltaY;128129 this._damage(vp.x, vp.y, vp.w, vp.h);130131 this.flip();132 }133134 viewportChangeSize(width, height) {135136 if (!this._clipViewport ||137 typeof(width) === "undefined" ||138 typeof(height) === "undefined") {139140 Log.Debug("Setting viewport to full display region");141 width = this._fbWidth;142 height = this._fbHeight;143 }144145 width = Math.floor(width);146 height = Math.floor(height);147148 if (width > this._fbWidth) {149 width = this._fbWidth;150 }151 if (height > this._fbHeight) {152 height = this._fbHeight;153 }154155 const vp = this._viewportLoc;156 if (vp.w !== width || vp.h !== height) {157 vp.w = width;158 vp.h = height;159160 const canvas = this._target;161 canvas.width = width;162 canvas.height = height;163164 // The position might need to be updated if we've grown165 this.viewportChangePos(0, 0);166167 this._damage(vp.x, vp.y, vp.w, vp.h);168 this.flip();169170 // Update the visible size of the target canvas171 this._rescale(this._scale);172 }173 }174175 absX(x) {176 if (this._scale === 0) {177 return 0;178 }179 return toSigned32bit(x / this._scale + this._viewportLoc.x);180 }181182 absY(y) {183 if (this._scale === 0) {184 return 0;185 }186 return toSigned32bit(y / this._scale + this._viewportLoc.y);187 }188189 resize(width, height) {190 this._prevDrawStyle = "";191192 this._fbWidth = width;193 this._fbHeight = height;194195 const canvas = this._backbuffer;196 if (canvas.width !== width || canvas.height !== height) {197198 // We have to save the canvas data since changing the size will clear it199 let saveImg = null;200 if (canvas.width > 0 && canvas.height > 0) {201 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);202 }203204 if (canvas.width !== width) {205 canvas.width = width;206 }207 if (canvas.height !== height) {208 canvas.height = height;209 }210211 if (saveImg) {212 this._drawCtx.putImageData(saveImg, 0, 0);213 }214 }215216 // Readjust the viewport as it may be incorrectly sized217 // and positioned218 const vp = this._viewportLoc;219 this.viewportChangeSize(vp.w, vp.h);220 this.viewportChangePos(0, 0);221 }222223 getImageData() {224 return this._drawCtx.getImageData(0, 0, this.width, this.height);225 }226227 toDataURL(type, encoderOptions) {228 return this._backbuffer.toDataURL(type, encoderOptions);229 }230231 toBlob(callback, type, quality) {232 return this._backbuffer.toBlob(callback, type, quality);233 }234235 // Track what parts of the visible canvas that need updating236 _damage(x, y, w, h) {237 if (x < this._damageBounds.left) {238 this._damageBounds.left = x;239 }240 if (y < this._damageBounds.top) {241 this._damageBounds.top = y;242 }243 if ((x + w) > this._damageBounds.right) {244 this._damageBounds.right = x + w;245 }246 if ((y + h) > this._damageBounds.bottom) {247 this._damageBounds.bottom = y + h;248 }249 }250251 // Update the visible canvas with the contents of the252 // rendering canvas253 flip(fromQueue) {254 if (this._renderQ.length !== 0 && !fromQueue) {255 this._renderQPush({256 'type': 'flip'257 });258 } else {259 let x = this._damageBounds.left;260 let y = this._damageBounds.top;261 let w = this._damageBounds.right - x;262 let h = this._damageBounds.bottom - y;263264 let vx = x - this._viewportLoc.x;265 let vy = y - this._viewportLoc.y;266267 if (vx < 0) {268 w += vx;269 x -= vx;270 vx = 0;271 }272 if (vy < 0) {273 h += vy;274 y -= vy;275 vy = 0;276 }277278 if ((vx + w) > this._viewportLoc.w) {279 w = this._viewportLoc.w - vx;280 }281 if ((vy + h) > this._viewportLoc.h) {282 h = this._viewportLoc.h - vy;283 }284285 if ((w > 0) && (h > 0)) {286 // FIXME: We may need to disable image smoothing here287 // as well (see copyImage()), but we haven't288 // noticed any problem yet.289 this._targetCtx.drawImage(this._backbuffer,290 x, y, w, h,291 vx, vy, w, h);292 }293294 this._damageBounds.left = this._damageBounds.top = 65535;295 this._damageBounds.right = this._damageBounds.bottom = 0;296 }297 }298299 pending() {300 return this._renderQ.length > 0;301 }302303 flush() {304 if (this._renderQ.length === 0) {305 return Promise.resolve();306 } else {307 if (this._flushPromise === null) {308 this._flushPromise = new Promise((resolve) => {309 this._flushResolve = resolve;310 });311 }312 return this._flushPromise;313 }314 }315316 fillRect(x, y, width, height, color, fromQueue) {317 if (this._renderQ.length !== 0 && !fromQueue) {318 this._renderQPush({319 'type': 'fill',320 'x': x,321 'y': y,322 'width': width,323 'height': height,324 'color': color325 });326 } else {327 this._setFillColor(color);328 this._drawCtx.fillRect(x, y, width, height);329 this._damage(x, y, width, height);330 }331 }332333 copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {334 if (this._renderQ.length !== 0 && !fromQueue) {335 this._renderQPush({336 'type': 'copy',337 'oldX': oldX,338 'oldY': oldY,339 'x': newX,340 'y': newY,341 'width': w,342 'height': h,343 });344 } else {345 // Due to this bug among others [1] we need to disable the image-smoothing to346 // avoid getting a blur effect when copying data.347 //348 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719349 //350 // We need to set these every time since all properties are reset351 // when the the size is changed352 this._drawCtx.mozImageSmoothingEnabled = false;353 this._drawCtx.webkitImageSmoothingEnabled = false;354 this._drawCtx.msImageSmoothingEnabled = false;355 this._drawCtx.imageSmoothingEnabled = false;356357 this._drawCtx.drawImage(this._backbuffer,358 oldX, oldY, w, h,359 newX, newY, w, h);360 this._damage(newX, newY, w, h);361 }362 }363364 imageRect(x, y, width, height, mime, arr) {365 /* The internal logic cannot handle empty images, so bail early */366 if ((width === 0) || (height === 0)) {367 return;368 }369370 const img = new Image();371 img.src = "data: " + mime + ";base64," + Base64.encode(arr);372373 this._renderQPush({374 'type': 'img',375 'img': img,376 'x': x,377 'y': y,378 'width': width,379 'height': height380 });381 }382383 videoFrame(x, y, width, height, frame) {384 this._renderQPush({385 'type': 'frame',386 'frame': frame,387 'x': x,388 'y': y,389 'width': width,390 'height': height391 });392 }393394 blitImage(x, y, width, height, arr, offset, fromQueue) {395 if (this._renderQ.length !== 0 && !fromQueue) {396 // NB(directxman12): it's technically more performant here to use preallocated arrays,397 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,398 // this probably isn't getting called *nearly* as much399 const newArr = new Uint8Array(width * height * 4);400 newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));401 this._renderQPush({402 'type': 'blit',403 'data': newArr,404 'x': x,405 'y': y,406 'width': width,407 'height': height,408 });409 } else {410 // NB(directxman12): arr must be an Type Array view411 let data = new Uint8ClampedArray(arr.buffer,412 arr.byteOffset + offset,413 width * height * 4);414 let img = new ImageData(data, width, height);415 this._drawCtx.putImageData(img, x, y);416 this._damage(x, y, width, height);417 }418 }419420 drawImage(img, ...args) {421 this._drawCtx.drawImage(img, ...args);422423 if (args.length <= 4) {424 const [x, y] = args;425 this._damage(x, y, img.width, img.height);426 } else {427 const [,, sw, sh, dx, dy] = args;428 this._damage(dx, dy, sw, sh);429 }430 }431432 autoscale(containerWidth, containerHeight) {433 let scaleRatio;434435 if (containerWidth === 0 || containerHeight === 0) {436 scaleRatio = 0;437438 } else {439440 const vp = this._viewportLoc;441 const targetAspectRatio = containerWidth / containerHeight;442 const fbAspectRatio = vp.w / vp.h;443444 if (fbAspectRatio >= targetAspectRatio) {445 scaleRatio = containerWidth / vp.w;446 } else {447 scaleRatio = containerHeight / vp.h;448 }449 }450451 this._rescale(scaleRatio);452 }453454 // ===== PRIVATE METHODS =====455456 _rescale(factor) {457 this._scale = factor;458 const vp = this._viewportLoc;459460 // NB(directxman12): If you set the width directly, or set the461 // style width to a number, the canvas is cleared.462 // However, if you set the style width to a string463 // ('NNNpx'), the canvas is scaled without clearing.464 const width = factor * vp.w + 'px';465 const height = factor * vp.h + 'px';466467 if ((this._target.style.width !== width) ||468 (this._target.style.height !== height)) {469 this._target.style.width = width;470 this._target.style.height = height;471 }472 }473474 _setFillColor(color) {475 const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';476 if (newStyle !== this._prevDrawStyle) {477 this._drawCtx.fillStyle = newStyle;478 this._prevDrawStyle = newStyle;479 }480 }481482 _renderQPush(action) {483 this._renderQ.push(action);484 if (this._renderQ.length === 1) {485 // If this can be rendered immediately it will be, otherwise486 // the scanner will wait for the relevant event487 this._scanRenderQ();488 }489 }490491 _resumeRenderQ() {492 // "this" is the object that is ready, not the493 // display object494 this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);495 this._noVNCDisplay._scanRenderQ();496 }497498 _scanRenderQ() {499 let ready = true;500 while (ready && this._renderQ.length > 0) {501 const a = this._renderQ[0];502 switch (a.type) {503 case 'flip':504 this.flip(true);505 break;506 case 'copy':507 this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);508 break;509 case 'fill':510 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);511 break;512 case 'blit':513 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);514 break;515 case 'img':516 if (a.img.complete) {517 if (a.img.width !== a.width || a.img.height !== a.height) {518 Log.Error("Decoded image has incorrect dimensions. Got " +519 a.img.width + "x" + a.img.height + ". Expected " +520 a.width + "x" + a.height + ".");521 return;522 }523 this.drawImage(a.img, a.x, a.y);524 } else {525 a.img._noVNCDisplay = this;526 a.img.addEventListener('load', this._resumeRenderQ);527 // We need to wait for this image to 'load'528 // to keep things in-order529 ready = false;530 }531 break;532 case 'frame':533 if (a.frame.ready) {534 // The encoded frame may be larger than the rect due to535 // limitations of the encoder, so we need to crop the536 // frame.537 let frame = a.frame.frame;538 if (frame.codedWidth < a.width || frame.codedHeight < a.height) {539 Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " +540 a.width + "x" + a.height + " but got " +541 frame.codedWidth + "x" + frame.codedHeight);542 }543 const sx = 0;544 const sy = 0;545 const sw = a.width;546 const sh = a.height;547 const dx = a.x;548 const dy = a.y;549 const dw = sw;550 const dh = sh;551 this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh);552 frame.close();553 } else {554 let display = this;555 a.frame.promise.then(() => {556 display._scanRenderQ();557 });558 ready = false;559 }560 break;561 }562563 if (ready) {564 this._renderQ.shift();565 }566 }567568 if (this._renderQ.length === 0 &&569 this._flushPromise !== null) {570 this._flushResolve();571 this._flushPromise = null;572 this._flushResolve = null;573 }574 }575}576