├── example
├── src
│ ├── api
│ │ └── .gitkeep
│ ├── extensions
│ │ ├── .gitkeep
│ │ └── upload
│ │ │ └── strapi-server.ts
│ ├── admin
│ │ ├── tsconfig.json
│ │ ├── webpack.config.example.js
│ │ └── app.example.tsx
│ └── index.ts
├── public
│ ├── uploads
│ │ └── .gitkeep
│ └── robots.txt
├── database
│ └── migrations
│ │ └── .gitkeep
├── favicon.png
├── config
│ ├── api.ts
│ ├── admin.ts
│ ├── middlewares.ts
│ ├── server.ts
│ ├── plugins.ts
│ └── database.ts
├── .env.example
├── .editorconfig
├── tsconfig.json
├── package.json
├── .gitignore
└── README.md
├── .npmignore
├── strapi-server.js
├── .gitignore
├── .prettierrc
├── assets
├── logo.png
└── logo.afdesign
├── server
├── utils
│ └── pluginId.ts
├── models
│ ├── index.ts
│ ├── strapi-helper.ts
│ ├── errors.ts
│ ├── file.ts
│ └── config.ts
├── index.ts
├── services
│ ├── index.ts
│ ├── settings-service.ts
│ └── image-optimizer-service.ts
└── config
│ ├── index.ts
│ └── schema.ts
├── .github
├── dependabot.yml
└── workflows
│ └── deploy.yml
├── tsconfig.json
├── LICENSE
├── package.json
├── .all-contributorsrc
└── README.md
/example/src/api/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/public/uploads/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/src/extensions/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/database/migrations/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .prettierrc
2 | /.github
3 | /assets
4 | /example
5 | /server
--------------------------------------------------------------------------------
/strapi-server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = require("./dist/server");
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .idea/
3 | server/.DS_Store
4 | .DS_Store
5 | /dist
6 | yarn.lock
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "singleQuote": false
5 | }
6 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marlokessler/strapi-plugin-image-optimizer/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/logo.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marlokessler/strapi-plugin-image-optimizer/HEAD/assets/logo.afdesign
--------------------------------------------------------------------------------
/example/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marlokessler/strapi-plugin-image-optimizer/HEAD/example/favicon.png
--------------------------------------------------------------------------------
/server/utils/pluginId.ts:
--------------------------------------------------------------------------------
1 | import packageJson from "../../package.json";
2 |
3 | export default packageJson.strapi.name;
4 |
--------------------------------------------------------------------------------
/example/config/api.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | rest: {
3 | defaultLimit: 25,
4 | maxLimit: 100,
5 | withCount: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/server/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./config";
2 | export * from "./errors";
3 | export * from "./file";
4 | export * from "./strapi-helper";
5 |
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | import config from "./config";
2 | import services from "./services";
3 |
4 | export default {
5 | config,
6 | services,
7 | };
8 |
--------------------------------------------------------------------------------
/server/models/strapi-helper.ts:
--------------------------------------------------------------------------------
1 | import { File } from ".";
2 |
3 | export interface StrapiImageFormat {
4 | key: string;
5 | file: File;
6 | }
7 |
--------------------------------------------------------------------------------
/example/public/robots.txt:
--------------------------------------------------------------------------------
1 | # To prevent search engines from seeing the site altogether, uncomment the next two lines:
2 | # User-Agent: *
3 | # Disallow: /
4 |
--------------------------------------------------------------------------------
/example/.env.example:
--------------------------------------------------------------------------------
1 | HOST=0.0.0.0
2 | PORT=1337
3 | APP_KEYS="toBeModified1,toBeModified2"
4 | API_TOKEN_SALT=tobemodified
5 | ADMIN_JWT_SECRET=tobemodified
6 | JWT_SECRET=tobemodified
7 |
--------------------------------------------------------------------------------
/server/models/errors.ts:
--------------------------------------------------------------------------------
1 | export class InvalidParametersError extends Error {
2 | constructor(message: string) {
3 | super(message);
4 | this.name = "InvalidParametersError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/server/services/index.ts:
--------------------------------------------------------------------------------
1 | import settingsService from "./settings-service";
2 | import imageOptimizerService from "./image-optimizer-service";
3 |
4 | export default {
5 | settingsService,
6 | imageOptimizerService,
7 | };
8 |
--------------------------------------------------------------------------------
/server/config/index.ts:
--------------------------------------------------------------------------------
1 | import { Config } from "../models";
2 | import configSchema from "./schema";
3 |
4 | export default {
5 | default: {},
6 | async validator(config: Config) {
7 | await configSchema.validate(config);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/example/config/admin.ts:
--------------------------------------------------------------------------------
1 | export default ({ env }) => ({
2 | auth: {
3 | secret: env('ADMIN_JWT_SECRET'),
4 | },
5 | apiToken: {
6 | salt: env('API_TOKEN_SALT'),
7 | },
8 | transfer: {
9 | token: {
10 | salt: env('TRANSFER_TOKEN_SALT'),
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/example/config/middlewares.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | 'strapi::errors',
3 | 'strapi::security',
4 | 'strapi::cors',
5 | 'strapi::poweredBy',
6 | 'strapi::logger',
7 | 'strapi::query',
8 | 'strapi::body',
9 | 'strapi::session',
10 | 'strapi::favicon',
11 | 'strapi::public',
12 | ];
13 |
--------------------------------------------------------------------------------
/example/config/server.ts:
--------------------------------------------------------------------------------
1 | export default ({ env }) => ({
2 | host: env('HOST', '0.0.0.0'),
3 | port: env.int('PORT', 1337),
4 | app: {
5 | keys: env.array('APP_KEYS'),
6 | },
7 | webhooks: {
8 | populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/example/src/admin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@strapi/typescript-utils/tsconfigs/admin",
3 | "include": [
4 | "../plugins/**/admin/src/**/*",
5 | "./"
6 | ],
7 | "exclude": [
8 | "node_modules/",
9 | "build/",
10 | "dist/",
11 | "**/*.test.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/example/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [{package.json,*.yml}]
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/server/services/settings-service.ts:
--------------------------------------------------------------------------------
1 | import {} from "@strapi/strapi";
2 | import { Config } from "../models";
3 | import pluginId from "../utils/pluginId";
4 |
5 | class SettingsService {
6 | static get settings(): Config {
7 | return strapi.config.get(`plugin.${pluginId}`);
8 | }
9 | }
10 |
11 | export default SettingsService;
12 |
--------------------------------------------------------------------------------
/example/src/admin/webpack.config.example.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /* eslint-disable no-unused-vars */
4 | module.exports = (config, webpack) => {
5 | // Note: we provide webpack above so you should not `require` it
6 | // Perform customizations to webpack config
7 | // Important: return the modified config
8 | return config;
9 | };
10 |
--------------------------------------------------------------------------------
/example/src/extensions/upload/strapi-server.ts:
--------------------------------------------------------------------------------
1 | import imageOptimizerService from "strapi-plugin-image-optimizer/dist/server/services/image-optimizer-service";
2 | import { LoadedPlugin } from "@strapi/types/dist/types/core/plugins";
3 |
4 | module.exports = (plugin: LoadedPlugin) => {
5 | plugin.services["image-manipulation"] = imageOptimizerService;
6 | return plugin;
7 | };
8 |
--------------------------------------------------------------------------------
/server/models/file.ts:
--------------------------------------------------------------------------------
1 | import { ReadStream } from "fs";
2 |
3 | export interface File {
4 | name: string;
5 | hash: string;
6 | mime: string;
7 | path?: string;
8 | ext: string;
9 | width?: number;
10 | height?: number;
11 | size?: number;
12 | getStream: () => ReadStream;
13 | }
14 |
15 | export interface SourceFile extends File {
16 | tmpWorkingDirectory: string;
17 | }
18 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@strapi/typescript-utils/tsconfigs/server",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "."
6 | },
7 | "include": [
8 | "./",
9 | "./**/*.ts",
10 | "./**/*.js",
11 | "src/**/*.json"
12 | ],
13 | "exclude": [
14 | "node_modules/",
15 | "build/",
16 | "dist/",
17 | ".cache/",
18 | ".tmp/",
19 | "src/admin/",
20 | "**/*.test.*",
21 | "src/plugins/**"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/example/src/index.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * An asynchronous register function that runs before
4 | * your application is initialized.
5 | *
6 | * This gives you an opportunity to extend code.
7 | */
8 | register(/*{ strapi }*/) {},
9 |
10 | /**
11 | * An asynchronous bootstrap function that runs before
12 | * your application gets started.
13 | *
14 | * This gives you an opportunity to set up your data model,
15 | * run jobs, or perform some special logic.
16 | */
17 | bootstrap(/*{ strapi }*/) {},
18 | };
19 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@strapi/typescript-utils/tsconfigs/server",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": ".",
6 | "declaration": true
7 | },
8 | "include": [
9 | // Include the root directory
10 | "server",
11 | // Force the JSON files in the src folder to be included
12 | "server/**/*.json"
13 | ],
14 | "exclude": [
15 | "node_modules/",
16 | "dist/",
17 | // Do not include admin files in the server compilation
18 | "admin/",
19 | // Do not include test files
20 | "**/*.test.ts"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/example/src/admin/app.example.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | config: {
3 | locales: [
4 | // 'ar',
5 | // 'fr',
6 | // 'cs',
7 | // 'de',
8 | // 'dk',
9 | // 'es',
10 | // 'he',
11 | // 'id',
12 | // 'it',
13 | // 'ja',
14 | // 'ko',
15 | // 'ms',
16 | // 'nl',
17 | // 'no',
18 | // 'pl',
19 | // 'pt-BR',
20 | // 'pt',
21 | // 'ru',
22 | // 'sk',
23 | // 'sv',
24 | // 'th',
25 | // 'tr',
26 | // 'uk',
27 | // 'vi',
28 | // 'zh-Hans',
29 | // 'zh',
30 | ],
31 | },
32 | bootstrap(app) {
33 | console.log(app);
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | release:
5 | types: [published]
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: write
10 | packages: write
11 |
12 | jobs:
13 | deploy:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout project
17 | uses: actions/checkout@v3
18 |
19 | - name: Install Node v18
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: "18.x"
23 | registry-url: "https://registry.npmjs.org/"
24 |
25 | - name: Install dependencies
26 | run: yarn install --frozen-lockfile
27 |
28 | - name: Transpile Typescript
29 | run: yarn build
30 |
31 | - name: Publish to NPM
32 | run: yarn publish
33 | env:
34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "private": true,
4 | "version": "0.1.0",
5 | "description": "A Strapi application",
6 | "scripts": {
7 | "develop": "strapi develop",
8 | "start": "strapi start",
9 | "build": "strapi build",
10 | "strapi": "strapi"
11 | },
12 | "devDependencies": {},
13 | "dependencies": {
14 | "react": "^18.0.0",
15 | "react-dom": "^18.0.0",
16 | "react-router-dom": "5.3.4",
17 | "@strapi/plugin-i18n": "4.20.4",
18 | "@strapi/plugin-users-permissions": "4.20.4",
19 | "@strapi/strapi": "4.20.4",
20 | "better-sqlite3": "8.0.1",
21 | "strapi-plugin-image-optimizer": "link:../",
22 | "styled-components": "^5.2.1"
23 | },
24 | "author": {
25 | "name": "A Strapi developer"
26 | },
27 | "strapi": {
28 | "uuid": "33f536a8-ba40-4664-bfb2-d50d6ab5abc8"
29 | },
30 | "engines": {
31 | "node": ">=18.0.0 <=20.x.x",
32 | "npm": ">=6.0.0"
33 | },
34 | "license": "MIT"
35 | }
36 |
--------------------------------------------------------------------------------
/example/config/plugins.ts:
--------------------------------------------------------------------------------
1 | export default ({ env }) => ({
2 | "image-optimizer": {
3 | enabled: true,
4 | config: {
5 | include: ["jpeg", "jpg", "png"],
6 | exclude: ["gif"],
7 | formats: ["original", "webp", "avif"],
8 | sizes: [
9 | {
10 | name: "xs",
11 | width: 300,
12 | },
13 | {
14 | name: "sm",
15 | width: 768,
16 | },
17 | {
18 | name: "md",
19 | width: 1280,
20 | },
21 | {
22 | name: "lg",
23 | width: 1920,
24 | },
25 | {
26 | name: "xl",
27 | width: 2840,
28 | // Won't create an image larger than the original size
29 | withoutEnlargement: true,
30 | },
31 | {
32 | // Uses original size but still transforms for formats
33 | name: "original",
34 | },
35 | ],
36 | additionalResolutions: [1.5, 2],
37 | quality: 70,
38 | },
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Marlo Kesser
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/config/schema.ts:
--------------------------------------------------------------------------------
1 | import { array, boolean, mixed, number, object, string } from "yup";
2 | import { fit as FitEnum } from "sharp";
3 |
4 | const imageFormats = [
5 | "avif",
6 | "dz",
7 | "fits",
8 | "gif",
9 | "heif",
10 | "input",
11 | "jpeg",
12 | "jpg",
13 | "jp2",
14 | "jxl",
15 | "magick",
16 | "openslide",
17 | "pdf",
18 | "png",
19 | "ppm",
20 | "raw",
21 | "svg",
22 | "tiff",
23 | "tif",
24 | "v",
25 | "webp",
26 | ];
27 |
28 | const formatTypes = ["original", ...imageFormats];
29 |
30 | const positions = [
31 | "top",
32 | "right top",
33 | "right",
34 | "right bottom",
35 | "bottom",
36 | "left bottom",
37 | "left",
38 | "left top",
39 | "center",
40 | "entropy",
41 | "attention",
42 | ];
43 |
44 | const configSchema = object({
45 | additionalResolutions: array().of(number().positive()),
46 | exclude: array().of(mixed().oneOf(imageFormats)),
47 | formats: array().of(mixed().oneOf(formatTypes)),
48 | include: array().of(mixed().oneOf(imageFormats)),
49 | sizes: array().of(
50 | object({
51 | name: string(),
52 | width: number().positive(),
53 | height: number().positive(),
54 | fit: mixed().oneOf(Object.values(FitEnum)),
55 | position: mixed().oneOf(positions),
56 | withoutEnlargement: boolean(),
57 | })
58 | ),
59 | quality: number().min(0).max(100),
60 | });
61 |
62 | export default configSchema;
63 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | ############################
2 | # OS X
3 | ############################
4 |
5 | .DS_Store
6 | .AppleDouble
7 | .LSOverride
8 | Icon
9 | .Spotlight-V100
10 | .Trashes
11 | ._*
12 |
13 |
14 | ############################
15 | # Linux
16 | ############################
17 |
18 | *~
19 |
20 |
21 | ############################
22 | # Windows
23 | ############################
24 |
25 | Thumbs.db
26 | ehthumbs.db
27 | Desktop.ini
28 | $RECYCLE.BIN/
29 | *.cab
30 | *.msi
31 | *.msm
32 | *.msp
33 |
34 |
35 | ############################
36 | # Packages
37 | ############################
38 |
39 | *.7z
40 | *.csv
41 | *.dat
42 | *.dmg
43 | *.gz
44 | *.iso
45 | *.jar
46 | *.rar
47 | *.tar
48 | *.zip
49 | *.com
50 | *.class
51 | *.dll
52 | *.exe
53 | *.o
54 | *.seed
55 | *.so
56 | *.swo
57 | *.swp
58 | *.swn
59 | *.swm
60 | *.out
61 | *.pid
62 |
63 |
64 | ############################
65 | # Logs and databases
66 | ############################
67 |
68 | .tmp
69 | *.log
70 | *.sql
71 | *.sqlite
72 | *.sqlite3
73 |
74 |
75 | ############################
76 | # Misc.
77 | ############################
78 |
79 | *#
80 | ssl
81 | .idea
82 | nbproject
83 | public/uploads/*
84 | !public/uploads/.gitkeep
85 |
86 | ############################
87 | # Node.js
88 | ############################
89 |
90 | lib-cov
91 | lcov.info
92 | pids
93 | logs
94 | results
95 | node_modules
96 | .node_history
97 |
98 | ############################
99 | # Tests
100 | ############################
101 |
102 | testApp
103 | coverage
104 |
105 | ############################
106 | # Strapi
107 | ############################
108 |
109 | .env
110 | license.txt
111 | exports
112 | *.cache
113 | dist
114 | build
115 | .strapi-updater.json
116 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "strapi-plugin-image-optimizer",
3 | "version": "2.2.1",
4 | "description": "Optimize your images for desktop, tablet and mobile and different image formats.",
5 | "homepage": "https://github.com/marlokessler/strapi-plugin-image-optimizer#readme",
6 | "license": "MIT",
7 | "main": "strapi-server.js",
8 | "strapi": {
9 | "displayName": "Image Optimizer",
10 | "name": "image-optimizer",
11 | "description": "Optimize your images for desktop, tablet and mobile and different image formats.",
12 | "kind": "plugin"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/marlokessler/strapi-plugin-image-optimizer.git"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/marlokessler/strapi-plugin-image-optimizer/issues"
20 | },
21 | "author": {
22 | "name": "Marlo Kessler (https://github.com/marlokessler)"
23 | },
24 | "maintainers": [
25 | {
26 | "name": "Marlo Kessler (https://github.com/marlokessler)"
27 | }
28 | ],
29 | "keywords": [
30 | "strapi",
31 | "image-optimize",
32 | "image",
33 | "optimize",
34 | "responsive"
35 | ],
36 | "publishConfig": {
37 | "registry": "https://registry.npmjs.org/",
38 | "tag": "latest"
39 | },
40 | "release": {
41 | "branches": [
42 | "main",
43 | {
44 | "name": "beta",
45 | "prerelease": true
46 | }
47 | ]
48 | },
49 | "engines": {
50 | "node": ">=18.0.0 <=20.x.x",
51 | "npm": ">=6.0.0"
52 | },
53 | "devDependencies": {
54 | "@strapi/typescript-utils": "^4.20.5",
55 | "install-peers": "^1.0.4",
56 | "typescript": "^5.4.2"
57 | },
58 | "peerDependencies": {
59 | "@strapi/core": "*",
60 | "@strapi/strapi": "^4.20.5",
61 | "@strapi/utils": "^4.20.5",
62 | "yup": "^1.4.0"
63 | },
64 | "scripts": {
65 | "add:apple_arm": "npm_config_build_from_source=true yarn add",
66 | "install:apple_arm": "npm_config_build_from_source=true yarn install",
67 | "upgrade:apple_arm": "npm_config_build_from_source=true yarn upgrade",
68 | "develop": "tsc -w",
69 | "build": "tsc"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "commitConvention": "angular",
8 | "contributors": [
9 | {
10 | "login": "marlokessler",
11 | "name": "Marlo Kesser",
12 | "avatar_url": "https://avatars.githubusercontent.com/u/48910761?v=4",
13 | "profile": "https://github.com/marlokessler",
14 | "contributions": [
15 | "code",
16 | "doc"
17 | ]
18 | },
19 | {
20 | "login": "yarikwest",
21 | "name": "Yaroslav Zakhidnyi",
22 | "avatar_url": "https://avatars.githubusercontent.com/u/32482428?v=4",
23 | "profile": "https://www.linkedin.com/in/yaroslav-zakhidnyi/",
24 | "contributions": [
25 | "bug",
26 | "code"
27 | ]
28 | },
29 | {
30 | "login": "JosefBredereck",
31 | "name": "Josef Bredreck",
32 | "avatar_url": "https://avatars.githubusercontent.com/u/13408112?v=4",
33 | "profile": "https://github.com/JosefBredereck",
34 | "contributions": [
35 | "code",
36 | "doc"
37 | ]
38 | },
39 | {
40 | "login": "Cretezy",
41 | "name": "Cretezy",
42 | "avatar_url": "https://avatars.githubusercontent.com/u/2672503?v=4",
43 | "profile": "https://cretezy.com",
44 | "contributions": [
45 | "code",
46 | "doc"
47 | ]
48 | },
49 | {
50 | "login": "Lucurious",
51 | "name": "Lucurious",
52 | "avatar_url": "https://avatars.githubusercontent.com/u/6559860?v=4",
53 | "profile": "https://github.com/Lucurious",
54 | "contributions": [
55 | "bug",
56 | "code"
57 | ]
58 | },
59 | {
60 | "login": "BirknerAlex",
61 | "name": "Alexander Birkner",
62 | "avatar_url": "https://avatars.githubusercontent.com/u/779751?v=4",
63 | "profile": "https://tyrola.dev",
64 | "contributions": [
65 | "bug",
66 | "code"
67 | ]
68 | }
69 | ],
70 | "contributorsPerLine": 7,
71 | "skipCi": true,
72 | "repoType": "github",
73 | "repoHost": "https://github.com",
74 | "projectName": "strapi-plugin-image-optimizer",
75 | "projectOwner": "marlokessler",
76 | "commitType": "docs"
77 | }
78 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # 🚀 Getting started with Strapi
2 |
3 | Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html) (CLI) which lets you scaffold and manage your project in seconds.
4 |
5 | ### `develop`
6 |
7 | Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-develop)
8 |
9 | ```
10 | npm run develop
11 | # or
12 | yarn develop
13 | ```
14 |
15 | ### `start`
16 |
17 | Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-start)
18 |
19 | ```
20 | npm run start
21 | # or
22 | yarn start
23 | ```
24 |
25 | ### `build`
26 |
27 | Build your admin panel. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-build)
28 |
29 | ```
30 | npm run build
31 | # or
32 | yarn build
33 | ```
34 |
35 | ## ⚙️ Deployment
36 |
37 | Strapi gives you many possible deployment options for your project. Find the one that suits you on the [deployment section of the documentation](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html).
38 |
39 | ## 📚 Learn more
40 |
41 | - [Resource center](https://strapi.io/resource-center) - Strapi resource center.
42 | - [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation.
43 | - [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community.
44 | - [Strapi blog](https://docs.strapi.io) - Official Strapi blog containing articles made by the Strapi team and the community.
45 | - [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements.
46 |
47 | Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome!
48 |
49 | ## ✨ Community
50 |
51 | - [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team.
52 | - [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members.
53 | - [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi.
54 |
55 | ---
56 |
57 | 🤫 Psst! [Strapi is hiring](https://strapi.io/careers).
58 |
--------------------------------------------------------------------------------
/server/models/config.ts:
--------------------------------------------------------------------------------
1 | export interface Config {
2 | /**
3 | * Additional resolutions to generate. The value is the factor by which the original image is multiplied. For example, if the original image is 1000x1000 and the factor is 2, then the generated image will be 2000x2000.
4 | */
5 | additionalResolutions?: number[];
6 | /**
7 | * The image formats to exclude. Exclude takes precedence over include. Default is [].
8 | */
9 | exclude?: SourceFormat[];
10 | /**
11 | * The image formats to generate. Default is ["original", "webp", "avif"].
12 | */
13 | formats?: OutputFormat[];
14 | /**
15 | * The image formats to include. Exclude takes precedence over include. Default is ["jpeg", "jpg", "png"].
16 | */
17 | include?: SourceFormat[];
18 | /**
19 | * The image sizes to generate. Default is [].
20 | */
21 | sizes: ImageSize[];
22 | /**
23 | * The quality of the generated images. Default is 80.
24 | */
25 | quality?: number;
26 | }
27 |
28 | export interface ImageSize {
29 | /**
30 | * The name of the size. This will be used as part of generated image's name and url.
31 | */
32 | name: string;
33 | /**
34 | * The width of the output image in pixels. If only width is specified then the height is calculated with the original aspect ratio.
35 | */
36 | width?: number;
37 | /**
38 | * The height of the output image in pixels. If only height is specified then the width is calculated with the original aspect ratio.
39 | */
40 | height?: number;
41 | /**
42 | * The image fit mode if both width and height are specified. Default is cover.
43 | */
44 | fit?: ImageFit;
45 | /**
46 | * The position of the image within the output image. This option is only used when fit is cover or contain. Default is center.
47 | */
48 | position?: ImagePosition;
49 | /**
50 | * When true, the image will not be enlarged if the input image is already smaller than the required dimensions. Default is false.
51 | */
52 | withoutEnlargement?: boolean;
53 | }
54 |
55 | export type SourceFormat =
56 | | "avif"
57 | | "dz"
58 | | "fits"
59 | | "gif"
60 | | "heif"
61 | | "input"
62 | | "jpeg"
63 | | "jpg"
64 | | "jp2"
65 | | "jxl"
66 | | "magick"
67 | | "openslide"
68 | | "pdf"
69 | | "png"
70 | | "ppm"
71 | | "raw"
72 | | "svg"
73 | | "tiff"
74 | | "tif"
75 | | "v"
76 | | "webp";
77 |
78 | export type OutputFormat = "original" | SourceFormat;
79 |
80 | export type ImageFit = "contain" | "cover" | "fill" | "inside" | "outside";
81 |
82 | export type ImagePosition =
83 | | "top"
84 | | "right top"
85 | | "right"
86 | | "right bottom"
87 | | "bottom"
88 | | "left bottom"
89 | | "left"
90 | | "left top"
91 | | "center"
92 | | "entropy"
93 | | "attention";
94 |
--------------------------------------------------------------------------------
/example/config/database.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | export default ({ env }) => {
4 | const client = env('DATABASE_CLIENT', 'sqlite');
5 |
6 | const connections = {
7 | mysql: {
8 | connection: {
9 | connectionString: env('DATABASE_URL'),
10 | host: env('DATABASE_HOST', 'localhost'),
11 | port: env.int('DATABASE_PORT', 3306),
12 | database: env('DATABASE_NAME', 'strapi'),
13 | user: env('DATABASE_USERNAME', 'strapi'),
14 | password: env('DATABASE_PASSWORD', 'strapi'),
15 | ssl: env.bool('DATABASE_SSL', false) && {
16 | key: env('DATABASE_SSL_KEY', undefined),
17 | cert: env('DATABASE_SSL_CERT', undefined),
18 | ca: env('DATABASE_SSL_CA', undefined),
19 | capath: env('DATABASE_SSL_CAPATH', undefined),
20 | cipher: env('DATABASE_SSL_CIPHER', undefined),
21 | rejectUnauthorized: env.bool(
22 | 'DATABASE_SSL_REJECT_UNAUTHORIZED',
23 | true
24 | ),
25 | },
26 | },
27 | pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
28 | },
29 | mysql2: {
30 | connection: {
31 | host: env('DATABASE_HOST', 'localhost'),
32 | port: env.int('DATABASE_PORT', 3306),
33 | database: env('DATABASE_NAME', 'strapi'),
34 | user: env('DATABASE_USERNAME', 'strapi'),
35 | password: env('DATABASE_PASSWORD', 'strapi'),
36 | ssl: env.bool('DATABASE_SSL', false) && {
37 | key: env('DATABASE_SSL_KEY', undefined),
38 | cert: env('DATABASE_SSL_CERT', undefined),
39 | ca: env('DATABASE_SSL_CA', undefined),
40 | capath: env('DATABASE_SSL_CAPATH', undefined),
41 | cipher: env('DATABASE_SSL_CIPHER', undefined),
42 | rejectUnauthorized: env.bool(
43 | 'DATABASE_SSL_REJECT_UNAUTHORIZED',
44 | true
45 | ),
46 | },
47 | },
48 | pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
49 | },
50 | postgres: {
51 | connection: {
52 | connectionString: env('DATABASE_URL'),
53 | host: env('DATABASE_HOST', 'localhost'),
54 | port: env.int('DATABASE_PORT', 5432),
55 | database: env('DATABASE_NAME', 'strapi'),
56 | user: env('DATABASE_USERNAME', 'strapi'),
57 | password: env('DATABASE_PASSWORD', 'strapi'),
58 | ssl: env.bool('DATABASE_SSL', false) && {
59 | key: env('DATABASE_SSL_KEY', undefined),
60 | cert: env('DATABASE_SSL_CERT', undefined),
61 | ca: env('DATABASE_SSL_CA', undefined),
62 | capath: env('DATABASE_SSL_CAPATH', undefined),
63 | cipher: env('DATABASE_SSL_CIPHER', undefined),
64 | rejectUnauthorized: env.bool(
65 | 'DATABASE_SSL_REJECT_UNAUTHORIZED',
66 | true
67 | ),
68 | },
69 | schema: env('DATABASE_SCHEMA', 'public'),
70 | },
71 | pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
72 | },
73 | sqlite: {
74 | connection: {
75 | filename: path.join(
76 | __dirname,
77 | '..',
78 | '..',
79 | env('DATABASE_FILENAME', '.tmp/data.db')
80 | ),
81 | },
82 | useNullAsDefault: true,
83 | },
84 | };
85 |
86 | return {
87 | connection: {
88 | client,
89 | ...connections[client],
90 | acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000),
91 | },
92 | };
93 | };
94 |
--------------------------------------------------------------------------------
/server/services/image-optimizer-service.ts:
--------------------------------------------------------------------------------
1 | import { ReadStream, createReadStream, createWriteStream } from "fs";
2 | import { join } from "path";
3 | import sharp, { Sharp, Metadata } from "sharp";
4 | import { file as fileUtils } from '@strapi/utils';
5 |
6 | // @ts-ignore - No types available
7 | import pluginUpload from "@strapi/plugin-upload/strapi-server";
8 | const imageManipulation = pluginUpload().services["image-manipulation"];
9 |
10 | import {
11 | OutputFormat,
12 | ImageSize,
13 | File,
14 | SourceFile,
15 | StrapiImageFormat,
16 | SourceFormat,
17 | } from "../models";
18 | import settingsService from "./settings-service";
19 |
20 | const defaultFormats: OutputFormat[] = ["original", "webp", "avif"];
21 | const defaultInclude: SourceFormat[] = ["jpeg", "jpg", "png"];
22 | const defaultQuality = 80;
23 |
24 | async function optimizeImage(file: SourceFile): Promise {
25 | // Get config
26 | const {
27 | exclude = [],
28 | formats = defaultFormats,
29 | include = defaultInclude,
30 | sizes,
31 | additionalResolutions,
32 | quality = defaultQuality,
33 | } = settingsService.settings;
34 |
35 | const sourceFileType = file.ext.replace(".", "");
36 | if (
37 | exclude.includes(sourceFileType.toLowerCase() as SourceFormat) ||
38 | !include.includes(sourceFileType.toLowerCase() as SourceFormat)
39 | ) {
40 | return Promise.all([]);
41 | }
42 |
43 | const imageFormatPromises: Promise[] = [];
44 |
45 | formats.forEach((format) => {
46 | sizes.forEach((size) => {
47 | imageFormatPromises.push(generateImage(file, format, size, quality));
48 | if (additionalResolutions) {
49 | additionalResolutions.forEach((resizeFactor) => {
50 | imageFormatPromises.push(
51 | generateImage(file, format, size, quality, resizeFactor)
52 | );
53 | });
54 | }
55 | });
56 | });
57 |
58 | return Promise.all(imageFormatPromises);
59 | }
60 |
61 | async function generateImage(
62 | sourceFile: SourceFile,
63 | format: OutputFormat,
64 | size: ImageSize,
65 | quality: number,
66 | resizeFactor = 1
67 | ): Promise {
68 | const resizeFactorPart = resizeFactor === 1 ? "" : `_${resizeFactor}x`;
69 | const sizeName = `${size.name}${resizeFactorPart}`;
70 | const formatPart = format === "original" ? "" : `_${format}`;
71 | return {
72 | key: `${sizeName}${formatPart}`,
73 | file: await resizeFileTo(
74 | sourceFile,
75 | sizeName,
76 | format,
77 | size,
78 | quality,
79 | resizeFactor
80 | ),
81 | };
82 | }
83 |
84 | async function resizeFileTo(
85 | sourceFile: SourceFile,
86 | sizeName: string,
87 | format: OutputFormat,
88 | size: ImageSize,
89 | quality: number,
90 | resizeFactor: number
91 | ): Promise {
92 | let sharpInstance = sharp();
93 | if (format !== "original") {
94 | sharpInstance = sharpInstance.toFormat(format);
95 | }
96 | sharpInstance = sharpAddFormatSettings(sharpInstance, { quality });
97 | sharpInstance = sharpAddResizeSettings(
98 | sharpInstance,
99 | size,
100 | resizeFactor,
101 | sourceFile
102 | );
103 |
104 | const imageHash = `${sizeName}_${format}_${sourceFile.hash}`;
105 | const filePath = join(sourceFile.tmpWorkingDirectory, imageHash);
106 | const newImageStream = sourceFile.getStream().pipe(sharpInstance);
107 | await writeStreamToFile(newImageStream, filePath);
108 |
109 | const metadata = await getMetadata(createReadStream(filePath));
110 | return {
111 | name: getFileName(sourceFile, sizeName),
112 | hash: imageHash,
113 | ext: getFileExtension(sourceFile, format),
114 | mime: getFileMimeType(sourceFile, format),
115 | path: sourceFile.path,
116 | width: metadata.width,
117 | height: metadata.height,
118 | size: metadata.size && fileUtils.bytesToKbytes(metadata.size),
119 | getStream: () => createReadStream(filePath),
120 | };
121 | }
122 |
123 | function sharpAddFormatSettings(
124 | sharpInstance: Sharp,
125 | { quality }: { quality?: number }
126 | ): Sharp {
127 | // TODO: Add jxl when it's no longer experimental
128 | return sharpInstance
129 | .jpeg({ quality, progressive: true, force: false })
130 | .png({
131 | compressionLevel: Math.floor(((quality ?? 100) / 100) * 9),
132 | progressive: true,
133 | force: false,
134 | })
135 | .webp({ quality, force: false })
136 | .avif({ quality, force: false })
137 | .heif({ quality, force: false })
138 | .tiff({ quality, force: false });
139 | }
140 |
141 | function sharpAddResizeSettings(
142 | sharpInstance: Sharp,
143 | size: ImageSize,
144 | factor: number,
145 | sourceFile: SourceFile
146 | ): Sharp {
147 | const originalSize = !size.width && !size.height;
148 | const { width, height } = originalSize
149 | ? { width: sourceFile.width, height: sourceFile.height }
150 | : { width: size.width, height: size.height };
151 |
152 | return sharpInstance.resize({
153 | width: width ? width * factor : undefined,
154 | height: height ? height * factor : undefined,
155 | fit: size.fit,
156 | // Position "center" cannot be set since it's the default (see: https://sharp.pixelplumbing.com/api-resize#resize).
157 | position: size.position === "center" ? undefined : size.position,
158 | withoutEnlargement: size.withoutEnlargement,
159 | });
160 | }
161 |
162 | async function writeStreamToFile(sharpsStream: Sharp, path: string) {
163 | return new Promise((resolve, reject) => {
164 | const writeStream = createWriteStream(path);
165 | // Reject promise if there is an error with the provided stream
166 | sharpsStream.on("error", reject);
167 | sharpsStream.pipe(writeStream);
168 | writeStream.on("close", resolve);
169 | writeStream.on("error", reject);
170 | });
171 | }
172 |
173 | async function getMetadata(readStream: ReadStream): Promise {
174 | return new Promise((resolve, reject) => {
175 | const sharpInstance = sharp();
176 | sharpInstance.metadata().then(resolve).catch(reject);
177 | readStream.pipe(sharpInstance);
178 | });
179 | }
180 |
181 | function getFileName(sourceFile: File, sizeName: string) {
182 | const fileNameWithoutExtension = sourceFile.name.replace(/\.[^\/.]+$/, "");
183 | return `${sizeName}_${fileNameWithoutExtension}`;
184 | }
185 |
186 | function getFileExtension(sourceFile: File, format: OutputFormat) {
187 | return format === "original" ? sourceFile.ext : `.${format}`;
188 | }
189 |
190 | function getFileMimeType(sourceFile: File, format: OutputFormat) {
191 | return format === "original" ? sourceFile.mime : `image/${format}`;
192 | }
193 |
194 | export default () => ({
195 | ...imageManipulation(),
196 | generateResponsiveFormats: optimizeImage,
197 | });
198 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚠️ Looking for a Maintainer ⚠️
2 |
3 | Hi there, I do not use Strapi anymore (Actually I never really used it). I evaluated Strapi as a headless CMS a while ago and since I was missing a functionallity to scale and format uploaded images, I was happy to find that [@nicolashmln](https://github.com/nicolashmln) already created a great plugin for this. Nevertheless, it has only one issue (at least it was an issue for me): The image sizes and format to transform the uploaded images to could not be declared in code. Therefore, I created a fork which I then published. Since I expected to use Strapi as CMS (because it made a good impression) I thought it makes sense to share this with the world.
4 |
5 | However, some weeks later I decided to move from Strapi due to a bunch of reasons which are not really important at this point (self-hosting, etc.). This plugin however is forced to use a semi-internal Strapi API which was and is sometimes subject to changes (because this is the only reason to hook into the upload process). Therefore, sometimes changes are required or otherwise the plugin even breaks with minor changes (which is pretty bad☹️). However, since I do not use Strapi anymore I think it is better to pass this plugin to somebody who knows Strapi better than I do. **If you use Strapi and this plugin regularly and feel confident to take over the maintainership of this repo contact me on LinkedIn or Instagram under @marlokessler.**
6 |
7 | Thank you!
8 |
9 | Cheers
10 | Marlo
11 |
12 | ---
13 |
14 |
15 |
16 | # Strapi plugin image optimizer
17 |
18 | 
19 | [](https://github.com/marlokessler/strapi-plugin-image-optimizer/blob/main/LICENSE)
20 | 
21 | [](https://github.com/marlokessler/strapi-plugin-image-optimizer/actions/workflows/deploy.yml)
22 | [](#contributors-)
23 |
24 | ## Table of contents
25 |
26 | - [Requirements](#requirements)
27 | - [Installation](#installation)
28 | - [1. Install package](#1-install-package)
29 | - [2. Extend Strapi's upload plugin](#2-extend-strapis-upload-plugin)
30 | - [3. Add config options](#3-add-config-options)
31 | - [Config options](#config-options)
32 | - [Object `Config`](#object-config)
33 | - [Object `ImageSize`](#object-imagesize)
34 | - [Type `SourceFormat`](#type-sourceformat)
35 | - [Type `OutputFormat`](#type-outputformat)
36 | - [Type `ImageFit`](#type-imagefit)
37 | - [Type `ImagePosition`](#type-imageposition)
38 | - [Example config](#example-config)
39 | - [Usage](#usage)
40 | - [Found a bug?](#found-a-bug)
41 | - [Contributors](#contributors-)
42 |
43 | ## Requirements
44 |
45 | Strapi version >= v4.11.7
46 |
47 | ## Note
48 |
49 | This plugin uses [sharp](https://sharp.pixelplumbing.com/) provided via [strapi core](https://github.com/strapi/strapi/blob/3bfe1c913cf037c85a167f83a4bda0d848c9ba50/packages/core/upload/package.json#L52). All settings and options are documented in more detail in the [sharp API documentation](https://sharp.pixelplumbing.com/api-resize).
50 |
51 | ## Installation
52 |
53 | ### 1. Install package
54 |
55 | Install the package via `npm install strapi-plugin-image-optimizer` or `yarn add strapi-plugin-image-optimizer`.
56 |
57 | ### 2. Extend Strapi's upload plugin
58 |
59 | To make this plugin work, you need to enter the following code to `./src/extensions/upload/strapi-server.ts`. If file and folders do not exist, you need to create them. This code overrides the default image manipulation service of Strapi's `upload` plugin.
60 |
61 | ```typescript
62 | // ./src/extensions/upload/strapi-server.ts
63 |
64 | import imageOptimizerService from "strapi-plugin-image-optimizer/dist/server/services/image-optimizer-service";
65 |
66 | module.exports = (plugin) => {
67 | plugin.services["image-manipulation"] = imageOptimizerService;
68 | return plugin;
69 | };
70 | ```
71 |
72 | ### 3. Add config options
73 |
74 | Configure the plugin in the `config/plugins.(js/ts)` file of your Strapi project.
75 |
76 | ## Config options
77 |
78 | ### Object `Config`
79 |
80 | | Option | Type | Description |
81 | | ----------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82 | | `additionalResolutions` | `number[]`
Min: 0 | Create additional resolutions for high res displays (e.g. Apples Retina Display which has a resolution of 2x). Default is `[]`. |
83 | | `exclude` | [`SourceFormat`](#type-sourceformat)`[]` | Exclude image formats from being optimized. Default is `[]`. |
84 | | `formats` | [`OutputFormat`](#type-outputformat)`[]` | Specifiy the formats images should be transformed to. Specifiying `original` means that the original format is kept. Default is `["original", "webp", "avif"]`. Only `jpeg`, `jpg`, `png`/`webp`, `avif`, `heif`, `tiff` and `tif` will adjust quality. |
85 | | `include` | [`SourceFormat`](#type-sourceformat)`[]` | Include image formats that should be optimized. Default is `["jpeg", "jpg", "png"]`. |
86 | | `sizes`\* | [`ImageSize`](#object-imagesize)`[]` | (required) - Specify the sizes to which the uploaded image should be transformed. |
87 | | `quality` | `number`
Min: 0
Max: 100 | Specific the image quality the output should be rendered in. Default is `80`. |
88 |
89 | ### Object `ImageSize`
90 |
91 | | Option | Type | Description |
92 | | -------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93 | | `fit` | [`ImageFit`](#type-imagefit) | The image fit mode if both width and height are specified. Default is `cover`. |
94 | | `height` | `number`
Min: 0 | The height of the output image in pixels. If only height is specified then the width is calculated with the original aspect ratio. If neither width nor height are set, the output will be the same size as the original. |
95 | | `name`\* | `string`
Min: 0 | (required) - The name of the size. This will be used as part of generated image's name and url. |
96 | | `position` | [`ImagePosition`](#type-imageposition) | The position of the image within the output image. This option is only used when fit is cover or contain. Default is `center`. |
97 | | `width` | `number`
Min: 0 | The width of the output image in pixels. If only width is specified then the height is calculated with the original aspect ratio. If neither width nor height are set, the output will be the same size as the original. |
98 | | `withoutEnlargement` | `boolean` | When true, the image will not be enlarged if the input image is already smaller than the required dimensions. Default is `false`. |
99 |
100 | ### Type `SourceFormat`
101 |
102 | ```typescript
103 | type SourceFormat =
104 | | "avif"
105 | | "dz"
106 | | "fits"
107 | | "gif"
108 | | "heif"
109 | | "input"
110 | | "jpeg"
111 | | "jpg"
112 | | "jp2"
113 | | "jxl"
114 | | "magick"
115 | | "openslide"
116 | | "pdf"
117 | | "png"
118 | | "ppm"
119 | | "raw"
120 | | "svg"
121 | | "tiff"
122 | | "tif"
123 | | "v"
124 | | "webp";
125 | ```
126 |
127 | ### Type `OutputFormat`
128 |
129 | ```typescript
130 | type OutputFormat = "original" | SourceFormat;
131 | ```
132 |
133 | ### Type `ImageFit`
134 |
135 | ```typescript
136 | type ImageFit = "contain" | "cover" | "fill" | "inside" | "outside";
137 | ```
138 |
139 | ### Type `ImagePosition`
140 |
141 | ```typescript
142 | type ImageFit =
143 | | "top"
144 | | "right top"
145 | | "right"
146 | | "right bottom"
147 | | "bottom"
148 | | "left bottom"
149 | | "left"
150 | | "left top"
151 | | "center"
152 | | "entropy" // only in combination with ImageFit cover
153 | | "attention"; // only in combination with ImageFit cover;
154 | ```
155 |
156 | ### Example config
157 |
158 | The following config would be a good starting point for your project.
159 |
160 | ```typescript
161 | // ./config/plugins.ts
162 |
163 | export default ({ env }) => ({
164 | // ...
165 | "image-optimizer": {
166 | enabled: true,
167 | config: {
168 | include: ["jpeg", "jpg", "png"],
169 | exclude: ["gif"],
170 | formats: ["original", "webp", "avif"],
171 | sizes: [
172 | {
173 | name: "xs",
174 | width: 300,
175 | },
176 | {
177 | name: "sm",
178 | width: 768,
179 | },
180 | {
181 | name: "md",
182 | width: 1280,
183 | },
184 | {
185 | name: "lg",
186 | width: 1920,
187 | },
188 | {
189 | name: "xl",
190 | width: 2840,
191 | // Won't create an image larger than the original size
192 | withoutEnlargement: true,
193 | },
194 | {
195 | // Uses original size but still transforms for formats
196 | name: "original",
197 | },
198 | ],
199 | additionalResolutions: [1.5, 3],
200 | quality: 70,
201 | },
202 | },
203 | // ...
204 | });
205 | ```
206 |
207 | If you want type safety, you can extend the configuration with our config typing.
208 |
209 | With that approach, you will get the possibility for property IntelliSense and static string type values.
210 |
211 | ```typescript
212 | import { Config as ImageOptimizerConfig } from "strapi-plugin-image-optimizer/dist/server/models/config";
213 |
214 | // ...
215 | export default ({ env }) => ({
216 | // ...
217 | "image-optimizer": {
218 | // ...
219 | config: {
220 | // ...
221 | } satisfies ImageOptimizerConfig,
222 | },
223 | });
224 | ```
225 |
226 | ## Usage
227 |
228 | When uploading an image in the media library, Image Optimizer resizes and converts the uploaded images as specified in the config.
229 |
230 | ## Found a bug?
231 |
232 | If you found a bug or have any questions please [submit an issue](https://github.com/marlokessler/strapi-plugin-image-optimizer/issues). If you think you found a way how to fix it, please feel free to create a pull request!
233 |
234 | ## Contributors ✨
235 |
236 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
237 |
238 |
239 |
240 |
241 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
269 |
270 | A special thanks to [@nicolashmln](https://github.com/nicolashmln), whose package [strapi-plugin-responsive-image](https://github.com/nicolashmln/strapi-plugin-responsive-image) served as inspiration for this one.
271 |
--------------------------------------------------------------------------------