diff --git a/build/build.js b/build/build.js index b3c9aad4..c4df034f 100644 --- a/build/build.js +++ b/build/build.js @@ -23,6 +23,9 @@ rm('-rf', assetsPath) mkdir('-p', assetsPath) cp('-R', 'static/*', assetsPath) +sed('-i', /'jxl_dec.js/, "'/node_modules/jxl.js/jxl_dec.js", 'node_modules/jxl.js/jxl.js') +sed('-i', /"jxl_dec.wasm/, '"/node_modules/jxl.js/jxl_dec.wasm', 'node_modules/jxl.js/jxl_dec.js') + webpack(webpackConfig, function (err, stats) { spinner.stop() if (err) throw err diff --git a/build/dev-server.js b/build/dev-server.js index 5acd0fed..ef275509 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -5,11 +5,15 @@ var path = require('path') var express = require('express') var webpack = require('webpack') var opn = require('opn') +require('shelljs/global') var proxyMiddleware = require('http-proxy-middleware') var webpackConfig = process.env.NODE_ENV === 'testing' ? require('./webpack.prod.conf') : require('./webpack.dev.conf') +sed('-i', /'jxl_dec.js/, "'/node_modules/jxl.js/jxl_dec.js", 'node_modules/jxl.js/jxl.js') +sed('-i', /"jxl_dec.wasm/, '"/node_modules/jxl.js/jxl_dec.wasm', 'node_modules/jxl.js/jxl_dec.js') + // default port where dev server listens for incoming traffic var port = process.env.PORT || config.dev.port // Define HTTP proxies to your custom API backend diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index 2a3db96e..665eaa21 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -2,6 +2,8 @@ var path = require('path') var config = require('../config') var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') +const CopyPlugin = require("copy-webpack-plugin"); +const WriteFilePlugin = require("write-file-webpack-plugin"); var { VueLoaderPlugin } = require('vue-loader') var env = process.env.NODE_ENV @@ -118,6 +120,14 @@ module.exports = { ] }, plugins: [ - new VueLoaderPlugin() + new VueLoaderPlugin(), + new CopyPlugin({ + patterns: [ + { + from: "node_modules/jxl.js/jxl*" + } + ] + }), + new WriteFilePlugin() ] } diff --git a/package.json b/package.json index 19a7e186..54a9c4af 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "diff": "3.5.0", "escape-html": "1.0.3", "js-cookie": "^3.0.1", + "jxl.js": "^1.0.0", "localforage": "1.10.0", "parse-link-header": "^2.0.0", "phoenix": "1.6.2", @@ -63,6 +64,7 @@ "chalk": "1.1.3", "chromedriver": "^107.0.3", "connect-history-api-fallback": "^2.0.0", + "copy-webpack-plugin": "^11.0.0", "cross-spawn": "^7.0.3", "css-loader": "^6.7.2", "custom-event-polyfill": "^1.0.7", @@ -124,7 +126,8 @@ "webpack-dev-middleware": "^5.3.3", "webpack-hot-middleware": "^2.25.1", "webpack-merge": "^5.8.0", - "workbox-webpack-plugin": "^6.5.4" + "workbox-webpack-plugin": "^6.5.4", + "write-file-webpack-plugin": "^4.5.1" }, "engines": { "node": ">= 16.0.0", diff --git a/src/App.js b/src/App.js index d4b3b41a..ef67a6a4 100644 --- a/src/App.js +++ b/src/App.js @@ -53,6 +53,9 @@ export default { unmounted () { window.removeEventListener('resize', this.updateMobileState) }, + mounted() { + import("./lib/jxl.js").then(jxl => jxl.startPolyfill()); + }, computed: { classes () { return [ diff --git a/src/lib/jxl.js b/src/lib/jxl.js new file mode 100644 index 00000000..568b5293 --- /dev/null +++ b/src/lib/jxl.js @@ -0,0 +1,80 @@ +const config = { + useCache: true +}; + +let cache, workers = {}; + +function imgDataToDataURL(img, imgData, isCSS) { + const jxlSrc = img.dataset.jxlSrc; + if (imgData instanceof Blob) { + const dataURL = URL.createObjectURL(imgData); + if (isCSS) + img.style.backgroundImage = 'url("' + dataURL + '")'; + else + img.src = dataURL; + } else if ('OffscreenCanvas' in window) { + const canvas = new OffscreenCanvas(imgData.width, imgData.height); + workers[jxlSrc].postMessage({ canvas, imgData }, [canvas]); + workers[jxlSrc].addEventListener('message', m => { + if (m.data.url && m.data.blob) { + if (isCSS) + img.style.backgroundImage = 'url("' + m.data.url + '")'; + else + img.src = m.data.url; + config.useCache && cache && cache.put(jxlSrc, new Response(m.data.blob)); + } + }); + } else { + const canvas = document.createElement('canvas'); + canvas.width = imgData.width; + canvas.height = imgData.height; + canvas.getContext('2d').putImageData(imgData, 0, 0); + canvas.toBlob(blob => { + const dataURL = URL.createObjectURL(blob); + if (isCSS) + img.style.backgroundImage = 'url("' + dataURL + '")'; + else + img.src = dataURL; + config.useCache && cache && cache.put(jxlSrc, new Response(blob)); + }, 'image/jpeg'); + } +} + + +async function decode(img, isCSS) { + const jxlSrc = img.dataset.jxlSrc = isCSS ? getComputedStyle(img).backgroundImage.slice(5, -2) : img.currentSrc; + img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; // blank 1x1 image + if (config.useCache) { + try { + cache = cache || await caches.open('jxl'); + } catch (e) { } + const cachedImg = cache && await cache.match(jxlSrc); + if (cachedImg) { + const cachedImgData = await cachedImg.blob(); + requestAnimationFrame(() => imgDataToDataURL(img, cachedImgData, isCSS)); + return; + } + } + const res = await fetch(jxlSrc); + const image = await res.arrayBuffer(); + workers[jxlSrc] = new Worker('/node_modules/jxl.js/jxl_dec.js'); + workers[jxlSrc].postMessage({ jxlSrc, image }); + workers[jxlSrc].addEventListener('message', m => m.data.imgData && requestAnimationFrame(() => imgDataToDataURL(img, m.data.imgData, isCSS))); +} + +function handleElement(el) { + if (el instanceof HTMLImageElement && el.src.endsWith('.jxl')) { + decode(el, false); + return; + } else if (el instanceof Element && getComputedStyle(el).backgroundImage.endsWith('.jxl)')) + decode(el, true); + el.childNodes.forEach(handleElement); +} + + +export function startPolyfill() { + new MutationObserver(mutations => mutations.forEach(mutation => { + mutation.addedNodes.forEach(handleElement); + })).observe(document.documentElement, { subtree: true, childList: true }); + handleElement(document.documentElement); // Run the polyfill once on the full DOM +} diff --git a/yarn.lock b/yarn.lock index bbceba0b..92cfd7cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2732,7 +2732,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.0: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2994,6 +2994,18 @@ cookie@0.4.2, cookie@~0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +copy-webpack-plugin@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" + integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== + dependencies: + fast-glob "^3.2.11" + glob-parent "^6.0.1" + globby "^13.1.1" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + core-js-compat@^3.20.2, core-js-compat@^3.21.0: version "3.24.1" resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.24.1.tgz" @@ -3187,7 +3199,7 @@ debug@4.3.1: dependencies: ms "2.1.2" -debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -4006,7 +4018,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.12: +fast-glob@^3.2.11, fast-glob@^3.2.12: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -4084,6 +4096,11 @@ filelist@^1.0.1: dependencies: minimatch "^5.0.1" +filesize@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" + integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" @@ -4343,6 +4360,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -4440,6 +4464,17 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +globby@^13.1.1: + version "13.1.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515" + integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" + globjoin@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz" @@ -4452,7 +4487,7 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -5314,6 +5349,11 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== +jxl.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jxl.js/-/jxl.js-1.0.0.tgz#e2c78e5c135d7d272c0d36f86b7f79de6f4b40b3" + integrity sha512-1br2vK02/9nYAbIEdrt7Qe6uRk5e+S0TGoRxUlxnGpXJYARfhFAfFHHArcQYjU4WMPNjLPci4oXBNsEUFVdEEQ== + karma-coverage@1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.2.tgz" @@ -5679,7 +5719,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@4.17.21, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: +lodash@4.17.21, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6007,6 +6047,11 @@ mocha@3.5.3: mkdirp "0.5.1" supports-color "3.1.2" +moment@^2.22.1: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@0.7.1: version "0.7.1" resolved "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" @@ -7414,6 +7459,11 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -8661,6 +8711,15 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +write-file-atomic@^2.3.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + write-file-atomic@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" @@ -8669,6 +8728,19 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +write-file-webpack-plugin@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/write-file-webpack-plugin/-/write-file-webpack-plugin-4.5.1.tgz#aeeb68889194da5ec8a864667d46da9e00ee92d5" + integrity sha512-AZ7qJUvhTCBiOtG21aFJUcNuLVo2FFM6JMGKvaUGAH+QDqQAp2iG0nq3GcuXmJOFQR2JjpjhyYkyPrbFKhdjNQ== + dependencies: + chalk "^2.4.0" + debug "^3.1.0" + filesize "^3.6.1" + lodash "^4.17.13" + mkdirp "^0.5.1" + moment "^2.22.1" + write-file-atomic "^2.3.0" + ws@~8.2.3: version "8.2.3" resolved "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz"