├── .github └── workflows │ ├── analyse.yml │ ├── changelog.yml │ ├── coverage.yml │ ├── style.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── art └── banner.png ├── composer.json ├── config └── image-optimize.php ├── dist └── js │ ├── statamic-image-optimize.js │ └── statamic-image-optimize.js.LICENSE.txt ├── package.json ├── phpstan.neon ├── phpunit.xml ├── resources ├── js │ ├── components │ │ └── cp │ │ │ └── image-resize │ │ │ └── ResizeForm.vue │ └── statamic-image-optimize.js ├── lang │ ├── en │ │ └── messages.php │ └── nl │ │ └── messages.php └── views │ └── cp │ └── image-resize │ └── index.blade.php ├── routes └── cp.php ├── src ├── Actions │ ├── OptimizeAssets.php │ ├── ResizeImage.php │ └── ResizeImages.php ├── Commands │ └── ResizeImagesCommand.php ├── Contracts │ ├── ResizesImage.php │ └── ResizesImages.php ├── Events │ ├── ImageEvent.php │ ├── ImageResizedEvent.php │ └── ImagesResizedEvent.php ├── Http │ └── Controllers │ │ └── CP │ │ └── ImageResizeController.php ├── Jobs │ ├── ResizeImageJob.php │ └── ResizeImagesJob.php ├── Listeners │ └── AssetUploadedListener.php └── ServiceProvider.php ├── tests ├── Actions │ ├── OptimizeAssetsTest.php │ ├── ResizeImageTest.php │ └── ResizeImagesTest.php ├── Http │ └── Controllers │ │ └── ImageResizeControllerTest.php ├── Jobs │ ├── ResizeImageJobTest.php │ └── ResizeImagesJobTest.php ├── Listeners │ └── AssetUploadedListenerTest.php ├── TestCase.php └── __fixtures__ │ ├── assets │ └── .gitignore │ ├── content │ └── assets │ │ └── test_container.yaml │ ├── dev-null │ └── .gitkeep │ └── uploads │ └── test.png └── webpack.mix.js /.github/workflows/analyse.yml: -------------------------------------------------------------------------------- 1 | name: analyse 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.3] 13 | laravel: [11.*] 14 | stability: [prefer-stable] 15 | include: 16 | - laravel: 11.* 17 | testbench: 9.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 30 | coverage: none 31 | 32 | - name: Install dependencies 33 | run: | 34 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 35 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 36 | - name: Analyse 37 | run: composer analyse -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [ published, edited, deleted ] 6 | 7 | jobs: 8 | generate: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | with: 15 | ref: ${{ github.event.release.target_commitish }} 16 | 17 | - name: Generate changelog 18 | uses: justbetter/generate-changelogs-action@main 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | repository: ${{ github.repository }} 23 | 24 | - name: Commit CHANGELOG 25 | uses: stefanzweifel/git-auto-commit-action@v4 26 | with: 27 | branch: ${{ github.event.release.target_commitish }} 28 | commit_message: Update CHANGELOG 29 | file_pattern: CHANGELOG.md -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.3] 13 | laravel: [11.*] 14 | stability: [prefer-stable] 15 | include: 16 | - laravel: 11.* 17 | testbench: 9.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, xdebug 30 | coverage: xdebug 31 | 32 | - name: Install dependencies 33 | run: | 34 | composer config allow-plugins.pestphp/pest-plugin true 35 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 36 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 37 | - name: Execute tests 38 | run: XDEBUG_MODE=coverage php vendor/bin/pest --coverage --min=100 -------------------------------------------------------------------------------- /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: style 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | style: 9 | name: Style 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: 8.3 20 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 21 | coverage: none 22 | 23 | - name: Install dependencies 24 | run: composer install 25 | 26 | - name: Style 27 | run: composer fix-style 28 | 29 | - name: Commit Changes 30 | uses: stefanzweifel/git-auto-commit-action@v4 31 | with: 32 | commit_message: Fix styling changes -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [ 'push', 'pull_request' ] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ ubuntu-latest ] 12 | php: [ 8.2, 8.3 ] 13 | laravel: [ 11.* ] 14 | stability: [ prefer-lowest, prefer-stable ] 15 | include: 16 | - laravel: 11.* 17 | testbench: 9.* 18 | exclude: 19 | - laravel: 11.* 20 | php: 8.1 21 | 22 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup PHP 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php }} 32 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 33 | coverage: none 34 | 35 | - name: Install dependencies 36 | run: | 37 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 38 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 39 | - name: Execute tests 40 | run: composer test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | node_modules 3 | composer.lock 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [Unreleased changes](https://github.com/justbetter/statamic-image-optimize/compare/4.0.0...main) 4 | ## [4.0.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/4.0.0) - 2025-05-20 5 | 6 | ### Added 7 | - Added Laravel 12 support (#33) 8 | 9 | ## [3.2.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/3.2.0) - 2024-11-05 10 | 11 | ### Added 12 | 13 | - Added tests (#30). 14 | 15 | ## [3.1.1](https://github.com/justbetter/statamic-image-optimize/releases/tag/3.1.1) - 2024-09-24 16 | 17 | ### What's Changed 18 | * Added banner by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/29 19 | 20 | 21 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/3.1.0...3.1.1 22 | 23 | ## [3.1.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/3.1.0) - 2024-08-01 24 | 25 | ### What's Changed 26 | * Added possibility to exclude asset containers from optimization by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/26 27 | 28 | 29 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/3.0.0...3.1.0 30 | 31 | ## [3.0.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/3.0.0) - 2024-06-21 32 | 33 | ### What's Changed 34 | * Feature/statamic 5 support by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/25 35 | 36 | 37 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/2.1.2...3.0.0 38 | 39 | ## [2.1.2](https://github.com/justbetter/statamic-image-optimize/releases/tag/2.1.2) - 2024-01-26 40 | 41 | ### What's Changed 42 | * Fix bug where image path was not set correctly by @physixc in https://github.com/justbetter/statamic-image-optimize/pull/24 43 | 44 | ### New Contributors 45 | * @physixc made their first contribution in https://github.com/justbetter/statamic-image-optimize/pull/24 46 | 47 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/2.1.1...2.1.2 48 | 49 | ## [2.1.1](https://github.com/justbetter/statamic-image-optimize/releases/tag/2.1.1) - 2023-09-06 50 | 51 | - Fix image resize job when not triggered by an event 52 | 53 | ## [2.1.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/2.1.0) - 2023-09-06 54 | 55 | Laravel 10 support 56 | 57 | ## [2.0.1](https://github.com/justbetter/statamic-image-optimize/releases/tag/2.0.1) - 2023-08-21 58 | 59 | ### [2.0.1] - 2023-08-21 60 | Added fix for image orientation 61 | 62 | ## [2.0.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/2.0.0) - 2023-06-22 63 | 64 | ### [2.0.0] - 2023-06-22 65 | Statamic 4 compatibility 66 | 67 | ## [1.3.3](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.3.3) - 2023-05-30 68 | 69 | ### What's Changed 70 | * Merge asset data instead of overriding by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/18 71 | 72 | 73 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.3.2...1.3.3 74 | 75 | ## [1.3.2](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.3.2) - 2023-05-11 76 | 77 | ### What's Changed 78 | * Shortened the routes by @BobWez98 in https://github.com/justbetter/statamic-image-optimize/pull/16 79 | 80 | ### New Contributors 81 | * @BobWez98 made their first contribution in https://github.com/justbetter/statamic-image-optimize/pull/16 82 | 83 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.3.1...1.3.2 84 | 85 | ## [1.3.1](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.3.1) - 2023-05-09 86 | 87 | ### What's Changed 88 | * Changed config file by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/15 89 | 90 | 91 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.3.0...1.3.1 92 | 93 | ## [1.3.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.3.0) - 2023-05-09 94 | 95 | ### What's Changed 96 | * Code optimizations by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/14 97 | 98 | 99 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.2.0...1.3.0 100 | 101 | ## [1.2.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.2.0) - 2023-04-11 102 | 103 | ### What's Changed 104 | * Added CP page by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/13 105 | 106 | 107 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.1.4...1.2.0 108 | 109 | ## [1.1.4](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.1.4) - 2022-12-06 110 | 111 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.1.3...1.1.4 112 | 113 | ## [1.1.3](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.1.3) - 2022-11-10 114 | 115 | ### What's Changed 116 | * Hydrate asset in bulk resize job by @stuartcusackie in https://github.com/justbetter/statamic-image-optimize/pull/7 117 | 118 | ### New Contributors 119 | * @stuartcusackie made their first contribution in https://github.com/justbetter/statamic-image-optimize/pull/7 120 | 121 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.1.2...1.1.3 122 | 123 | ## [1.1.2](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.1.2) - 2022-11-08 124 | 125 | ### What's Changed 126 | * Reworked asset write to also work for s3 buckets by @kevinmeijer97 in https://github.com/justbetter/statamic-image-optimize/pull/5 127 | 128 | Thanks to @stuartcusackie for reporting the issue 129 | 130 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.1.1...1.1.2 131 | 132 | ## [1.1.1](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.1.1) - 2022-11-07 133 | 134 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.1.0...1.1.1 135 | 136 | ## [1.1.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.1.0) - 2022-11-05 137 | 138 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.0.4...1.1.0 139 | 140 | ## [1.0.4](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.0.4) - 2022-11-01 141 | 142 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.0.3...1.0.4 143 | 144 | ## [1.0.3](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.0.3) - 2022-11-01 145 | 146 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.0.2...1.0.3 147 | 148 | ## [1.0.2](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.0.2) - 2022-11-01 149 | 150 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.0.1...1.0.2 151 | 152 | ## [1.0.1](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.0.1) - 2022-11-01 153 | 154 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/compare/1.0.0...1.0.1 155 | 156 | ## [1.0.0](https://github.com/justbetter/statamic-image-optimize/releases/tag/1.0.0) - 2022-10-28 157 | 158 | **Full Changelog**: https://github.com/justbetter/statamic-image-optimize/commits/1.0.0 159 | 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) JustBetter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Image Optimize 6 | 7 | > Image optimization after upload 8 | 9 | ## How to Install 10 | 11 | You can search for this addon in the `Tools > Addons` section of the Statamic control panel and click **install**, or run the following command from your project root: 12 | 13 | ``` bash 14 | composer require justbetter/statamic-image-optimize 15 | ``` 16 | 17 | ## Requirements 18 | The addon makes use of batches to optimize the images. 19 | Because of this you need an active Database connection that contains the `job_batches` table. 20 | You can generate this table by running the following commands: 21 | 22 | ``` 23 | php artisan queue:batches-table 24 | php artisan migrate 25 | ``` 26 | 27 | ## Config 28 | 29 | ### Publish 30 | 31 | ``` 32 | php artisan vendor:publish --provider="JustBetter\ImageOptimize\ServiceProvider" 33 | ``` 34 | 35 | ### Settings 36 | 37 | It's possible to change to default resize width and height by overriding the config file and changing the parameters within. 38 | 39 | 40 | ## Commands 41 | ``` 42 | php artisan justbetter:optimize:images 43 | ``` 44 | 45 | By running this command you can recursively optimize all the images in the assets folder. 46 | 47 | ### Options 48 | 49 | Add the `--forceAll` option to force the command to optimize all images. 50 | Otherwise the command will only optimize images that have not been optimized yet. 51 | 52 | You can also use the verbose option by adding `-v` to your command, 53 | this will show a progress bar containing the amount of jobs left in the batch. 54 | 55 | ## Features 56 | 57 | - After an image is uploaded an event will trigger to optimize the image. 58 | The event optimizes the images and resizes it to a specified size, this is being controlled by the config file. 59 | 60 | - By using the resize images command you can recursively optimize all the images in the assets folder. 61 | 62 | - Added an action in the CP Asset overview that allows you to select assets and trigger the optimize job manually. 63 | - Added an CP page to manually optimize all images, triggering this will show a progress bar containing the remaining images. 64 | -------------------------------------------------------------------------------- /art/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justbetter/statamic-image-optimize/00a4054241c06ca561069a3c077e93bb25308e23/art/banner.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "justbetter/statamic-image-optimize", 3 | "description": "Image optimization after upload", 4 | "type": "package", 5 | "license": "MIT", 6 | "keywords": [ 7 | "justbetter", 8 | "asset-uploads" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Kevin Meijer", 13 | "email": "kevin@justbetter.nl", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.2|^8.3", 19 | "ext-fileinfo": "*", 20 | "statamic/cms": "^5.0", 21 | "laravel/framework": "^11.0 || ^12.0", 22 | "league/glide": "^2.3" 23 | }, 24 | "require-dev": { 25 | "laravel/pint": "^1.7", 26 | "larastan/larastan": "^2.5", 27 | "phpstan/phpstan-mockery": "^1.1", 28 | "phpunit/phpunit": "^10.1 || ^11.5", 29 | "orchestra/testbench": "^8.0|^9.0|^10.0", 30 | "pestphp/pest": "^2.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "JustBetter\\ImageOptimize\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "JustBetter\\ImageOptimize\\Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "phpunit", 44 | "analyse": "phpstan", 45 | "style": "pint --test", 46 | "quality": [ 47 | "@test", 48 | "@analyse", 49 | "@style" 50 | ], 51 | "fix-style": "pint" 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "allow-plugins": { 56 | "pixelfear/composer-dist-plugin": true, 57 | "pestphp/pest-plugin": true 58 | } 59 | }, 60 | "extra": { 61 | "statamic": { 62 | "name": "Image Optimize", 63 | "description": "Image optimization after upload" 64 | }, 65 | "laravel": { 66 | "providers": [ 67 | "JustBetter\\ImageOptimize\\ServiceProvider" 68 | ] 69 | } 70 | }, 71 | "minimum-stability": "stable", 72 | "prefer-stable": true 73 | } 74 | -------------------------------------------------------------------------------- /config/image-optimize.php: -------------------------------------------------------------------------------- 1 | env('IMAGE_OPTIMIZE_WIDTH', 1600), 6 | 7 | // Set the default resize height in pixels 8 | 'default_resize_height' => env('IMAGE_OPTIMIZE_HEIGHT', 1600), 9 | 10 | // Set the default queue name 11 | 'default_queue_name' => env('IMAGE_OPTIMIZE_QUEUE_NAME', 'default'), 12 | 13 | // Set the default queue connection 14 | 'default_queue_connection' => env('IMAGE_OPTIMIZE_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')), 15 | 16 | // The following mime types will be used to optimize images 17 | 'mime_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], 18 | 19 | // You can exclude containers from optimization entirely here 20 | 'excluded_containers' => [], 21 | ]; 22 | -------------------------------------------------------------------------------- /dist/js/statamic-image-optimize.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see statamic-image-optimize.js.LICENSE.txt */ 2 | (()=>{"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(){e=function(){return r};var r={},n=Object.prototype,o=n.hasOwnProperty,i=Object.defineProperty||function(t,e,r){t[e]=r.value},s="function"==typeof Symbol?Symbol:{},a=s.iterator||"@@iterator",c=s.asyncIterator||"@@asyncIterator",u=s.toStringTag||"@@toStringTag";function l(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{l({},"")}catch(t){l=function(t,e,r){return t[e]=r}}function f(t,e,r,n){var o=e&&e.prototype instanceof p?e:p,s=Object.create(o.prototype),a=new k(n||[]);return i(s,"_invoke",{value:C(t,r,a)}),s}function h(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}r.wrap=f;var d={};function p(){}function v(){}function m(){}var g={};l(g,a,(function(){return this}));var y=Object.getPrototypeOf,b=y&&y(y(L([])));b&&b!==n&&o.call(b,a)&&(g=b);var w=m.prototype=p.prototype=Object.create(g);function x(t){["next","throw","return"].forEach((function(e){l(t,e,(function(t){return this._invoke(e,t)}))}))}function _(e,r){function n(i,s,a,c){var u=h(e[i],e,s);if("throw"!==u.type){var l=u.arg,f=l.value;return f&&"object"==t(f)&&o.call(f,"__await")?r.resolve(f.__await).then((function(t){n("next",t,a,c)}),(function(t){n("throw",t,a,c)})):r.resolve(f).then((function(t){l.value=t,a(l)}),(function(t){return n("throw",t,a,c)}))}c(u.arg)}var s;i(this,"_invoke",{value:function(t,e){function o(){return new r((function(r,o){n(t,e,r,o)}))}return s=s?s.then(o,o):o()}})}function C(t,e,r){var n="suspendedStart";return function(o,i){if("executing"===n)throw new Error("Generator is already running");if("completed"===n){if("throw"===o)throw i;return E()}for(r.method=o,r.arg=i;;){var s=r.delegate;if(s){var a=z(s,r);if(a){if(a===d)continue;return a}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if("suspendedStart"===n)throw n="completed",r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n="executing";var c=h(t,e,r);if("normal"===c.type){if(n=r.done?"completed":"suspendedYield",c.arg===d)continue;return{value:c.arg,done:r.done}}"throw"===c.type&&(n="completed",r.method="throw",r.arg=c.arg)}}}function z(t,e){var r=e.method,n=t.iterator[r];if(void 0===n)return e.delegate=null,"throw"===r&&t.iterator.return&&(e.method="return",e.arg=void 0,z(t,e),"throw"===e.method)||"return"!==r&&(e.method="throw",e.arg=new TypeError("The iterator does not provide a '"+r+"' method")),d;var o=h(n,t.iterator,e.arg);if("throw"===o.type)return e.method="throw",e.arg=o.arg,e.delegate=null,d;var i=o.arg;return i?i.done?(e[t.resultName]=i.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=void 0),e.delegate=null,d):i:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,d)}function j(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function O(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function k(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(j,this),this.reset(!0)}function L(t){if(t){var e=t[a];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var r=-1,n=function e(){for(;++r=0;--n){var i=this.tryEntries[n],s=i.completion;if("root"===i.tryLoc)return r("end");if(i.tryLoc<=this.prev){var a=o.call(i,"catchLoc"),c=o.call(i,"finallyLoc");if(a&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&o.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),O(r),d}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;O(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:L(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=void 0),d}},r}function r(t,e,r,n,o,i,s){try{var a=t[i](s),c=a.value}catch(t){return void r(t)}a.done?e(c):Promise.resolve(c).then(n,o)}function n(t){return function(){var e=this,n=arguments;return new Promise((function(o,i){var s=t.apply(e,n);function a(t){r(s,o,i,a,c,"next",t)}function c(t){r(s,o,i,a,c,"throw",t)}a(void 0)}))}}var o=function(t,e,r,n,o,i,s,a){var c,u="function"==typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=r,u._compiled=!0),n&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),s?(c=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),o&&o.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(s)},u._ssrRegister=c):o&&(c=a?function(){o.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:o),c)if(u.functional){u._injectStyles=c;var l=u.render;u.render=function(t,e){return c.call(e),l(t,e)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,c):[c]}return{exports:t,options:u}}({data:function(){return{batchId:null,jobCount:this.unoptimizedAssets,currentJobCount:this.unoptimizedAssets,jobsDone:0,checkJobs:!1,jobStarted:!1,resizeUrl:"/cp/statamic-image-optimize/resize-images/",resizeAllUrl:"/cp/statamic-image-optimize/resize-images/force-all",resizeCheckUrl:"/cp/statamic-image-optimize/resize-images-count/"}},props:{title:String,buttonText:String,totalAssets:Number,unoptimizedAssets:Number,canOptimize:Number},computed:{loadingMessage:function(){return this.jobStarted?(this.jobsDone=this.jobCount-this.currentJobCount,this.jobsDone+" of "+this.jobCount+" images have been optimized."):""},checkAllDisabled:function(){return this.unoptimizedAssets>0},canOptimizeAssets:function(){return this.canOptimize>=1}},methods:{onTriggerResizeImages:function(){var t=arguments,r=this;return n(e().mark((function o(){var i,s,a;return e().wrap((function(o){for(;;)switch(o.prev=o.next){case 0:return i=t.length>0&&void 0!==t[0]&&t[0],s=r,r.jobStarted=!0,r.checkJobs=!0,o.next=6,r.resizeImages(i);case 6:a=setInterval(n(e().mark((function t(){var r;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,s.checkResizeImages();case 2:if(void 0!==(r=t.sent).assetsToOptimize&&void 0!==r.assetTotal){t.next=7;break}return this.checkJobs=!1,clearInterval(a),t.abrupt("return");case 7:s.jobCount=r.assetTotal,s.currentJobCount=r.assetsToOptimize,0===r.assetsToOptimize&&(this.checkJobs=!1,clearInterval(a));case 10:case"end":return t.stop()}}),t,this)}))),1e3);case 7:case"end":return o.stop()}}),o)})))()},resizeImages:function(){var t=arguments,r=this;return n(e().mark((function n(){var o,i;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return o=t.length>0&&void 0!==t[0]&&t[0],i=r,e.next=4,fetch(o?r.resizeAllUrl:r.resizeUrl).then((function(t){return t.json()})).then((function(t){return i.batchId=t.batchId,i.checkJobs=!1,t})).catch((function(t){console.error(t)}));case 4:return e.abrupt("return",e.sent);case 5:case"end":return e.stop()}}),n)})))()},checkResizeImages:function(){var t=this;return n(e().mark((function r(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,fetch(t.resizeCheckUrl+t.batchId).then((function(t){return t.json()})).then((function(t){return t})).catch((function(t){console.error(t)}));case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),r)})))()}}},(function(){var t=this,e=t._self._c;return e("div",[e("div",{staticClass:"flex items-center justify-between mb-3"},[e("h1",{domProps:{textContent:t._s(t.title)}}),t._v(" "),t.canOptimizeAssets?e("div",[e("button",{staticClass:"btn-primary",attrs:{disabled:t.checkJobs||!t.checkAllDisabled},on:{click:function(e){return t.onTriggerResizeImages(!1)}}},[t._v("Optimize remaining images")]),t._v(" "),e("button",{staticClass:"btn-primary",attrs:{disabled:t.checkJobs},on:{click:function(e){return t.onTriggerResizeImages(!0)}}},[t._v("Optimize all images")])]):t._e()]),t._v(" "),e("div",{directives:[{name:"show",rawName:"v-show",value:!t.canOptimizeAssets,expression:"!canOptimizeAssets"}],staticClass:"mt-2"},[t._m(0)]),t._v(" "),e("div",{directives:[{name:"show",rawName:"v-show",value:!t.loadingMessage&&t.canOptimizeAssets,expression:"!loadingMessage && canOptimizeAssets"}],staticClass:"mt-2"},[e("div",{staticClass:"w-full mb-2"},[e("div",{staticClass:"mt-2 grid grid-cols-1 gap-5 sm:grid-cols-2"},[e("div",{staticClass:"overflow-hidden rounded-lg bg-white shadow"},[e("div",{staticClass:"flex flex-col justify-between items-center w-full h-full p-5"},[e("div",{staticClass:"truncate text-xl text-gray-500"},[t._v("\n Total amount of images\n ")]),t._v(" "),e("div",{staticClass:"text-5xl font-medium text-gray-900",domProps:{textContent:t._s(t.totalAssets)}})])]),t._v(" "),e("div",{staticClass:"overflow-hidden rounded-lg bg-white shadow"},[e("div",{staticClass:"flex flex-col justify-between items-center w-full h-full p-5"},[e("div",{staticClass:"truncate text-xl text-gray-500"},[t._v("\n Images to optimize\n ")]),t._v(" "),e("div",{staticClass:"text-5xl font-medium text-gray-900",domProps:{textContent:t._s(t.unoptimizedAssets)}})])])])])]),t._v(" "),e("div",{directives:[{name:"show",rawName:"v-show",value:t.loadingMessage&&t.canOptimizeAssets,expression:"loadingMessage && canOptimizeAssets"}],staticClass:"mt-2"},[e("div",{staticClass:"w-full mb-2"},[e("div",{staticClass:"mt-2 grid grid-cols-1"},[e("div",{staticClass:"overflow-hidden rounded-lg bg-white shadow"},[e("div",{staticClass:"flex flex-col justify-between items-center w-full h-full p-5"},[e("div",{staticClass:"truncate text-xl text-gray-500",domProps:{textContent:t._s(t.loadingMessage)}})])])])])])])}),[function(){var t=this._self._c;return t("ul",{staticClass:"card p-0 mb-2"},[t("li",{staticClass:"flex items-center justify-between py-1 px-2 border-b group"},[this._v("\n You need an active database connection in order to use the optimize addon.\n ")])])}],!1,null,null,null);const i=o.exports;Statamic.booting((function(){Statamic.component("justbetter-statamic-optimize-image-form",i)}))})(); -------------------------------------------------------------------------------- /dist/js/statamic-image-optimize.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statamic-image-optimize", 3 | "private": true, 4 | "description": "Image optimization after upload", 5 | "scripts": { 6 | "development": "mix", 7 | "watch": "mix watch", 8 | "production": "mix --production" 9 | }, 10 | "author": "JustBetter", 11 | "dependencies": { 12 | "vue": "^2.6.12", 13 | "vue-loader": "^15.9.8", 14 | "vue-template-compiler": "^2.6.12" 15 | }, 16 | "devDependencies": { 17 | "cross-env": "^7.0.3", 18 | "laravel-mix": "^6.0.49", 19 | "postcss": "^8.4.21" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/larastan/larastan/extension.neon 3 | - ./vendor/phpstan/phpstan-mockery/extension.neon 4 | 5 | parameters: 6 | paths: 7 | - src 8 | - tests 9 | level: 8 10 | ignoreErrors: 11 | - identifier: missingType.iterableValue 12 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/* 6 | 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/js/components/cp/image-resize/ResizeForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Optimize remaining images 7 | Optimize all images 8 | 9 | 10 | 11 | 12 | 13 | 14 | You need an active database connection in order to use the optimize addon. 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Total amount of images 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Images to optimize 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 154 | -------------------------------------------------------------------------------- /resources/js/statamic-image-optimize.js: -------------------------------------------------------------------------------- 1 | import ResizeForm from './components/cp/image-resize/ResizeForm'; 2 | 3 | Statamic.booting(() => { 4 | Statamic.component('justbetter-statamic-optimize-image-form', ResizeForm); 5 | }); -------------------------------------------------------------------------------- /resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | 'Optimize', 5 | ]; 6 | -------------------------------------------------------------------------------- /resources/lang/nl/messages.php: -------------------------------------------------------------------------------- 1 | 'Optimaliseer', 5 | ]; 6 | -------------------------------------------------------------------------------- /resources/views/cp/image-resize/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('statamic::layout') 2 | 3 | @section('content') 4 | 5 | @stop 6 | -------------------------------------------------------------------------------- /routes/cp.php: -------------------------------------------------------------------------------- 1 | name('statamic-image-optimize.') 8 | ->controller(ImageResizeController::class) 9 | ->group(function () { 10 | Route::get('/', 'index')->name('index'); 11 | Route::get('/resize-images/{forceAll?}', 'resizeImages')->name('resize-images'); 12 | Route::get('/resize-images-count/{batchId?}', 'resizeImagesJobCount')->name('resize-images-count'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/Actions/OptimizeAssets.php: -------------------------------------------------------------------------------- 1 | visibleTo($items->first()); 26 | } 27 | 28 | // @phpstan-ignore-next-line 29 | public function run($assets, $values): void 30 | { 31 | // @phpstan-ignore-next-line 32 | collect($assets ?? []) 33 | ->filter(fn (mixed $asset): bool => $asset instanceof Asset) 34 | ->each(function (Asset $asset): void { 35 | ResizeImageJob::dispatch($asset); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Actions/ResizeImage.php: -------------------------------------------------------------------------------- 1 | exists() || 17 | ! $asset->isImage() || 18 | in_array($asset->containerHandle(), config('image-optimize.excluded_containers')) 19 | ) { 20 | return; 21 | } 22 | 23 | $width ??= (int) config('image-optimize.default_resize_width'); 24 | $height ??= (int) config('image-optimize.default_resize_height'); 25 | 26 | // Prevents exceptions occurring when resizing non-compatible filetypes like SVG. 27 | try { 28 | $orientedImage = Image::make($asset->resolvedPath())->orientate(); 29 | 30 | $image = (new Size)->runMaxResize($orientedImage, $width, $height); 31 | 32 | $asset->disk()->filesystem()->put($asset->path(), $image->encode()); 33 | 34 | $asset->merge(['image-optimized' => '1']); 35 | 36 | $asset->save(); 37 | $asset->meta(); 38 | } catch (NotReadableException) { 39 | return; 40 | } 41 | 42 | ImageResizedEvent::dispatch(); 43 | } 44 | 45 | public static function bind(): void 46 | { 47 | app()->singleton(ResizesImage::class, static::class); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Actions/ResizeImages.php: -------------------------------------------------------------------------------- 1 | getOptimizableAssets() // @phpstan-ignore-line 22 | ->when(! $forceAll, fn () => $assets->whereNull('image-optimized')); 23 | 24 | $jobs = $assets 25 | ->filter(fn (Asset $asset): bool => $asset->isImage()) 26 | ->map(fn (Asset $asset) => new ResizeImageJob($asset->hydrate())); 27 | 28 | return Bus::batch($jobs) 29 | ->name('image-optimize') 30 | ->onConnection(config('image-optimize.default_queue_connection')) 31 | ->onQueue(config('image-optimize.default_queue_name')) 32 | ->then(function (): void { 33 | ImagesResizedEvent::dispatch(); 34 | }) 35 | ->dispatch(); 36 | } 37 | 38 | public static function bind(): void 39 | { 40 | app()->singleton(ResizesImages::class, static::class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/ResizeImagesCommand.php: -------------------------------------------------------------------------------- 1 | option('forceAll'); 21 | 22 | try { 23 | DB::connection()->getPdo(); 24 | } catch (\Exception $e) { 25 | $this->error('You need an active database connection in order to use the optimize addon.'); 26 | 27 | return static::FAILURE; 28 | } 29 | 30 | if ($this->getOutput()->isVerbose()) { 31 | $this->line('Starting the resize images job'); 32 | 33 | if ($forceAll) { 34 | $this->comment('Forcing to optimize all images'); 35 | } 36 | 37 | $batch = $resizesImages->resize($forceAll); 38 | 39 | $progress = $this->output->createProgressBar($batch->totalJobs); 40 | $progress->start(); 41 | 42 | while ($batch->pendingJobs && ! $batch->finished() && ! $batch->cancelled()) { 43 | $batch = $batch->fresh(); 44 | $progress->setProgress($batch->processedJobs()); 45 | } 46 | 47 | $progress->finish(); 48 | 49 | $this->output->newLine(2); 50 | $this->info('All images have been resized'); 51 | } else { 52 | ResizeImagesJob::dispatch($forceAll); 53 | $this->info('Jobs dispatched'); 54 | } 55 | 56 | return static::SUCCESS; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Contracts/ResizesImage.php: -------------------------------------------------------------------------------- 1 | getOptimizableAssets(); // @phpstan-ignore-line 22 | $unoptimizedAssets = $assets->whereNull('image-optimized'); 23 | $databaseConnected = true; 24 | 25 | try { 26 | DB::connection()->getPdo(); 27 | } catch (\Exception $e) { 28 | $databaseConnected = false; 29 | } 30 | 31 | return view('statamic-image-optimize::cp.image-resize.index', [ 32 | 'title' => 'JustBetter Image Optimize', 33 | 'total_assets' => $assets->count(), 34 | 'unoptimized_assets' => $unoptimizedAssets->count(), 35 | 'can_optimize' => $databaseConnected, 36 | ]); 37 | } 38 | 39 | public function resizeImages(ResizesImages $resizesImages, ?string $forceAll = null): JsonResponse 40 | { 41 | $batch = $resizesImages->resize($forceAll !== null); 42 | 43 | return response()->json([ 44 | 'imagesOptimized' => true, 45 | 'batchId' => $batch->id, 46 | ]); 47 | } 48 | 49 | public function resizeImagesJobCount(?string $batchId = null): JsonResponse 50 | { 51 | $batch = $batchId ? Bus::findBatch($batchId) : null; 52 | 53 | if ($batch) { 54 | return response()->json([ 55 | 'assetsToOptimize' => $batch->pendingJobs, 56 | 'assetTotal' => $batch->totalJobs, 57 | ]); 58 | } 59 | 60 | $allAssets = Asset::all(); 61 | $assets = $allAssets->getOptimizableAssets() // @phpstan-ignore-line 62 | ->whereNull('image-optimized'); 63 | 64 | return response()->json([ 65 | 'assetsToOptimize' => $assets->count(), 66 | 'assetTotal' => $allAssets->count(), 67 | ]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Jobs/ResizeImageJob.php: -------------------------------------------------------------------------------- 1 | onConnection(config('image-optimize.default_queue_connection')); 27 | $this->onQueue(config('image-optimize.default_queue_name')); 28 | } 29 | 30 | public function handle(ResizesImage $contract): void 31 | { 32 | $contract->resize($this->asset, $this->width, $this->height); 33 | } 34 | 35 | public function uniqueId(): string 36 | { 37 | return $this->asset->id(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Jobs/ResizeImagesJob.php: -------------------------------------------------------------------------------- 1 | onConnection(config('image-optimize.default_queue_connection')); 22 | $this->onQueue(config('image-optimize.default_queue_name')); 23 | } 24 | 25 | public function handle(ResizesImages $contract): void 26 | { 27 | $contract->resize($this->forceAll); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Listeners/AssetUploadedListener.php: -------------------------------------------------------------------------------- 1 | asset; 16 | 17 | ResizeImageJob::dispatch($asset); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/../routes/cp.php', 25 | ]; 26 | 27 | protected $scripts = [ 28 | __DIR__.'/../dist/js/statamic-image-optimize.js', 29 | ]; 30 | 31 | public function register(): void 32 | { 33 | $this->registerConfig() 34 | ->registerActions() 35 | ->registerMacros(); 36 | } 37 | 38 | protected function registerConfig(): static 39 | { 40 | $this->mergeConfigFrom(__DIR__.'/../config/image-optimize.php', 'image-optimize'); 41 | 42 | return $this; 43 | } 44 | 45 | protected function registerActions(): static 46 | { 47 | ResizeImage::bind(); 48 | ResizeImages::bind(); 49 | 50 | return $this; 51 | } 52 | 53 | protected function registerMacros(): static 54 | { 55 | AssetCollection::macro('getOptimizableAssets', function () { 56 | return $this 57 | ->whereNotIn('container', config('image-optimize.excluded_containers')) 58 | ->whereIn('mime_type', config('image-optimize.mime_types')); 59 | }); 60 | 61 | return $this; 62 | } 63 | 64 | public function boot(): void 65 | { 66 | parent::boot(); 67 | 68 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'statamic-image-optimize'); 69 | 70 | $this->bootPublishables() 71 | ->bootEvents() 72 | ->bootCommands() 73 | ->bootNav() 74 | ->handleTranslations(); 75 | } 76 | 77 | public function bootEvents(): static 78 | { 79 | Event::listen([AssetUploaded::class, AssetReuploaded::class], AssetUploadedListener::class); 80 | 81 | return $this; 82 | } 83 | 84 | protected function bootCommands(): static 85 | { 86 | $this->commands([ 87 | ResizeImagesCommand::class, 88 | ]); 89 | 90 | return $this; 91 | } 92 | 93 | protected function bootPublishables(): static 94 | { 95 | $this->publishes([ 96 | __DIR__.'/../config/image-optimize.php' => config_path('image-optimize.php'), 97 | ], 'config'); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * @codeCoverageIgnore 104 | */ 105 | protected function bootNav(): static 106 | { 107 | Nav::extend(function (Navigation $nav): void { 108 | $nav->create('Image Optimize') 109 | ->section('Tools') 110 | ->route('statamic-image-optimize.index') 111 | ->icon('collection'); 112 | }); 113 | 114 | return $this; 115 | } 116 | 117 | protected function handleTranslations(): static 118 | { 119 | $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'image-optimize'); 120 | 121 | $this->publishes([ 122 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/statamic-image-optimize'), 123 | ], 'image-optimize-translations'); 124 | 125 | return $this; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Actions/OptimizeAssetsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Optimize', OptimizeAssets::title()); 17 | } 18 | 19 | #[Test] 20 | public function it_can_be_visible(): void 21 | { 22 | $asset = $this->createAsset(); 23 | 24 | /** @var OptimizeAssets $action */ 25 | $action = app(OptimizeAssets::class); 26 | 27 | $this->assertTrue( 28 | $action->visibleTo($asset) 29 | ); 30 | } 31 | 32 | #[Test] 33 | public function it_can_be_visible_to_bulk(): void 34 | { 35 | $assets = collect([ 36 | $this->createAsset(), 37 | ]); 38 | 39 | /** @var OptimizeAssets $action */ 40 | $action = app(OptimizeAssets::class); 41 | 42 | $this->assertTrue( 43 | $action->visibleToBulk($assets) 44 | ); 45 | } 46 | 47 | #[Test] 48 | public function it_can_run(): void 49 | { 50 | Bus::fake(); 51 | 52 | $assets = collect([ 53 | $this->createAsset(), 54 | 'asset', 55 | false, 56 | null, 57 | ]); 58 | 59 | /** @var OptimizeAssets $action */ 60 | $action = app(OptimizeAssets::class); 61 | $action->run($assets, null); 62 | 63 | Bus::assertDispatched(ResizeImageJob::class, 1); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Actions/ResizeImageTest.php: -------------------------------------------------------------------------------- 1 | createAsset(); 21 | 22 | /** @var ResizeImage $action */ 23 | $action = app(ResizeImage::class); 24 | $action->resize($asset, 100, 100); 25 | 26 | $this->assertEquals(100, $asset->meta('width')); 27 | $this->assertEquals(63, $asset->meta('height')); 28 | $this->assertEquals(1, $asset->meta('data.image-optimized')); 29 | 30 | Event::assertDispatched(ImageResizedEvent::class); 31 | } 32 | 33 | #[Test] 34 | public function it_can_ignore_excluded_containers(): void 35 | { 36 | Event::fake(); 37 | 38 | config()->set('image-optimize.excluded_containers', [ 39 | 'test_container', 40 | ]); 41 | 42 | $asset = $this->createAsset(); 43 | 44 | /** @var ResizeImage $action */ 45 | $action = app(ResizeImage::class); 46 | $action->resize($asset); 47 | 48 | Event::assertNotDispatched(ImageResizedEvent::class); 49 | } 50 | 51 | #[Test] 52 | public function it_can_catch_exceptions(): void 53 | { 54 | Event::fake(); 55 | 56 | Image::spy() 57 | ->shouldReceive('make') 58 | ->andThrow(NotReadableException::class); 59 | 60 | $asset = $this->createAsset(); 61 | 62 | /** @var ResizeImage $action */ 63 | $action = app(ResizeImage::class); 64 | $action->resize($asset); 65 | 66 | Event::assertNotDispatched(ImageResizedEvent::class); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Actions/ResizeImagesTest.php: -------------------------------------------------------------------------------- 1 | createAsset(); 23 | 24 | /** @var ResizeImages $action */ 25 | $action = app(ResizeImages::class); 26 | $action->resize(); 27 | 28 | Bus::assertBatched(fn (PendingBatchFake $batch): bool => $batch->jobs->count() === 1); 29 | } 30 | 31 | #[Test] 32 | #[WithMigration('queue')] 33 | public function it_can_dispatch_events(): void 34 | { 35 | Bus::fake(); 36 | Event::fake(); 37 | 38 | /** @var ResizeImages $action */ 39 | $action = app(ResizeImages::class); 40 | 41 | $batch = $action->resize(); 42 | 43 | /** @var non-empty-array $thenCallbacks */ 44 | $thenCallbacks = $batch->then; // @phpstan-ignore-line 45 | 46 | call_user_func($thenCallbacks[0], $batch); 47 | 48 | Event::assertDispatched(ImagesResizedEvent::class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Http/Controllers/ImageResizeControllerTest.php: -------------------------------------------------------------------------------- 1 | toImmutable()); 18 | 19 | $this->mock(ResizesImages::class, function (MockInterface $mock) use ($fakeBatch): void { 20 | $mock 21 | ->shouldReceive('resize') 22 | ->andReturn($fakeBatch); 23 | }); 24 | 25 | $this 26 | ->withoutMiddleware() 27 | ->get(route('statamic.cp.statamic-image-optimize.resize-images')) 28 | ->assertSuccessful() 29 | ->assertJson([ 30 | 'imagesOptimized' => true, 31 | 'batchId' => '::batch-id::', 32 | ]); 33 | } 34 | 35 | #[Test] 36 | public function it_can_get_resize_images_count(): void 37 | { 38 | Bus::fake(); 39 | 40 | $this->createAsset(); 41 | 42 | $this 43 | ->withoutMiddleware() 44 | ->get(route('statamic.cp.statamic-image-optimize.resize-images-count')) 45 | ->assertSuccessful() 46 | ->assertJson([ 47 | 'assetsToOptimize' => 1, 48 | 'assetTotal' => 1, 49 | ]); 50 | } 51 | 52 | #[Test] 53 | public function it_can_get_resize_images_count_with_batch(): void 54 | { 55 | Bus::fake(); 56 | 57 | $this->createAsset(); 58 | $fakeBatch = new BatchFake('::batch-id::', '::name::', 1, 0, 0, [], [], now()->toImmutable()); 59 | 60 | Bus::spy() 61 | ->shouldReceive('findBatch') 62 | ->andReturn($fakeBatch); 63 | 64 | $this 65 | ->withoutMiddleware() 66 | ->get(route('statamic.cp.statamic-image-optimize.resize-images-count', ['batchId' => '::batch-id::'])) 67 | ->assertSuccessful() 68 | ->assertJson([ 69 | 'assetsToOptimize' => 0, 70 | 'assetTotal' => 1, 71 | ]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Jobs/ResizeImageJobTest.php: -------------------------------------------------------------------------------- 1 | createAsset(); 17 | 18 | $this->mock(ResizesImage::class, function (MockInterface $mock): void { 19 | $mock 20 | ->shouldReceive('resize') 21 | ->once(); 22 | }); 23 | 24 | ResizeImageJob::dispatch($asset, 100, 100); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Jobs/ResizeImagesJobTest.php: -------------------------------------------------------------------------------- 1 | mock(ResizesImages::class, function (MockInterface $mock) use ($forceAll): void { 19 | $mock 20 | ->shouldReceive('resize') 21 | ->with($forceAll) 22 | ->once(); 23 | }); 24 | 25 | ResizeImagesJob::dispatch($forceAll); 26 | } 27 | 28 | public static function cases(): array 29 | { 30 | return [ 31 | 'true' => [ 32 | 'forceAll' => true, 33 | ], 34 | 'false' => [ 35 | 'forceAll' => false, 36 | ], 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Listeners/AssetUploadedListenerTest.php: -------------------------------------------------------------------------------- 1 | createAsset(); 20 | 21 | $event = new AssetUploaded($asset); 22 | 23 | /** @var AssetUploadedListener $listener */ 24 | $listener = app(AssetUploadedListener::class); 25 | $listener->handle($event); 26 | 27 | Bus::assertDispatched(ResizeImageJob::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF'); 35 | 36 | $app['config']->set('statamic.assets.image_manipulation.driver', 'gd'); 37 | 38 | $app['config']->set('filesystems.disks.assets', [ 39 | 'driver' => 'local', 40 | 'root' => $this->fixturePath('assets'), 41 | ]); 42 | 43 | $app['config']->set('database.default', 'testbench'); 44 | $app['config']->set('queue.batching.database', 'testbench'); 45 | $app['config']->set('queue.failed.database', 'testbench'); 46 | $app['config']->set('database.connections.testbench', [ 47 | 'driver' => 'sqlite', 48 | 'database' => ':memory:', 49 | 'prefix' => '', 50 | ]); 51 | } 52 | 53 | protected function assetContainer(): AssetContainer 54 | { 55 | if ($this->assetContainer === null) { 56 | $this->assetContainer = (new AssetContainer) // @phpstan-ignore-line 57 | ->handle('test_container') 58 | ->disk('assets') 59 | ->save(); 60 | } 61 | 62 | return $this->assetContainer; 63 | } 64 | 65 | protected function fixturePath(string $file = ''): string 66 | { 67 | $path = __DIR__.'/__fixtures__'; 68 | 69 | if (strlen($file) > 0) { 70 | $path .= '/'.$file; 71 | } 72 | 73 | return $path; 74 | } 75 | 76 | protected function createAsset(string $filename = 'test.png'): Asset 77 | { 78 | Storage::disk('assets')->put($filename, file_get_contents($this->fixturePath('uploads/test.png'))); // @phpstan-ignore-line 79 | 80 | /** @var Asset $asset */ 81 | $asset = (new Asset)->container($this->assetContainer())->path($filename); // @phpstan-ignore-line 82 | $asset->save(); 83 | 84 | return $asset; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/__fixtures__/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/__fixtures__/content/assets/test_container.yaml: -------------------------------------------------------------------------------- 1 | disk: assets 2 | -------------------------------------------------------------------------------- /tests/__fixtures__/dev-null/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justbetter/statamic-image-optimize/00a4054241c06ca561069a3c077e93bb25308e23/tests/__fixtures__/dev-null/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/uploads/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justbetter/statamic-image-optimize/00a4054241c06ca561069a3c077e93bb25308e23/tests/__fixtures__/uploads/test.png -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix'); 2 | 3 | mix.js('resources/js/statamic-image-optimize.js', 'dist/js/statamic-image-optimize.js').vue({ version: 2 }); --------------------------------------------------------------------------------