[Notebooks] Don't save images on the object (#3792)

* Create and store image data into new domain object of type 'notebookSnapshotImage'
* Reduced thumbnail size to 30px
* Image migration script for old notebooks.
* Saves thumbnail image on notebook instead of new object.
This commit is contained in:
Nikhil 2021-06-21 15:42:33 -07:00 committed by GitHub
parent fa5aceb7b3
commit 925518c83f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 302 additions and 56 deletions

View File

@ -41,10 +41,10 @@
"jsdoc": "^3.3.2",
"karma": "5.1.1",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "1.3.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "1.3.0",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
@ -60,7 +60,7 @@
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"node-sass": "^4.14.1",
"painterro": "^1.0.35",
"painterro": "^1.2.56",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",

View File

@ -48,12 +48,12 @@ define(
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {string} type of image to convert the element to.
* @param {object} options Image options.
* @returns {promise}
*/
ExportImageService.prototype.renderElement = function (element, imageType, className) {
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",
@ -90,7 +90,16 @@ define(
dialog.dismiss();
return new Promise(function (resolve, reject) {
return canvas.toBlob(resolve, mimeType);
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);
@ -109,6 +118,17 @@ define(
});
};
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
@ -119,9 +139,13 @@ define(
ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "jpg", className).then(function (img) {
saveAs(img, processedFilename);
});
return this.renderElement(element, {
imageType: 'jpg',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
};
/**
@ -134,9 +158,13 @@ define(
ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "png", className).then(function (img) {
saveAs(img, processedFilename);
});
return this.renderElement(element, {
imageType: 'png',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
};
/**
@ -146,8 +174,12 @@ define(
* @returns {promise}
*/
ExportImageService.prototype.exportPNGtoSRC = function (element, className) {
return this.renderElement(element, "png", className);
ExportImageService.prototype.exportPNGtoSRC = function (element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
};
function replaceDotsWithUnderscores(filename) {

View File

@ -4,7 +4,7 @@
class="c-ne__embed__snap-thumb"
@click="openSnapshot()"
>
<img :src="embed.snapshot.src">
<img :src="thumbnailImage">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
@ -25,11 +25,14 @@
<script>
import Moment from 'moment';
import PopupMenu from './PopupMenu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction';
import RemoveDialog from '../utils/removeDialog';
import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue';
export default {
@ -59,6 +62,11 @@ export default {
computed: {
createdOn() {
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
},
thumbnailImage() {
return this.embed.snapshot.thumbnailImage
? this.embed.snapshot.thumbnailImage.src
: this.embed.snapshot.src;
}
},
mounted() {
@ -85,7 +93,7 @@ export default {
template: '<div id="snap-annotation"></div>'
}).$mount();
const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot);
const painterroInstance = new PainterroInstance(annotateVue.$el);
const annotateOverlay = this.openmct.overlays.overlay({
element: annotateVue.$el,
size: 'large',
@ -102,10 +110,12 @@ export default {
{
label: 'Save',
callback: () => {
painterroInstance.save();
annotateOverlay.dismiss();
this.snapshotOverlay.dismiss();
this.openSnapshot();
painterroInstance.save((snapshotObject) => {
annotateOverlay.dismiss();
this.snapshotOverlay.dismiss();
this.updateSnapshot(snapshotObject);
this.openSnapshotOverlay(snapshotObject.fullSizeImage.src);
});
}
}
],
@ -115,7 +125,19 @@ export default {
});
painterroInstance.intialize();
painterroInstance.show(this.embed.snapshot.src);
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
painterroInstance.show(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
painterroInstance.show(object.configuration.fullSizeImageURL);
});
},
changeLocation() {
const hash = this.embed.historicLink;
@ -159,12 +181,29 @@ export default {
removeDialog.show();
},
openSnapshot() {
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
this.openSnapshotOverlay(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
});
},
openSnapshotOverlay(src) {
const self = this;
this.snapshot = new Vue({
data: () => {
return {
createdOn: this.createdOn,
embed: this.embed
name: this.embed.name,
cssClass: this.embed.cssClass,
src
};
},
methods: {
@ -217,7 +256,9 @@ export default {
this.$emit('updateEmbed', embed);
},
updateSnapshot(snapshotObject) {
this.embed.snapshot = snapshotObject;
this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
this.updateEmbed(this.embed);
}
}

View File

@ -62,7 +62,6 @@
<NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:entry="entry"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
@ -254,6 +253,7 @@ export default {
},
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1);
this.$emit('updateEntry', this.entry);

View File

@ -4,9 +4,9 @@
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<span class="c-object-label l-browse-bar__object-name"
v-bind:class="embed.cssClass"
v-bind:class="cssClass"
>
<span class="c-object-label__name">{{ embed.name }}</span>
<span class="c-object-label__name">{{ name }}</span>
</span>
</div>
</div>
@ -40,7 +40,7 @@
<div
ref="snapshot-image"
class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
:style="{ backgroundImage: 'url(' + src + ')' }"
>
</div>
</div>

View File

@ -2,7 +2,10 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container';
import {NOTEBOOK_TYPE} from './notebook-constants';
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
import { NOTEBOOK_TYPE } from './notebook-constants';
import Vue from 'vue';
export default function NotebookPlugin() {
@ -85,6 +88,19 @@ export default function NotebookPlugin() {
};
openmct.types.addType(NOTEBOOK_TYPE, notebookType);
const notebookSnapshotImageType = {
name: 'Notebook Snapshot Image Storage',
description: 'Notebook Snapshot Image Storage object',
creatable: false,
initialize: domainObject => {
domainObject.configuration = {
fullSizeImageURL: undefined,
thumbnailImageURL: undefined
};
}
};
openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({
components: {
@ -138,5 +154,16 @@ export default function NotebookPlugin() {
};
}
});
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'notebook';
},
invoke: (identifier, domainObject) => {
notebookImageMigration(openmct, domainObject);
return domainObject;
}
});
};
}

View File

@ -1,6 +1,8 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import SnapshotContainer from './snapshot-container';
export default class Snapshot {
@ -14,12 +16,17 @@ export default class Snapshot {
capture(snapshotMeta, notebookType, domElement) {
const exportImageService = this.openmct.$injector.get('exportImageService');
exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
.then(function (blob) {
const options = {
className: 's-status-taking-snapshot',
thumbnailSize: DEFAULT_SIZE
};
exportImageService.exportPNGtoSRC(domElement, options)
.then(function ({blob, thumbnail}) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
this._saveSnapShot(notebookType, reader.result, snapshotMeta);
this._saveSnapShot(notebookType, reader.result, thumbnail, snapshotMeta);
}.bind(this);
}.bind(this));
}
@ -27,16 +34,23 @@ export default class Snapshot {
/**
* @private
*/
_saveSnapShot(notebookType, imageUrl, snapshotMeta) {
const snapshot = imageUrl ? { src: imageUrl } : '';
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
_saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {
createNotebookImageDomainObject(this.openmct, fullSizeImageURL)
.then(object => {
const thumbnailImage = { src: thumbnailImageURL || '' };
const snapshot = {
fullSizeImageObjectIdentifier: object.identifier,
thumbnailImage
};
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
return;
}
return;
}
this._saveToNotebookSnapshots(embed);
this._saveToNotebookSnapshots(embed);
});
}
/**

View File

@ -0,0 +1,78 @@
import uuid from 'uuid';
export const DEFAULT_SIZE = {
width: 30,
height: 30
};
export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
const identifier = {
key: uuid(),
namespace: ''
};
const viewType = 'notebookSnapshotImage';
const object = {
name: 'Notebook Snapshot Image',
type: viewType,
identifier,
configuration: {
fullSizeImageURL
}
};
return new Promise((resolve, reject) => {
openmct.objects.save(object)
.then(result => {
if (result) {
resolve(object);
}
reject();
})
.catch(e => {
console.error(e);
reject();
});
});
}
export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_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('image/png');
}
export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) {
return new Promise(resolve => {
const image = new Image();
const canvas = document.createElement('canvas');
canvas.width = size.width;
canvas.height = size.height;
image.onload = function () {
canvas.getContext('2d')
.drawImage(image, 0, 0, size.width, size.height);
resolve(canvas.toDataURL('image/png'));
};
image.src = imageUrl;
});
}
export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {
openmct.objects.get(identifier)
.then(domainObject => {
const configuration = domainObject.configuration;
configuration.fullSizeImageURL = fullSizeImage.src;
openmct.objects.mutate(domainObject, 'configuration', configuration);
});
}

View File

@ -0,0 +1,43 @@
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl } from './notebook-image';
import { mutateObject } from './notebook-entries';
export function notebookImageMigration(openmct, domainObject) {
const configuration = domainObject.configuration;
const notebookEntries = configuration.entries;
const imageMigrationVer = configuration.imageMigrationVer;
if (imageMigrationVer && imageMigrationVer === 'v1') {
return;
}
configuration.imageMigrationVer = 'v1';
// to avoid muliple notebookImageMigration calls updating images.
mutateObject(openmct, domainObject, 'configuration', configuration);
configuration.sections.forEach(section => {
const sectionId = section.id;
section.pages.forEach(page => {
const pageId = page.id;
const notebookSection = notebookEntries && notebookEntries[sectionId] || {};
const pageEntries = notebookSection && notebookSection[pageId] || [];
pageEntries.forEach(entry => {
entry.embeds.forEach(async (embed) => {
const snapshot = embed.snapshot;
const fullSizeImageURL = snapshot.src;
if (fullSizeImageURL) {
const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
const notebookImageDomainObject = await createNotebookImageDomainObject(openmct, fullSizeImageURL);
embed.snapshot = {
fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,
thumbnailImage: { src: thumbnailImageURL || '' }
};
mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);
}
});
});
});
});
}

View File

@ -1,4 +1,5 @@
import Painterro from 'painterro';
import { getThumbnailURLFromimageUrl } from './notebook-image';
const DEFAULT_CONFIG = {
activeColor: '#ff0000',
@ -25,11 +26,11 @@ const DEFAULT_CONFIG = {
};
export default class PainterroInstance {
constructor(element, saveCallback) {
constructor(element) {
this.elementId = element.id;
this.isSave = false;
this.painterroInstance = null;
this.saveCallback = saveCallback;
this.painterroInstance = undefined;
this.saveCallback = undefined;
}
dismiss() {
@ -46,31 +47,41 @@ export default class PainterroInstance {
this.painterro = Painterro(this.config);
}
save() {
save(callback) {
this.saveCallback = callback;
this.isSave = true;
this.painterroInstance.save();
}
saveHandler(image, done) {
if (this.isSave) {
const self = this;
const url = image.asBlob();
const reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = () => {
const snapshot = reader.result;
reader.onloadend = async () => {
const fullSizeImageURL = reader.result;
const thumbnailURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
const snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
fullSizeImage: {
src: fullSizeImageURL,
type: url.type,
size: url.size,
modified: Date.now()
},
thumbnailImage: {
src: thumbnailURL,
modified: Date.now()
}
};
self.saveCallback(snapshotObject);
};
}
this.saveCallback(snapshotObject);
done(true);
done(true);
};
} else {
done(true);
}
}
show(src) {

View File

@ -22,7 +22,7 @@
import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
import NOTEBOOK_TYPE from '../../notebook/notebook-constants.js';
import { NOTEBOOK_TYPE } from '../../notebook/notebook-constants.js';
const REV = "_rev";
const ID = "_id";