├── 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 | image optimizer logo 15 | 16 | # Strapi plugin image optimizer 17 | 18 | ![Version](https://img.shields.io/npm/v/strapi-plugin-image-optimizer?label=strapi-plugin-image-optimizer) 19 | [![License](https://img.shields.io/github/license/marlokessler/strapi-plugin-image-optimizer)](https://github.com/marlokessler/strapi-plugin-image-optimizer/blob/main/LICENSE) 20 | ![Dependencies](https://img.shields.io/librariesio/github/marlokessler/strapi-plugin-image-optimizer) 21 | [![Deploy](https://github.com/marlokessler/strapi-plugin-image-optimizer/actions/workflows/deploy.yml/badge.svg)](https://github.com/marlokessler/strapi-plugin-image-optimizer/actions/workflows/deploy.yml) 22 | [![All Contributors](https://img.shields.io/github/all-contributors/marlokessler/strapi-plugin-image-optimizer)](#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 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 |
Marlo Kesser
Marlo Kesser

💻 📖
Yaroslav Zakhidnyi
Yaroslav Zakhidnyi

🐛 💻
Josef Bredreck
Josef Bredreck

💻 📖
Cretezy
Cretezy

💻 📖
Lucurious
Lucurious

🐛 💻
Alexander Birkner
Alexander Birkner

🐛 💻
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 | --------------------------------------------------------------------------------