Re-implements ImageExportService as ES6 class instead of Angular managed service.

Co-authored-by: John Hill <jchill2.spam@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Scott Bell 2021-07-30 00:05:18 -05:00 committed by GitHub
parent f58b3881f2
commit 5c15e53abb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 265 additions and 259 deletions

View File

@ -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: [

View File

@ -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;
}
);

View File

@ -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;

View File

@ -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);
});
});
});

View File

@ -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() {

View File

@ -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);

View File

@ -72,7 +72,8 @@
</template>
<script>
import eventHelpers from "./lib/eventHelpers";
import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import MctPlot from './MctPlot.vue';
export default {
@ -102,8 +103,7 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.exportImageService = this.openmct.$injector.get('exportImageService');
this.imageExporter = new ImageExporter(this.openmct);
},
beforeDestroy() {
this.destroy();
@ -118,14 +118,12 @@ export default {
exportJPG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot');
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
},
exportPNG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot');
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
},
toggleCursorGuide() {

View File

@ -325,14 +325,6 @@ describe("the plugin", function () {
start: 0,
end: 4
});
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
testTelemetryObject = {
identifier: {
namespace: "",

View File

@ -67,8 +67,9 @@
</template>
<script>
import eventHelpers from "../lib/eventHelpers";
import StackedPlotItem from "./StackedPlotItem.vue";
import eventHelpers from '../lib/eventHelpers';
import StackedPlotItem from './StackedPlotItem.vue';
import ImageExporter from '../../../exporters/ImageExporter';
export default {
components: {
@ -103,7 +104,7 @@ export default {
mounted() {
eventHelpers.extend(this);
this.exportImageService = this.openmct.$injector.get('exportImageService');
this.imageExporter = new ImageExporter(this.openmct);
this.tickWidthMap = {};
@ -159,9 +160,9 @@ export default {
exportJPG() {
this.hideExportButtons = true;
const plotElement = this.$refs.plotContainer;
const plotElement = this.$el;
this.exportImageService.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot')
this.imageExporter.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot')
.finally(function () {
this.hideExportButtons = false;
}.bind(this));
@ -170,9 +171,9 @@ export default {
exportPNG() {
this.hideExportButtons = true;
const plotElement = this.$refs.plotContainer;
const plotElement = this.$el;
this.exportImageService.exportPNG(plotElement, 'stacked-plot.png', 'export-plot')
this.imageExporter.exportPNG(plotElement, 'stacked-plot.png', 'export-plot')
.finally(function () {
this.hideExportButtons = false;
}.bind(this));

View File

@ -66,8 +66,6 @@ describe("the plugin", () => {
};
}
};
done();
});
afterEach(() => {