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
{
16 this._c_forceCanvas
= false;
18 this._renderQ
= []; // queue drawing actions for in-oder rendering
19 this._flushing
= false;
21 // the full frame buffer (logical canvas) size
25 this._prevDrawStyle
= "";
27 this._tile16x16
= null;
31 Log
.Debug(">> Display.constructor");
34 this._target
= target
;
37 throw new Error("Target must be set");
40 if (typeof this._target
=== 'string') {
41 throw new Error('target must be a DOM element');
44 if (!this._target
.getContext
) {
45 throw new Error("no getContext method");
48 this._targetCtx
= this._target
.getContext('2d');
50 // the visible canvas viewport (i.e. what actually gets seen)
51 this._viewportLoc
= { 'x': 0, 'y': 0, 'w': this._target
.width
, 'h': this._target
.height
};
53 // The hidden canvas, where we do the actual rendering
54 this._backbuffer
= document
.createElement('canvas');
55 this._drawCtx
= this._backbuffer
.getContext('2d');
57 this._damageBounds
= { left
: 0, top
: 0,
58 right
: this._backbuffer
.width
,
59 bottom
: this._backbuffer
.height
};
61 Log
.Debug("User Agent: " + navigator
.userAgent
);
63 // Check canvas features
64 if (!('createImageData' in this._drawCtx
)) {
65 throw new Error("Canvas does not support createImageData");
68 this._tile16x16
= this._drawCtx
.createImageData(16, 16);
69 Log
.Debug("<< Display.constructor");
71 // ===== PROPERTIES =====
74 this._clipViewport
= false;
76 // ===== EVENT HANDLERS =====
78 this.onflush
= () => {}; // A flush request has finished
81 // ===== PROPERTIES =====
83 get scale() { return this._scale
; }
88 get clipViewport() { return this._clipViewport
; }
89 set clipViewport(viewport
) {
90 this._clipViewport
= viewport
;
91 // May need to readjust the viewport dimensions
92 const vp
= this._viewportLoc
;
93 this.viewportChangeSize(vp
.w
, vp
.h
);
94 this.viewportChangePos(0, 0);
98 return this._fb_width
;
102 return this._fb_height
;
105 // ===== PUBLIC METHODS =====
107 viewportChangePos(deltaX
, deltaY
) {
108 const vp
= this._viewportLoc
;
109 deltaX
= Math
.floor(deltaX
);
110 deltaY
= Math
.floor(deltaY
);
112 if (!this._clipViewport
) {
113 deltaX
= -vp
.w
; // clamped later of out of bounds
117 const vx2
= vp
.x
+ vp
.w
- 1;
118 const vy2
= vp
.y
+ vp
.h
- 1;
122 if (deltaX
< 0 && vp
.x
+ deltaX
< 0) {
125 if (vx2
+ deltaX
>= this._fb_width
) {
126 deltaX
-= vx2
+ deltaX
- this._fb_width
+ 1;
129 if (vp
.y
+ deltaY
< 0) {
132 if (vy2
+ deltaY
>= this._fb_height
) {
133 deltaY
-= (vy2
+ deltaY
- this._fb_height
+ 1);
136 if (deltaX
=== 0 && deltaY
=== 0) {
139 Log
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
144 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
149 viewportChangeSize(width
, height
) {
151 if (!this._clipViewport
||
152 typeof(width
) === "undefined" ||
153 typeof(height
) === "undefined") {
155 Log
.Debug("Setting viewport to full display region");
156 width
= this._fb_width
;
157 height
= this._fb_height
;
160 width
= Math
.floor(width
);
161 height
= Math
.floor(height
);
163 if (width
> this._fb_width
) {
164 width
= this._fb_width
;
166 if (height
> this._fb_height
) {
167 height
= this._fb_height
;
170 const vp
= this._viewportLoc
;
171 if (vp
.w
!== width
|| vp
.h
!== height
) {
175 const canvas
= this._target
;
176 canvas
.width
= width
;
177 canvas
.height
= height
;
179 // The position might need to be updated if we've grown
180 this.viewportChangePos(0, 0);
182 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
185 // Update the visible size of the target canvas
186 this._rescale(this._scale
);
191 if (this._scale
=== 0) {
194 return x
/ this._scale
+ this._viewportLoc
.x
;
198 if (this._scale
=== 0) {
201 return y
/ this._scale
+ this._viewportLoc
.y
;
204 resize(width
, height
) {
205 this._prevDrawStyle
= "";
207 this._fb_width
= width
;
208 this._fb_height
= height
;
210 const canvas
= this._backbuffer
;
211 if (canvas
.width
!== width
|| canvas
.height
!== height
) {
213 // We have to save the canvas data since changing the size will clear it
215 if (canvas
.width
> 0 && canvas
.height
> 0) {
216 saveImg
= this._drawCtx
.getImageData(0, 0, canvas
.width
, canvas
.height
);
219 if (canvas
.width
!== width
) {
220 canvas
.width
= width
;
222 if (canvas
.height
!== height
) {
223 canvas
.height
= height
;
227 this._drawCtx
.putImageData(saveImg
, 0, 0);
231 // Readjust the viewport as it may be incorrectly sized
233 const vp
= this._viewportLoc
;
234 this.viewportChangeSize(vp
.w
, vp
.h
);
235 this.viewportChangePos(0, 0);
238 // Track what parts of the visible canvas that need updating
239 _damage(x
, y
, w
, h
) {
240 if (x
< this._damageBounds
.left
) {
241 this._damageBounds
.left
= x
;
243 if (y
< this._damageBounds
.top
) {
244 this._damageBounds
.top
= y
;
246 if ((x
+ w
) > this._damageBounds
.right
) {
247 this._damageBounds
.right
= x
+ w
;
249 if ((y
+ h
) > this._damageBounds
.bottom
) {
250 this._damageBounds
.bottom
= y
+ h
;
254 // Update the visible canvas with the contents of the
257 if (this._renderQ
.length
!== 0 && !from_queue
) {
262 let x
= this._damageBounds
.left
;
263 let y
= this._damageBounds
.top
;
264 let w
= this._damageBounds
.right
- x
;
265 let h
= this._damageBounds
.bottom
- y
;
267 let vx
= x
- this._viewportLoc
.x
;
268 let vy
= y
- this._viewportLoc
.y
;
281 if ((vx
+ w
) > this._viewportLoc
.w
) {
282 w
= this._viewportLoc
.w
- vx
;
284 if ((vy
+ h
) > this._viewportLoc
.h
) {
285 h
= this._viewportLoc
.h
- vy
;
288 if ((w
> 0) && (h
> 0)) {
289 // FIXME: We may need to disable image smoothing here
290 // as well (see copyImage()), but we haven't
291 // noticed any problem yet.
292 this._targetCtx
.drawImage(this._backbuffer
,
297 this._damageBounds
.left
= this._damageBounds
.top
= 65535;
298 this._damageBounds
.right
= this._damageBounds
.bottom
= 0;
303 return this._renderQ
.length
> 0;
307 if (this._renderQ
.length
=== 0) {
310 this._flushing
= true;
314 fillRect(x
, y
, width
, height
, color
, from_queue
) {
315 if (this._renderQ
.length
!== 0 && !from_queue
) {
325 this._setFillColor(color
);
326 this._drawCtx
.fillRect(x
, y
, width
, height
);
327 this._damage(x
, y
, width
, height
);
331 copyImage(old_x
, old_y
, new_x
, new_y
, w
, h
, from_queue
) {
332 if (this._renderQ
.length
!== 0 && !from_queue
) {
343 // Due to this bug among others [1] we need to disable the image-smoothing to
344 // avoid getting a blur effect when copying data.
346 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
348 // We need to set these every time since all properties are reset
349 // when the the size is changed
350 this._drawCtx
.mozImageSmoothingEnabled
= false;
351 this._drawCtx
.webkitImageSmoothingEnabled
= false;
352 this._drawCtx
.msImageSmoothingEnabled
= false;
353 this._drawCtx
.imageSmoothingEnabled
= false;
355 this._drawCtx
.drawImage(this._backbuffer
,
358 this._damage(new_x
, new_y
, w
, h
);
362 imageRect(x
, y
, width
, height
, mime
, arr
) {
363 /* The internal logic cannot handle empty images, so bail early */
364 if ((width
=== 0) || (height
=== 0)) {
368 const img
= new Image();
369 img
.src
= "data: " + mime
+ ";base64," + Base64
.encode(arr
);
381 // start updating a tile
382 startTile(x
, y
, width
, height
, color
) {
385 if (width
=== 16 && height
=== 16) {
386 this._tile
= this._tile16x16
;
388 this._tile
= this._drawCtx
.createImageData(width
, height
);
391 const red
= color
[2];
392 const green
= color
[1];
393 const blue
= color
[0];
395 const data
= this._tile
.data
;
396 for (let i
= 0; i
< width
* height
* 4; i
+= 4) {
404 // update sub-rectangle of the current tile
405 subTile(x
, y
, w
, h
, color
) {
406 const red
= color
[2];
407 const green
= color
[1];
408 const blue
= color
[0];
412 const data
= this._tile
.data
;
413 const width
= this._tile
.width
;
414 for (let j
= y
; j
< yend
; j
++) {
415 for (let i
= x
; i
< xend
; i
++) {
416 const p
= (i
+ (j
* width
)) * 4;
425 // draw the current tile to the screen
427 this._drawCtx
.putImageData(this._tile
, this._tile_x
, this._tile_y
);
428 this._damage(this._tile_x
, this._tile_y
,
429 this._tile
.width
, this._tile
.height
);
432 blitImage(x
, y
, width
, height
, arr
, offset
, from_queue
) {
433 if (this._renderQ
.length
!== 0 && !from_queue
) {
434 // NB(directxman12): it's technically more performant here to use preallocated arrays,
435 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
436 // this probably isn't getting called *nearly* as much
437 const new_arr
= new Uint8Array(width
* height
* 4);
438 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
448 this._bgrxImageData(x
, y
, width
, height
, arr
, offset
);
452 blitRgbImage(x
, y
, width
, height
, arr
, offset
, from_queue
) {
453 if (this._renderQ
.length
!== 0 && !from_queue
) {
454 // NB(directxman12): it's technically more performant here to use preallocated arrays,
455 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
456 // this probably isn't getting called *nearly* as much
457 const new_arr
= new Uint8Array(width
* height
* 3);
458 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
468 this._rgbImageData(x
, y
, width
, height
, arr
, offset
);
472 blitRgbxImage(x
, y
, width
, height
, arr
, offset
, from_queue
) {
473 if (this._renderQ
.length
!== 0 && !from_queue
) {
474 // NB(directxman12): it's technically more performant here to use preallocated arrays,
475 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
476 // this probably isn't getting called *nearly* as much
477 const new_arr
= new Uint8Array(width
* height
* 4);
478 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
488 this._rgbxImageData(x
, y
, width
, height
, arr
, offset
);
492 drawImage(img
, x
, y
) {
493 this._drawCtx
.drawImage(img
, x
, y
);
494 this._damage(x
, y
, img
.width
, img
.height
);
497 autoscale(containerWidth
, containerHeight
) {
500 if (containerWidth
=== 0 || containerHeight
=== 0) {
505 const vp
= this._viewportLoc
;
506 const targetAspectRatio
= containerWidth
/ containerHeight
;
507 const fbAspectRatio
= vp
.w
/ vp
.h
;
509 if (fbAspectRatio
>= targetAspectRatio
) {
510 scaleRatio
= containerWidth
/ vp
.w
;
512 scaleRatio
= containerHeight
/ vp
.h
;
516 this._rescale(scaleRatio
);
519 // ===== PRIVATE METHODS =====
522 this._scale
= factor
;
523 const vp
= this._viewportLoc
;
525 // NB(directxman12): If you set the width directly, or set the
526 // style width to a number, the canvas is cleared.
527 // However, if you set the style width to a string
528 // ('NNNpx'), the canvas is scaled without clearing.
529 const width
= factor
* vp
.w
+ 'px';
530 const height
= factor
* vp
.h
+ 'px';
532 if ((this._target
.style
.width
!== width
) ||
533 (this._target
.style
.height
!== height
)) {
534 this._target
.style
.width
= width
;
535 this._target
.style
.height
= height
;
539 _setFillColor(color
) {
540 const newStyle
= 'rgb(' + color
[2] + ',' + color
[1] + ',' + color
[0] + ')';
541 if (newStyle
!== this._prevDrawStyle
) {
542 this._drawCtx
.fillStyle
= newStyle
;
543 this._prevDrawStyle
= newStyle
;
547 _rgbImageData(x
, y
, width
, height
, arr
, offset
) {
548 const img
= this._drawCtx
.createImageData(width
, height
);
549 const data
= img
.data
;
550 for (let i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 3) {
552 data
[i
+ 1] = arr
[j
+ 1];
553 data
[i
+ 2] = arr
[j
+ 2];
554 data
[i
+ 3] = 255; // Alpha
556 this._drawCtx
.putImageData(img
, x
, y
);
557 this._damage(x
, y
, img
.width
, img
.height
);
560 _bgrxImageData(x
, y
, width
, height
, arr
, offset
) {
561 const img
= this._drawCtx
.createImageData(width
, height
);
562 const data
= img
.data
;
563 for (let i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 4) {
564 data
[i
] = arr
[j
+ 2];
565 data
[i
+ 1] = arr
[j
+ 1];
566 data
[i
+ 2] = arr
[j
];
567 data
[i
+ 3] = 255; // Alpha
569 this._drawCtx
.putImageData(img
, x
, y
);
570 this._damage(x
, y
, img
.width
, img
.height
);
573 _rgbxImageData(x
, y
, width
, height
, arr
, offset
) {
574 // NB(directxman12): arr must be an Type Array view
576 if (supportsImageMetadata
) {
577 img
= new ImageData(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4), width
, height
);
579 img
= this._drawCtx
.createImageData(width
, height
);
580 img
.data
.set(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4));
582 this._drawCtx
.putImageData(img
, x
, y
);
583 this._damage(x
, y
, img
.width
, img
.height
);
586 _renderQ_push(action
) {
587 this._renderQ
.push(action
);
588 if (this._renderQ
.length
=== 1) {
589 // If this can be rendered immediately it will be, otherwise
590 // the scanner will wait for the relevant event
591 this._scan_renderQ();
596 // "this" is the object that is ready, not the
598 this.removeEventListener('load', this._noVNC_display
._resume_renderQ
);
599 this._noVNC_display
._scan_renderQ();
604 while (ready
&& this._renderQ
.length
> 0) {
605 const a
= this._renderQ
[0];
611 this.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
, true);
614 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
, true);
617 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
620 this.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
623 this.blitRgbxImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
626 /* IE tends to set "complete" prematurely, so check dimensions */
627 if (a
.img
.complete
&& (a
.img
.width
!== 0) && (a
.img
.height
!== 0)) {
628 if (a
.img
.width
!== a
.width
|| a
.img
.height
!== a
.height
) {
629 Log
.Error("Decoded image has incorrect dimensions. Got " +
630 a
.img
.width
+ "x" + a
.img
.height
+ ". Expected " +
631 a
.width
+ "x" + a
.height
+ ".");
634 this.drawImage(a
.img
, a
.x
, a
.y
);
636 a
.img
._noVNC_display
= this;
637 a
.img
.addEventListener('load', this._resume_renderQ
);
638 // We need to wait for this image to 'load'
639 // to keep things in-order
646 this._renderQ
.shift();
650 if (this._renderQ
.length
=== 0 && this._flushing
) {
651 this._flushing
= false;