├── .env.test ├── .github ├── actions │ └── prepare │ │ └── action.yml └── workflows │ ├── build.yml │ ├── eslint.yml │ ├── publish.yml │ ├── release-please.yml │ └── test.yml ├── .gitignore ├── .nvmrc ├── .release-please-manifest.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker-compose.test.yaml ├── eslint.config.js ├── extension.config.js ├── mocks └── mappings │ ├── colors.json │ ├── index.json │ ├── tags.json │ └── uploads.json ├── package.json ├── pnpm-lock.yaml ├── release-please-config.json ├── renovate.json ├── src └── index.ts ├── tests ├── extension.test.ts └── paris.jpeg ├── tsconfig.json └── vitest.config.ts /.env.test: -------------------------------------------------------------------------------- 1 | ADMIN_EMAIL="test@test.com" 2 | ADMIN_PASSWORD="test" 3 | ADMIN_TOKEN="test" 4 | PUBLIC_URL="http://127.0.0.1:8055" 5 | IMAGGA_API="http://imagga:8080" 6 | IMAGGA_TAGS_ENABLE=true 7 | -------------------------------------------------------------------------------- /.github/actions/prepare/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare 2 | description: Install dependencies and build 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Install pnpm package manager 8 | uses: pnpm/action-setup@v4 9 | 10 | - name: Setup Node.js environment 11 | uses: actions/setup-node@v4 12 | with: 13 | node-version-file: ".nvmrc" 14 | cache: "pnpm" 15 | scope: "@bicou" 16 | registry-url: "https://registry.npmjs.org/" 17 | 18 | - name: Install dependencies 19 | shell: bash 20 | run: pnpm install --frozen-lockfile 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Prepare and Build 17 | uses: ./.github/actions/prepare 18 | 19 | - name: Build 20 | run: pnpm build 21 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # ESLint is a tool for identifying and reporting on patterns 6 | # found in ECMAScript/JavaScript code. 7 | # More details at https://github.com/eslint/eslint 8 | # and https://eslint.org 9 | 10 | name: ESLint 11 | 12 | on: 13 | push: 14 | branches: [ "main" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "main" ] 18 | 19 | jobs: 20 | eslint: 21 | name: Run eslint scanning 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | security-events: write 26 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Prepare 32 | uses: ./.github/actions/prepare 33 | 34 | - name: Install ESLint SARIF 35 | run: pnpm install @microsoft/eslint-formatter-sarif 36 | 37 | - name: Run ESLint 38 | run: pnpm lint --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif 39 | continue-on-error: true 40 | 41 | - name: Upload analysis results to GitHub 42 | uses: github/codeql-action/upload-sarif@v3 43 | with: 44 | sarif_file: eslint-results.sarif 45 | wait-for-processing: true 46 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | publish: 10 | name: Publish 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | id-token: write 15 | env: 16 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup node and pnpm 22 | uses: ./.github/actions/prepare 23 | 24 | - name: Build 25 | run: pnpm build 26 | 27 | - name: Publish 28 | run: pnpm publish --no-git-checks --provenance 29 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | concurrency: ${{ github.workflow }}-${{ github.ref }} 13 | 14 | jobs: 15 | release-please: 16 | name: Release Please 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/create-github-app-token@v2 20 | id: app-token 21 | with: 22 | app-id: ${{ vars.BOT_APP_ID }} 23 | private-key: ${{ secrets.BOT_PRIVATE_KEY }} 24 | 25 | - uses: googleapis/release-please-action@v4 26 | with: 27 | token: ${{ steps.app-token.outputs.token }} 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Prepare and Build 18 | uses: ./.github/actions/prepare 19 | 20 | - name: Build 21 | run: pnpm build 22 | 23 | - name: Start docker services 24 | uses: hoverkraft-tech/compose-action@v2 25 | with: 26 | compose-file: docker-compose.test.yaml 27 | up-flags: '--wait' 28 | 29 | - name: Run tests 30 | run: pnpm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .idea 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.16.0 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {".":"1.6.6"} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.3.0](https://github.com/gbicou/directus-extension-imagga/compare/v1.2.2...v1.3.0) (2023-11-26) 2 | 3 | ## [1.6.6](https://github.com/gbicou/directus-extension-imagga/compare/directus-extension-imagga-v1.6.5...directus-extension-imagga-v1.6.6) (2025-05-04) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **deps:** update dependency @directus/api to v27 ([#322](https://github.com/gbicou/directus-extension-imagga/issues/322)) ([a5aa8db](https://github.com/gbicou/directus-extension-imagga/commit/a5aa8dbf40be2849f190758cde5037fabd6d9ffa)) 9 | * Remplace useEnv with env from hook ([ef3555b](https://github.com/gbicou/directus-extension-imagga/commit/ef3555b5dfe1debc9be90697b2723d48149dfaf6)) 10 | 11 | ## [1.6.5](https://github.com/gbicou/directus-extension-imagga/compare/directus-extension-imagga-v1.6.4...directus-extension-imagga-v1.6.5) (2025-04-04) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * **deps:** update dependency @directus/api to v26 ([#278](https://github.com/gbicou/directus-extension-imagga/issues/278)) ([a9cafe7](https://github.com/gbicou/directus-extension-imagga/commit/a9cafe7331f0d1d1e6c8412acdafe367ca0da5e3)) 17 | * **deps:** update dependency @directus/api to v26.0.1 ([#289](https://github.com/gbicou/directus-extension-imagga/issues/289)) ([614a351](https://github.com/gbicou/directus-extension-imagga/commit/614a351177e09561b02358e25ce3dfbd52d0d3e9)) 18 | * **deps:** update dependency axios to v1.8.4 ([#267](https://github.com/gbicou/directus-extension-imagga/issues/267)) ([0e6d91d](https://github.com/gbicou/directus-extension-imagga/commit/0e6d91d52b5481d6f1905c82a6a67f74f361d48e)) 19 | * **deps:** update directus ([#277](https://github.com/gbicou/directus-extension-imagga/issues/277)) ([d6eb301](https://github.com/gbicou/directus-extension-imagga/commit/d6eb301ed1f42a8a532be3c775ec82655af8d0c6)) 20 | 21 | ## [1.6.4](https://github.com/gbicou/directus-extension-imagga/compare/directus-extension-imagga-v1.6.3...directus-extension-imagga-v1.6.4) (2025-03-15) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * **deps:** update dependency axios to v1.8.2 ([bb8e01d](https://github.com/gbicou/directus-extension-imagga/commit/bb8e01dfbac8ca4013068309890da8b5192eb6b5)) 27 | * **deps:** update dependency axios to v1.8.2 ([c9299a2](https://github.com/gbicou/directus-extension-imagga/commit/c9299a27c81cdb06eec16a5a340052508662779b)) 28 | * **deps:** update dependency axios to v1.8.3 ([#260](https://github.com/gbicou/directus-extension-imagga/issues/260)) ([acded5d](https://github.com/gbicou/directus-extension-imagga/commit/acded5d21897671d71dbd22d059b90ce787daf1b)) 29 | 30 | ## [1.6.3](https://github.com/gbicou/directus-extension-imagga/compare/directus-extension-imagga-v1.6.2...directus-extension-imagga-v1.6.3) (2025-03-06) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * **deps:** update dependency axios to v1.7.8 ([660bf3f](https://github.com/gbicou/directus-extension-imagga/commit/660bf3f8246eeba2b650909469b90e45c3c17cad)) 36 | * **deps:** update dependency axios to v1.7.8 ([b0c6dbe](https://github.com/gbicou/directus-extension-imagga/commit/b0c6dbe649e9a3b7380075460d63bd8a2ea1fb42)) 37 | * **deps:** update dependency axios to v1.7.9 ([3a42a2b](https://github.com/gbicou/directus-extension-imagga/commit/3a42a2bb676a8529b210ede65cea1b18875f7c51)) 38 | * **deps:** update dependency axios to v1.7.9 ([a8c1701](https://github.com/gbicou/directus-extension-imagga/commit/a8c1701fe05f03b9e438f1b947f2119c6d606b22)) 39 | * **deps:** update dependency axios to v1.8.1 ([5f3c6d8](https://github.com/gbicou/directus-extension-imagga/commit/5f3c6d8cd36c6cc4714ae4fcced7e4ce0306318c)) 40 | * **deps:** update dependency axios to v1.8.1 ([b6cba4c](https://github.com/gbicou/directus-extension-imagga/commit/b6cba4c0b9138419a1f76f293a4eb78b16b9d219)) 41 | * **deps:** update dependency axios to v1.8.1 ([83e4dd2](https://github.com/gbicou/directus-extension-imagga/commit/83e4dd22527e08b95c184bb0c3f446d3d761520a)) 42 | * **deps:** update dependency axios to v1.8.1 ([be28951](https://github.com/gbicou/directus-extension-imagga/commit/be28951cdd3a4dfaaa4857555c8fea26012355a8)) 43 | * **deps:** update dependency form-data to v4.0.2 ([6e322e8](https://github.com/gbicou/directus-extension-imagga/commit/6e322e80e23652ef5d8dcc2169f77cc5440173db)) 44 | * **deps:** update dependency form-data to v4.0.2 ([3f79006](https://github.com/gbicou/directus-extension-imagga/commit/3f790064f45db5e20954b3db25984549c2e4a066)) 45 | * **deps:** update dependency form-data to v4.0.2 ([04e096a](https://github.com/gbicou/directus-extension-imagga/commit/04e096a6cdc962d4a23c80ee9b56c8bc2e73876e)) 46 | * **deps:** update dependency form-data to v4.0.2 ([8b20aff](https://github.com/gbicou/directus-extension-imagga/commit/8b20aff25d12a6532dfe0d2c48d50eace33e5ac3)) 47 | * Lockfile update ([69f0818](https://github.com/gbicou/directus-extension-imagga/commit/69f08185e6ab3fa1f922dbb61588644a315fbe13)) 48 | 49 | ## 1.6.2 50 | 51 | ### Patch Changes 52 | 53 | - [`dddc25c`](https://github.com/gbicou/directus-extension-imagga/commit/dddc25c1f5baa1e04e11e9508ed7441f80b1eed9) Thanks [@gbicou](https://github.com/gbicou)! - upgrade directus dependencies 54 | 55 | - [`ba631d6`](https://github.com/gbicou/directus-extension-imagga/commit/ba631d6c376338ebabd302e81820099d7696daad) Thanks [@gbicou](https://github.com/gbicou)! - use english as default language 56 | 57 | ## 1.6.1 58 | 59 | ### Patch Changes 60 | 61 | - [`21d9d0a`](https://github.com/gbicou/directus-extension-imagga/commit/21d9d0aca30e976a63fc608ecd1fc6ca127baf38) Thanks [@gbicou](https://github.com/gbicou)! - pin and upgrade deps 62 | 63 | ## 1.6.0 64 | 65 | ### Minor Changes 66 | 67 | - [`2a10ab4`](https://github.com/gbicou/directus-extension-imagga/commit/2a10ab48837568a6bf27cfc51d42cfa5b352b63e) Thanks [@gbicou](https://github.com/gbicou)! - upgrade dependencies 68 | 69 | ## 1.5.0 70 | 71 | ### Minor Changes 72 | 73 | - [`db4cb2d`](https://github.com/gbicou/directus-extension-imagga/commit/db4cb2d398cd306d91ad49aab83716c1b3aecc1d) Thanks [@gbicou](https://github.com/gbicou)! - upgrade directus dependency 74 | 75 | ## 1.4.0 76 | 77 | ### Minor Changes 78 | 79 | - [`7fff14c`](https://github.com/gbicou/directus-extension-imagga/commit/7fff14cd2c6122e786b3c9ea2c05444cb8ead717) Thanks [@gbicou](https://github.com/gbicou)! - directus upgrade 80 | 81 | ### Patch Changes 82 | 83 | - [`1ad6c97`](https://github.com/gbicou/directus-extension-imagga/commit/1ad6c9738bb5e44aad0fbf4d5edc2e0e9105b126) Thanks [@gbicou](https://github.com/gbicou)! - wait between api calls 84 | 85 | ## 1.3.1 86 | 87 | ### Patch Changes 88 | 89 | - [`16f2419`](https://github.com/gbicou/directus-extension-imagga/commit/16f2419a1220e4042957a621a2684ce70952526d) Thanks [@gbicou](https://github.com/gbicou)! - upgrade to directus api v15 90 | 91 | ### Features 92 | 93 | - upgrade directus api to v14 ([075c445](https://github.com/gbicou/directus-extension-imagga/commit/075c44585ebc57f7811c91ec9a6b0c8089abf146)) 94 | 95 | ## [1.2.2](https://github.com/gbicou/directus-extension-imagga/compare/v1.2.1...v1.2.2) (2023-06-05) 96 | 97 | ### Bug Fixes 98 | 99 | - asset service transformation parameters ([3dacb73](https://github.com/gbicou/directus-extension-imagga/commit/3dacb73cc000bd7e23ee99af38dbee37638167bf)) 100 | 101 | ## [1.2.1](https://github.com/gbicou/directus-extension-imagga/compare/v1.2.0...v1.2.1) (2023-05-12) 102 | 103 | ### Bug Fixes 104 | 105 | - use ESM build format ([d9f83f0](https://github.com/gbicou/directus-extension-imagga/commit/d9f83f0a9a66d6da04110b93418915b4f7c4c4d1)) 106 | 107 | # [1.2.0](https://github.com/gbicou/directus-extension-imagga/compare/v1.1.0...v1.2.0) (2023-04-28) 108 | 109 | ### Features 110 | 111 | - upgrade to directus v10 ([afa7530](https://github.com/gbicou/directus-extension-imagga/commit/afa75306b16ed960825c9a2ceafae8d5eb095841)), closes [#9](https://github.com/gbicou/directus-extension-imagga/issues/9) [#8](https://github.com/gbicou/directus-extension-imagga/issues/8) [#7](https://github.com/gbicou/directus-extension-imagga/issues/7) [#5](https://github.com/gbicou/directus-extension-imagga/issues/5) [#4](https://github.com/gbicou/directus-extension-imagga/issues/4) 112 | 113 | # [1.1.0](https://github.com/gbicou/directus-extension-imagga/compare/v1.0.1...v1.1.0) (2023-04-11) 114 | 115 | ### Bug Fixes 116 | 117 | - delete uploaded file at imagga ([55f7d6d](https://github.com/gbicou/directus-extension-imagga/commit/55f7d6d62f9c3f8a8b4882ec571a134487ca82a2)) 118 | 119 | ### Features 120 | 121 | - add imagga colors service ([c7f3916](https://github.com/gbicou/directus-extension-imagga/commit/c7f3916d63fd398552226b72cdd10dc485644261)) 122 | 123 | ## [1.0.1](https://github.com/gbicou/directus-extension-imagga/compare/v1.0.0...v1.0.1) (2023-04-10) 124 | 125 | ### Bug Fixes 126 | 127 | - publish readme ([fa45e55](https://github.com/gbicou/directus-extension-imagga/commit/fa45e55dbb770c4db79cc5334fb2eca8a5ab7e69)) 128 | 129 | # 1.0.0 (2023-04-10) 130 | 131 | ### Bug Fixes 132 | 133 | - log API errors ([0dd06b0](https://github.com/gbicou/directus-extension-imagga/commit/0dd06b0a3b6d087d9a693064989f0881b99e3506)) 134 | - wrong names ([20b5bef](https://github.com/gbicou/directus-extension-imagga/commit/20b5beff072d470429ee437914257f8c8267c1a1)) 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Benjamin VIELLARD 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imagga for Directus 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![License][license-src]][license-href] 6 | 7 | This is a [Directus](https://directus.io/) hook for file uploads to automatically tag images with [Imagga API](https://imagga.com/). 8 | 9 | - npm package : `@bicou/directus-extension-imagga` 10 | - [✨  Release Notes](/CHANGELOG.md) 11 | 12 | ## Requirements 13 | 14 | This extension requires Directus 10 or higher to be installed. 15 | 16 | ## Installation 17 | 18 | Add `@bicou/directus-extension-imagga` dependency to your directus project. 19 | 20 | ```bash 21 | # Using pnpm 22 | pnpm add @bicou/directus-extension-imagga 23 | # Using yarn 24 | yarn add @bicou/directus-extension-imagga 25 | # Using npm 26 | npm install @bicou/directus-extension-imagga 27 | ``` 28 | 29 | ## Usage 30 | 31 | Configure the extension with environment variables (in a .env file for example) : 32 | 33 | ### Authorization 34 | 35 | The credentials are available in [your Imagga user dashboard](https://imagga.com/profile/dashboard). 36 | 37 | | Key | Description | 38 | | --------------- | ---------------------------------------- | 39 | | `IMAGGA_KEY` | Imagga API key on your user dashboard | 40 | | `IMAGGA_SECRET` | Imagga API secret on your user dashboard | 41 | 42 | ### Configuration 43 | 44 | #### Tags service 45 | 46 | Automatically suggest textual tags from images. 47 | 48 | | Key | Description | Default | 49 | | ----------------------- | ------------------------------------------------ | ------------- | 50 | | `IMAGGA_TAGS_ENABLE` | enables auto tagging | true | 51 | | `IMAGGA_TAGS_LIMIT` | limits the number of tags in the result | -1 (no limit) | 52 | | `IMAGGA_TAGS_THRESHOLD` | thresholds the confidence of tags in the result | 0.0 | 53 | | `IMAGGA_TAGS_LANGUAGE` | get a translation of the tags in other languages | en | 54 | 55 | Please refer to [Imagga Tags API documentation](https://docs.imagga.com/#tags) for more information. 56 | 57 | #### Colors service 58 | 59 | Analyse and extract the predominant colors from images. 60 | 61 | | Key | Description | Default | 62 | | ---------------------- | ------------------------- | ------- | 63 | | `IMAGGA_COLORS_ENABLE` | enables colors extraction | false | 64 | 65 | Please refer to [Imagga Colors API documentation](https://docs.imagga.com/#colors) for more information. 66 | 67 | ## Demo 68 | 69 | An example of an image uploaded in the file library of Directus whose tags are automatically filled in : 70 | 71 | [Play screencast video](https://user-images.githubusercontent.com/174636/230939020-6f8871fb-ba9b-4ebf-bfc0-779b8c730741.webm) 72 | 73 | ## Results 74 | 75 | ### Tags 76 | 77 | The result is stored as an array of strings in the `directus_files.tags` field. 78 | 79 | ```json5 80 | { 81 | "tags": ["lake", "boathouse", "lakeside", "mountain", "shore", /*...*/] 82 | } 83 | ``` 84 | 85 | ### Colors 86 | 87 | The result is stored under the `colors` key of the `directus_files.metadata` field. 88 | 89 | ```json5 90 | { 91 | "colors": { 92 | "background": [ 93 | { "r": 25, "g": 40, "b": 36, "html_code": "#192824", "percent": 55.2785682678223 }, // ... 94 | ], 95 | "foreground": [ 96 | { "r": 92, "g": 88, "b": 92, "html_code": "#5c585c", "percent": 41.5135154724121 }, // ... 97 | ], 98 | "image": [ 99 | { "r": 24, "g": 36, "b": 34, "html_code": "#182422", "percent": 43.4364776611328 }, // ... 100 | ] 101 | } 102 | } 103 | ``` 104 | 105 | ## License 106 | 107 | This extension is released under the MIT license. See the LICENSE file for more details. 108 | 109 | 110 | 111 | [npm-version-src]: https://img.shields.io/npm/v/@bicou/directus-extension-imagga/latest.svg?style=flat&colorA=18181B&colorB=28CF8D 112 | [npm-version-href]: https://npmjs.com/package/@bicou/directus-extension-imagga 113 | [npm-downloads-src]: https://img.shields.io/npm/dm/@bicou/directus-extension-imagga.svg?style=flat&colorA=18181B&colorB=28CF8D 114 | [npm-downloads-href]: https://npmjs.com/package/@bicou/directus-extension-imagga 115 | [license-src]: https://img.shields.io/npm/l/@bicou/directus-extension-imagga.svg?style=flat&colorA=18181B&colorB=28CF8D 116 | [license-href]: https://npmjs.com/package/@bicou/directus-extension-imagga 117 | -------------------------------------------------------------------------------- /docker-compose.test.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | directus: 3 | image: directus/directus:11.8.0 4 | ports: 5 | - 8055:8055 6 | volumes: 7 | - ./:/directus/extensions/imagga 8 | env_file: ".env.test" 9 | healthcheck: 10 | test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:8055/server/health || exit 1"] 11 | start_period: 20s 12 | imagga: 13 | image: wiremock/wiremock:3.13.0 14 | ports: 15 | - 8080:8080 16 | volumes: 17 | - ./mocks:/home/wiremock 18 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js' 3 | import tseslint from 'typescript-eslint' 4 | import stylistic from '@stylistic/eslint-plugin' 5 | import unicorn from 'eslint-plugin-unicorn' 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: ['dist/'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommended, 13 | ...tseslint.configs.stylistic, 14 | stylistic.configs['recommended-flat'], 15 | unicorn.configs['flat/recommended'], 16 | ) 17 | -------------------------------------------------------------------------------- /extension.config.js: -------------------------------------------------------------------------------- 1 | import externals from 'rollup-plugin-node-externals' 2 | 3 | export default { 4 | plugins: [externals()], 5 | } 6 | -------------------------------------------------------------------------------- /mocks/mappings/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "request": { 4 | "method": "GET", 5 | "url": "/colors", 6 | "queryParameters": { 7 | "image_upload_id": { 8 | "equalTo": "i05e132196706b94b1d85efb5f3SaM1j" 9 | } 10 | } 11 | }, 12 | "response": { 13 | "status": 200, 14 | "jsonBody": { 15 | "result": { 16 | "colors": { 17 | "background_colors": [ 18 | { 19 | "b": 47, 20 | "closest_palette_color": "light bronze", 21 | "closest_palette_color_html_code": "#8c5e37", 22 | "closest_palette_color_parent": "skin", 23 | "closest_palette_distance": 1.70506228322597, 24 | "g": 92, 25 | "html_code": "#8c5c2f", 26 | "percent": 48.0033950805664, 27 | "r": 140 28 | }, 29 | { 30 | "b": 146, 31 | "closest_palette_color": "cerulean", 32 | "closest_palette_color_html_code": "#0074a8", 33 | "closest_palette_color_parent": "blue", 34 | "closest_palette_distance": 5.53350780052479, 35 | "g": 116, 36 | "html_code": "#467492", 37 | "percent": 39.0454025268555, 38 | "r": 70 39 | }, 40 | { 41 | "b": 30, 42 | "closest_palette_color": "dark bronze", 43 | "closest_palette_color_html_code": "#542e0c", 44 | "closest_palette_color_parent": "skin", 45 | "closest_palette_distance": 5.47689735887696, 46 | "g": 48, 47 | "html_code": "#4f301e", 48 | "percent": 12.9512014389038, 49 | "r": 79 50 | } 51 | ], 52 | "color_percent_threshold": 1.75, 53 | "color_variance": 36, 54 | "foreground_colors": [ 55 | { 56 | "b": 147, 57 | "closest_palette_color": "larkspur", 58 | "closest_palette_color_html_code": "#6e7e99", 59 | "closest_palette_color_parent": "blue", 60 | "closest_palette_distance": 8.60114706674971, 61 | "g": 125, 62 | "html_code": "#577d93", 63 | "percent": 52.3429222106934, 64 | "r": 87 65 | }, 66 | { 67 | "b": 145, 68 | "closest_palette_color": "pewter", 69 | "closest_palette_color_html_code": "#84898c", 70 | "closest_palette_color_parent": "grey", 71 | "closest_palette_distance": 1.75501013175431, 72 | "g": 142, 73 | "html_code": "#898e91", 74 | "percent": 30.0293598175049, 75 | "r": 137 76 | }, 77 | { 78 | "b": 42, 79 | "closest_palette_color": "brownie", 80 | "closest_palette_color_html_code": "#584039", 81 | "closest_palette_color_parent": "brown", 82 | "closest_palette_distance": 4.99189248709017, 83 | "g": 58, 84 | "html_code": "#593a2a", 85 | "percent": 17.6277160644531, 86 | "r": 89 87 | } 88 | ], 89 | "image_colors": [ 90 | { 91 | "b": 146, 92 | "closest_palette_color": "cerulean", 93 | "closest_palette_color_html_code": "#0074a8", 94 | "closest_palette_color_parent": "blue", 95 | "closest_palette_distance": 7.85085588656478, 96 | "g": 121, 97 | "html_code": "#547992", 98 | "percent": 48.3686981201172, 99 | "r": 84 100 | }, 101 | { 102 | "b": 46, 103 | "closest_palette_color": "light bronze", 104 | "closest_palette_color_html_code": "#8c5e37", 105 | "closest_palette_color_parent": "skin", 106 | "closest_palette_distance": 3.05634270891355, 107 | "g": 86, 108 | "html_code": "#83562e", 109 | "percent": 47.9353446960449, 110 | "r": 131 111 | }, 112 | { 113 | "b": 46, 114 | "closest_palette_color": "navy blue", 115 | "closest_palette_color_html_code": "#2b2e43", 116 | "closest_palette_color_parent": "navy blue", 117 | "closest_palette_distance": 6.62790662069936, 118 | "g": 27, 119 | "html_code": "#1f1b2e", 120 | "percent": 3.60131478309631, 121 | "r": 31 122 | } 123 | ], 124 | "object_percentage": 20.790994644165 125 | } 126 | }, 127 | "status": { 128 | "text": "", 129 | "type": "success" 130 | } 131 | } 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /mocks/mappings/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/" 4 | }, 5 | "response": { 6 | "status": 200, 7 | "body": "OK" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /mocks/mappings/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPath": "/tags", 5 | "queryParameters": { 6 | "image_upload_id": { 7 | "equalTo": "i05e132196706b94b1d85efb5f3SaM1j" 8 | } 9 | } 10 | }, 11 | "response": { 12 | "status": 200, 13 | "jsonBody": { 14 | "result": { 15 | "tags": [ 16 | { 17 | "confidence": 61.4116096496582, 18 | "tag": { 19 | "en": "mountain" 20 | } 21 | }, 22 | { 23 | "confidence": 54.3507270812988, 24 | "tag": { 25 | "en": "landscape" 26 | } 27 | } 28 | ] 29 | }, 30 | "status": { 31 | "text": "", 32 | "type": "success" 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mocks/mappings/uploads.json: -------------------------------------------------------------------------------- 1 | { 2 | "mappings": [ 3 | { 4 | "request": { 5 | "method": "POST", 6 | "url": "/uploads" 7 | }, 8 | "response": { 9 | "status": 200, 10 | "jsonBody": { 11 | "result": { 12 | "upload_id": "i05e132196706b94b1d85efb5f3SaM1j" 13 | }, 14 | "status": { 15 | "text": "", 16 | "type": "success" 17 | } 18 | } 19 | } 20 | }, 21 | { 22 | "request": { 23 | "method": "DELETE", 24 | "url": "/uploads/i05e132196706b94b1d85efb5f3SaM1j" 25 | }, 26 | "response": { 27 | "status": 200, 28 | "jsonBody": { 29 | "status": { 30 | "text": "", 31 | "type": "success" 32 | } 33 | } 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bicou/directus-extension-imagga", 3 | "description": "Automatic image tagging of Directus files with Imagga API", 4 | "homepage": "https://github.com/gbicou/directus-extension-imagga", 5 | "author": "Benjamin VIELLARD ", 6 | "license": "MIT", 7 | "repository": "github:gbicou/directus-extension-imagga", 8 | "icon": "extension", 9 | "version": "1.6.6", 10 | "keywords": [ 11 | "directus", 12 | "directus-extension", 13 | "directus-custom-hook", 14 | "image", 15 | "tag", 16 | "auto-tagging", 17 | "imagga" 18 | ], 19 | "directus:extension": { 20 | "type": "hook", 21 | "path": "dist/index.mjs", 22 | "source": "src/index.ts", 23 | "host": "^11.1.0" 24 | }, 25 | "type": "module", 26 | "files": [ 27 | "dist" 28 | ], 29 | "scripts": { 30 | "build": "directus-extension build", 31 | "dev": "directus-extension build -w --no-minify", 32 | "lint": "eslint .", 33 | "test": "vitest run" 34 | }, 35 | "dependencies": { 36 | "@directus/api": "^27.0.2", 37 | "@directus/extensions-sdk": "^13.1.0", 38 | "axios": "^1.9.0", 39 | "form-data": "^4.0.2" 40 | }, 41 | "devDependencies": { 42 | "@directus/sdk": "19.1.0", 43 | "@directus/tsconfig": "3.0.0", 44 | "@eslint/js": "9.28.0", 45 | "@stylistic/eslint-plugin": "4.4.0", 46 | "@types/node": "22.15.29", 47 | "dotenv": "16.5.0", 48 | "eslint": "9.28.0", 49 | "eslint-plugin-unicorn": "59.0.1", 50 | "rollup-plugin-node-externals": "8.0.0", 51 | "typescript": "5.8.3", 52 | "typescript-eslint": "8.33.0", 53 | "vitest": "3.1.4", 54 | "wiremock-captain": "4.1.1" 55 | }, 56 | "peerDependencies": { 57 | "@directus/api": "^25.0.1 || ^26.0.0 || ^27.0.0", 58 | "@directus/env": "^5.0.2", 59 | "@directus/extensions-sdk": "^13.0.3" 60 | }, 61 | "publishConfig": { 62 | "access": "public" 63 | }, 64 | "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977", 65 | "pnpm": { 66 | "ignoredBuiltDependencies": [ 67 | "oracledb", 68 | "sqlite3" 69 | ], 70 | "onlyBuiltDependencies": [ 71 | "argon2", 72 | "esbuild", 73 | "isolated-vm", 74 | "sharp", 75 | "vue-demi" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "bootstrap-sha": "7cd5ca43c57dca317484491248ecd8edf0e917c8", 4 | "always-update": true, 5 | "packages": { 6 | ".": { 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "addLabels": ["dependencies"], 5 | "packageRules": [ 6 | { 7 | "groupName": "directus", 8 | "matchPackageNames": ["@directus/**"] 9 | } 10 | ], 11 | "lockFileMaintenance": { 12 | "enabled": true, 13 | "automerge": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { defineHook } from '@directus/extensions-sdk' 2 | import { AssetsService } from '@directus/api/services/assets' 3 | import { FilesService } from '@directus/api/services/files' 4 | import axios from 'axios' 5 | import FormData from 'form-data' 6 | 7 | /** 8 | * Imagga API response base 9 | */ 10 | interface ApiResponse { 11 | status: { 12 | // depending on whether the request was processed successfully 13 | type: 'success' | 'error' 14 | // human-readable reason why the processing was unsuccessful 15 | text: string 16 | } 17 | } 18 | 19 | /** 20 | * Imagga API /upload response 21 | */ 22 | interface UploadResponse extends ApiResponse { 23 | result: { 24 | // the id of the uploaded file 25 | upload_id: string 26 | } 27 | } 28 | 29 | /** 30 | * Imagga API /tags response 31 | */ 32 | interface TagsResponse extends ApiResponse { 33 | result: { 34 | // an array of all the tags the API has suggested for this image 35 | tags: { 36 | // a number representing a percentage from 0 to 100 where 100 means that the API is absolutely sure this tag must be relevant 37 | // and confidence < 30 means that there is a higher chance that the tag might not be such 38 | confidence: number 39 | // the tag itself which could be an object, concept, color, etc. describing something from the photo scene 40 | tag: Record 41 | }[] 42 | } 43 | } 44 | 45 | /** 46 | * Imagga API color item 47 | */ 48 | interface ColorData { 49 | // red 50 | r: number 51 | // green 52 | g: number 53 | // blue 54 | b: number 55 | // hex code 56 | html_code: string 57 | // what part of the image is in this color 58 | percent: number 59 | } 60 | 61 | /** 62 | * Imagga API /colors response 63 | */ 64 | interface ColorsResponse extends ApiResponse { 65 | result: { 66 | // contains the information about the color analysis of the photo 67 | colors: { 68 | // an array with up to 3 color results 69 | background_colors: ColorData[] 70 | // an array containing up to 3 color results 71 | foreground_colors: ColorData[] 72 | // an array containing up to 5 color results 73 | image_colors: ColorData[] 74 | } 75 | } 76 | } 77 | 78 | function mapColorData(input: ColorData[]) { 79 | return input.map(({ r, g, b, html_code, percent }) => ({ 80 | r, 81 | g, 82 | b, 83 | html_code, 84 | percent, 85 | })) 86 | } 87 | 88 | export default defineHook(({ action }, { services, logger, env }) => { 89 | /** 90 | * Imagga API endpoint 91 | */ 92 | const IMAGGA_API = (env['IMAGGA_API'] as string) ?? 'https://api.imagga.com/v2' 93 | 94 | /** 95 | * Imagga API key 96 | */ 97 | const IMAGGA_KEY = (env['IMAGGA_KEY'] as string) ?? '' 98 | 99 | /** 100 | * Imagga API secret 101 | */ 102 | const IMAGGA_SECRET = (env['IMAGGA_SECRET'] as string) ?? '' 103 | 104 | /** 105 | * Retrieve tags 106 | */ 107 | const IMAGGA_TAGS_ENABLE = env['IMAGGA_TAGS_ENABLE'] as boolean 108 | 109 | /** 110 | * If you’d like to get a translation of the tags in other languages, you should use the language parameter. 111 | * Specify the languages you want to receive your results in, separated by comma. 112 | */ 113 | const IMAGGA_TAGS_LANGUAGE = env['IMAGGA_TAGS_LANGUAGE'] as string ?? 'en' 114 | 115 | /** 116 | * Limits the number of tags in the result to the number you set. 117 | */ 118 | const IMAGGA_TAGS_LIMIT = (env['IMAGGA_TAGS_LIMIT'] as string) ?? '-1' 119 | 120 | /** 121 | * Thresholds the confidence of tags in the result to the number you set. 122 | * By default, all tags with confidence above 7 are being returned, and you cannot go lower than that. 123 | */ 124 | const IMAGGA_TAGS_THRESHOLD = (env['IMAGGA_TAGS_THRESHOLD'] as string) ?? '0.0' 125 | 126 | /** 127 | * Extract colors 128 | */ 129 | const IMAGGA_COLORS_ENABLE = env['IMAGGA_COLORS_ENABLE'] as boolean 130 | 131 | const auth = { username: IMAGGA_KEY, password: IMAGGA_SECRET } 132 | 133 | action('files.upload', async ({ payload, key }, context) => { 134 | if (payload.type.startsWith('image/')) { 135 | // retrieve asset resized to API best pratices as a readable stream 136 | // @see https://docs.imagga.com/#best-practices 137 | const assets: AssetsService = new services.AssetsService(context) 138 | const { stream } = await assets.getAsset(key, { 139 | transformationParams: { 140 | key: 'imagga', 141 | format: 'jpeg', 142 | width: 300, 143 | height: 300, 144 | fit: 'outside', 145 | withoutEnlargement: true, 146 | }, 147 | }) 148 | 149 | // upload asset to imagga upload service 150 | const uploadData = new FormData() 151 | uploadData.append('image', stream) 152 | const uploadResponse = await axios.post(IMAGGA_API + '/uploads', uploadData, { 153 | auth, 154 | }) 155 | logger.debug(uploadResponse.data) 156 | if (uploadResponse.data.status.type !== 'success') { 157 | logger.error(uploadResponse.data.status.text) 158 | return 159 | } 160 | const upload_id = uploadResponse.data.result.upload_id 161 | 162 | // retrieve tags 163 | let tags: string[] = [] 164 | if (IMAGGA_TAGS_ENABLE) { 165 | const response = await axios.get(IMAGGA_API + '/tags', { 166 | params: { 167 | image_upload_id: upload_id, 168 | language: IMAGGA_TAGS_LANGUAGE, 169 | limit: IMAGGA_TAGS_LIMIT, 170 | threshold: IMAGGA_TAGS_THRESHOLD, 171 | }, 172 | auth, 173 | }) 174 | logger.debug(response.data) 175 | if (response.data.status.type !== 'success') { 176 | logger.error(response.data.status.text) 177 | return 178 | } 179 | tags = response.data.result.tags.map(tag => tag.tag[IMAGGA_TAGS_LANGUAGE] ?? '') 180 | } 181 | 182 | // retrieve colors 183 | let colors: object | undefined 184 | if (IMAGGA_COLORS_ENABLE) { 185 | // wait 1 second to avoid rate limiting 186 | if (IMAGGA_TAGS_ENABLE) await new Promise(resolve => setTimeout(resolve, 1000)) 187 | 188 | // imagga api call 189 | const response = await axios.get(IMAGGA_API + '/colors', { 190 | params: { 191 | image_upload_id: upload_id, 192 | }, 193 | auth, 194 | }) 195 | logger.debug(response.data) 196 | if (response.data.status.type !== 'success') { 197 | logger.error(response.data.status.text) 198 | return 199 | } 200 | const colorsData = response.data.result.colors 201 | colors = { 202 | background: mapColorData(colorsData.background_colors), 203 | foreground: mapColorData(colorsData.foreground_colors), 204 | image: mapColorData(colorsData.image_colors), 205 | } 206 | } 207 | 208 | // update file data 209 | const files: FilesService = new services.FilesService(context) 210 | const payloadTags: string[] = payload.tags ?? [] 211 | const payloadMetadata = payload.metadata ?? {} 212 | await files.updateOne( 213 | key, 214 | { tags: [...payloadTags, ...tags], metadata: { ...payloadMetadata, colors } }, 215 | { emitEvents: false }, 216 | ) 217 | 218 | // delete uploaded file to imagga 219 | await axios.delete(IMAGGA_API + '/uploads/' + upload_id, { 220 | auth, 221 | }) 222 | } 223 | }) 224 | }) 225 | -------------------------------------------------------------------------------- /tests/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { createDirectus, rest, staticToken, readExtensions, uploadFiles, readFile } from '@directus/sdk' 3 | import package_ from '../package.json' 4 | import { readFile as readFileFs } from 'node:fs/promises' 5 | import { WireMock } from 'wiremock-captain' 6 | 7 | describe('extension', () => { 8 | // directus client 9 | const directus = createDirectus(process.env.PUBLIC_URL as string) 10 | .with(rest()) 11 | .with(staticToken(process.env.ADMIN_TOKEN as string)) 12 | 13 | // imagga mock server 14 | const mock = new WireMock('http://127.0.0.1:8080') 15 | 16 | it('register correctly in directus', async () => { 17 | const extensions = await directus.request(readExtensions()) 18 | 19 | expect(extensions).toBeDefined() 20 | expect(extensions.map(extension => extension.schema?.name)).toContain(package_.name) 21 | }) 22 | 23 | it('tags image with imagga api', { timeout: 10_000 }, async () => { 24 | await mock.clearAllExceptDefault() 25 | 26 | // upload image 27 | const data = new FormData() 28 | const paris = await readFileFs(`${import.meta.dirname}/paris.jpeg`) 29 | data.append('file', new Blob([paris], { type: 'image/jpeg' })) 30 | const upload = await directus.request(uploadFiles(data)) 31 | 32 | expect(upload.id).toBeDefined() 33 | 34 | let apiUploads: unknown[] = [] 35 | let retry = 0 36 | while (retry < 5 && !(apiUploads?.length === 1)) { 37 | apiUploads = await mock.getRequestsForAPI('POST', '/uploads') 38 | if (!(apiUploads?.length === 1)) { 39 | await new Promise(resolve => setTimeout(resolve, 1000)) 40 | } 41 | retry++ 42 | } 43 | 44 | expect(apiUploads).toHaveLength(1) 45 | 46 | // check if the image is tagged with api results 47 | const file = await directus.request(readFile(upload.id)) 48 | expect(file).toBeDefined() 49 | expect(file.tags).toEqual(['mountain', 'landscape']) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/paris.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbicou/directus-extension-imagga/ca3b66aea7abac87d4665c942ff6be3bea4252ec/tests/paris.jpeg -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@directus/tsconfig/node22", 3 | "compilerOptions": { 4 | "moduleResolution": "Node16", 5 | "noPropertyAccessFromIndexSignature": false, 6 | "resolveJsonModule": true, 7 | "declaration": false, 8 | "rootDir": "./src", 9 | "esModuleInterop": true 10 | }, 11 | "include": ["./src/**/*.ts", "./tests/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import { config } from 'dotenv' 3 | 4 | export default defineConfig(({ mode }) => { 5 | return { 6 | test: { 7 | include: ['tests/*.{test,spec}.{js,ts}'], 8 | env: { ...config({ path: `.env.${mode}` }).parsed }, 9 | }, 10 | } 11 | }) 12 | --------------------------------------------------------------------------------