2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2019 The noVNC Authors
4 * Licensed under MPL 2.0 (see LICENSE.txt)
6 * See README.md for usage and integration instructions.
9 import * as Log
from './util/logging.js';
10 import Base64
from "./base64.js";
11 import { supportsImageMetadata
} from './util/browser.js';
13 export default class Display
{
17 this._renderQ
= []; // queue drawing actions for in-oder rendering
18 this._flushing
= false;
20 // the full frame buffer (logical canvas) size
24 this._prevDrawStyle
= "";
26 Log
.Debug(">> Display.constructor");
29 this._target
= target
;
32 throw new Error("Target must be set");
35 if (typeof this._target
=== 'string') {
36 throw new Error('target must be a DOM element');
39 if (!this._target
.getContext
) {
40 throw new Error("no getContext method");
43 this._targetCtx
= this._target
.getContext('2d');
45 // 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
};
48 // The hidden canvas, where we do the actual rendering
49 this._backbuffer
= document
.createElement('canvas');
50 this._drawCtx
= this._backbuffer
.getContext('2d');
52 this._damageBounds
= { left
: 0, top
: 0,
53 right
: this._backbuffer
.width
,
54 bottom
: this._backbuffer
.height
};
56 Log
.Debug("User Agent: " + navigator
.userAgent
);
58 // Check canvas features
59 if (!('createImageData' in this._drawCtx
)) {
60 throw new Error("Canvas does not support createImageData");
63 Log
.Debug("<< Display.constructor");
65 // ===== PROPERTIES =====
68 this._clipViewport
= false;
70 // ===== EVENT HANDLERS =====
72 this.onflush
= () => {}; // A flush request has finished
75 // ===== PROPERTIES =====
77 get scale() { return this._scale
; }
82 get clipViewport() { return this._clipViewport
; }
83 set clipViewport(viewport
) {
84 this._clipViewport
= viewport
;
85 // May need to readjust the viewport dimensions
86 const vp
= this._viewportLoc
;
87 this.viewportChangeSize(vp
.w
, vp
.h
);
88 this.viewportChangePos(0, 0);
96 return this._fbHeight
;
99 // ===== PUBLIC METHODS =====
101 viewportChangePos(deltaX
, deltaY
) {
102 const vp
= this._viewportLoc
;
103 deltaX
= Math
.floor(deltaX
);
104 deltaY
= Math
.floor(deltaY
);
106 if (!this._clipViewport
) {
107 deltaX
= -vp
.w
; // clamped later of out of bounds
111 const vx2
= vp
.x
+ vp
.w
- 1;
112 const vy2
= vp
.y
+ vp
.h
- 1;
116 if (deltaX
< 0 && vp
.x
+ deltaX
< 0) {
119 if (vx2
+ deltaX
>= this._fbWidth
) {
120 deltaX
-= vx2
+ deltaX
- this._fbWidth
+ 1;
123 if (vp
.y
+ deltaY
< 0) {
126 if (vy2
+ deltaY
>= this._fbHeight
) {
127 deltaY
-= (vy2
+ deltaY
- this._fbHeight
+ 1);
130 if (deltaX
=== 0 && deltaY
=== 0) {
133 Log
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
138 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
143 viewportChangeSize(width
, height
) {
145 if (!this._clipViewport
||
146 typeof(width
) === "undefined" ||
147 typeof(height
) === "undefined") {
149 Log
.Debug("Setting viewport to full display region");
150 width
= this._fbWidth
;
151 height
= this._fbHeight
;
154 width
= Math
.floor(width
);
155 height
= Math
.floor(height
);
157 if (width
> this._fbWidth
) {
158 width
= this._fbWidth
;
160 if (height
> this._fbHeight
) {
161 height
= this._fbHeight
;
164 const vp
= this._viewportLoc
;
165 if (vp
.w
!== width
|| vp
.h
!== height
) {
169 const canvas
= this._target
;
170 canvas
.width
= width
;
171 canvas
.height
= height
;
173 // The position might need to be updated if we've grown
174 this.viewportChangePos(0, 0);
176 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
179 // Update the visible size of the target canvas
180 this._rescale(this._scale
);
185 if (this._scale
=== 0) {
188 return x
/ this._scale
+ this._viewportLoc
.x
;
192 if (this._scale
=== 0) {
195 return y
/ this._scale
+ this._viewportLoc
.y
;
198 resize(width
, height
) {
199 this._prevDrawStyle
= "";
201 this._fbWidth
= width
;
202 this._fbHeight
= height
;
204 const canvas
= this._backbuffer
;
205 if (canvas
.width
!== width
|| canvas
.height
!== height
) {
207 // We have to save the canvas data since changing the size will clear it
209 if (canvas
.width
> 0 && canvas
.height
> 0) {
210 saveImg
= this._drawCtx
.getImageData(0, 0, canvas
.width
, canvas
.height
);
213 if (canvas
.width
!== width
) {
214 canvas
.width
= width
;
216 if (canvas
.height
!== height
) {
217 canvas
.height
= height
;
221 this._drawCtx
.putImageData(saveImg
, 0, 0);
225 // Readjust the viewport as it may be incorrectly sized
227 const vp
= this._viewportLoc
;
228 this.viewportChangeSize(vp
.w
, vp
.h
);
229 this.viewportChangePos(0, 0);
232 // Track what parts of the visible canvas that need updating
233 _damage(x
, y
, w
, h
) {
234 if (x
< this._damageBounds
.left
) {
235 this._damageBounds
.left
= x
;
237 if (y
< this._damageBounds
.top
) {
238 this._damageBounds
.top
= y
;
240 if ((x
+ w
) > this._damageBounds
.right
) {
241 this._damageBounds
.right
= x
+ w
;
243 if ((y
+ h
) > this._damageBounds
.bottom
) {
244 this._damageBounds
.bottom
= y
+ h
;
248 // Update the visible canvas with the contents of the
251 if (this._renderQ
.length
!== 0 && !fromQueue
) {
256 let x
= this._damageBounds
.left
;
257 let y
= this._damageBounds
.top
;
258 let w
= this._damageBounds
.right
- x
;
259 let h
= this._damageBounds
.bottom
- y
;
261 let vx
= x
- this._viewportLoc
.x
;
262 let vy
= y
- this._viewportLoc
.y
;
275 if ((vx
+ w
) > this._viewportLoc
.w
) {
276 w
= this._viewportLoc
.w
- vx
;
278 if ((vy
+ h
) > this._viewportLoc
.h
) {
279 h
= this._viewportLoc
.h
- vy
;
282 if ((w
> 0) && (h
> 0)) {
283 // FIXME: We may need to disable image smoothing here
284 // as well (see copyImage()), but we haven't
285 // noticed any problem yet.
286 this._targetCtx
.drawImage(this._backbuffer
,
291 this._damageBounds
.left
= this._damageBounds
.top
= 65535;
292 this._damageBounds
.right
= this._damageBounds
.bottom
= 0;
297 return this._renderQ
.length
> 0;
301 if (this._renderQ
.length
=== 0) {
304 this._flushing
= true;
308 fillRect(x
, y
, width
, height
, color
, fromQueue
) {
309 if (this._renderQ
.length
!== 0 && !fromQueue
) {
319 this._setFillColor(color
);
320 this._drawCtx
.fillRect(x
, y
, width
, height
);
321 this._damage(x
, y
, width
, height
);
325 copyImage(oldX
, oldY
, newX
, newY
, w
, h
, fromQueue
) {
326 if (this._renderQ
.length
!== 0 && !fromQueue
) {
337 // Due to this bug among others [1] we need to disable the image-smoothing to
338 // avoid getting a blur effect when copying data.
340 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
342 // We need to set these every time since all properties are reset
343 // when the the size is changed
344 this._drawCtx
.mozImageSmoothingEnabled
= false;
345 this._drawCtx
.webkitImageSmoothingEnabled
= false;
346 this._drawCtx
.msImageSmoothingEnabled
= false;
347 this._drawCtx
.imageSmoothingEnabled
= false;
349 this._drawCtx
.drawImage(this._backbuffer
,
352 this._damage(newX
, newY
, w
, h
);
356 imageRect(x
, y
, width
, height
, mime
, arr
) {
357 /* The internal logic cannot handle empty images, so bail early */
358 if ((width
=== 0) || (height
=== 0)) {
362 const img
= new Image();
363 img
.src
= "data: " + mime
+ ";base64," + Base64
.encode(arr
);
375 blitImage(x
, y
, width
, height
, arr
, offset
, fromQueue
) {
376 if (this._renderQ
.length
!== 0 && !fromQueue
) {
377 // NB(directxman12): it's technically more performant here to use preallocated arrays,
378 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
379 // this probably isn't getting called *nearly* as much
380 const newArr
= new Uint8Array(width
* height
* 4);
381 newArr
.set(new Uint8Array(arr
.buffer
, 0, newArr
.length
));
391 // NB(directxman12): arr must be an Type Array view
392 let data
= new Uint8ClampedArray(arr
.buffer
,
393 arr
.byteOffset
+ offset
,
396 if (supportsImageMetadata
) {
397 img
= new ImageData(data
, width
, height
);
399 img
= this._drawCtx
.createImageData(width
, height
);
402 this._drawCtx
.putImageData(img
, x
, y
);
403 this._damage(x
, y
, width
, height
);
407 drawImage(img
, x
, y
) {
408 this._drawCtx
.drawImage(img
, x
, y
);
409 this._damage(x
, y
, img
.width
, img
.height
);
412 autoscale(containerWidth
, containerHeight
) {
415 if (containerWidth
=== 0 || containerHeight
=== 0) {
420 const vp
= this._viewportLoc
;
421 const targetAspectRatio
= containerWidth
/ containerHeight
;
422 const fbAspectRatio
= vp
.w
/ vp
.h
;
424 if (fbAspectRatio
>= targetAspectRatio
) {
425 scaleRatio
= containerWidth
/ vp
.w
;
427 scaleRatio
= containerHeight
/ vp
.h
;
431 this._rescale(scaleRatio
);
434 // ===== PRIVATE METHODS =====
437 this._scale
= factor
;
438 const vp
= this._viewportLoc
;
440 // NB(directxman12): If you set the width directly, or set the
441 // style width to a number, the canvas is cleared.
442 // However, if you set the style width to a string
443 // ('NNNpx'), the canvas is scaled without clearing.
444 const width
= factor
* vp
.w
+ 'px';
445 const height
= factor
* vp
.h
+ 'px';
447 if ((this._target
.style
.width
!== width
) ||
448 (this._target
.style
.height
!== height
)) {
449 this._target
.style
.width
= width
;
450 this._target
.style
.height
= height
;
454 _setFillColor(color
) {
455 const newStyle
= 'rgb(' + color
[0] + ',' + color
[1] + ',' + color
[2] + ')';
456 if (newStyle
!== this._prevDrawStyle
) {
457 this._drawCtx
.fillStyle
= newStyle
;
458 this._prevDrawStyle
= newStyle
;
462 _renderQPush(action
) {
463 this._renderQ
.push(action
);
464 if (this._renderQ
.length
=== 1) {
465 // If this can be rendered immediately it will be, otherwise
466 // the scanner will wait for the relevant event
472 // "this" is the object that is ready, not the
474 this.removeEventListener('load', this._noVNCDisplay
._resumeRenderQ
);
475 this._noVNCDisplay
._scanRenderQ();
480 while (ready
&& this._renderQ
.length
> 0) {
481 const a
= this._renderQ
[0];
487 this.copyImage(a
.oldX
, a
.oldY
, a
.x
, a
.y
, a
.width
, a
.height
, true);
490 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
, true);
493 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
496 /* IE tends to set "complete" prematurely, so check dimensions */
497 if (a
.img
.complete
&& (a
.img
.width
!== 0) && (a
.img
.height
!== 0)) {
498 if (a
.img
.width
!== a
.width
|| a
.img
.height
!== a
.height
) {
499 Log
.Error("Decoded image has incorrect dimensions. Got " +
500 a
.img
.width
+ "x" + a
.img
.height
+ ". Expected " +
501 a
.width
+ "x" + a
.height
+ ".");
504 this.drawImage(a
.img
, a
.x
, a
.y
);
506 a
.img
._noVNCDisplay
= this;
507 a
.img
.addEventListener('load', this._resumeRenderQ
);
508 // We need to wait for this image to 'load'
509 // to keep things in-order
516 this._renderQ
.shift();
520 if (this._renderQ
.length
=== 0 && this._flushing
) {
521 this._flushing
= false;