From 0e936a9946c541957a22557e2788290b780b6926 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Sat, 6 Feb 2021 15:07:38 +0100 Subject: [PATCH 1/3] Add processing of blurhash-based LQIP --- addon/services/responsive-image.ts | 9 ++- lib/plugins/lqip-blurhash.js | 71 ++++++++++++++++++ package.json | 1 + .../public/assets/images/lqip/blurhash.jpg | Bin 0 -> 12327 bytes yarn.lock | 5 ++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 lib/plugins/lqip-blurhash.js create mode 100644 tests/dummy/public/assets/images/lqip/blurhash.jpg diff --git a/addon/services/responsive-image.ts b/addon/services/responsive-image.ts index cfab24bde..f10022129 100644 --- a/addon/services/responsive-image.ts +++ b/addon/services/responsive-image.ts @@ -23,6 +23,13 @@ export interface LqipColor extends LqipBase { color: string; } +export interface LqipBlurhash extends LqipBase { + type: 'blurhash'; + hash: string; + width: number; + height: number; +} + export interface ImageMeta { image: string; width: number; @@ -32,7 +39,7 @@ export interface ImageMeta { export interface Meta { images: ImageMeta[]; - lqip?: LqipInline | LqipColor; + lqip?: LqipInline | LqipColor | LqipBlurhash; } /** diff --git a/lib/plugins/lqip-blurhash.js b/lib/plugins/lqip-blurhash.js new file mode 100644 index 000000000..5b5bf32a7 --- /dev/null +++ b/lib/plugins/lqip-blurhash.js @@ -0,0 +1,71 @@ +const sharp = require('sharp'); + +class LqipBlurhashPlugin { + constructor(addon) { + this.processed = []; + this.metaData = new Map(); + this.blurhash = require('blurhash'); + + addon.addMetadataExtension(this.addMetaData, this); + addon.addImagePreProcessor(this.imagePreProcessor, this); + } + + canProcessImage(config) { + return config.lqip && config.lqip.type === 'blurhash'; + } + + async getLqipDimensions(config, sharped) { + const meta = await sharped.metadata(); + const targetPixels = config.lqip.targetPixels || 12; + const aspectRatio = meta.width / meta.height; + + // taken from https://github.com/google/eleventy-high-performance-blog/blob/5ed39db7fd3f21ae82ac1a8e833bf283355bd3d0/_11ty/blurry-placeholder.js#L74-L92 + let bitmapHeight = targetPixels / aspectRatio; + bitmapHeight = Math.sqrt(bitmapHeight); + const bitmapWidth = targetPixels / bitmapHeight; + return { width: Math.round(bitmapWidth), height: Math.round(bitmapHeight) }; + } + + async imagePreProcessor(sharped, image, _width, config) { + if (this.processed.includes(image) || !this.canProcessImage(config)) { + return sharped; + } + this.processed.push(image); + + const { width, height } = await this.getLqipDimensions(config, sharped); + const rawWidth = width * 8; + const rawHeight = height * 8; + const buffer = await sharped.toBuffer(); + const lqi = await sharp(buffer) + .ensureAlpha() + .resize(rawWidth, rawHeight, { + fit: 'fill', + }) + .raw(); + + const data = new Uint8ClampedArray(await lqi.toBuffer()); + const hash = this.blurhash.encode(data, rawWidth, rawHeight, width, height); + + const meta = { + type: 'blurhash', + hash, + width, + height, + }; + + this.metaData.set(image, meta); + + return sharped; + } + + addMetaData(image, metadata /*, config*/) { + const ourMeta = this.metaData.get(image); + if (ourMeta) { + metadata.lqip = ourMeta; + } + + return metadata; + } +} + +module.exports = LqipBlurhashPlugin; diff --git a/package.json b/package.json index 925d7abe7..f1bba4318 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@glimmer/component": "^1.0.3", "@glimmer/tracking": "^1.0.3", "async-q": "^0.3.1", + "blurhash": "^1.1.3", "broccoli-caching-writer": "^3.0.3", "broccoli-funnel": "^2.0.1", "broccoli-merge-trees": "^4.2.0", diff --git a/tests/dummy/public/assets/images/lqip/blurhash.jpg b/tests/dummy/public/assets/images/lqip/blurhash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..40c3c1e39d89e8a24af797ada5adac46dfe12624 GIT binary patch literal 12327 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 XmN`0=33nk=JbTp1bPNOD?fm}%_`R3e literal 0 HcmV?d00001 diff --git a/yarn.lock b/yarn.lock index 9bce45586..efd916b4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3763,6 +3763,11 @@ bluebird@^3.1.1, bluebird@^3.4.6, bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +blurhash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e" + integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: version "4.11.9" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" From 06e8adb438c4d9f8c1c6f32e59fb810c212e781b Mon Sep 17 00:00:00 2001 From: simonihmig Date: Sat, 6 Feb 2021 16:50:14 +0100 Subject: [PATCH 2/3] Render blurhash-based LQIP --- addon/components/responsive-image.hbs | 1 + addon/components/responsive-image.ts | 53 ++++++++++++++++++ index.js | 7 ++- lib/plugins/lqip-blurhash.js | 8 ++- package.json | 3 +- tests/dummy/app/templates/index.hbs | 4 ++ .../components/responsive-image-test.js | 36 ++++++++++++- yarn.lock | 54 +++++++++++++++++++ 8 files changed, 162 insertions(+), 4 deletions(-) diff --git a/addon/components/responsive-image.hbs b/addon/components/responsive-image.hbs index 661cd2c1a..3e12b1995 100644 --- a/addon/components/responsive-image.hbs +++ b/addon/components/responsive-image.hbs @@ -16,6 +16,7 @@ ...attributes {{style (if this.showLqipImage (hash background-image=this.lqipImage background-size="cover")) + (if this.showLqipBlurhash (hash background-image=this.lqipBlurhash background-size="cover")) (if this.showLqipColor (hash background-color=this.lqipColor)) }} {{on "load" this.onLoad}} diff --git a/addon/components/responsive-image.ts b/addon/components/responsive-image.ts index 4a9d1ec1b..30e5b0e10 100644 --- a/addon/components/responsive-image.ts +++ b/addon/components/responsive-image.ts @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; import ResponsiveImageService, { ImageMeta, ImageType, + LqipBlurhash, LqipColor, LqipInline, Meta, @@ -12,6 +13,11 @@ import dataUri from 'ember-responsive-image/utils/data-uri'; import blurrySvg from 'ember-responsive-image/utils/blurry-svg'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +import { macroCondition, getOwnConfig, importSync } from '@embroider/macros'; + +declare module '@embroider/macros' { + export function getOwnConfig(): { usesBlurhash: boolean }; +} interface ResponsiveImageComponentArgs { image: string; @@ -34,6 +40,11 @@ enum Layout { } const PIXEL_DENSITIES = [1, 2]; +const canvas = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (typeof FastBoot === 'undefined' && + document.createElement('canvas')) as HTMLCanvasElement; // determines the order of sources, prefereing next-gen formats over legacy const typeScore = new Map([ @@ -215,6 +226,48 @@ export default class ResponsiveImageComponent extends Component) + .lqip as LqipBlurhash; + const { decode } = importSync('blurhash') as any; + + const blurWidth = width * 40; + const blurHeight = height * 40; + const pixels = decode(hash, blurWidth, blurHeight); + canvas.width = blurWidth; + canvas.height = blurHeight; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return undefined; + } + + const imageData = ctx.createImageData(blurWidth, blurHeight); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + const uri = canvas.toDataURL('image/png'); + + return `url("${uri}")`; + } else { + return undefined; + } + } + @action onLoad(): void { this.isLoaded = true; diff --git a/index.js b/index.js index 35252b156..d771f0bc4 100644 --- a/index.js +++ b/index.js @@ -23,7 +23,6 @@ const defaultConfig = { */ module.exports = { name: require('./package').name, - options: {}, metaData: {}, configData: {}, app: null, @@ -33,6 +32,12 @@ module.exports = { imagePostProcessors: [], plugins: [], + options: { + '@embroider/macros': { + setOwnConfig: {}, + }, + }, + /** * Add a callback function to change the generated metaData per origin image. * The callback method you provide must have the following signature: diff --git a/lib/plugins/lqip-blurhash.js b/lib/plugins/lqip-blurhash.js index 5b5bf32a7..6c781e109 100644 --- a/lib/plugins/lqip-blurhash.js +++ b/lib/plugins/lqip-blurhash.js @@ -5,18 +5,23 @@ class LqipBlurhashPlugin { this.processed = []; this.metaData = new Map(); this.blurhash = require('blurhash'); + this.macrosConfig = addon.options['@embroider/macros'].setOwnConfig; addon.addMetadataExtension(this.addMetaData, this); addon.addImagePreProcessor(this.imagePreProcessor, this); } + ensureMacrosConfig() { + this.macrosConfig.usesBlurhash = true; + } + canProcessImage(config) { return config.lqip && config.lqip.type === 'blurhash'; } async getLqipDimensions(config, sharped) { const meta = await sharped.metadata(); - const targetPixels = config.lqip.targetPixels || 12; + const targetPixels = config.lqip.targetPixels || 16; const aspectRatio = meta.width / meta.height; // taken from https://github.com/google/eleventy-high-performance-blog/blob/5ed39db7fd3f21ae82ac1a8e833bf283355bd3d0/_11ty/blurry-placeholder.js#L74-L92 @@ -31,6 +36,7 @@ class LqipBlurhashPlugin { return sharped; } this.processed.push(image); + this.ensureMacrosConfig(); const { width, height } = await this.getLqipDimensions(config, sharped); const rawWidth = width * 8; diff --git a/package.json b/package.json index f1bba4318..e52853ae9 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "postpack": "ember ts:clean" }, "dependencies": { + "@embroider/macros": "^0.36.0", "@glimmer/component": "^1.0.3", "@glimmer/tracking": "^1.0.3", "async-q": "^0.3.1", @@ -38,6 +39,7 @@ "broccoli-funnel": "^2.0.1", "broccoli-merge-trees": "^4.2.0", "broccoli-stew": "^3.0.0", + "ember-auto-import": "^1.10.1", "ember-cli-babel": "^7.23.1", "ember-cli-htmlbars": "^5.3.1", "ember-cli-typescript": "^4.1.0", @@ -78,7 +80,6 @@ "@typescript-eslint/parser": "^4.14.2", "babel-eslint": "^10.1.0", "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^1.10.1", "ember-cli": "~3.24.0", "ember-cli-app-version": "^4.0.0", "ember-cli-dependency-checker": "^3.2.0", diff --git a/tests/dummy/app/templates/index.hbs b/tests/dummy/app/templates/index.hbs index e52feacf1..fc4fd31c1 100644 --- a/tests/dummy/app/templates/index.hbs +++ b/tests/dummy/app/templates/index.hbs @@ -1,3 +1,7 @@ +

LQIP Blurhash

+ + +

LQIP blurry

diff --git a/tests/integration/components/responsive-image-test.js b/tests/integration/components/responsive-image-test.js index 0fb2001fb..4e1a35abb 100644 --- a/tests/integration/components/responsive-image-test.js +++ b/tests/integration/components/responsive-image-test.js @@ -402,7 +402,7 @@ module('Integration: Responsive Image Component', function (hooks) { }); module('color', function () { - test('it sets LQIP SVG as background', async function (assert) { + test('it sets background-color', async function (assert) { let resolve; const waitUntilLoaded = new Promise((r) => { resolve = r; @@ -422,6 +422,40 @@ module('Integration: Responsive Image Component', function (hooks) { 'after image is loaded the background color is removed' ); }); + + module('blurhash', function () { + test('it sets LQIP from blurhash as background', async function (assert) { + let resolve; + const waitUntilLoaded = new Promise((r) => { + resolve = r; + }); + this.onload = () => setTimeout(resolve, 0); + + await render( + hbs`` + ); + + assert.ok( + this.element + .querySelector('img') + .style.backgroundImage?.match(/data:image\/png/), + 'it has a background PNG' + ); + assert.dom('img').hasStyle({ 'background-size': 'cover' }); + assert.ok( + this.element.querySelector('img').style.backgroundImage?.length > + 100, + 'the background SVG has a reasonable length' + ); + + await waitUntilLoaded; + + assert.notOk( + this.element.querySelector('img').style.backgroundImage, + 'after image is loaded the background PNG is removed' + ); + }); + }); }); }); diff --git a/yarn.lock b/yarn.lock index efd916b4b..508fa9ca9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1186,6 +1186,45 @@ walk-sync "^1.1.3" wrap-legacy-hbs-plugin-if-needed "^1.0.1" +"@embroider/core@0.36.0": + version "0.36.0" + resolved "https://registry.yarnpkg.com/@embroider/core/-/core-0.36.0.tgz#fbbd60d29c3fcbe02b4e3e63e6043a43de2b9ce3" + integrity sha512-J6esENP+aNt+/r070cF1RCJyCi/Rn1I6uFp37vxyLWwvGDuT0E7wGcaPU29VBkBFqxi4Z1n4F796BaGHv+kX6w== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.12.3" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-runtime" "^7.12.1" + "@babel/runtime" "^7.12.5" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + "@embroider/macros" "0.36.0" + assert-never "^1.1.0" + babel-plugin-syntax-dynamic-import "^6.18.0" + broccoli-node-api "^1.7.0" + broccoli-persistent-filter "^3.1.2" + broccoli-plugin "^4.0.1" + broccoli-source "^3.0.0" + debug "^3.1.0" + escape-string-regexp "^4.0.0" + fast-sourcemap-concat "^1.4.0" + filesize "^4.1.2" + fs-extra "^7.0.1" + fs-tree-diff "^2.0.0" + handlebars "^4.4.2" + js-string-escape "^1.0.1" + jsdom "^16.4.0" + json-stable-stringify "^1.0.1" + lodash "^4.17.10" + pkg-up "^3.1.0" + resolve "^1.8.1" + resolve-package-path "^1.2.2" + semver "^7.3.2" + strip-bom "^3.0.0" + typescript-memoize "^1.0.0-alpha.3" + walk-sync "^1.1.3" + wrap-legacy-hbs-plugin-if-needed "^1.0.1" + "@embroider/macros@0.33.0": version "0.33.0" resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-0.33.0.tgz#d5826ea7565bb69b57ba81ed528315fe77acbf9d" @@ -1201,6 +1240,21 @@ resolve "^1.8.1" semver "^7.3.2" +"@embroider/macros@0.36.0", "@embroider/macros@^0.36.0": + version "0.36.0" + resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-0.36.0.tgz#5330f1e6f12112f0f68e34b3e4855dc7dd3c69a5" + integrity sha512-w37G4uXG+Wi3K3EHSFBSr/n6kGFXYG8nzZ9ptzDOC7LP3Oh5/MskBnVZW3+JkHXUPEqKsDGlxPxCVpPl1kQyjQ== + dependencies: + "@babel/core" "^7.12.3" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + "@embroider/core" "0.36.0" + assert-never "^1.1.0" + ember-cli-babel "^7.23.0" + lodash "^4.17.10" + resolve "^1.8.1" + semver "^7.3.2" + "@embroider/test-setup@^0.36.0": version "0.36.0" resolved "https://registry.yarnpkg.com/@embroider/test-setup/-/test-setup-0.36.0.tgz#67ace15d69f04f282adde38ee39e4d48208173b7" From f483e394319e7d3a074bd1553d971e8da4a040eb Mon Sep 17 00:00:00 2001 From: simonihmig Date: Sun, 7 Feb 2021 12:40:39 +0100 Subject: [PATCH 3/3] Workaround importSync not working, fixes after rebase --- addon/components/responsive-image.ts | 12 ++++++++++-- ember-cli-build.js | 9 +++++++++ lib/plugins/lqip-blurhash.js | 19 +++++++++++-------- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/addon/components/responsive-image.ts b/addon/components/responsive-image.ts index 30e5b0e10..bce8fcaa2 100644 --- a/addon/components/responsive-image.ts +++ b/addon/components/responsive-image.ts @@ -13,7 +13,11 @@ import dataUri from 'ember-responsive-image/utils/data-uri'; import blurrySvg from 'ember-responsive-image/utils/blurry-svg'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { macroCondition, getOwnConfig, importSync } from '@embroider/macros'; +import { + macroCondition, + getOwnConfig /*, importSync*/, +} from '@embroider/macros'; +import { decode } from 'blurhash'; declare module '@embroider/macros' { export function getOwnConfig(): { usesBlurhash: boolean }; @@ -245,7 +249,11 @@ export default class ResponsiveImageComponent extends Component) .lqip as LqipBlurhash; - const { decode } = importSync('blurhash') as any; + + // This does not work correctly, see https://github.com/embroider-build/embroider/issues/684 + // The idea was to pull `blurhash` into our vendor.js only when needed + // Currently we are instead importing it at the module head, but this comes at a cost for all users that don't need it. + // const { decode } = importSync('blurhash') as any; const blurWidth = width * 40; const blurHeight = height * 40; diff --git a/ember-cli-build.js b/ember-cli-build.js index 8e9bb81f2..dc191df40 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -47,6 +47,15 @@ module.exports = function (defaults) { removeSource: true, justCopy: false, }, + { + include: 'assets/images/lqip/blurhash.jpg', + quality: 50, + supportedWidths: [100, 640], + lqip: { + type: 'blurhash', + }, + removeSource: true, + }, ], }); diff --git a/lib/plugins/lqip-blurhash.js b/lib/plugins/lqip-blurhash.js index 6c781e109..d4ac4da2f 100644 --- a/lib/plugins/lqip-blurhash.js +++ b/lib/plugins/lqip-blurhash.js @@ -4,15 +4,19 @@ class LqipBlurhashPlugin { constructor(addon) { this.processed = []; this.metaData = new Map(); - this.blurhash = require('blurhash'); - this.macrosConfig = addon.options['@embroider/macros'].setOwnConfig; - addon.addMetadataExtension(this.addMetaData, this); - addon.addImagePreProcessor(this.imagePreProcessor, this); - } + if ( + addon.addonOptions.find( + (imageConfig) => + imageConfig.lqip && imageConfig.lqip.type === 'blurhash' + ) + ) { + this.blurhash = require('blurhash'); + addon.options['@embroider/macros'].setOwnConfig.usesBlurhash = true; - ensureMacrosConfig() { - this.macrosConfig.usesBlurhash = true; + addon.addMetadataExtension(this.addMetaData, this); + addon.addImagePreProcessor(this.imagePreProcessor, this); + } } canProcessImage(config) { @@ -36,7 +40,6 @@ class LqipBlurhashPlugin { return sharped; } this.processed.push(image); - this.ensureMacrosConfig(); const { width, height } = await this.getLqipDimensions(config, sharped); const rawWidth = width * 8;