Skip to content

Commit

Permalink
feat: add unit for percentage resize with sharp
Browse files Browse the repository at this point in the history
  • Loading branch information
OlenDavis authored Jul 26, 2024
1 parent bf1853c commit a83f491
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 5 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@ The plugin supports the following query parameters:
- `height`/`h` - allows you to set the image height
- `as` - to specify the [preset](#preset) option

**Only supported for `sharp` currently:**

- `unit`/`u` - can be `px` or `percent` and allows you to resize by a percentage of the image's size.

Examples:

```js
Expand All @@ -483,6 +487,8 @@ const myImage3 = new URL("image.png?w=150", import.meta.url);
const myImage4 = new URL("image.png?as=webp&w=150&h=120", import.meta.url);
// You can use `auto` to reset `width` or `height` from the `preset` option
const myImage5 = new URL("image.png?as=webp&w=150&h=auto", import.meta.url);
// You can use `unit` to get the non-retina resize of images that are retina sized
const myImage1 = new URL("image.png?width=50&unit=percent", import.meta.url);
```
```css
Expand Down Expand Up @@ -1494,6 +1500,43 @@ module.exports = {
You can find more information [here](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh).
For only `sharp` currently, you can even generate the non-retina resizes of images:
**webpack.config.js**
```js
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

module.exports = {
optimization: {
minimizer: [
"...",
new ImageMinimizerPlugin({
generator: [
{
// You can apply generator using `?as=webp-1x`, you can use any name and provide more options
preset: "webp-1x",
implementation: ImageMinimizerPlugin.sharpGenerate,
options: {
resize: {
enabled: true,
width: 50,
unit: "percent",
},
encodeOptions: {
webp: {
quality: 90,
},
},
},
},
],
}),
],
},
};
```
#### Generator example for user defined implementation
You can use your own generator implementation.
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const {
* @typedef {Object} ResizeOptions
* @property {number} [width]
* @property {number} [height]
* @property {"px" | "percent"} [unit]
* @property {boolean} [enabled]
*/

Expand Down
18 changes: 15 additions & 3 deletions src/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ function changeResource(loaderContext, isAbsolute, output, query) {
* @param {Minimizer<T>[]} transformers
* @param {string | null} widthQuery
* @param {string | null} heightQuery
* @param {string | null} unitQuery
* @return {Minimizer<T>[]}
*/
function processSizeQuery(transformers, widthQuery, heightQuery) {
function processSizeQuery(transformers, widthQuery, heightQuery, unitQuery) {
return transformers.map((transformer) => {
const minimizer = { ...transformer };

Expand Down Expand Up @@ -80,6 +81,10 @@ function processSizeQuery(transformers, widthQuery, heightQuery) {
}
}

if (unitQuery === "px" || unitQuery === "percent") {
minimizerOptions.resize.unit = unitQuery;
}

return minimizer;
});
}
Expand Down Expand Up @@ -170,15 +175,22 @@ async function loader(content) {
if (parsedQuery) {
const widthQuery = parsedQuery.get("width") ?? parsedQuery.get("w");
const heightQuery = parsedQuery.get("height") ?? parsedQuery.get("h");
const unitQuery = parsedQuery.get("unit") ?? parsedQuery.get("u");

if (widthQuery || heightQuery) {
if (widthQuery || heightQuery || unitQuery) {
if (Array.isArray(transformer)) {
transformer = processSizeQuery(transformer, widthQuery, heightQuery);
transformer = processSizeQuery(
transformer,
widthQuery,
heightQuery,
unitQuery,
);
} else {
[transformer] = processSizeQuery(
[transformer],
widthQuery,
heightQuery,
unitQuery,
);
}
}
Expand Down
30 changes: 28 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ squooshMinify.teardown = squooshImagePoolTeardown;

/** @typedef {import("sharp")} SharpLib */
/** @typedef {import("sharp").Sharp} Sharp */
/** @typedef {import("sharp").ResizeOptions & { enabled?: boolean }} ResizeOptions */
/** @typedef {import("sharp").ResizeOptions & { enabled?: boolean; unit?: "px" | "percent" }} ResizeOptions */

/**
* @typedef SharpEncodeOptions
Expand Down Expand Up @@ -1095,12 +1095,38 @@ async function sharpTransform(
// ====== resize ======

if (minimizerOptions.resize) {
const { enabled = true, ...params } = minimizerOptions.resize;
const { enabled = true, unit = "px", ...params } = minimizerOptions.resize;

if (
enabled &&
(typeof params.width === "number" || typeof params.height === "number")
) {
if (unit === "percent") {
const originalMetadata = await sharp(original.data).metadata();

if (
typeof params.width === "number" &&
originalMetadata.width &&
Number.isFinite(originalMetadata.width) &&
originalMetadata.width > 0
) {
params.width = Math.ceil(
(originalMetadata.width * params.width) / 100,
);
}

if (
typeof params.height === "number" &&
originalMetadata.height &&
Number.isFinite(originalMetadata.height) &&
originalMetadata.height > 0
) {
params.height = Math.ceil(
(originalMetadata.height * params.height) / 100,
);
}
}

imagePipeline.resize(params);
}
}
Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/generator-and-minimizer-percent-resize-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require("./loader-test.png?width=100&unit=percent");
require("./loader-test.png?w=150&u=percent");
require("./loader-test.png?height=200&unit=percent");
require("./loader-test.png?h=250&u=percent");
require("./loader-test.png?width=300&height=auto&unit=percent");
require("./loader-test.png?width=auto&height=320&unit=percent");
require("./loader-test.png?width=350&height=350&unit=percent");

require("./loader-test.png?as=webp&width=100&unit=percent");
require("./loader-test.png?as=webp&w=150&u=percent");
require("./loader-test.png?as=webp&height=200&unit=percent");
require("./loader-test.png?as=webp&h=250&u=percent");
require("./loader-test.png?as=webp&width=300&height=auto&unit=percent");
require("./loader-test.png?as=webp&width=auto&height=320&unit=percent");
require("./loader-test.png?as=webp&width=350&height=350&unit=percent");
158 changes: 158 additions & 0 deletions test/resize-query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,164 @@ describe("resize query (sharp)", () => {
expect(errors).toHaveLength(0);
}
});

it("should generate and resize with percent unit resize options", async () => {
const stats = await runWebpack({
entry: path.join(
fixturesPath,
"./generator-and-minimizer-resize-query.js",
),
imageminLoaderOptions: {
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
filename: "[name]-[width]x[height][ext]",
options: {
resize: {
width: 400,
height: 400,
unit: "percent",
},
encodeOptions: {
png: {},
},
},
},
generator: [
{
preset: "webp",
implementation: ImageMinimizerPlugin.sharpGenerate,
filename: "[name]-[width]x[height][ext]",
options: {
resize: {
width: 400,
height: 400,
unit: "percent",
},
encodeOptions: {
webp: {},
},
},
},
],
},
});

const { compilation } = stats;
const { warnings, errors } = compilation;
const sizeOf = promisify(imageSize);

const assetsList = [
// asset path, width, height, mime regExp
["./loader-test-500x2000.png", 500, 2000, /image\/png/i],
["./loader-test-750x2000.png", 750, 2000, /image\/png/i],
["./loader-test-2000x1000.png", 2000, 1000, /image\/png/i],
["./loader-test-2000x1250.png", 2000, 1250, /image\/png/i],
["./loader-test-1500x1500.png", 1500, 1500, /image\/png/i],
["./loader-test-1600x1600.png", 1600, 1600, /image\/png/i],
["./loader-test-1750x1750.png", 1750, 1750, /image\/png/i],

["./loader-test-500x2000.webp", 500, 2000, /image\/webp/i],
["./loader-test-750x2000.webp", 750, 2000, /image\/webp/i],
["./loader-test-2000x1000.webp", 2000, 1000, /image\/webp/i],
["./loader-test-2000x1250.webp", 2000, 1250, /image\/webp/i],
["./loader-test-1500x1500.webp", 1500, 1500, /image\/webp/i],
["./loader-test-1600x1600.webp", 1600, 1600, /image\/webp/i],
["./loader-test-1750x1750.webp", 1750, 1750, /image\/webp/i],
];

for (const [assetPath, width, height, mimeRegExp] of assetsList) {
const transformedAsset = path.resolve(
__dirname,
compilation.options.output.path,
assetPath,
);

// eslint-disable-next-line no-await-in-loop
const transformedExt = await fileType.fromFile(transformedAsset);
// eslint-disable-next-line no-await-in-loop
const dimensions = await sizeOf(transformedAsset);

expect(dimensions.width).toBe(width);
expect(dimensions.height).toBe(height);
expect(mimeRegExp.test(transformedExt.mime)).toBe(true);
expect(warnings).toHaveLength(0);
expect(errors).toHaveLength(0);
}
});

it("should generate and resize with percent unit query without resize options", async () => {
const stats = await runWebpack({
entry: path.join(
fixturesPath,
"./generator-and-minimizer-percent-resize-query.js",
),
imageminLoaderOptions: {
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
filename: "[name]-[width]x[height][ext]",
options: {
encodeOptions: {
png: {},
},
},
},
generator: [
{
preset: "webp",
implementation: ImageMinimizerPlugin.sharpGenerate,
filename: "[name]-[width]x[height][ext]",
options: {
encodeOptions: {
webp: {},
},
},
},
],
},
});

const { compilation } = stats;
const { warnings, errors } = compilation;
const sizeOf = promisify(imageSize);

const assetsList = [
// asset path, width, height, mime regExp
["./loader-test-500x500.png", 500, 500, /image\/png/i],
["./loader-test-750x750.png", 750, 750, /image\/png/i],
["./loader-test-1000x1000.png", 1000, 1000, /image\/png/i],
["./loader-test-1250x1250.png", 1250, 1250, /image\/png/i],
["./loader-test-1500x1500.png", 1500, 1500, /image\/png/i],
["./loader-test-1600x1600.png", 1600, 1600, /image\/png/i],
["./loader-test-1750x1750.png", 1750, 1750, /image\/png/i],

["./loader-test-500x500.webp", 500, 500, /image\/webp/i],
["./loader-test-750x750.webp", 750, 750, /image\/webp/i],
["./loader-test-1000x1000.webp", 1000, 1000, /image\/webp/i],
["./loader-test-1250x1250.webp", 1250, 1250, /image\/webp/i],
["./loader-test-1500x1500.webp", 1500, 1500, /image\/webp/i],
["./loader-test-1600x1600.webp", 1600, 1600, /image\/webp/i],
["./loader-test-1750x1750.webp", 1750, 1750, /image\/webp/i],
];

for (const [assetPath, width, height, mimeRegExp] of assetsList) {
const transformedAsset = path.resolve(
__dirname,
compilation.options.output.path,
assetPath,
);

// eslint-disable-next-line no-await-in-loop
const transformedExt = await fileType.fromFile(transformedAsset);
// eslint-disable-next-line no-await-in-loop
const dimensions = await sizeOf(transformedAsset);

expect(dimensions.width).toBe(width);
expect(dimensions.height).toBe(height);
expect(mimeRegExp.test(transformedExt.mime)).toBe(true);
expect(warnings).toHaveLength(0);
expect(errors).toHaveLength(0);
}
});
});

describe("resize query (squoosh)", () => {
Expand Down
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type BasicTransformerOptions<T> = InferDefaultType<T> | undefined;
type ResizeOptions = {
width?: number | undefined;
height?: number | undefined;
unit?: "px" | "percent" | undefined;
enabled?: boolean | undefined;
};
type BasicTransformerImplementation<T> = (
Expand Down
1 change: 1 addition & 0 deletions types/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type SharpLib = typeof import("sharp");
export type Sharp = import("sharp").Sharp;
export type ResizeOptions = import("sharp").ResizeOptions & {
enabled?: boolean;
unit?: "px" | "percent";
};
export type SharpEncodeOptions = {
avif?: import("sharp").AvifOptions | undefined;
Expand Down

0 comments on commit a83f491

Please sign in to comment.