display.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /*
  2. * noVNC: HTML5 VNC client
  3. * Copyright (C) 2019 The noVNC Authors
  4. * Licensed under MPL 2.0 (see LICENSE.txt)
  5. *
  6. * See README.md for usage and integration instructions.
  7. */
  8. import * as Log from './util/logging.js';
  9. import Base64 from "./base64.js";
  10. import { toSigned32bit } from './util/int.js';
  11. export default class Display {
  12. constructor(target) {
  13. this._drawCtx = null;
  14. this._renderQ = []; // queue drawing actions for in-oder rendering
  15. this._flushPromise = null;
  16. // the full frame buffer (logical canvas) size
  17. this._fbWidth = 0;
  18. this._fbHeight = 0;
  19. this._prevDrawStyle = "";
  20. Log.Debug(">> Display.constructor");
  21. // The visible canvas
  22. this._target = target;
  23. if (!this._target) {
  24. throw new Error("Target must be set");
  25. }
  26. if (typeof this._target === 'string') {
  27. throw new Error('target must be a DOM element');
  28. }
  29. if (!this._target.getContext) {
  30. throw new Error("no getContext method");
  31. }
  32. this._targetCtx = this._target.getContext('2d');
  33. // the visible canvas viewport (i.e. what actually gets seen)
  34. this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
  35. // The hidden canvas, where we do the actual rendering
  36. this._backbuffer = document.createElement('canvas');
  37. this._drawCtx = this._backbuffer.getContext('2d');
  38. this._damageBounds = { left: 0, top: 0,
  39. right: this._backbuffer.width,
  40. bottom: this._backbuffer.height };
  41. Log.Debug("User Agent: " + navigator.userAgent);
  42. Log.Debug("<< Display.constructor");
  43. // ===== PROPERTIES =====
  44. this._scale = 1.0;
  45. this._clipViewport = false;
  46. }
  47. // ===== PROPERTIES =====
  48. get scale() { return this._scale; }
  49. set scale(scale) {
  50. this._rescale(scale);
  51. }
  52. get clipViewport() { return this._clipViewport; }
  53. set clipViewport(viewport) {
  54. this._clipViewport = viewport;
  55. // May need to readjust the viewport dimensions
  56. const vp = this._viewportLoc;
  57. this.viewportChangeSize(vp.w, vp.h);
  58. this.viewportChangePos(0, 0);
  59. }
  60. get width() {
  61. return this._fbWidth;
  62. }
  63. get height() {
  64. return this._fbHeight;
  65. }
  66. // ===== PUBLIC METHODS =====
  67. viewportChangePos(deltaX, deltaY) {
  68. const vp = this._viewportLoc;
  69. deltaX = Math.floor(deltaX);
  70. deltaY = Math.floor(deltaY);
  71. if (!this._clipViewport) {
  72. deltaX = -vp.w; // clamped later of out of bounds
  73. deltaY = -vp.h;
  74. }
  75. const vx2 = vp.x + vp.w - 1;
  76. const vy2 = vp.y + vp.h - 1;
  77. // Position change
  78. if (deltaX < 0 && vp.x + deltaX < 0) {
  79. deltaX = -vp.x;
  80. }
  81. if (vx2 + deltaX >= this._fbWidth) {
  82. deltaX -= vx2 + deltaX - this._fbWidth + 1;
  83. }
  84. if (vp.y + deltaY < 0) {
  85. deltaY = -vp.y;
  86. }
  87. if (vy2 + deltaY >= this._fbHeight) {
  88. deltaY -= (vy2 + deltaY - this._fbHeight + 1);
  89. }
  90. if (deltaX === 0 && deltaY === 0) {
  91. return;
  92. }
  93. Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
  94. vp.x += deltaX;
  95. vp.y += deltaY;
  96. this._damage(vp.x, vp.y, vp.w, vp.h);
  97. this.flip();
  98. }
  99. viewportChangeSize(width, height) {
  100. if (!this._clipViewport ||
  101. typeof(width) === "undefined" ||
  102. typeof(height) === "undefined") {
  103. Log.Debug("Setting viewport to full display region");
  104. width = this._fbWidth;
  105. height = this._fbHeight;
  106. }
  107. width = Math.floor(width);
  108. height = Math.floor(height);
  109. if (width > this._fbWidth) {
  110. width = this._fbWidth;
  111. }
  112. if (height > this._fbHeight) {
  113. height = this._fbHeight;
  114. }
  115. const vp = this._viewportLoc;
  116. if (vp.w !== width || vp.h !== height) {
  117. vp.w = width;
  118. vp.h = height;
  119. const canvas = this._target;
  120. canvas.width = width;
  121. canvas.height = height;
  122. // The position might need to be updated if we've grown
  123. this.viewportChangePos(0, 0);
  124. this._damage(vp.x, vp.y, vp.w, vp.h);
  125. this.flip();
  126. // Update the visible size of the target canvas
  127. this._rescale(this._scale);
  128. }
  129. }
  130. absX(x) {
  131. if (this._scale === 0) {
  132. return 0;
  133. }
  134. return toSigned32bit(x / this._scale + this._viewportLoc.x);
  135. }
  136. absY(y) {
  137. if (this._scale === 0) {
  138. return 0;
  139. }
  140. return toSigned32bit(y / this._scale + this._viewportLoc.y);
  141. }
  142. resize(width, height) {
  143. this._prevDrawStyle = "";
  144. this._fbWidth = width;
  145. this._fbHeight = height;
  146. const canvas = this._backbuffer;
  147. if (canvas.width !== width || canvas.height !== height) {
  148. // We have to save the canvas data since changing the size will clear it
  149. let saveImg = null;
  150. if (canvas.width > 0 && canvas.height > 0) {
  151. saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
  152. }
  153. if (canvas.width !== width) {
  154. canvas.width = width;
  155. }
  156. if (canvas.height !== height) {
  157. canvas.height = height;
  158. }
  159. if (saveImg) {
  160. this._drawCtx.putImageData(saveImg, 0, 0);
  161. }
  162. }
  163. // Readjust the viewport as it may be incorrectly sized
  164. // and positioned
  165. const vp = this._viewportLoc;
  166. this.viewportChangeSize(vp.w, vp.h);
  167. this.viewportChangePos(0, 0);
  168. }
  169. getImageData() {
  170. return this._drawCtx.getImageData(0, 0, this.width, this.height);
  171. }
  172. toDataURL(type, encoderOptions) {
  173. return this._backbuffer.toDataURL(type, encoderOptions);
  174. }
  175. toBlob(callback, type, quality) {
  176. return this._backbuffer.toBlob(callback, type, quality);
  177. }
  178. // Track what parts of the visible canvas that need updating
  179. _damage(x, y, w, h) {
  180. if (x < this._damageBounds.left) {
  181. this._damageBounds.left = x;
  182. }
  183. if (y < this._damageBounds.top) {
  184. this._damageBounds.top = y;
  185. }
  186. if ((x + w) > this._damageBounds.right) {
  187. this._damageBounds.right = x + w;
  188. }
  189. if ((y + h) > this._damageBounds.bottom) {
  190. this._damageBounds.bottom = y + h;
  191. }
  192. }
  193. // Update the visible canvas with the contents of the
  194. // rendering canvas
  195. flip(fromQueue) {
  196. if (this._renderQ.length !== 0 && !fromQueue) {
  197. this._renderQPush({
  198. 'type': 'flip'
  199. });
  200. } else {
  201. let x = this._damageBounds.left;
  202. let y = this._damageBounds.top;
  203. let w = this._damageBounds.right - x;
  204. let h = this._damageBounds.bottom - y;
  205. let vx = x - this._viewportLoc.x;
  206. let vy = y - this._viewportLoc.y;
  207. if (vx < 0) {
  208. w += vx;
  209. x -= vx;
  210. vx = 0;
  211. }
  212. if (vy < 0) {
  213. h += vy;
  214. y -= vy;
  215. vy = 0;
  216. }
  217. if ((vx + w) > this._viewportLoc.w) {
  218. w = this._viewportLoc.w - vx;
  219. }
  220. if ((vy + h) > this._viewportLoc.h) {
  221. h = this._viewportLoc.h - vy;
  222. }
  223. if ((w > 0) && (h > 0)) {
  224. // FIXME: We may need to disable image smoothing here
  225. // as well (see copyImage()), but we haven't
  226. // noticed any problem yet.
  227. this._targetCtx.drawImage(this._backbuffer,
  228. x, y, w, h,
  229. vx, vy, w, h);
  230. }
  231. this._damageBounds.left = this._damageBounds.top = 65535;
  232. this._damageBounds.right = this._damageBounds.bottom = 0;
  233. }
  234. }
  235. pending() {
  236. return this._renderQ.length > 0;
  237. }
  238. flush() {
  239. if (this._renderQ.length === 0) {
  240. return Promise.resolve();
  241. } else {
  242. if (this._flushPromise === null) {
  243. this._flushPromise = new Promise((resolve) => {
  244. this._flushResolve = resolve;
  245. });
  246. }
  247. return this._flushPromise;
  248. }
  249. }
  250. fillRect(x, y, width, height, color, fromQueue) {
  251. if (this._renderQ.length !== 0 && !fromQueue) {
  252. this._renderQPush({
  253. 'type': 'fill',
  254. 'x': x,
  255. 'y': y,
  256. 'width': width,
  257. 'height': height,
  258. 'color': color
  259. });
  260. } else {
  261. this._setFillColor(color);
  262. this._drawCtx.fillRect(x, y, width, height);
  263. this._damage(x, y, width, height);
  264. }
  265. }
  266. copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
  267. if (this._renderQ.length !== 0 && !fromQueue) {
  268. this._renderQPush({
  269. 'type': 'copy',
  270. 'oldX': oldX,
  271. 'oldY': oldY,
  272. 'x': newX,
  273. 'y': newY,
  274. 'width': w,
  275. 'height': h,
  276. });
  277. } else {
  278. // Due to this bug among others [1] we need to disable the image-smoothing to
  279. // avoid getting a blur effect when copying data.
  280. //
  281. // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
  282. //
  283. // We need to set these every time since all properties are reset
  284. // when the the size is changed
  285. this._drawCtx.mozImageSmoothingEnabled = false;
  286. this._drawCtx.webkitImageSmoothingEnabled = false;
  287. this._drawCtx.msImageSmoothingEnabled = false;
  288. this._drawCtx.imageSmoothingEnabled = false;
  289. this._drawCtx.drawImage(this._backbuffer,
  290. oldX, oldY, w, h,
  291. newX, newY, w, h);
  292. this._damage(newX, newY, w, h);
  293. }
  294. }
  295. imageRect(x, y, width, height, mime, arr) {
  296. /* The internal logic cannot handle empty images, so bail early */
  297. if ((width === 0) || (height === 0)) {
  298. return;
  299. }
  300. const img = new Image();
  301. img.src = "data: " + mime + ";base64," + Base64.encode(arr);
  302. this._renderQPush({
  303. 'type': 'img',
  304. 'img': img,
  305. 'x': x,
  306. 'y': y,
  307. 'width': width,
  308. 'height': height
  309. });
  310. }
  311. blitImage(x, y, width, height, arr, offset, fromQueue) {
  312. if (this._renderQ.length !== 0 && !fromQueue) {
  313. // NB(directxman12): it's technically more performant here to use preallocated arrays,
  314. // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
  315. // this probably isn't getting called *nearly* as much
  316. const newArr = new Uint8Array(width * height * 4);
  317. newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
  318. this._renderQPush({
  319. 'type': 'blit',
  320. 'data': newArr,
  321. 'x': x,
  322. 'y': y,
  323. 'width': width,
  324. 'height': height,
  325. });
  326. } else {
  327. // NB(directxman12): arr must be an Type Array view
  328. let data = new Uint8ClampedArray(arr.buffer,
  329. arr.byteOffset + offset,
  330. width * height * 4);
  331. let img = new ImageData(data, width, height);
  332. this._drawCtx.putImageData(img, x, y);
  333. this._damage(x, y, width, height);
  334. }
  335. }
  336. drawImage(img, x, y) {
  337. this._drawCtx.drawImage(img, x, y);
  338. this._damage(x, y, img.width, img.height);
  339. }
  340. autoscale(containerWidth, containerHeight) {
  341. let scaleRatio;
  342. if (containerWidth === 0 || containerHeight === 0) {
  343. scaleRatio = 0;
  344. } else {
  345. const vp = this._viewportLoc;
  346. const targetAspectRatio = containerWidth / containerHeight;
  347. const fbAspectRatio = vp.w / vp.h;
  348. if (fbAspectRatio >= targetAspectRatio) {
  349. scaleRatio = containerWidth / vp.w;
  350. } else {
  351. scaleRatio = containerHeight / vp.h;
  352. }
  353. }
  354. this._rescale(scaleRatio);
  355. }
  356. // ===== PRIVATE METHODS =====
  357. _rescale(factor) {
  358. this._scale = factor;
  359. const vp = this._viewportLoc;
  360. // NB(directxman12): If you set the width directly, or set the
  361. // style width to a number, the canvas is cleared.
  362. // However, if you set the style width to a string
  363. // ('NNNpx'), the canvas is scaled without clearing.
  364. const width = factor * vp.w + 'px';
  365. const height = factor * vp.h + 'px';
  366. if ((this._target.style.width !== width) ||
  367. (this._target.style.height !== height)) {
  368. this._target.style.width = width;
  369. this._target.style.height = height;
  370. }
  371. }
  372. _setFillColor(color) {
  373. const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
  374. if (newStyle !== this._prevDrawStyle) {
  375. this._drawCtx.fillStyle = newStyle;
  376. this._prevDrawStyle = newStyle;
  377. }
  378. }
  379. _renderQPush(action) {
  380. this._renderQ.push(action);
  381. if (this._renderQ.length === 1) {
  382. // If this can be rendered immediately it will be, otherwise
  383. // the scanner will wait for the relevant event
  384. this._scanRenderQ();
  385. }
  386. }
  387. _resumeRenderQ() {
  388. // "this" is the object that is ready, not the
  389. // display object
  390. this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
  391. this._noVNCDisplay._scanRenderQ();
  392. }
  393. _scanRenderQ() {
  394. let ready = true;
  395. while (ready && this._renderQ.length > 0) {
  396. const a = this._renderQ[0];
  397. switch (a.type) {
  398. case 'flip':
  399. this.flip(true);
  400. break;
  401. case 'copy':
  402. this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
  403. break;
  404. case 'fill':
  405. this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
  406. break;
  407. case 'blit':
  408. this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
  409. break;
  410. case 'img':
  411. if (a.img.complete) {
  412. if (a.img.width !== a.width || a.img.height !== a.height) {
  413. Log.Error("Decoded image has incorrect dimensions. Got " +
  414. a.img.width + "x" + a.img.height + ". Expected " +
  415. a.width + "x" + a.height + ".");
  416. return;
  417. }
  418. this.drawImage(a.img, a.x, a.y);
  419. } else {
  420. a.img._noVNCDisplay = this;
  421. a.img.addEventListener('load', this._resumeRenderQ);
  422. // We need to wait for this image to 'load'
  423. // to keep things in-order
  424. ready = false;
  425. }
  426. break;
  427. }
  428. if (ready) {
  429. this._renderQ.shift();
  430. }
  431. }
  432. if (this._renderQ.length === 0 &&
  433. this._flushPromise !== null) {
  434. this._flushResolve();
  435. this._flushPromise = null;
  436. this._flushResolve = null;
  437. }
  438. }
  439. }