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.
9import * as Log from './util/logging.js';
10import Base64 from "./base64.js";
11import { toSigned32bit } from './util/int.js';
13export default class Display {
17 this._renderQ = []; // queue drawing actions for in-oder rendering
18 this._flushPromise = null;
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 Log.Debug("<< Display.constructor");
60 // ===== PROPERTIES =====
63 this._clipViewport = false;
66 // ===== PROPERTIES =====
68 get scale() { return this._scale; }
73 get clipViewport() { return this._clipViewport; }
74 set clipViewport(viewport) {
75 this._clipViewport = viewport;
76 // May need to readjust the viewport dimensions
77 const vp = this._viewportLoc;
78 this.viewportChangeSize(vp.w, vp.h);
79 this.viewportChangePos(0, 0);
87 return this._fbHeight;
90 // ===== PUBLIC METHODS =====
92 viewportChangePos(deltaX, deltaY) {
93 const vp = this._viewportLoc;
94 deltaX = Math.floor(deltaX);
95 deltaY = Math.floor(deltaY);
97 if (!this._clipViewport) {
98 deltaX = -vp.w; // clamped later of out of bounds
102 const vx2 = vp.x + vp.w - 1;
103 const vy2 = vp.y + vp.h - 1;
107 if (deltaX < 0 && vp.x + deltaX < 0) {
110 if (vx2 + deltaX >= this._fbWidth) {
111 deltaX -= vx2 + deltaX - this._fbWidth + 1;
114 if (vp.y + deltaY < 0) {
117 if (vy2 + deltaY >= this._fbHeight) {
118 deltaY -= (vy2 + deltaY - this._fbHeight + 1);
121 if (deltaX === 0 && deltaY === 0) {
124 Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
129 this._damage(vp.x, vp.y, vp.w, vp.h);
134 viewportChangeSize(width, height) {
136 if (!this._clipViewport ||
137 typeof(width) === "undefined" ||
138 typeof(height) === "undefined") {
140 Log.Debug("Setting viewport to full display region");
141 width = this._fbWidth;
142 height = this._fbHeight;
145 width = Math.floor(width);
146 height = Math.floor(height);
148 if (width > this._fbWidth) {
149 width = this._fbWidth;
151 if (height > this._fbHeight) {
152 height = this._fbHeight;
155 const vp = this._viewportLoc;
156 if (vp.w !== width || vp.h !== height) {
160 const canvas = this._target;
161 canvas.width = width;
162 canvas.height = height;
164 // The position might need to be updated if we've grown
165 this.viewportChangePos(0, 0);
167 this._damage(vp.x, vp.y, vp.w, vp.h);
170 // Update the visible size of the target canvas
171 this._rescale(this._scale);
176 if (this._scale === 0) {
179 return toSigned32bit(x / this._scale + this._viewportLoc.x);
183 if (this._scale === 0) {
186 return toSigned32bit(y / this._scale + this._viewportLoc.y);
189 resize(width, height) {
190 this._prevDrawStyle = "";
192 this._fbWidth = width;
193 this._fbHeight = height;
195 const canvas = this._backbuffer;
196 if (canvas.width !== width || canvas.height !== height) {
198 // We have to save the canvas data since changing the size will clear it
200 if (canvas.width > 0 && canvas.height > 0) {
201 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
204 if (canvas.width !== width) {
205 canvas.width = width;
207 if (canvas.height !== height) {
208 canvas.height = height;
212 this._drawCtx.putImageData(saveImg, 0, 0);
216 // Readjust the viewport as it may be incorrectly sized
218 const vp = this._viewportLoc;
219 this.viewportChangeSize(vp.w, vp.h);
220 this.viewportChangePos(0, 0);
224 return this._drawCtx.getImageData(0, 0, this.width, this.height);
227 toDataURL(type, encoderOptions) {
228 return this._backbuffer.toDataURL(type, encoderOptions);
231 toBlob(callback, type, quality) {
232 return this._backbuffer.toBlob(callback, type, quality);
235 // Track what parts of the visible canvas that need updating
236 _damage(x, y, w, h) {
237 if (x < this._damageBounds.left) {
238 this._damageBounds.left = x;
240 if (y < this._damageBounds.top) {
241 this._damageBounds.top = y;
243 if ((x + w) > this._damageBounds.right) {
244 this._damageBounds.right = x + w;
246 if ((y + h) > this._damageBounds.bottom) {
247 this._damageBounds.bottom = y + h;
251 // Update the visible canvas with the contents of the
254 if (this._renderQ.length !== 0 && !fromQueue) {
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;
264 let vx = x - this._viewportLoc.x;
265 let vy = y - this._viewportLoc.y;
278 if ((vx + w) > this._viewportLoc.w) {
279 w = this._viewportLoc.w - vx;
281 if ((vy + h) > this._viewportLoc.h) {
282 h = this._viewportLoc.h - vy;
285 if ((w > 0) && (h > 0)) {
286 // FIXME: We may need to disable image smoothing here
287 // as well (see copyImage()), but we haven't
288 // noticed any problem yet.
289 this._targetCtx.drawImage(this._backbuffer,
294 this._damageBounds.left = this._damageBounds.top = 65535;
295 this._damageBounds.right = this._damageBounds.bottom = 0;
300 return this._renderQ.length > 0;
304 if (this._renderQ.length === 0) {
305 return Promise.resolve();
307 if (this._flushPromise === null) {
308 this._flushPromise = new Promise((resolve) => {
309 this._flushResolve = resolve;
312 return this._flushPromise;
316 fillRect(x, y, width, height, color, fromQueue) {
317 if (this._renderQ.length !== 0 && !fromQueue) {
327 this._setFillColor(color);
328 this._drawCtx.fillRect(x, y, width, height);
329 this._damage(x, y, width, height);
333 copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
334 if (this._renderQ.length !== 0 && !fromQueue) {
345 // Due to this bug among others [1] we need to disable the image-smoothing to
346 // avoid getting a blur effect when copying data.
348 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
350 // We need to set these every time since all properties are reset
351 // when the the size is changed
352 this._drawCtx.mozImageSmoothingEnabled = false;
353 this._drawCtx.webkitImageSmoothingEnabled = false;
354 this._drawCtx.msImageSmoothingEnabled = false;
355 this._drawCtx.imageSmoothingEnabled = false;
357 this._drawCtx.drawImage(this._backbuffer,
360 this._damage(newX, newY, w, h);
364 imageRect(x, y, width, height, mime, arr) {
365 /* The internal logic cannot handle empty images, so bail early */
366 if ((width === 0) || (height === 0)) {
370 const img = new Image();
371 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
383 blitImage(x, y, width, height, arr, offset, fromQueue) {
384 if (this._renderQ.length !== 0 && !fromQueue) {
385 // NB(directxman12): it's technically more performant here to use preallocated arrays,
386 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
387 // this probably isn't getting called *nearly* as much
388 const newArr = new Uint8Array(width * height * 4);
389 newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
399 // NB(directxman12): arr must be an Type Array view
400 let data = new Uint8ClampedArray(arr.buffer,
401 arr.byteOffset + offset,
403 let img = new ImageData(data, width, height);
404 this._drawCtx.putImageData(img, x, y);
405 this._damage(x, y, width, height);
409 drawImage(img, x, y) {
410 this._drawCtx.drawImage(img, x, y);
411 this._damage(x, y, img.width, img.height);
414 autoscale(containerWidth, containerHeight) {
417 if (containerWidth === 0 || containerHeight === 0) {
422 const vp = this._viewportLoc;
423 const targetAspectRatio = containerWidth / containerHeight;
424 const fbAspectRatio = vp.w / vp.h;
426 if (fbAspectRatio >= targetAspectRatio) {
427 scaleRatio = containerWidth / vp.w;
429 scaleRatio = containerHeight / vp.h;
433 this._rescale(scaleRatio);
436 // ===== PRIVATE METHODS =====
439 this._scale = factor;
440 const vp = this._viewportLoc;
442 // NB(directxman12): If you set the width directly, or set the
443 // style width to a number, the canvas is cleared.
444 // However, if you set the style width to a string
445 // ('NNNpx'), the canvas is scaled without clearing.
446 const width = factor * vp.w + 'px';
447 const height = factor * vp.h + 'px';
449 if ((this._target.style.width !== width) ||
450 (this._target.style.height !== height)) {
451 this._target.style.width = width;
452 this._target.style.height = height;
456 _setFillColor(color) {
457 const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
458 if (newStyle !== this._prevDrawStyle) {
459 this._drawCtx.fillStyle = newStyle;
460 this._prevDrawStyle = newStyle;
464 _renderQPush(action) {
465 this._renderQ.push(action);
466 if (this._renderQ.length === 1) {
467 // If this can be rendered immediately it will be, otherwise
468 // the scanner will wait for the relevant event
474 // "this" is the object that is ready, not the
476 this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
477 this._noVNCDisplay._scanRenderQ();
482 while (ready && this._renderQ.length > 0) {
483 const a = this._renderQ[0];
489 this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
492 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
495 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
498 if (a.img.complete) {
499 if (a.img.width !== a.width || a.img.height !== a.height) {
500 Log.Error("Decoded image has incorrect dimensions. Got " +
501 a.img.width + "x" + a.img.height + ". Expected " +
502 a.width + "x" + a.height + ".");
505 this.drawImage(a.img, a.x, a.y);
507 a.img._noVNCDisplay = this;
508 a.img.addEventListener('load', this._resumeRenderQ);
509 // We need to wait for this image to 'load'
510 // to keep things in-order
517 this._renderQ.shift();
521 if (this._renderQ.length === 0 &&
522 this._flushPromise !== null) {
523 this._flushResolve();
524 this._flushPromise = null;
525 this._flushResolve = null;