diff --git a/dev/data/structured-content-overrides.css b/dev/data/structured-content-overrides.css index 0878eede2..1fe72276f 100644 --- a/dev/data/structured-content-overrides.css +++ b/dev/data/structured-content-overrides.css @@ -28,10 +28,6 @@ .gloss-image-link:hover { /* remove-rule */ } -:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, -:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image { - /* remove-rule */ -} .gloss-sc-thead, .gloss-sc-tfoot, .gloss-sc-th { diff --git a/ext/css/structured-content.css b/ext/css/structured-content.css index b285874f0..747a1d33d 100644 --- a/ext/css/structured-content.css +++ b/ext/css/structured-content.css @@ -19,6 +19,11 @@ /* Glossary images */ .gloss-image-container { display: inline-block; + max-width: 100%; + max-height: 100vh; + position: relative; + vertical-align: top; + line-height: 0; font-size: calc(1em / var(--font-size-no-units)); overflow: hidden; } @@ -39,6 +44,48 @@ .gloss-image-link[href]:hover { cursor: pointer; } +.gloss-image-container-overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + font-size: calc(1em * var(--font-size-no-units)); + line-height: var(--line-height); + display: table; + table-layout: fixed; + white-space: normal; + color: var(--text-color-light3); +} +.gloss-image-link[data-has-image=true][data-image-load-state=load-error] .gloss-image-container-overlay::after { + content: 'Image failed to load'; + display: table-cell; + width: 100%; + height: 100%; + vertical-align: middle; + text-align: center; + padding: 0.25em; +} +.gloss-image-background { + --image: none; + + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--text-color); + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center center; + -webkit-mask-mode: alpha; + -webkit-mask-size: contain; + -webkit-mask-image: var(--image); + mask-repeat: no-repeat; + mask-position: center center; + mask-mode: alpha; + mask-size: contain; + mask-image: var(--image); +} .gloss-image { display: inline-block; vertical-align: top; @@ -47,27 +94,54 @@ outline: none; width: 100%; } -.gloss-image-link[data-image-rendering=pixelated] .gloss-image { +.gloss-image-link[data-image-rendering=pixelated] .gloss-image, +.gloss-image-link[data-image-rendering=pixelated] .gloss-image-background { image-rendering: auto; image-rendering: -moz-crisp-edges; image-rendering: -webkit-optimize-contrast; image-rendering: pixelated; image-rendering: crisp-edges; } -.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image { +.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, +.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background { image-rendering: auto; image-rendering: -moz-crisp-edges; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; } :root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, -:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image { +:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background, +:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, +:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background { image-rendering: auto; } +.gloss-image-link[data-has-aspect-ratio=true] .gloss-image-sizer { + display: inline-block; + width: 0; + vertical-align: top; + font-size: 0; +} +.gloss-image-link-text { + display: none; + line-height: var(--line-height); +} +.gloss-image-link-text::before { + content: '['; +} +.gloss-image-link-text::after { + content: ']'; +} +.gloss-image-description { + display: block; + white-space: pre-line; +} .gloss-image-link[data-appearance=monochrome] .gloss-image { filter: grayscale(1); } +.gloss-image-link:not([data-appearance=monochrome]) .gloss-image-background { + display: none; +} .gloss-image-link[data-size-units=em] .gloss-image-container { font-size: 1em; @@ -105,6 +179,14 @@ :root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true]:focus .gloss-image-container { display: block; } +.gloss-image-link[data-collapsed=true] .gloss-image-link-text, +:root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true] .gloss-image-link-text { + display: inline; +} +.gloss-image-link[data-collapsed=true]~.gloss-image-description, +:root[data-glossary-layout-mode=compact] .gloss-image-description { + display: inline; +} /* Links */ diff --git a/ext/data/structured-content-style.json b/ext/data/structured-content-style.json index caae8761b..f6fdedf8a 100644 --- a/ext/data/structured-content-style.json +++ b/ext/data/structured-content-style.json @@ -3,6 +3,11 @@ "selectors": [".gloss-image-container"], "styles": [ ["display", "inline-block"], + ["max-width", "100%"], + ["max-height", "100vh"], + ["position", "relative"], + ["vertical-align", "top"], + ["line-height", "0"], ["overflow", "hidden"], ["font-size", "1px"] ] @@ -24,6 +29,56 @@ ["cursor", "pointer"] ] }, + { + "selectors": [".gloss-image-container-overlay"], + "styles": [ + ["position", "absolute"], + ["left", "0"], + ["top", "0"], + ["width", "100%"], + ["height", "100%"], + ["font-size", "calc(1em * var(--font-size-no-units))"], + ["line-height", "var(--line-height)"], + ["display", "table"], + ["table-layout", "fixed"], + ["white-space", "normal"], + ["color", "var(--text-color-light3)"] + ] + }, + { + "selectors": [".gloss-image-link[data-has-image=true][data-image-load-state=load-error] .gloss-image-container-overlay::after"], + "styles": [ + ["content", "'Image failed to load'"], + ["display", "table-cell"], + ["width", "100%"], + ["height", "100%"], + ["vertical-align", "middle"], + ["text-align", "center"], + ["padding", "0.25em"] + ] + }, + { + "selectors": [".gloss-image-background"], + "styles": [ + ["--image", "none"], + ["position", "absolute"], + ["left", "0"], + ["top", "0"], + ["width", "100%"], + ["height", "100%"], + ["background-color", "var(--text-color)"], + ["-webkit-mask-repeat", "no-repeat"], + ["-webkit-mask-position", "center center"], + ["-webkit-mask-mode", "alpha"], + ["-webkit-mask-size", "contain"], + ["-webkit-mask-image", "var(--image)"], + ["mask-repeat", "no-repeat"], + ["mask-position", "center center"], + ["mask-mode", "alpha"], + ["mask-size", "contain"], + ["mask-image", "var(--image)"] + ] + }, { "selectors": [".gloss-image"], "styles": [ @@ -36,7 +91,10 @@ ] }, { - "selectors": [".gloss-image-link[data-image-rendering=pixelated] .gloss-image"], + "selectors": [ + ".gloss-image-link[data-image-rendering=pixelated] .gloss-image", + ".gloss-image-link[data-image-rendering=pixelated] .gloss-image-background" + ], "styles": [ ["image-rendering", "auto"], ["image-rendering", "-moz-crisp-edges"], @@ -46,7 +104,10 @@ ] }, { - "selectors": [".gloss-image-link[data-image-rendering=crisp-edges] .gloss-image"], + "selectors": [ + ".gloss-image-link[data-image-rendering=crisp-edges] .gloss-image", + ".gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background" + ], "styles": [ ["image-rendering", "auto"], ["image-rendering", "-moz-crisp-edges"], @@ -54,12 +115,64 @@ ["image-rendering", "crisp-edges"] ] }, + { + "selectors": [ + ":root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image", + ":root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background", + ":root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image", + ":root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background" + ], + "styles": [ + ["image-rendering", "auto"] + ] + }, + { + "selectors": [".gloss-image-link[data-has-aspect-ratio=true] .gloss-image-sizer"], + "styles": [ + ["display", "inline-block"], + ["width", "0"], + ["vertical-align", "top"], + ["font-size", "0"] + ] + }, + { + "selectors": [".gloss-image-link-text"], + "styles": [ + ["display", "none"], + ["line-height", "var(--line-height)"] + ] + }, + { + "selectors": [".gloss-image-link-text::before"], + "styles": [ + ["content", "'['"] + ] + }, + { + "selectors": [".gloss-image-link-text::after"], + "styles": [ + ["content", "']'"] + ] + }, + { + "selectors": [".gloss-image-description"], + "styles": [ + ["display", "block"], + ["white-space", "pre-line"] + ] + }, { "selectors": [".gloss-image-link[data-appearance=monochrome] .gloss-image"], "styles": [ ["filter", "grayscale(1)"] ] }, + { + "selectors": [".gloss-image-link:not([data-appearance=monochrome]) .gloss-image-background"], + "styles": [ + ["display", "none"] + ] + }, { "selectors": [".gloss-image-link[data-size-units=em] .gloss-image-container"], "styles": [ @@ -157,6 +270,24 @@ ["display", "block"] ] }, + { + "selectors": [ + ".gloss-image-link[data-collapsed=true] .gloss-image-link-text", + ":root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true] .gloss-image-link-text" + ], + "styles": [ + ["display", "inline"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-collapsed=true]~.gloss-image-description", + ":root[data-glossary-layout-mode=compact] .gloss-image-description" + ], + "styles": [ + ["display", "inline"] + ] + }, { "selectors": [".gloss-link-external-icon"], "styles": [ diff --git a/ext/js/dictionary/dictionary-database.js b/ext/js/dictionary/dictionary-database.js index 98347de52..5da2cd064 100644 --- a/ext/js/dictionary/dictionary-database.js +++ b/ext/js/dictionary/dictionary-database.js @@ -462,7 +462,7 @@ export class DictionaryDatabase { } else { const image = new Blob([m.content], {type: m.mediaType}); // eslint-disable-next-line no-undef - await createImageBitmap(image).then((decodedImage) => { + await createImageBitmap(image, {resizeWidth: m.canvasWidth, resizeHeight: m.canvasHeight, resizeQuality: 'high'}).then((decodedImage) => { // we need to do a dumb hack where we convert this ImageBitmap to an ImageData by drawing it to a temporary canvas, because Firefox doesn't support transferring ImageBitmaps cross-process const canvas = new OffscreenCanvas(decodedImage.width, decodedImage.height); const ctx = canvas.getContext('2d'); @@ -815,9 +815,9 @@ export class DictionaryDatabase { * @param {import('dictionary-database').FindMultiBulkData} data * @returns {import('dictionary-database').DrawMedia} */ - _createDrawMedia(row, {itemIndex: index, item: {canvasIndexes, canvasWidth, generation}}) { + _createDrawMedia(row, {itemIndex: index, item: {canvasIndexes, canvasWidth, canvasHeight, generation}}) { const {dictionary, path, mediaType, width, height, content} = row; - return {index, dictionary, path, mediaType, width, height, content, canvasIndexes, canvasWidth, generation}; + return {index, dictionary, path, mediaType, width, height, content, canvasIndexes, canvasWidth, canvasHeight, generation}; } /** diff --git a/ext/js/display/structured-content-generator.js b/ext/js/display/structured-content-generator.js index 6cdc981bd..6ff8cf12f 100644 --- a/ext/js/display/structured-content-generator.js +++ b/ext/js/display/structured-content-generator.js @@ -98,6 +98,12 @@ export class StructuredContentGenerator { const imageContainer = this._createElement('span', 'gloss-image-container'); node.appendChild(imageContainer); + const imageBackground = this._createElement('span', 'gloss-image-background'); + imageContainer.appendChild(imageBackground); + + const overlay = this._createElement('span', 'gloss-image-container-overlay'); + imageContainer.appendChild(overlay); + node.dataset.path = path; node.dataset.dictionary = dictionary; node.dataset.imageLoadState = 'not-loaded'; @@ -125,7 +131,15 @@ export class StructuredContentGenerator { const image = this._contentManager instanceof DisplayContentManager ? /** @type {HTMLCanvasElement} */ (this._createElement('canvas', 'gloss-image')) : /** @type {HTMLImageElement} */ (this._createElement('img', 'gloss-image')); - image.width = width; + if (sizeUnits === 'em' && (hasPreferredWidth || hasPreferredHeight)) { + const emSize = 14; // We could Number.parseFloat(getComputedStyle(document.documentElement).fontSize); here for more accuracy but it would cause a layout and be extremely slow; possible improvement would be to calculate and cache the value + const scaleFactor = 2 * window.devicePixelRatio; + image.style.width = `${usedWidth}em`; + image.style.height = `${usedWidth * invAspectRatio}em`; + image.width = usedWidth * emSize * scaleFactor; + } else { + image.width = usedWidth; + } image.height = image.width * invAspectRatio; // Anki will not render images correctly without specifying to use 100% width and height @@ -145,10 +159,10 @@ export class StructuredContentGenerator { path, dictionary, (url) => { - (/** @type {HTMLImageElement} */(image)).src = url; + this._setImageData(node, /** @type {HTMLImageElement} */ (image), imageBackground, url, false); }, () => { - (/** @type {HTMLImageElement} */(image)).removeAttribute('src'); + this._setImageData(node, /** @type {HTMLImageElement} */ (image), imageBackground, null, true); }, ); } @@ -230,6 +244,27 @@ export class StructuredContentGenerator { } } + /** + * @param {HTMLAnchorElement} node + * @param {HTMLImageElement} image + * @param {HTMLElement} imageBackground + * @param {?string} url + * @param {boolean} unloaded + */ + _setImageData(node, image, imageBackground, url, unloaded) { + if (url !== null) { + image.src = url; + node.href = url; + node.dataset.imageLoadState = 'loaded'; + imageBackground.style.setProperty('--image', `url("${url}")`); + } else { + image.removeAttribute('src'); + node.removeAttribute('href'); + node.dataset.imageLoadState = unloaded ? 'unloaded' : 'load-error'; + imageBackground.style.removeProperty('--image'); + } + } + /** * @param {import('structured-content').Element} content * @param {string} dictionary diff --git a/test/data/anki-note-builder-test-results.json b/test/data/anki-note-builder-test-results.json index 5aacb9eeb..fc9cbf85b 100644 --- a/test/data/anki-note-builder-test-results.json +++ b/test/data/anki-note-builder-test-results.json @@ -863,12 +863,12 @@ "frequency-average-occurrence": "0", "furigana": "画像がぞう", "furigana-plain": "画像[がぞう]", - "glossary": "
(n, termsDictAlias)
", - "glossary-brief": "
", - "glossary-no-dictionary": "
(n)
", - "glossary-first": "
(n, termsDictAlias)
", - "glossary-first-brief": "
", - "glossary-first-no-dictionary": "
(n)
", + "glossary": "
(n, termsDictAlias)
", + "glossary-brief": "
", + "glossary-no-dictionary": "
(n)
", + "glossary-first": "
(n, termsDictAlias)
", + "glossary-first-brief": "
", + "glossary-first-no-dictionary": "
(n)
", "part-of-speech": "Noun", "pitch-accents": "", "pitch-accent-graphs": "", @@ -1570,12 +1570,12 @@ "frequency-average-occurrence": "0", "furigana": "画像がぞう", "furigana-plain": "画像[がぞう]", - "glossary": "
(n, termsDictAlias)
", - "glossary-brief": "
", - "glossary-no-dictionary": "
(n)
", - "glossary-first": "
(n, termsDictAlias)
", - "glossary-first-brief": "
", - "glossary-first-no-dictionary": "
(n)
", + "glossary": "
(n, termsDictAlias)
", + "glossary-brief": "
", + "glossary-no-dictionary": "
(n)
", + "glossary-first": "
(n, termsDictAlias)
", + "glossary-first-brief": "
", + "glossary-first-no-dictionary": "
(n)
", "part-of-speech": "Noun", "pitch-accents": "", "pitch-accent-graphs": "", diff --git a/types/ext/dictionary-database.d.ts b/types/ext/dictionary-database.d.ts index 69078c114..33aaf7ade 100644 --- a/types/ext/dictionary-database.d.ts +++ b/types/ext/dictionary-database.d.ts @@ -40,7 +40,7 @@ type MediaType = ArrayBuffer | string | null; export type Media = {index: number} & MediaDataBase; -export type DrawMedia = {index: number} & MediaDataBase & {canvasWidth: number, canvasIndexes: number[], generation: number}; +export type DrawMedia = {index: number} & MediaDataBase & {canvasWidth: number, canvasHeight: number, canvasIndexes: number[], generation: number}; export type DatabaseTermEntry = { expression: string;