Add support for importing and storing media files

This commit is contained in:
toasted-nutbread 2020-03-01 22:36:42 -05:00
parent 3b2663ba09
commit 8106f4744b
5 changed files with 255 additions and 3 deletions

View File

@ -36,6 +36,7 @@
<script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/japanese.js"></script>
<script src="/bg/js/json-schema.js"></script>
<script src="/bg/js/media-utility.js"></script>
<script src="/bg/js/options.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/request.js"></script>

View File

@ -31,8 +31,85 @@
"type": "array",
"description": "Array of definitions for the term/expression.",
"items": {
"oneOf": [
{
"type": "string",
"description": "Single definition for the term/expression."
},
{
"type": "object",
"description": "Single detailed definition for the term/expression.",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"description": "The type of the data for this definition.",
"enum": ["text", "image"]
}
},
"oneOf": [
{
"required": [
"type",
"text"
],
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"enum": ["text"]
},
"text": {
"type": "string",
"description": "Single definition for the term/expression."
}
}
},
{
"required": [
"type",
"path"
],
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"enum": ["image"]
},
"path": {
"type": "string",
"description": "Path to the image file in the archive."
},
"width": {
"type": "integer",
"description": "Preferred width of the image.",
"minimum": 1
},
"height": {
"type": "integer",
"description": "Preferred width of the image.",
"minimum": 1
},
"title": {
"type": "string",
"description": "Hover text for the image."
},
"description": {
"type": "string",
"description": "Description of the image."
},
"pixelated": {
"type": "boolean",
"description": "Whether or not the image should appear pixelated at sizes larger than the image's native resolution.",
"default": false
}
}
}
]
}
]
}
},
{

View File

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

View File

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

View File

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