From ba8e6e253d6721e288687c168a7d6cbf59380cd3 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Fri, 5 Mar 2021 09:59:59 +0100 Subject: [PATCH 1/4] Refactor meta format --- addon/services/responsive-image.ts | 75 +++++++++------ ember-cli-build.js | 2 +- lib/image-writer.js | 39 +++----- tests/unit/services/responsive-image-test.js | 94 ------------------- tests/unit/services/responsive-image-test.ts | 99 ++++++++++++++++++++ 5 files changed, 161 insertions(+), 148 deletions(-) delete mode 100644 tests/unit/services/responsive-image-test.js create mode 100644 tests/unit/services/responsive-image-test.ts diff --git a/addon/services/responsive-image.ts b/addon/services/responsive-image.ts index 78aa016ca..18d2eb03c 100644 --- a/addon/services/responsive-image.ts +++ b/addon/services/responsive-image.ts @@ -36,10 +36,14 @@ export interface ImageMeta { } export interface Meta { - images: ImageMeta[]; + widths: number[]; + formats: ImageType[]; + aspectRatio: number; lqip?: LqipInline | LqipColor | LqipBlurhash; } +const imageExtensions: Map = new Map([['jpeg', 'jpg']]); + /** * Service class to provides images generated by the responsive images package */ @@ -61,18 +65,25 @@ export default class ResponsiveImageService extends Service { * return the images with the different widths */ getImages(imageName: string, type?: ImageType): ImageMeta[] { - let images = this.getMeta(imageName).images; - if (type) { - images = images.filter((image) => image.type === type); + imageName = this.normalizeImageName(imageName); + const meta = this.getMeta(imageName); + const images: ImageMeta[] = []; + + for (const width of meta.widths) { + if (type) { + images.push(this.getImageMetaByWidth(imageName, width, type)); + } else { + for (const type of meta.formats) { + images.push(this.getImageMetaByWidth(imageName, width, type)); + } + } } return images; } getMeta(imageName: string): Meta { - if (imageName.charAt(0) === '/') { - imageName = imageName.slice(1); - } + imageName = this.normalizeImageName(imageName); assert( `There is no data for image ${imageName}: ${this.meta}`, Object.prototype.hasOwnProperty.call(this.meta, imageName) @@ -81,6 +92,10 @@ export default class ResponsiveImageService extends Service { return this.meta[imageName]; } + private normalizeImageName(imageName: string): string { + return imageName.charAt(0) === '/' ? imageName.slice(1) : imageName; + } + private getType(imageName: string): ImageType { const extension = imageName.split('.').pop(); assert(`No extension found for ${imageName}`, extension); @@ -88,12 +103,7 @@ export default class ResponsiveImageService extends Service { } getAvailableTypes(imageName: string): ImageType[] { - return ( - this.getImages(imageName) - .map((image) => image.type) - // unique - .filter((value, index, self) => self.indexOf(value) === index) - ); + return this.getMeta(imageName).formats; } /** @@ -115,26 +125,35 @@ export default class ResponsiveImageService extends Service { imageName: string, width: number, type: ImageType = this.getType(imageName) - ): ImageMeta | undefined { - return this.getImages(imageName) - .filter((img) => img.type === type) - .reduce((prevValue: ImageMeta | undefined, imageMeta: ImageMeta) => { - if (prevValue === undefined) { - return imageMeta; - } - - if (imageMeta.width >= width && prevValue.width >= width) { - return imageMeta.width >= prevValue.width ? prevValue : imageMeta; + ): ImageMeta { + const imageWidth = this.getMeta(imageName).widths.reduce( + (prevValue: number, w: number) => { + if (w >= width && prevValue >= width) { + return w >= prevValue ? prevValue : w; } else { - return imageMeta.width >= prevValue.width ? imageMeta : prevValue; + return w >= prevValue ? w : prevValue; } - }, undefined); + }, + 0 + ); + const height = Math.round(imageWidth / this.getAspectRatio(imageName)); + return { + image: this.getImageFilename(imageName, imageWidth, type), + width: imageWidth, + type, + height, + }; } - getAspectRatio(imageName: string): number | undefined { - const meta = this.getImages(imageName)[0]; + getAspectRatio(imageName: string): number { + return this.getMeta(imageName).aspectRatio; + } - return meta ? meta.width / meta.height : undefined; + getImageFilename(image: string, width: number, format: ImageType): string { + // this must match `generateFilename()` of ImageWriter broccoli plugin! + const ext = imageExtensions.get(format) ?? format; + const base = image.substr(0, image.lastIndexOf('.')); + return `/${base}${width}w.${ext}`; } private getDestinationWidthBySize(size: number): number { diff --git a/ember-cli-build.js b/ember-cli-build.js index 29cad9c52..abf83fae0 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -6,7 +6,7 @@ module.exports = function (defaults) { let app = new EmberAddon(defaults, { // Add options here fingerprint: { - enabled: true, + enabled: false, extensions: ['js', 'css', 'png', 'jpg', 'gif', 'map', 'webp', 'avif'], exclude: ['testem.js'], customHash: '00e24234f1b58e32b935b1041432916f', diff --git a/lib/image-writer.js b/lib/image-writer.js index f63072460..426575d77 100644 --- a/lib/image-writer.js +++ b/lib/image-writer.js @@ -82,6 +82,7 @@ class ImageResizer extends CachingWriter { } else { newTasks = this.generateImages(file, image, meta, destinationPath); } + this.generateMetaData(file, image, meta); tasks = [...tasks, ...newTasks]; } @@ -146,7 +147,6 @@ class ImageResizer extends CachingWriter { format ); const destination = path.join(destinationPath, generatedFilename); - this.insertMetadata(filename, generatedFilename, format, width, meta); const preProcessedSharp = await this.preProcessImage( sharp(source), filename, @@ -200,18 +200,16 @@ class ImageResizer extends CachingWriter { postProcessedSharp, destinationPath, format, - width, - meta + width ) ) ); } - async saveImage(filename, image, destinationPath, format, width, meta) { + async saveImage(filename, image, destinationPath, format, width) { const generatedFilename = this.generateFilename(filename, width, format); const destination = path.join(destinationPath, generatedFilename); await fs.ensureDir(path.dirname(destination)); - this.insertMetadata(filename, generatedFilename, format, width, meta); await image.toFormat(format).toFile(destination); } @@ -250,7 +248,7 @@ class ImageResizer extends CachingWriter { async postProcessImage(sharp, filename, width) { let result = sharp; for (let processor of this.imagePostProcessors) { - let result = await processor.callback.call( + result = await processor.callback.call( processor.target, result, filename, @@ -268,28 +266,19 @@ class ImageResizer extends CachingWriter { return `${base}${width}w.${ext}`; } - insertMetadata(filename, imagename, format, width, meta) { - let image = path.join(this.image_options.destinationDir, imagename); - if (process.platform === 'win32') { - image = image.replace(/\\/g, '/'); - } + generateMetaData(imageWebPath, sharpImage, sharpMeta) { let aspectRatio = 1; - if (meta.height > 0) { - aspectRatio = Math.round((meta.width / meta.height) * 100) / 100; + if (sharpMeta.height > 0) { + aspectRatio = + Math.round((sharpMeta.width / sharpMeta.height) * 100) / 100; } - let height = Math.round(width / aspectRatio); - let metadata = { - image, - type: format, - width, - height, + const formats = this.imageFormatsFor(sharpMeta); + + this.metaData[imageWebPath] = { + widths: this.image_options.widths, + formats, + aspectRatio, }; - if ( - Object.prototype.hasOwnProperty.call(this.metaData, filename) === false - ) { - this.metaData[filename] = { images: [] }; - } - this.metaData[filename].images.push(metadata); } /** diff --git a/tests/unit/services/responsive-image-test.js b/tests/unit/services/responsive-image-test.js deleted file mode 100644 index d5f072d4c..000000000 --- a/tests/unit/services/responsive-image-test.js +++ /dev/null @@ -1,94 +0,0 @@ -import { setupTest } from 'ember-qunit'; -import { module, test } from 'qunit'; - -const meta = { - prepend: '', - 'test.png': { - images: [ - { - image: '/assets/images/responsive/test100w.png', - width: 100, - height: 100, - type: 'png', - }, - { - image: '/assets/images/responsive/test50w.png', - width: 50, - height: 50, - type: 'png', - }, - { - image: - '/assets/images/responsive/test100w-00e24234f1b58e32b935b1041432916f.webp', - width: 100, - height: 100, - type: 'webp', - }, - { - image: - '/assets/images/responsive/test50w-00e24234f1b58e32b935b1041432916f.webp', - width: 50, - height: 50, - type: 'webp', - }, - ], - }, -}; - -module('ResponsiveImageService', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - let service = this.owner.lookup('service:responsive-image'); - service.set('meta', meta); - }); - - test('retrieve generated images by name', function (assert) { - let service = this.owner.lookup('service:responsive-image'); - let images = service.getImages('test.png'); - assert.deepEqual(images, meta['test.png'].images); - }); - - test('handle absolute pats', function (assert) { - let service = this.owner.lookup('service:responsive-image'); - let images = service.getImages('/test.png'); - assert.deepEqual(images, meta['test.png'].images); - }); - - test('retrieve generated images by name and type', function (assert) { - let service = this.owner.lookup('service:responsive-image'); - let images = service.getImages('test.png', 'png'); - assert.deepEqual(images, meta['test.png'].images.slice(0, 2)); - - images = service.getImages('test.png', 'webp'); - assert.deepEqual(images, meta['test.png'].images.slice(2, 4)); - }); - - test('get available types', function (assert) { - let service = this.owner.lookup('service:responsive-image'); - let types = service.getAvailableTypes('test.png'); - assert.deepEqual(types, ['png', 'webp']); - }); - - test('retrieve generated image data by size', function (assert) { - let service = this.owner.lookup('service:responsive-image'); - service.physicalWidth = 100; - let images = service.getImageMetaBySize('test.png', 120); - assert.deepEqual(images, meta['test.png'].images[0]); - images = service.getImageMetaBySize('test.png', 60); - assert.deepEqual(images, meta['test.png'].images[0]); - images = service.getImageMetaBySize('test.png', 45); - assert.deepEqual(images, meta['test.png'].images[1]); - }); - - test('retrieve generated image data by size and type', function (assert) { - let service = this.owner.lookup('service:responsive-image'); - service.physicalWidth = 100; - let images = service.getImageMetaBySize('test.png', 120, 'webp'); - assert.deepEqual(images, meta['test.png'].images[2]); - images = service.getImageMetaBySize('test.png', 60, 'webp'); - assert.deepEqual(images, meta['test.png'].images[2]); - images = service.getImageMetaBySize('test.png', 45, 'webp'); - assert.deepEqual(images, meta['test.png'].images[3]); - }); -}); diff --git a/tests/unit/services/responsive-image-test.ts b/tests/unit/services/responsive-image-test.ts new file mode 100644 index 000000000..f1f836a05 --- /dev/null +++ b/tests/unit/services/responsive-image-test.ts @@ -0,0 +1,99 @@ +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import { + ImageMeta, + Meta, +} from 'ember-responsive-image/services/responsive-image'; + +const meta: Record = { + 'test.png': { + widths: [50, 100], + formats: ['png', 'webp'], + aspectRatio: 1, + }, +}; + +const imageMetas: ImageMeta[] = [ + { + image: '/test50w.png', + width: 50, + height: 50, + type: 'png', + }, + { + image: '/test50w.webp', + width: 50, + height: 50, + type: 'webp', + }, + { + image: '/test100w.png', + width: 100, + height: 100, + type: 'png', + }, + { + image: '/test100w.webp', + width: 100, + height: 100, + type: 'webp', + }, +]; + +module('ResponsiveImageService', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + const service = this.owner.lookup('service:responsive-image'); + service.set('meta', meta); + }); + + test('retrieve generated images by name', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + const images = service.getImages('test.png'); + assert.deepEqual(images, imageMetas); + }); + + test('handle absolute paths', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + const images = service.getImages('/test.png'); + assert.deepEqual(images, imageMetas); + }); + + test('retrieve generated images by name and type', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + let images = service.getImages('test.png', 'png'); + assert.deepEqual(images, [imageMetas[0], imageMetas[2]]); + + images = service.getImages('test.png', 'webp'); + assert.deepEqual(images, [imageMetas[1], imageMetas[3]]); + }); + + test('get available types', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + const types = service.getAvailableTypes('test.png'); + assert.deepEqual(types, ['png', 'webp']); + }); + + test('retrieve generated image data by size', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + service.physicalWidth = 100; + let images = service.getImageMetaBySize('test.png', 120); + assert.deepEqual(images, imageMetas[2]); + images = service.getImageMetaBySize('test.png', 60); + assert.deepEqual(images, imageMetas[2]); + images = service.getImageMetaBySize('test.png', 45); + assert.deepEqual(images, imageMetas[0]); + }); + + test('retrieve generated image data by size and type', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + service.physicalWidth = 100; + let images = service.getImageMetaBySize('test.png', 120, 'webp'); + assert.deepEqual(images, imageMetas[3]); + images = service.getImageMetaBySize('test.png', 60, 'webp'); + assert.deepEqual(images, imageMetas[3]); + images = service.getImageMetaBySize('test.png', 45, 'webp'); + assert.deepEqual(images, imageMetas[1]); + }); +}); From 22900b9ca87e6f67c210de9571320b2ca7d22d88 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Fri, 5 Mar 2021 15:23:19 +0100 Subject: [PATCH 2/4] Implement our own fingerprinting --- addon/services/responsive-image.ts | 4 +- ember-cli-build.js | 151 ++++++------- index.js | 64 ++++-- lib/image-writer.js | 72 +++++-- tests/fastboot/image-test.js | 5 +- .../components/responsive-image-test.js | 161 +++++++++----- .../helpers/responsive-image-resolve-test.js | 12 +- tests/unit/services/responsive-image-test.ts | 198 +++++++++++------- 8 files changed, 435 insertions(+), 232 deletions(-) diff --git a/addon/services/responsive-image.ts b/addon/services/responsive-image.ts index 18d2eb03c..b13559e86 100644 --- a/addon/services/responsive-image.ts +++ b/addon/services/responsive-image.ts @@ -39,6 +39,7 @@ export interface Meta { widths: number[]; formats: ImageType[]; aspectRatio: number; + fingerprint?: string; lqip?: LqipInline | LqipColor | LqipBlurhash; } @@ -153,7 +154,8 @@ export default class ResponsiveImageService extends Service { // this must match `generateFilename()` of ImageWriter broccoli plugin! const ext = imageExtensions.get(format) ?? format; const base = image.substr(0, image.lastIndexOf('.')); - return `/${base}${width}w.${ext}`; + const fingerprint = this.getMeta(image).fingerprint; + return `/${base}${width}w${fingerprint ? '-' + fingerprint : ''}.${ext}`; } private getDestinationWidthBySize(size: number): number { diff --git a/ember-cli-build.js b/ember-cli-build.js index abf83fae0..6191607ea 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -6,91 +6,92 @@ module.exports = function (defaults) { let app = new EmberAddon(defaults, { // Add options here fingerprint: { - enabled: false, + enabled: true, extensions: ['js', 'css', 'png', 'jpg', 'gif', 'map', 'webp', 'avif'], exclude: ['testem.js'], - customHash: '00e24234f1b58e32b935b1041432916f', }, - 'responsive-image': [ - { - include: 'assets/images/tests/**/*', - exclude: [ - 'assets/images/tests/small.png', - 'assets/images/tests/lqip/**/*', - ], - quality: 50, - widths: [50, 100, 640], - lqip: { - type: 'color', + 'responsive-image': { + images: [ + { + include: 'assets/images/tests/**/*', + exclude: [ + 'assets/images/tests/small.png', + 'assets/images/tests/lqip/**/*', + ], + quality: 50, + widths: [50, 100, 640], + lqip: { + type: 'color', + }, + removeSource: true, }, - removeSource: true, - }, - { - include: 'assets/images/tests/small.png', - quality: 10, - removeSource: false, - widths: [10, 25], - }, - { - include: 'assets/images/tests/lqip/inline.jpg', - quality: 50, - widths: [100, 640], - lqip: { - type: 'inline', + { + include: 'assets/images/tests/small.png', + quality: 10, + removeSource: false, + widths: [10, 25], }, - removeSource: true, - }, - { - include: 'assets/images/tests/lqip/color.jpg', - quality: 50, - widths: [100, 640], - lqip: { - type: 'color', + { + include: 'assets/images/tests/lqip/inline.jpg', + quality: 50, + widths: [100, 640], + lqip: { + type: 'inline', + }, + removeSource: true, }, - removeSource: true, - justCopy: false, - }, - { - include: 'assets/images/tests/lqip/blurhash.jpg', - quality: 50, - widths: [100, 640], - lqip: { - type: 'blurhash', + { + include: 'assets/images/tests/lqip/color.jpg', + quality: 50, + widths: [100, 640], + lqip: { + type: 'color', + }, + removeSource: true, + justCopy: false, }, - removeSource: true, - }, - { - include: 'assets/images/docs/**/*', - exclude: [ - 'assets/images/docs/lqip-color.jpg', - 'assets/images/docs/lqip-blurhash.jpg', - ], - quality: 50, - widths: [1920, 1280, 640, 320], - lqip: { - type: 'inline', + { + include: 'assets/images/tests/lqip/blurhash.jpg', + quality: 50, + widths: [100, 640], + lqip: { + type: 'blurhash', + }, + removeSource: true, }, - removeSource: true, - }, - { - include: 'assets/images/docs/lqip-color.jpg', - quality: 50, - widths: [1920, 1280, 640, 320], - lqip: { - type: 'color', + { + include: 'assets/images/docs/**/*', + exclude: [ + 'assets/images/docs/lqip-color.jpg', + 'assets/images/docs/lqip-blurhash.jpg', + ], + quality: 50, + widths: [1920, 1280, 640, 320], + lqip: { + type: 'inline', + }, + removeSource: true, }, - removeSource: true, - }, - { - include: 'assets/images/docs/lqip-blurhash.jpg', - quality: 50, - widths: [1920, 1280, 640, 320], - lqip: { - type: 'blurhash', + { + include: 'assets/images/docs/lqip-color.jpg', + quality: 50, + widths: [1920, 1280, 640, 320], + lqip: { + type: 'color', + }, + removeSource: true, }, - removeSource: true, - }, - ], + { + include: 'assets/images/docs/lqip-blurhash.jpg', + quality: 50, + widths: [1920, 1280, 640, 320], + lqip: { + type: 'blurhash', + }, + removeSource: true, + }, + ], + }, }); /* diff --git a/index.js b/index.js index 966d1836f..5f1ec5546 100644 --- a/index.js +++ b/index.js @@ -155,13 +155,15 @@ module.exports = { return this.extendedMetaData; }, - included(parent) { + included(app) { this._super.included.apply(this, arguments); - this.initConfig(parent); + this.app = app; + this.initConfig(); + this.setupFingerprinting(); this.initPlugins(); this.processingTree = this.createProcessingTree(); - this.usesBlurhash = this.addonOptions.some( + this.usesBlurhash = this.addonOptions.images.some( (imageConfig) => imageConfig.lqip && imageConfig.lqip.type === 'blurhash' ); this.options[ @@ -169,22 +171,23 @@ module.exports = { ].setOwnConfig.usesBlurhash = this.usesBlurhash; }, - initConfig(app) { - let config = app.options['responsive-image']; + initConfig() { + this.addonOptions = this.app.options['responsive-image']; - if (!config) { + if (!this.addonOptions) { this.ui.writeWarnLine( 'Could not find config for ember-responsive-image, skipping image processing...' ); - this.addonOptions = []; - return; + this.addonOptions = { images: [] }; } - if (!Array.isArray(config)) { - config = [config]; + if (!Array.isArray(this.addonOptions.images)) { + throw new SilentError( + 'Config for ember-responsive-image must include an `images` array.' + ); } - this.addonOptions = config.map((item) => { + this.addonOptions.images = this.addonOptions.images.map((item) => { this.validateConfigItem(item); let extendedConfig = { ...defaultConfig, ...item }; // extendedConfig.rootURL = url; @@ -209,6 +212,38 @@ module.exports = { ); }, + setupFingerprinting() { + if (this.app.project.findAddonByName('broccoli-asset-rev')) { + const assetRevOptions = this.app.options.fingerprint; + this.addonOptions.fingerprint = + this.addonOptions.fingerprint !== undefined + ? this.addonOptions.fingerprint + : assetRevOptions === false + ? false + : assetRevOptions && assetRevOptions.enabled !== undefined + ? assetRevOptions.enabled + : this.app.env === 'production'; + + if (this.addonOptions.fingerprint) { + // exclude our own images from broccoli-asset-rev, as we will handle fingerprinting on our own + + const excludeGlobs = this.addonOptions.images.reduce( + (globs, imageConfig) => [...globs, ...imageConfig.include], + [] + ); + + assetRevOptions.exclude = assetRevOptions.exclude + ? [...assetRevOptions.exclude, ...excludeGlobs] + : excludeGlobs; + } + } else { + this.addonOptions.fingerprint = + this.addonOptions.fingerprint !== undefined + ? this.addonOptions.fingerprint + : this.app.env === 'production'; + } + }, + validateConfigItem(config) { if (!config.include) { throw new SilentError( @@ -345,8 +380,9 @@ module.exports = { createProcessingTree() { const tree = this._findHost().trees.public; - const trees = this.addonOptions.map((options) => { - return this.resizeImages(tree, options); + const { fingerprint } = this.addonOptions; + const trees = this.addonOptions.images.map((options) => { + return this.resizeImages(tree, { ...options, fingerprint }); }); return mergeTrees(trees, { overwrite: true }); @@ -355,7 +391,7 @@ module.exports = { postBuild(result) { // remove original images that have `removeSource` set const processedImages = Object.keys(this.metaData); - this.addonOptions.forEach((options) => { + this.addonOptions.images.forEach((options) => { if (options.removeSource) { const globs = processedImages .filter((file) => { diff --git a/lib/image-writer.js b/lib/image-writer.js index 426575d77..335f8c4eb 100644 --- a/lib/image-writer.js +++ b/lib/image-writer.js @@ -5,6 +5,7 @@ const fs = require('fs-extra'); const CachingWriter = require('broccoli-caching-writer'); const async = require('async-q'); const sharp = require('sharp'); +const crypto = require('crypto'); const imageExtensions = { jpeg: 'jpg', @@ -76,13 +77,28 @@ class ImageResizer extends CachingWriter { this.addConfigData(file); const meta = await image.metadata(); + const fingerprint = this.image_options.fingerprint + ? await this.generateFingerprint(image) + : false; let newTasks; if (justCopy) { - newTasks = this.copyImages(file, image, meta, destinationPath); + newTasks = this.copyImages( + file, + image, + meta, + destinationPath, + fingerprint + ); } else { - newTasks = this.generateImages(file, image, meta, destinationPath); + newTasks = this.generateImages( + file, + image, + meta, + destinationPath, + fingerprint + ); } - this.generateMetaData(file, image, meta); + this.generateMetaData(file, image, meta, fingerprint); tasks = [...tasks, ...newTasks]; } @@ -96,9 +112,10 @@ class ImageResizer extends CachingWriter { * @param image * @param meta * @param destinationPath + * @param fingerprint * @returns {Array} an array of promise-functions */ - generateImages(filename, image, meta, destinationPath) { + generateImages(filename, image, meta, destinationPath, fingerprint) { return this.image_options.widths.map((width) => { return async () => { await this.generateResizedImages( @@ -106,7 +123,8 @@ class ImageResizer extends CachingWriter { image, destinationPath, width, - meta + meta, + fingerprint ); }; }); @@ -131,9 +149,10 @@ class ImageResizer extends CachingWriter { * @param image * @param meta * @param destinationPath + * @param fingerprint * @returns {Array} an array of promise-functions */ - copyImages(filename, image, meta, destinationPath) { + copyImages(filename, image, meta, destinationPath, fingerprint) { const source = path.join(this.inputPaths[0], filename); const formats = this.imageFormatsFor(meta); const tasks = []; @@ -144,7 +163,8 @@ class ImageResizer extends CachingWriter { const generatedFilename = this.generateFilename( filename, width, - format + format, + fingerprint ); const destination = path.join(destinationPath, generatedFilename); const preProcessedSharp = await this.preProcessImage( @@ -171,6 +191,7 @@ class ImageResizer extends CachingWriter { * @param destinationPath * @param width * @param {Object} meta + * @param fingerprint * @returns {deferred.promise|*} */ async generateResizedImages( @@ -178,7 +199,8 @@ class ImageResizer extends CachingWriter { sharpObject, destinationPath, width, - meta + meta, + fingerprint ) { const preProcessedSharp = await this.preProcessImage( sharpObject.clone(), @@ -200,14 +222,27 @@ class ImageResizer extends CachingWriter { postProcessedSharp, destinationPath, format, - width + width, + fingerprint ) ) ); } - async saveImage(filename, image, destinationPath, format, width) { - const generatedFilename = this.generateFilename(filename, width, format); + async saveImage( + filename, + image, + destinationPath, + format, + width, + fingerprint + ) { + const generatedFilename = this.generateFilename( + filename, + width, + format, + fingerprint + ); const destination = path.join(destinationPath, generatedFilename); await fs.ensureDir(path.dirname(destination)); @@ -260,13 +295,21 @@ class ImageResizer extends CachingWriter { return result; } - generateFilename(file, width, format) { + generateFilename(file, width, format, fingerprint) { const ext = imageExtensions[format] || format; const base = file.substr(0, file.lastIndexOf('.')); - return `${base}${width}w.${ext}`; + const fp = fingerprint !== false ? `-${fingerprint}` : ''; + return `${base}${width}w${fp}.${ext}`; + } + + async generateFingerprint(sharpImage) { + const md5 = crypto.createHash('md5'); + md5.update(await sharpImage.toBuffer()); + md5.update(JSON.stringify(this.image_options)); + return md5.digest('hex'); } - generateMetaData(imageWebPath, sharpImage, sharpMeta) { + generateMetaData(imageWebPath, sharpImage, sharpMeta, fingerprint) { let aspectRatio = 1; if (sharpMeta.height > 0) { aspectRatio = @@ -278,6 +321,7 @@ class ImageResizer extends CachingWriter { widths: this.image_options.widths, formats, aspectRatio, + fingerprint: fingerprint !== false ? fingerprint : undefined, }; } diff --git a/tests/fastboot/image-test.js b/tests/fastboot/image-test.js index 86add4bfa..d4fb6ad34 100644 --- a/tests/fastboot/image-test.js +++ b/tests/fastboot/image-test.js @@ -13,7 +13,10 @@ module('FastBoot | image', function (hooks) { assert.dom('img[data-test-simple-image]').exists(); assert .dom('img[data-test-simple-image]') - .hasAttribute('src', '/assets/images/tests/test640w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test640w(-\\w+)?.png') + ); }); test('it renders lqip color', async function (assert) { diff --git a/tests/integration/components/responsive-image-test.js b/tests/integration/components/responsive-image-test.js index 2786bf41c..d520b3057 100644 --- a/tests/integration/components/responsive-image-test.js +++ b/tests/integration/components/responsive-image-test.js @@ -40,19 +40,19 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test640w.png 640w') + new RegExp('/assets/images/tests/test640w(-\\w+)?.png 640w') ); assert .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test100w.png 100w') + new RegExp('/assets/images/tests/test100w(-\\w+)?.png 100w') ); assert .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test50w.png 50w') + new RegExp('/assets/images/tests/test50w(-\\w+)?.png 50w') ); // webp @@ -60,19 +60,19 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test640w.webp 640w') + new RegExp('/assets/images/tests/test640w(-\\w+)?.webp 640w') ); assert .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test100w.webp 100w') + new RegExp('/assets/images/tests/test100w(-\\w+)?.webp 100w') ); assert .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test50w.webp 50w') + new RegExp('/assets/images/tests/test50w(-\\w+)?.webp 50w') ); // avif @@ -80,19 +80,19 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test640w.avif 640w') + new RegExp('/assets/images/tests/test640w(-\\w+)?.avif 640w') ); assert .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test100w.avif 100w') + new RegExp('/assets/images/tests/test100w(-\\w+)?.avif 100w') ); assert .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test50w.avif 50w') + new RegExp('/assets/images/tests/test50w(-\\w+)?.avif 50w') ); await render( @@ -103,13 +103,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small10w.png 10w') + new RegExp('/assets/images/tests/small10w(-\\w+)?.png 10w') ); assert .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small25w.png 25w') + new RegExp('/assets/images/tests/small25w(-\\w+)?.png 25w') ); // webp @@ -117,13 +117,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small10w.webp 10w') + new RegExp('/assets/images/tests/small10w(-\\w+)?.webp 10w') ); assert .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small25w.webp 25w') + new RegExp('/assets/images/tests/small25w(-\\w+)?.webp 25w') ); // avif @@ -131,13 +131,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small10w.avif 10w') + new RegExp('/assets/images/tests/small10w(-\\w+)?.avif 10w') ); assert .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small25w.avif 25w') + new RegExp('/assets/images/tests/small25w(-\\w+)?.avif 25w') ); await render( @@ -148,13 +148,17 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test100w.png 100w') + new RegExp( + '/assets/images/tests/recursive/dir/test100w(-\\w+)?.png 100w' + ) ); assert .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test50w.png 50w') + new RegExp( + '/assets/images/tests/recursive/dir/test50w(-\\w+)?.png 50w' + ) ); // webp @@ -162,13 +166,17 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test100w.webp 100w') + new RegExp( + '/assets/images/tests/recursive/dir/test100w(-\\w+)?.webp 100w' + ) ); assert .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test50w.webp 50w') + new RegExp( + '/assets/images/tests/recursive/dir/test50w(-\\w+)?.webp 50w' + ) ); // avif @@ -176,13 +184,17 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test100w.avif 100w') + new RegExp( + '/assets/images/tests/recursive/dir/test100w(-\\w+)?.avif 100w' + ) ); assert .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test50w.avif 50w') + new RegExp( + '/assets/images/tests/recursive/dir/test50w(-\\w+)?.avif 50w' + ) ); }); @@ -190,40 +202,60 @@ module('Integration: Responsive Image Component', function (hooks) { let service = this.owner.lookup('service:responsive-image'); service.set('physicalWidth', 45); await render(hbs``); - assert.dom('img').hasAttribute('src', '/assets/images/tests/test50w.png'); + assert + .dom('img') + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test50w(-\\w+)?.png') + ); service.set('physicalWidth', 51); await render(hbs``); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/test100w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test100w(-\\w+)?.png') + ); service.set('physicalWidth', 9); await render( hbs`` ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/small10w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/small10w(-\\w+)?.png') + ); service.set('physicalWidth', 11); await render( hbs`` ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/small25w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/small25w(-\\w+)?.png') + ); service.set('physicalWidth', 45); await render( hbs`` ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/recursive/dir/test50w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/recursive/dir/test50w(-\\w+)?.png') + ); service.set('physicalWidth', 51); await render( hbs`` ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/recursive/dir/test100w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/recursive/dir/test100w(-\\w+)?.png') + ); }); test('it renders a given size as sizes', async function (assert) { @@ -304,13 +336,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test100w.png 2x') + new RegExp('/assets/images/tests/test100w(-\\w+)?.png 2x') ); assert .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test50w.png 1x') + new RegExp('/assets/images/tests/test50w(-\\w+)?.png 1x') ); // webp @@ -318,13 +350,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test100w.webp 2x') + new RegExp('/assets/images/tests/test100w(-\\w+)?.webp 2x') ); assert .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test50w.webp 1x') + new RegExp('/assets/images/tests/test50w(-\\w+)?.webp 1x') ); // avif @@ -332,13 +364,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test100w.avif 2x') + new RegExp('/assets/images/tests/test100w(-\\w+)?.avif 2x') ); assert .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/test50w.avif 1x') + new RegExp('/assets/images/tests/test50w(-\\w+)?.avif 1x') ); await render( @@ -349,13 +381,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small10w.png 1x') + new RegExp('/assets/images/tests/small10w(-\\w+)?.png 1x') ); assert .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small25w.png 2x') + new RegExp('/assets/images/tests/small25w(-\\w+)?.png 2x') ); // webp @@ -363,13 +395,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small10w.webp 1x') + new RegExp('/assets/images/tests/small10w(-\\w+)?.webp 1x') ); assert .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small25w.webp 2x') + new RegExp('/assets/images/tests/small25w(-\\w+)?.webp 2x') ); // avif @@ -377,13 +409,13 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small10w.avif 1x') + new RegExp('/assets/images/tests/small10w(-\\w+)?.avif 1x') ); assert .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/small25w.avif 2x') + new RegExp('/assets/images/tests/small25w(-\\w+)?.avif 2x') ); await render( @@ -394,13 +426,17 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test100w.png 100w') + new RegExp( + '/assets/images/tests/recursive/dir/test100w(-\\w+)?.png 100w' + ) ); assert .dom('picture source[type="image/png"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test50w.png 50w') + new RegExp( + '/assets/images/tests/recursive/dir/test50w(-\\w+)?.png 50w' + ) ); // webp @@ -408,13 +444,17 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test100w.webp 100w') + new RegExp( + '/assets/images/tests/recursive/dir/test100w(-\\w+)?.webp 100w' + ) ); assert .dom('picture source[type="image/webp"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test50w.webp 50w') + new RegExp( + '/assets/images/tests/recursive/dir/test50w(-\\w+)?.webp 50w' + ) ); // avif @@ -422,13 +462,17 @@ module('Integration: Responsive Image Component', function (hooks) { .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test100w.avif 100w') + new RegExp( + '/assets/images/tests/recursive/dir/test100w(-\\w+)?.avif 100w' + ) ); assert .dom('picture source[type="image/avif"]') .hasAttribute( 'srcset', - new RegExp('/assets/images/tests/recursive/dir/test50w.avif 50w') + new RegExp( + '/assets/images/tests/recursive/dir/test50w(-\\w+)?.avif 50w' + ) ); }); @@ -438,33 +482,50 @@ module('Integration: Responsive Image Component', function (hooks) { ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/test640w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test640w(-\\w+)?.png') + ); await render( hbs`` ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/test640w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test640w(-\\w+)?.png') + ); await render( hbs`` ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/test100w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test100w(-\\w+)?.png') + ); await render( hbs`` ); assert .dom('img') - .hasAttribute('src', '/assets/images/tests/test100w.png'); + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test100w(-\\w+)?.png') + ); await render( hbs`` ); - assert.dom('img').hasAttribute('src', '/assets/images/tests/test50w.png'); + assert + .dom('img') + .hasAttribute( + 'src', + new RegExp('/assets/images/tests/test50w(-\\w+)?.png') + ); }); }); diff --git a/tests/integration/helpers/responsive-image-resolve-test.js b/tests/integration/helpers/responsive-image-resolve-test.js index b66fcf4b5..ecdea9c45 100644 --- a/tests/integration/helpers/responsive-image-resolve-test.js +++ b/tests/integration/helpers/responsive-image-resolve-test.js @@ -10,7 +10,9 @@ module('Helper: ResponsiveImageResolve', function (hooks) { await render( hbs`

{{responsive-image-resolve "assets/images/tests/test.png"}}

` ); - assert.dom('h1').hasText('/assets/images/tests/test640w.png'); + assert + .dom('h1') + .hasText(new RegExp('/assets/images/tests/test640w(-\\w+)?.png')); }); test('supports size', async function (assert) { @@ -20,7 +22,9 @@ module('Helper: ResponsiveImageResolve', function (hooks) { hbs`

{{responsive-image-resolve "assets/images/tests/test.png" size=45}}

` ); - assert.dom('h1').hasText('/assets/images/tests/test50w.png'); + assert + .dom('h1') + .hasText(new RegExp('/assets/images/tests/test50w(-\\w+)?.png')); }); test('supports format', async function (assert) { @@ -28,6 +32,8 @@ module('Helper: ResponsiveImageResolve', function (hooks) { hbs`

{{responsive-image-resolve "assets/images/tests/test.png" format="webp"}}

` ); - assert.dom('h1').hasText('/assets/images/tests/test640w.webp'); + assert + .dom('h1') + .hasText(new RegExp('/assets/images/tests/test640w(-\\w+)?.webp')); }); }); diff --git a/tests/unit/services/responsive-image-test.ts b/tests/unit/services/responsive-image-test.ts index f1f836a05..24d027ded 100644 --- a/tests/unit/services/responsive-image-test.ts +++ b/tests/unit/services/responsive-image-test.ts @@ -5,95 +5,145 @@ import { Meta, } from 'ember-responsive-image/services/responsive-image'; -const meta: Record = { - 'test.png': { - widths: [50, 100], - formats: ['png', 'webp'], - aspectRatio: 1, - }, -}; - -const imageMetas: ImageMeta[] = [ - { - image: '/test50w.png', - width: 50, - height: 50, - type: 'png', - }, - { - image: '/test50w.webp', - width: 50, - height: 50, - type: 'webp', - }, +interface TestCase { + moduleTitle: string; + meta: Record; + imageMetas: ImageMeta[]; +} +const testCases: TestCase[] = [ { - image: '/test100w.png', - width: 100, - height: 100, - type: 'png', + moduleTitle: 'without fingerprinting', + meta: { + 'test.png': { + widths: [50, 100], + formats: ['png', 'webp'], + aspectRatio: 1, + }, + }, + imageMetas: [ + { + image: '/test50w.png', + width: 50, + height: 50, + type: 'png', + }, + { + image: '/test50w.webp', + width: 50, + height: 50, + type: 'webp', + }, + { + image: '/test100w.png', + width: 100, + height: 100, + type: 'png', + }, + { + image: '/test100w.webp', + width: 100, + height: 100, + type: 'webp', + }, + ], }, { - image: '/test100w.webp', - width: 100, - height: 100, - type: 'webp', + moduleTitle: 'with fingerprinting', + meta: { + 'test.png': { + widths: [50, 100], + formats: ['png', 'webp'], + aspectRatio: 1, + fingerprint: '1234567890', + }, + }, + imageMetas: [ + { + image: '/test50w-1234567890.png', + width: 50, + height: 50, + type: 'png', + }, + { + image: '/test50w-1234567890.webp', + width: 50, + height: 50, + type: 'webp', + }, + { + image: '/test100w-1234567890.png', + width: 100, + height: 100, + type: 'png', + }, + { + image: '/test100w-1234567890.webp', + width: 100, + height: 100, + type: 'webp', + }, + ], }, ]; module('ResponsiveImageService', function (hooks) { setupTest(hooks); - hooks.beforeEach(function () { - const service = this.owner.lookup('service:responsive-image'); - service.set('meta', meta); - }); + testCases.forEach(({ moduleTitle, meta, imageMetas }) => { + module(moduleTitle, function (hooks) { + hooks.beforeEach(function () { + const service = this.owner.lookup('service:responsive-image'); + service.set('meta', meta); + }); - test('retrieve generated images by name', function (assert) { - const service = this.owner.lookup('service:responsive-image'); - const images = service.getImages('test.png'); - assert.deepEqual(images, imageMetas); - }); + test('retrieve generated images by name', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + const images = service.getImages('test.png'); + assert.deepEqual(images, imageMetas); + }); - test('handle absolute paths', function (assert) { - const service = this.owner.lookup('service:responsive-image'); - const images = service.getImages('/test.png'); - assert.deepEqual(images, imageMetas); - }); + test('handle absolute paths', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + const images = service.getImages('/test.png'); + assert.deepEqual(images, imageMetas); + }); - test('retrieve generated images by name and type', function (assert) { - const service = this.owner.lookup('service:responsive-image'); - let images = service.getImages('test.png', 'png'); - assert.deepEqual(images, [imageMetas[0], imageMetas[2]]); + test('retrieve generated images by name and type', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + let images = service.getImages('test.png', 'png'); + assert.deepEqual(images, [imageMetas[0], imageMetas[2]]); - images = service.getImages('test.png', 'webp'); - assert.deepEqual(images, [imageMetas[1], imageMetas[3]]); - }); + images = service.getImages('test.png', 'webp'); + assert.deepEqual(images, [imageMetas[1], imageMetas[3]]); + }); - test('get available types', function (assert) { - const service = this.owner.lookup('service:responsive-image'); - const types = service.getAvailableTypes('test.png'); - assert.deepEqual(types, ['png', 'webp']); - }); + test('get available types', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + const types = service.getAvailableTypes('test.png'); + assert.deepEqual(types, ['png', 'webp']); + }); - test('retrieve generated image data by size', function (assert) { - const service = this.owner.lookup('service:responsive-image'); - service.physicalWidth = 100; - let images = service.getImageMetaBySize('test.png', 120); - assert.deepEqual(images, imageMetas[2]); - images = service.getImageMetaBySize('test.png', 60); - assert.deepEqual(images, imageMetas[2]); - images = service.getImageMetaBySize('test.png', 45); - assert.deepEqual(images, imageMetas[0]); - }); + test('retrieve generated image data by size', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + service.physicalWidth = 100; + let images = service.getImageMetaBySize('test.png', 120); + assert.deepEqual(images, imageMetas[2]); + images = service.getImageMetaBySize('test.png', 60); + assert.deepEqual(images, imageMetas[2]); + images = service.getImageMetaBySize('test.png', 45); + assert.deepEqual(images, imageMetas[0]); + }); - test('retrieve generated image data by size and type', function (assert) { - const service = this.owner.lookup('service:responsive-image'); - service.physicalWidth = 100; - let images = service.getImageMetaBySize('test.png', 120, 'webp'); - assert.deepEqual(images, imageMetas[3]); - images = service.getImageMetaBySize('test.png', 60, 'webp'); - assert.deepEqual(images, imageMetas[3]); - images = service.getImageMetaBySize('test.png', 45, 'webp'); - assert.deepEqual(images, imageMetas[1]); + test('retrieve generated image data by size and type', function (assert) { + const service = this.owner.lookup('service:responsive-image'); + service.physicalWidth = 100; + let images = service.getImageMetaBySize('test.png', 120, 'webp'); + assert.deepEqual(images, imageMetas[3]); + images = service.getImageMetaBySize('test.png', 60, 'webp'); + assert.deepEqual(images, imageMetas[3]); + images = service.getImageMetaBySize('test.png', 45, 'webp'); + assert.deepEqual(images, imageMetas[1]); + }); + }); }); }); From df51d1f94842dfa814495da277a6d260dcddb908 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Fri, 5 Mar 2021 15:35:55 +0100 Subject: [PATCH 3/4] Update Readme for fingerprinting --- README.md | 65 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ae51e70ba..fa819eee3 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,14 @@ Add a basic configuration to your `ember-cli-build.js`, to point the addon to wh ```js module.exports = function (defaults) { let app = new EmberAddon(defaults, { - 'responsive-image': [ - { - include: 'assets/images/**/*', - widths: [2048, 1536, 1080, 750, 640], - } - ] + 'responsive-image': { + images: [ + { + include: 'assets/images/**/*', + widths: [2048, 1536, 1080, 750, 640], + } + ], + } }); }; ``` @@ -197,40 +199,49 @@ is less suited if you have just a few images, but shines if you need placeholder ## Configuration -Configuration of the addon is done in your `ember-cli-build.js`. It expects an array of configuration items, with a number -of different available options: +Configuration of the addon is done in your `ember-cli-build.js`: ```js let app = new EmberAddon(defaults, { - 'responsive-image': [ - { - include: ['path/to/images/**/*'], - exclude: ['path/to/images/but-not-this/**/*'], - widths: [2048, 1536, 1080, 750, 640], - formats: ['avif', 'webp'], - quality: 50, - lqip: { - type: 'inline', - targetPixels: 60, + 'responsive-image': { + fingerprint: true, + images: [ + { + include: ['path/to/images/**/*'], + exclude: ['path/to/images/but-not-this/**/*'], + widths: [2048, 1536, 1080, 750, 640], + formats: ['avif', 'webp'], + quality: 50, + lqip: { + type: 'inline', + targetPixels: 60, + }, + removeSource: true, + justCopy: false, }, - removeSource: true, - justCopy: false, - }, - // possible more items - ] + // possible more items + ], + } }); ``` -You must define at least one configuration item, with at least `include` defined. But you can provide more, to create separate -configurations for different images. +### Options + +* **fingerprint:** Can be used to enable/disable fingerprinting of the generated image files. In most cases you can omit +setting this explicitly, as it will follow whatever you have set under the main `fingerprint` options (used by the `broccoli-asset-rev` addon), +with the default being to enable fingerprinting only in production builds. +* **images**: The main configuration how the addon generated images happens here, see the following section for details. + +### Image Options + +The main configuration happens with the `images` array. There you must define at least one configuration item, with at least `include` defined. +But you can provide more, to create separate configurations for different images. For example if you have a gallery of logos, of which all will be displayed with a width of max. 300px or less,it makes no sense to create very large images for these, so a setting of `widths: [300, 600],` would make sense here (600px for the `2x` version aka "retina"). > Make sure you don't have multiple `include` definitions accidentally overlapping! You can use `exclude` in this case to prevent this. -### Options - * **include:** Glob pattern for which images should be processed based on this configuration. * **exclude:** Optional pattern which images to exclude, takes precedence over `include`. * **widths:** These are the widths of the resized images. From 5be3b14e1e75bb559b7432070f85879224789781 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Fri, 5 Mar 2021 16:04:24 +0100 Subject: [PATCH 4/4] Fix test --- ember-cli-build.js | 5 +++++ lib/image-writer.js | 3 +++ node-tests/generate.test.js | 9 ++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ember-cli-build.js b/ember-cli-build.js index 6191607ea..72b8245fa 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -11,6 +11,11 @@ module.exports = function (defaults) { exclude: ['testem.js'], }, 'responsive-image': { + fingerprint: + // used only for testing here, to override the md5 fingerprint with a constant (for deterministic testing) + process.env.ERI_FINGERPRINT !== undefined + ? process.env.ERI_FINGERPRINT + : true, images: [ { include: 'assets/images/tests/**/*', diff --git a/lib/image-writer.js b/lib/image-writer.js index 335f8c4eb..cf7224c32 100644 --- a/lib/image-writer.js +++ b/lib/image-writer.js @@ -303,6 +303,9 @@ class ImageResizer extends CachingWriter { } async generateFingerprint(sharpImage) { + if (typeof this.image_options.fingerprint === 'string') { + return this.image_options.fingerprint; + } const md5 = crypto.createHash('md5'); md5.update(await sharpImage.toBuffer()); md5.update(JSON.stringify(this.image_options)); diff --git a/node-tests/generate.test.js b/node-tests/generate.test.js index 925f95bbd..1f3205e51 100644 --- a/node-tests/generate.test.js +++ b/node-tests/generate.test.js @@ -5,7 +5,6 @@ const fs = require('fs'); const path = require('path'); const hash = '00e24234f1b58e32b935b1041432916f'; -// compare with tests/dummy/config/environment.js const images = [ { file: 'assets/images/tests/image.jpg', @@ -87,7 +86,11 @@ const appDir = './dist'; beforeAll(function () { jest.setTimeout(300000); - return execa('./node_modules/ember-cli/bin/ember', ['build']); + return execa('./node_modules/ember-cli/bin/ember', ['build'], { + env: { + ERI_FINGERPRINT: hash, + }, + }); }); images.forEach((img) => { @@ -98,7 +101,7 @@ images.forEach((img) => { const imageData = fs.readFileSync( path.join(appDir, `${filename}${width}w-${hash}.${ext}`) ); - const originalSource = path.join(appDir, `${filename}-${hash}.${ext}`); + const originalSource = path.join(appDir, `${filename}.${ext}`); const meta = await sharp(imageData).metadata(); expect(meta).toBeDefined();