Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor internal meta format, add own fingerprinting implementation #180

Merged
merged 4 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}
],
}
});
};
```
Expand Down Expand Up @@ -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.
Expand Down
77 changes: 49 additions & 28 deletions addon/services/responsive-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ export interface ImageMeta {
}

export interface Meta {
images: ImageMeta[];
widths: number[];
formats: ImageType[];
aspectRatio: number;
fingerprint?: string;
lqip?: LqipInline | LqipColor | LqipBlurhash;
}

const imageExtensions: Map<ImageType, string> = new Map([['jpeg', 'jpg']]);

/**
* Service class to provides images generated by the responsive images package
*/
Expand All @@ -61,18 +66,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)
Expand All @@ -81,19 +93,18 @@ 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);
return extentionTypeMapping.get(extension) ?? (extension as ImageType);
}

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

/**
Expand All @@ -115,26 +126,36 @@ 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('.'));
const fingerprint = this.getMeta(image).fingerprint;
return `/${base}${width}w${fingerprint ? '-' + fingerprint : ''}.${ext}`;
}

private getDestinationWidthBySize(size: number): number {
Expand Down
154 changes: 80 additions & 74 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,88 +9,94 @@ module.exports = function (defaults) {
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': {
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/**/*',
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,
},
],
},
});

/*
Expand Down
Loading