From 5c15e53abbed3d1ce2a87660d4147de07eca7ced Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Fri, 30 Jul 2021 00:05:18 -0500 Subject: [PATCH] Mct4039 (#4057) Re-implements ImageExportService as ES6 class instead of Angular managed service. Co-authored-by: John Hill Co-authored-by: Andrew Henry --- src/adapter/bundle.js | 13 +- src/adapter/services/ExportImageService.js | 218 ------------------ src/exporters/ImageExporter.js | 185 +++++++++++++++ src/exporters/ImageExporterSpec.js | 58 +++++ .../notebook/components/NotebookEmbed.vue | 7 +- src/plugins/notebook/snapshot.js | 6 +- src/plugins/plot/Plot.vue | 12 +- src/plugins/plot/pluginSpec.js | 8 - src/plugins/plot/stackedPlot/StackedPlot.vue | 15 +- src/plugins/viewDatumAction/pluginSpec.js | 2 - 10 files changed, 265 insertions(+), 259 deletions(-) delete mode 100644 src/adapter/services/ExportImageService.js create mode 100644 src/exporters/ImageExporter.js create mode 100644 src/exporters/ImageExporterSpec.js diff --git a/src/adapter/bundle.js b/src/adapter/bundle.js index 894e8df910..0fc7c2d110 100644 --- a/src/adapter/bundle.js +++ b/src/adapter/bundle.js @@ -36,8 +36,7 @@ define([ './views/installLegacyViews', './policies/LegacyCompositionPolicyAdapter', './actions/LegacyActionAdapter', - './services/LegacyPersistenceAdapter', - './services/ExportImageService' + './services/LegacyPersistenceAdapter' ], function ( ActionDialogDecorator, AdapterCapability, @@ -54,8 +53,7 @@ define([ installLegacyViews, legacyCompositionPolicyAdapter, LegacyActionAdapter, - LegacyPersistenceAdapter, - ExportImageService + LegacyPersistenceAdapter ) { return { name: 'src/adapter', @@ -84,13 +82,6 @@ define([ "identifierService", "cacheService" ] - }, - { - "key": "exportImageService", - "implementation": ExportImageService, - "depends": [ - "dialogService" - ] } ], components: [ diff --git a/src/adapter/services/ExportImageService.js b/src/adapter/services/ExportImageService.js deleted file mode 100644 index 014e4a911e..0000000000 --- a/src/adapter/services/ExportImageService.js +++ /dev/null @@ -1,218 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2021, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/** - * Module defining ExportImageService. Created by hudsonfoo on 09/02/16 - */ -define( - [ - "html2canvas", - "saveAs" - ], - function ( - html2canvas, - { saveAs } - ) { - - /** - * The export image service will export any HTML node to - * JPG, or PNG. - * @param {object} dialogService - * @constructor - */ - function ExportImageService(dialogService) { - this.dialogService = dialogService; - this.exportCount = 0; - } - - /** - * Converts an HTML element into a PNG or JPG Blob. - * @private - * @param {node} element that will be converted to an image - * @param {object} options Image options. - * @returns {promise} - */ - ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) { - const self = this; - const dialogService = this.dialogService; - const dialog = dialogService.showBlockingMessage({ - title: "Capturing...", - hint: "Capturing an image", - unknownProgress: true, - severity: "info", - delay: true - }); - - let mimeType = "image/png"; - if (imageType === "jpg") { - mimeType = "image/jpeg"; - } - - let exportId = undefined; - let oldId = undefined; - if (className) { - exportId = 'export-element-' + this.exportCount; - this.exportCount++; - oldId = element.id; - element.id = exportId; - } - - return html2canvas(element, { - onclone: function (document) { - if (className) { - const clonedElement = document.getElementById(exportId); - clonedElement.classList.add(className); - } - - element.id = oldId; - }, - removeContainer: true // Set to false to debug what html2canvas renders - }).then(function (canvas) { - dialog.dismiss(); - - return new Promise(function (resolve, reject) { - if (thumbnailSize) { - const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize); - - return canvas.toBlob(blob => resolve({ - blob, - thumbnail - }), mimeType); - } - - return canvas.toBlob(blob => resolve({ blob }), mimeType); - }); - }, function (error) { - console.log('error capturing image', error); - dialog.dismiss(); - const errorDialog = dialogService.showBlockingMessage({ - title: "Error capturing image", - severity: "error", - hint: "Image was not captured successfully!", - options: [{ - label: "OK", - callback: function () { - errorDialog.dismiss(); - } - }] - }); - }); - }; - - ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) { - const thumbnailCanvas = document.createElement('canvas'); - thumbnailCanvas.setAttribute('width', size.width); - thumbnailCanvas.setAttribute('height', size.height); - const ctx = thumbnailCanvas.getContext('2d'); - ctx.globalCompositeOperation = "copy"; - ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); - - return thumbnailCanvas.toDataURL(mimeType); - }; - - /** - * Takes a screenshot of a DOM node and exports to JPG. - * @param {node} element to be exported - * @param {string} filename the exported image - * @param {string} className to be added to element before capturing (optional) - * @returns {promise} - */ - ExportImageService.prototype.exportJPG = function (element, filename, className) { - const processedFilename = replaceDotsWithUnderscores(filename); - - return this.renderElement(element, { - imageType: 'jpg', - className - }) - .then(function (img) { - saveAs(img.blob, processedFilename); - }); - }; - - /** - * Takes a screenshot of a DOM node and exports to PNG. - * @param {node} element to be exported - * @param {string} filename the exported image - * @param {string} className to be added to element before capturing (optional) - * @returns {promise} - */ - ExportImageService.prototype.exportPNG = function (element, filename, className) { - const processedFilename = replaceDotsWithUnderscores(filename); - - return this.renderElement(element, { - imageType: 'png', - className - }) - .then(function (img) { - saveAs(img.blob, processedFilename); - }); - }; - - /** - * Takes a screenshot of a DOM node in PNG format. - * @param {node} element to be exported - * @param {string} filename the exported image - * @returns {promise} - */ - - ExportImageService.prototype.exportPNGtoSRC = function (element, options) { - - return this.renderElement(element, { - imageType: 'png', - ...options - }); - }; - - function replaceDotsWithUnderscores(filename) { - const regex = /\./gi; - - return filename.replace(regex, '_'); - } - - /** - * canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill - * implements the method in browsers that would not otherwise support it. - * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob - */ - function polyfillToBlob() { - if (!HTMLCanvasElement.prototype.toBlob) { - Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", { - value: function (callback, mimeType, quality) { - const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]); - const len = binStr.length; - const arr = new Uint8Array(len); - - for (let i = 0; i < len; i++) { - arr[i] = binStr.charCodeAt(i); - } - - callback(new Blob([arr], {type: mimeType || "image/png"})); - } - }); - } - } - - polyfillToBlob(); - - return ExportImageService; - } -); diff --git a/src/exporters/ImageExporter.js b/src/exporters/ImageExporter.js new file mode 100644 index 0000000000..1fea59e168 --- /dev/null +++ b/src/exporters/ImageExporter.js @@ -0,0 +1,185 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2021, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * Class defining an image exporter for JPG/PNG output. + * Originally created by hudsonfoo on 09/02/16 + */ + +function replaceDotsWithUnderscores(filename) { + const regex = /\./gi; + + return filename.replace(regex, '_'); +} + +import {saveAs} from 'file-saver/FileSaver'; +import html2canvas from 'html2canvas'; +import uuid from 'uuid'; + +class ImageExporter { + constructor(openmct) { + this.openmct = openmct; + } + /** + * Converts an HTML element into a PNG or JPG Blob. + * @private + * @param {node} element that will be converted to an image + * @param {object} options Image options. + * @returns {promise} + */ + renderElement(element, { imageType, className, thumbnailSize }) { + const self = this; + const overlays = this.openmct.overlays; + const dialog = overlays.dialog({ + iconClass: 'info', + message: 'Caputuring an image', + buttons: [ + { + label: 'Cancel', + emphasis: true, + callback: function () { + dialog.dismiss(); + } + } + ] + }); + + let mimeType = 'image/png'; + if (imageType === 'jpg') { + mimeType = 'image/jpeg'; + } + + let exportId = undefined; + let oldId = undefined; + if (className) { + const newUUID = uuid(); + exportId = `$export-element-${newUUID}`; + oldId = element.id; + element.id = exportId; + } + + return html2canvas(element, { + onclone: function (document) { + if (className) { + const clonedElement = document.getElementById(exportId); + clonedElement.classList.add(className); + } + + element.id = oldId; + }, + removeContainer: true // Set to false to debug what html2canvas renders + }).then(function (canvas) { + dialog.dismiss(); + + return new Promise(function (resolve, reject) { + if (thumbnailSize) { + const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize); + + return canvas.toBlob(blob => resolve({ + blob, + thumbnail + }), mimeType); + } + + return canvas.toBlob(blob => resolve({ blob }), mimeType); + }); + }, function (error) { + console.log('error capturing image', error); + dialog.dismiss(); + const errorDialog = overlays.dialog({ + iconClass: 'error', + message: 'Image was not captured successfully!', + buttons: [ + { + label: "OK", + emphasis: true, + callback: function () { + errorDialog.dismiss(); + } + } + ] + }); + }); + } + + getThumbnail(canvas, mimeType, size) { + const thumbnailCanvas = document.createElement('canvas'); + thumbnailCanvas.setAttribute('width', size.width); + thumbnailCanvas.setAttribute('height', size.height); + const ctx = thumbnailCanvas.getContext('2d'); + ctx.globalCompositeOperation = "copy"; + ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); + + return thumbnailCanvas.toDataURL(mimeType); + } + + /** + * Takes a screenshot of a DOM node and exports to JPG. + * @param {node} element to be exported + * @param {string} filename the exported image + * @param {string} className to be added to element before capturing (optional) + * @returns {promise} + */ + async exportJPG(element, filename, className) { + const processedFilename = replaceDotsWithUnderscores(filename); + + const img = await this.renderElement(element, { + imageType: 'jpg', + className + }); + saveAs(img.blob, processedFilename); + } + + /** + * Takes a screenshot of a DOM node and exports to PNG. + * @param {node} element to be exported + * @param {string} filename the exported image + * @param {string} className to be added to element before capturing (optional) + * @returns {promise} + */ + async exportPNG(element, filename, className) { + const processedFilename = replaceDotsWithUnderscores(filename); + + const img = await this.renderElement(element, { + imageType: 'png', + className + }); + saveAs(img.blob, processedFilename); + } + + /** + * Takes a screenshot of a DOM node in PNG format. + * @param {node} element to be exported + * @param {string} filename the exported image + * @returns {promise} + */ + + exportPNGtoSRC(element, options) { + return this.renderElement(element, { + imageType: 'png', + ...options + }); + } +} + +export default ImageExporter; + diff --git a/src/exporters/ImageExporterSpec.js b/src/exporters/ImageExporterSpec.js new file mode 100644 index 0000000000..bb00cfd4a5 --- /dev/null +++ b/src/exporters/ImageExporterSpec.js @@ -0,0 +1,58 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2021, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import ImageExporter from './ImageExporter'; +import { createOpenMct, resetApplicationState } from '../utils/testing'; + +describe('The Image Exporter', () => { + let openmct; + let imageExporter; + + beforeEach(() => { + openmct = createOpenMct(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe("basic instatation", () => { + it("can be instatiated", () => { + imageExporter = new ImageExporter(openmct); + + expect(imageExporter).not.toEqual(null); + }); + it("can render an element to a blob", async () => { + const mockHeadElement = document.createElement("h1"); + const mockTextNode = document.createTextNode('foo bar'); + mockHeadElement.appendChild(mockTextNode); + document.body.appendChild(mockHeadElement); + imageExporter = new ImageExporter(openmct); + const returnedBlob = await imageExporter.renderElement(document.body, { + imageType: 'png' + }); + expect(returnedBlob).not.toEqual(null); + expect(returnedBlob.blob).not.toEqual(null); + expect(returnedBlob.blob).toBeInstanceOf(Blob); + }); + }); +}); diff --git a/src/plugins/notebook/components/NotebookEmbed.vue b/src/plugins/notebook/components/NotebookEmbed.vue index c343299f2c..6011a0d544 100644 --- a/src/plugins/notebook/components/NotebookEmbed.vue +++ b/src/plugins/notebook/components/NotebookEmbed.vue @@ -31,6 +31,7 @@ import PainterroInstance from '../utils/painterroInstance'; import SnapshotTemplate from './snapshot-template.html'; import { updateNotebookImageDomainObject } from '../utils/notebook-image'; +import ImageExporter from '../../../exporters/ImageExporter'; import PopupMenu from './PopupMenu.vue'; import Vue from 'vue'; @@ -71,7 +72,7 @@ export default { }, mounted() { this.addPopupMenuItems(); - this.exportImageService = this.openmct.$injector.get('exportImageService'); + this.imageExporter = new ImageExporter(this.openmct); }, methods: { addPopupMenuItems() { @@ -234,9 +235,9 @@ export default { let element = this.snapshot.$refs['snapshot-image']; if (type === 'png') { - this.exportImageService.exportPNG(element, this.embed.name); + this.imageExporter.exportPNG(element, this.embed.name); } else { - this.exportImageService.exportJPG(element, this.embed.name); + this.imageExporter.exportJPG(element, this.embed.name); } }, previewEmbed() { diff --git a/src/plugins/notebook/snapshot.js b/src/plugins/notebook/snapshot.js index 05a384d3ad..fd43be5fec 100644 --- a/src/plugins/notebook/snapshot.js +++ b/src/plugins/notebook/snapshot.js @@ -4,24 +4,24 @@ import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants'; import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image'; import SnapshotContainer from './snapshot-container'; +import ImageExporter from '../../exporters/ImageExporter'; export default class Snapshot { constructor(openmct) { this.openmct = openmct; this.snapshotContainer = new SnapshotContainer(openmct); + this.imageExporter = new ImageExporter(openmct); this.capture = this.capture.bind(this); this._saveSnapShot = this._saveSnapShot.bind(this); } capture(snapshotMeta, notebookType, domElement) { - const exportImageService = this.openmct.$injector.get('exportImageService'); - const options = { className: 's-status-taking-snapshot', thumbnailSize: DEFAULT_SIZE }; - exportImageService.exportPNGtoSRC(domElement, options) + this.imageExporter.exportPNGtoSRC(domElement, options) .then(function ({blob, thumbnail}) { const reader = new window.FileReader(); reader.readAsDataURL(blob); diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue index 0512830eae..c8c1e3ffc1 100644 --- a/src/plugins/plot/Plot.vue +++ b/src/plugins/plot/Plot.vue @@ -72,7 +72,8 @@