aboutsummaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-03-01 22:36:42 -0500
committertoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-04-18 21:31:09 -0400
commit8106f4744b07833526d16acf656eda11d29b99ad (patch)
tree8d840ab694ee81f232dccc5154cc5d1e25e7c2cf /ext/bg/js
parent3b2663ba0957c65be959ba18dc80e13625e28f02 (diff)
Add support for importing and storing media files
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/database.js11
-rw-r--r--ext/bg/js/dictionary-importer.js90
-rw-r--r--ext/bg/js/media-utility.js75
3 files changed, 175 insertions, 1 deletions
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index 260c815a..0c7eee6a 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -33,7 +33,7 @@ class Database {
}
try {
- this.db = await Database._open('dict', 5, (db, transaction, oldVersion) => {
+ this.db = await Database._open('dict', 6, (db, transaction, oldVersion) => {
Database._upgrade(db, transaction, oldVersion, [
{
version: 2,
@@ -90,6 +90,15 @@ class Database {
indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse']
}
}
+ },
+ {
+ version: 6,
+ stores: {
+ media: {
+ primaryKey: {keyPath: 'id', autoIncrement: true},
+ indices: ['dictionary', 'path']
+ }
+ }
}
]);
});
diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js
index bf6809ec..8a4497a3 100644
--- a/ext/bg/js/dictionary-importer.js
+++ b/ext/bg/js/dictionary-importer.js
@@ -18,6 +18,7 @@
/* global
* JSZip
* JsonSchema
+ * mediaUtility
* requestJson
*/
@@ -148,6 +149,22 @@ class DictionaryImporter {
}
}
+ // Extended data support
+ const extendedDataContext = {
+ archive,
+ media: new Map()
+ };
+ for (const entry of termList) {
+ const glossaryList = entry.glossary;
+ for (let i = 0, ii = glossaryList.length; i < ii; ++i) {
+ const glossary = glossaryList[i];
+ if (typeof glossary !== 'object' || glossary === null) { continue; }
+ glossaryList[i] = await this._formatDictionaryTermGlossaryObject(glossary, extendedDataContext, entry);
+ }
+ }
+
+ const media = [...extendedDataContext.media.values()];
+
// Add dictionary
const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported});
@@ -188,6 +205,7 @@ class DictionaryImporter {
await bulkAdd('kanji', kanjiList);
await bulkAdd('kanjiMeta', kanjiMetaList);
await bulkAdd('tagMeta', tagList);
+ await bulkAdd('media', media);
return {result: summary, errors};
}
@@ -275,4 +293,76 @@ class DictionaryImporter {
return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank];
}
+
+ async _formatDictionaryTermGlossaryObject(data, context, entry) {
+ switch (data.type) {
+ case 'text':
+ return data.text;
+ case 'image':
+ return await this._formatDictionaryTermGlossaryImage(data, context, entry);
+ default:
+ throw new Error(`Unhandled data type: ${data.type}`);
+ }
+ }
+
+ async _formatDictionaryTermGlossaryImage(data, context, entry) {
+ const dictionary = entry.dictionary;
+ const {path, width: preferredWidth, height: preferredHeight, title, description, pixelated} = data;
+ if (context.media.has(path)) {
+ // Already exists
+ return data;
+ }
+
+ let errorSource = entry.expression;
+ if (entry.reading.length > 0) {
+ errorSource += ` (${entry.reading});`;
+ }
+
+ const file = context.archive.file(path);
+ if (file === null) {
+ throw new Error(`Could not find image at path ${JSON.stringify(path)} for ${errorSource}`);
+ }
+
+ const source = await file.async('base64');
+ const mediaType = 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.loadImage(mediaType, source);
+ } catch (e) {
+ throw new Error(`Could not load image at path ${JSON.stringify(path)} for ${errorSource}`);
+ }
+
+ const width = image.naturalWidth;
+ const height = image.naturalHeight;
+
+ // Create image data
+ const mediaData = {
+ dictionary,
+ path,
+ mediaType,
+ width,
+ height,
+ source
+ };
+ context.media.set(path, mediaData);
+
+ // Create new data
+ const newData = {
+ type: 'image',
+ path,
+ width,
+ height
+ };
+ if (typeof preferredWidth === 'number') { newData.preferredWidth = preferredWidth; }
+ if (typeof preferredHeight === 'number') { newData.preferredHeight = preferredHeight; }
+ if (typeof title === 'string') { newData.title = title; }
+ if (typeof description === 'string') { newData.description = description; }
+ if (typeof pixelated === 'boolean') { newData.pixelated = pixelated; }
+
+ return newData;
+ }
}
diff --git a/ext/bg/js/media-utility.js b/ext/bg/js/media-utility.js
new file mode 100644
index 00000000..24686838
--- /dev/null
+++ b/ext/bg/js/media-utility.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+const mediaUtility = (() => {
+ function getFileNameExtension(fileName) {
+ const match = /\.[^.]*$/.exec(fileName);
+ return match !== null ? match[0] : '';
+ }
+
+ function getImageMediaTypeFromFileName(fileName) {
+ switch (getFileNameExtension(fileName).toLowerCase()) {
+ case '.apng':
+ return 'image/apng';
+ case '.bmp':
+ return 'image/bmp';
+ case '.gif':
+ return 'image/gif';
+ case '.ico':
+ case '.cur':
+ return 'image/x-icon';
+ case '.jpg':
+ case '.jpeg':
+ case '.jfif':
+ case '.pjpeg':
+ case '.pjp':
+ return 'image/jpeg';
+ case '.png':
+ return 'image/png';
+ case '.svg':
+ return 'image/svg+xml';
+ case '.tif':
+ case '.tiff':
+ return 'image/tiff';
+ case '.webp':
+ return 'image/webp';
+ default:
+ return null;
+ }
+ }
+
+ function loadImage(mediaType, base64Source) {
+ 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,${base64Source}`;
+ });
+ }
+
+ return {
+ getImageMediaTypeFromFileName,
+ loadImage
+ };
+})();