From 079307899f3b57f2aec121631b56fb8780430e8e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 26 Sep 2020 13:42:31 -0400 Subject: Media utility refactor (#859) * Move loadImageBase64 into DictionaryImporter * Convert mediaUtility to a class * Add getFileExtensionFromImageMediaType to MediaUtility * Use MediaUtility instead of _getImageExtensionFromMediaType * Use MediaUtility in ClipboardReader to validate images before reading --- ext/bg/background.html | 1 + ext/bg/js/backend.js | 19 +++++------- ext/bg/js/clipboard-reader.js | 23 ++++++++++----- ext/bg/js/dictionary-importer.js | 30 +++++++++++++++++-- ext/bg/js/media-utility.js | 63 +++++++++++++++++++++------------------- 5 files changed, 84 insertions(+), 52 deletions(-) (limited to 'ext') diff --git a/ext/bg/background.html b/ext/bg/background.html index ba0710e6..f42a411d 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -35,6 +35,7 @@ + diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index b9d85b84..2a90e8e1 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -24,6 +24,7 @@ * Environment * JsonSchemaValidator * Mecab + * MediaUtility * ObjectPropertyAccessor * OptionsUtil * ProfileConditions @@ -39,10 +40,12 @@ class Backend { this._translator = new Translator(this._dictionaryDatabase); this._anki = new AnkiConnect(); this._mecab = new Mecab(); + this._mediaUtility = new MediaUtility(); this._clipboardReader = new ClipboardReader({ document: (typeof document === 'object' && document !== null ? document : null), pasteTargetSelector: '#clipboard-paste-target', - imagePasteTargetSelector: '#clipboard-image-paste-target' + imagePasteTargetSelector: '#clipboard-image-paste-target', + mediaUtility: this._mediaUtility }); this._clipboardMonitor = new ClipboardMonitor({ clipboardReader: this._clipboardReader @@ -1570,7 +1573,8 @@ class Backend { const dataUrl = await this._getScreenshot(windowId, tabId, ownerFrameId, format, quality); const {mediaType, data} = this._getDataUrlInfo(dataUrl); - const extension = this._getImageExtensionFromMediaType(mediaType); + const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType); + if (extension === null) { throw new Error('Unknown image media type'); } let fileName = `yomichan_browser_screenshot_${reading}_${this._ankNoteDateToString(now)}.${extension}`; fileName = this._replaceInvalidFileNameCharacters(fileName); @@ -1593,7 +1597,8 @@ class Backend { } const {mediaType, data} = this._getDataUrlInfo(dataUrl); - const extension = this._getImageExtensionFromMediaType(mediaType); + const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType); + if (extension === null) { throw new Error('Unknown image media type'); } let fileName = `yomichan_clipboard_image_${reading}_${this._ankNoteDateToString(now)}.${extension}`; fileName = this._replaceInvalidFileNameCharacters(fileName); @@ -1636,14 +1641,6 @@ class Backend { return {mediaType, data}; } - _getImageExtensionFromMediaType(mediaType) { - switch (mediaType.toLowerCase()) { - case 'image/png': return 'png'; - case 'image/jpeg': return 'jpeg'; - default: throw new Error('Unknown image media type'); - } - } - _triggerDatabaseUpdated(type, cause) { this._translator.clearDatabaseCaches(); this._sendMessageAllTabs('databaseUpdated', {type, cause}); diff --git a/ext/bg/js/clipboard-reader.js b/ext/bg/js/clipboard-reader.js index 66cf0c25..d8c80c21 100644 --- a/ext/bg/js/clipboard-reader.js +++ b/ext/bg/js/clipboard-reader.js @@ -25,13 +25,14 @@ class ClipboardReader { * @param pasteTargetSelector The selector for the paste target element. * @param imagePasteTargetSelector The selector for the image paste target element. */ - constructor({document=null, pasteTargetSelector=null, imagePasteTargetSelector=null}) { + constructor({document=null, pasteTargetSelector=null, imagePasteTargetSelector=null, mediaUtility=null}) { this._document = document; this._browser = null; this._pasteTarget = null; this._pasteTargetSelector = pasteTargetSelector; this._imagePasteTarget = null; this._imagePasteTargetSelector = imagePasteTargetSelector; + this._mediaUtility = mediaUtility; } /** @@ -99,14 +100,20 @@ class ClipboardReader { */ async getImage() { // See browser-specific notes in getText - if (this._isFirefox()) { - if (typeof navigator.clipboard !== 'undefined' && typeof navigator.clipboard.read === 'function') { - // This function is behind the flag: dom.events.asyncClipboard.dataTransfer - const {files} = await navigator.clipboard.read(); - if (files.length === 0) { return null; } - const result = await this._readFileAsDataURL(files[0]); - return result; + if ( + this._isFirefox() && + this._mediaUtility !== null && + typeof navigator.clipboard !== 'undefined' && + typeof navigator.clipboard.read === 'function' + ) { + // This function is behind the Firefox flag: dom.events.asyncClipboard.dataTransfer + const {files} = await navigator.clipboard.read(); + for (const file of files) { + if (this._mediaUtility.getFileExtensionFromImageMediaType(file.type) !== null) { + return await this._readFileAsDataURL(file); + } } + return null; } const document = this._document; diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js index 2ad2ebe4..b641de3a 100644 --- a/ext/bg/js/dictionary-importer.js +++ b/ext/bg/js/dictionary-importer.js @@ -18,13 +18,14 @@ /* global * JSZip * JsonSchemaValidator - * mediaUtility + * MediaUtility */ class DictionaryImporter { constructor() { this._schemas = new Map(); this._jsonSchemaValidator = new JsonSchemaValidator(); + this._mediaUtility = new MediaUtility(); } async importDictionary(dictionaryDatabase, archiveSource, details, onProgress) { @@ -324,14 +325,14 @@ class DictionaryImporter { } const content = await file.async('base64'); - const mediaType = mediaUtility.getImageMediaTypeFromFileName(path); + const mediaType = this._mediaUtility.getImageMediaTypeFromFileName(path); if (mediaType === null) { throw new Error(`Could not determine media type for image at path ${JSON.stringify(path)} for ${errorSource}`); } let image; try { - image = await mediaUtility.loadImageBase64(mediaType, content); + image = await this._loadImageBase64(mediaType, content); } catch (e) { throw new Error(`Could not load image at path ${JSON.stringify(path)} for ${errorSource}`); } @@ -380,4 +381,27 @@ class DictionaryImporter { } return await response.json(); } + + /** + * Attempts to load an image using a base64 encoded content and a media type. + * @param mediaType The media type for the image content. + * @param content The binary content for the image, encoded in base64. + * @returns A Promise which resolves with an HTMLImageElement instance on + * successful load, otherwise an error is thrown. + */ + _loadImageBase64(mediaType, content) { + return new Promise((resolve, reject) => { + const image = new Image(); + const eventListeners = new EventListenerCollection(); + eventListeners.addEventListener(image, 'load', () => { + eventListeners.removeAllEventListeners(); + resolve(image); + }, false); + eventListeners.addEventListener(image, 'error', () => { + eventListeners.removeAllEventListeners(); + reject(new Error('Image failed to load')); + }, false); + image.src = `data:${mediaType};base64,${content}`; + }); + } } diff --git a/ext/bg/js/media-utility.js b/ext/bg/js/media-utility.js index 1f93b2b4..52e32113 100644 --- a/ext/bg/js/media-utility.js +++ b/ext/bg/js/media-utility.js @@ -16,9 +16,9 @@ */ /** - * mediaUtility is an object containing helper methods related to media processing. + * MediaUtility is a class containing helper methods related to media processing. */ -const mediaUtility = (() => { +class MediaUtility { /** * Gets the file extension of a file path. URL search queries and hash * fragments are not handled. @@ -26,7 +26,7 @@ const mediaUtility = (() => { * @returns The file extension, including the '.', or an empty string * if there is no file extension. */ - function getFileNameExtension(path) { + getFileNameExtension(path) { const match = /\.[^./\\]*$/.exec(path); return match !== null ? match[0] : ''; } @@ -37,8 +37,8 @@ const mediaUtility = (() => { * @returns The media type string if it can be determined from the file path, * otherwise null. */ - function getImageMediaTypeFromFileName(path) { - switch (getFileNameExtension(path).toLowerCase()) { + getImageMediaTypeFromFileName(path) { + switch (this.getFileNameExtension(path).toLowerCase()) { case '.apng': return 'image/apng'; case '.bmp': @@ -69,30 +69,33 @@ const mediaUtility = (() => { } /** - * Attempts to load an image using a base64 encoded content and a media type. - * @param mediaType The media type for the image content. - * @param content The binary content for the image, encoded in base64. - * @returns A Promise which resolves with an HTMLImageElement instance on - * successful load, otherwise an error is thrown. + * Gets the file extension for a corresponding media type. + * @param mediaType The media type to use. + * @returns A file extension including the dot for the media type, + * otherwise null. */ - function loadImageBase64(mediaType, content) { - return new Promise((resolve, reject) => { - const image = new Image(); - const eventListeners = new EventListenerCollection(); - eventListeners.addEventListener(image, 'load', () => { - eventListeners.removeAllEventListeners(); - resolve(image); - }, false); - eventListeners.addEventListener(image, 'error', () => { - eventListeners.removeAllEventListeners(); - reject(new Error('Image failed to load')); - }, false); - image.src = `data:${mediaType};base64,${content}`; - }); + getFileExtensionFromImageMediaType(mediaType) { + switch (mediaType) { + case 'image/apng': + return '.apng'; + case 'image/bmp': + return '.bmp'; + case 'image/gif': + return '.gif'; + case 'image/x-icon': + return '.ico'; + case 'image/jpeg': + return '.jpeg'; + case 'image/png': + return '.png'; + case 'image/svg+xml': + return '.svg'; + case 'image/tiff': + return '.tiff'; + case 'image/webp': + return '.webp'; + default: + return null; + } } - - return { - getImageMediaTypeFromFileName, - loadImageBase64 - }; -})(); +} -- cgit v1.2.3