├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── composer.json ├── ecs.php ├── phpstan.neon └── src ├── ImageOptimize.php ├── assetbundles └── imageoptimize │ └── ImageOptimizeAsset.php ├── config.php ├── console └── controllers │ └── OptimizeController.php ├── fields └── OptimizedImages.php ├── gql └── types │ ├── OptimizedImagesType.php │ └── generators │ └── OptimizedImagesGenerator.php ├── helpers ├── Color.php ├── Image.php ├── PluginTemplate.php └── UrlHelper.php ├── icon-mask.svg ├── icon.svg ├── imagetransforms ├── CraftImageTransform.php ├── ImageTransform.php ├── ImageTransformInterface.php └── ImageTransformTrait.php ├── jobs └── ResaveOptimizedImages.php ├── lib └── Potracio.php ├── models ├── BaseImageTag.php ├── BaseTag.php ├── ImgTag.php ├── LinkPreloadTag.php ├── OptimizedImage.php ├── PictureTag.php ├── Settings.php ├── TagInterface.php └── TagTrait.php ├── services ├── Optimize.php ├── OptimizedImages.php ├── Placeholder.php └── ServicesTrait.php ├── templates ├── _components │ ├── fields │ │ ├── OptimizedImages_error.twig │ │ ├── OptimizedImages_input.twig │ │ ├── OptimizedImages_settings.twig │ │ └── focal-point.svg │ └── utilities │ │ └── ImageOptimizeUtility_content.twig ├── _includes │ ├── checkboxGroup.twig │ └── macros.twig ├── _layouts │ └── imageoptimize-cp.twig ├── frontend │ ├── lazysizes-fallback.twig.js │ └── lazysizes.twig.js ├── settings │ ├── _settings.twig │ ├── image-transforms │ │ └── craft.twig │ └── index.twig └── welcome.twig ├── translations └── en │ └── image-optimize.php ├── utilities └── ImageOptimizeUtility.php ├── validators └── EmbeddedModelValidator.php ├── variables └── ImageOptimizeVariable.php └── web └── assets └── dist ├── assets ├── field-D_XkB3eu.js ├── field-D_XkB3eu.js.gz ├── field-D_XkB3eu.js.map ├── field-D_XkB3eu.js.map.gz ├── imageoptimize-B4gebLDH.css ├── imageoptimize-B4gebLDH.css.gz ├── imageoptimize-Cb5BVZk9.js ├── imageoptimize-Cb5BVZk9.js.map ├── welcome-2KWkXHu8.js ├── welcome-2KWkXHu8.js.gz ├── welcome-2KWkXHu8.js.map └── welcome-2KWkXHu8.js.map.gz ├── img └── ImageOptimize-icon.svg ├── manifest.json └── stats.html /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ImageOptimize Changelog 2 | 3 | ## 5.0.7 - 2025.02.13 4 | ### Fixed 5 | * Fixed an issue where an exception could be thrown using the default "shortcut" syntax due to a regression ([#422](https://github.com/nystudio107/craft-imageoptimize/issues/422)) 6 | 7 | ## 5.0.6 - 2025.02.12 8 | ### Added 9 | * Added the `.placeholder('none')` parameter to allow specifying that no placeholder background image CSS should be used for lazy loaded images (useful for transparent PNGs) ([#410](https://github.com/nystudio107/craft-imageoptimize/issues/410)) 10 | 11 | ### Changed 12 | * Update buildchain to Vite 6 & Tailwind CSS 4 13 | 14 | ## 5.0.5 - 2025.02.06 15 | ### Fixed 16 | * Reverted `srcset` width filtering ([#416](https://github.com/nystudio107/craft-imageoptimize/pull/416)) to address ([#418](https://github.com/nystudio107/craft-imageoptimize/issues/418)) 17 | 18 | ## 5.0.4 - 2025.01.13 19 | ### Fixed 20 | * Don't try to apply filters to assets that are seemingly corrupt, and have a `0` width or `0` height ([#383](https://github.com/nystudio107/craft-imageoptimize/issues/383)) 21 | * Fixed `srcset` width filtering ([#416](https://github.com/nystudio107/craft-imageoptimize/pull/416)) 22 | 23 | ## 5.0.3 - 2024.10.21 24 | ### Changed 25 | * Allow for empty `alt` tags for screen readers as per WCAG ([411](https://github.com/nystudio107/craft-imageoptimize/issues/411)) 26 | 27 | ### Fixed 28 | * Don't add image variants if no variant creator for them exists ([#410](https://github.com/nystudio107/craft-imageoptimize/issues/410)) 29 | * Fix a visual issue with the sizing arrows for Optimized Image fields 30 | * Don't apply background placeholder CSS to images that may be transparent like SVGs or GIFs ([#410](https://github.com/nystudio107/craft-imageoptimize/issues/410)) 31 | 32 | ## 5.0.2 - 2024.06.19 33 | ### Fixed 34 | * Fixed an issue where `srcsetMaxWidth()` could return incorrect results ([#407](https://github.com/nystudio107/craft-imageoptimize/issues/407)) 35 | * Fixed an issue where the data-uri for inline SVG styles were incorrect in some browsers because the spaces were not URL-encoded ([#408](https://github.com/nystudio107/craft-imageoptimize/issues/408)) 36 | 37 | ## 5.0.1 - 2024.05.09 38 | ### Fixed 39 | * Fixed an issue where field content was not propagated to other sites on multi-site installs, causing missing images 40 | * Fixed an issue where the `.imgTag()` and `.pictureTag()` would output and invalid `style` attribute for lazy loaded images ([#400](https://github.com/nystudio107/craft-imageoptimize/issues/400)) 41 | * Fixed an issue where the Subpath wasn't being included for remote volumes like S3 & Google Cloud ([#403](https://github.com/nystudio107/craft-imageoptimize/issues/403)) 42 | 43 | ## 5.0.0 - 2024.04.15 44 | ### Added 45 | * Stable release for Craft CMS 5 46 | 47 | ## 5.0.0-beta.2 - 2024.04.04 48 | ### Added 49 | * Added the ability to pass in a config array to `.imgTag()`, `.pictureTag()` and `.linkPreloadTag()` 50 | 51 | ### Changed 52 | * Changed `.loading()` → `.loadingStrategy()`, `.artDirection()` → `addSourceFrom()` 53 | 54 | ## 5.0.0-beta.1 - 2024.04.02 55 | ### Added 56 | * Initial Craft CMS 5 compatibility 57 | * Add `.imgTag()` to the `OptimizedImage` model, which generates a complete `` tag from the `OptimizedImage` 58 | * Add `.pictureTag()` to the `OptimizedImage` model, which generates a complete `` tag from the `OptimizedImage` 59 | * Add `.linkPreloadTag()` to the `OptimizedImage` model, which generates a complete `` tag from the `OptimizedImage` 60 | * Add `craft.imageOptimize.renderLazySizesJs()` to render the LazySizes JavaScript for lazy loading images 61 | * Add `craft.imageOptimize.renderLazySizesFallbackJs()` to render the LazySizes JavaScript with a support script that uses LazySizes as a fallback for browsers that don't support the `loading` property 62 | 63 | ### Changed 64 | * Added **PDF** to the **Ignore Files** field settings ([#364](https://github.com/nystudio107/craft-imageoptimize/issues/364)) 65 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © nystudio107 2 | 3 | Permission is hereby granted to any person obtaining a copy of this software 4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be 9 | included in all copies or substantial portions of the Software. 10 | 11 | 2. **Don’t use the same license on more than one project.** Each licensed copy 12 | of the Software shall be actively installed in no more than one production 13 | environment at a time. 14 | 15 | 3. **Don’t mess with the licensing features.** Software features related to 16 | licensing shall not be altered or circumvented in any way, including (but 17 | not limited to) license validation, payment prompts, feature restrictions, 18 | and update eligibility. 19 | 20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice, 21 | prompt, reminder, or other message indicating that a payment is owed. 22 | 23 | 5. **Follow the law.** All use of the Software shall not violate any applicable 24 | law or regulation, nor infringe the rights of any other person or entity. 25 | 26 | Failure to comply with the foregoing conditions will automatically and 27 | immediately result in termination of the permission granted hereby. This 28 | license does not include any right to receive updates to the Software or 29 | technical support. Licensees bear all risk related to the quality and 30 | performance of the Software and any modifications made or obtained to it, 31 | including liability for actual and consequential harm, such as loss or 32 | corruption of data, and any necessary service, repair, or correction. 33 | 34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN 39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAJOR_VERSION?=5 2 | PLUGINDEV_PROJECT_DIR?=/Users/andrew/webdev/sites/plugindev/cms_v${MAJOR_VERSION}/ 3 | VENDOR?=nystudio107 4 | PROJECT_PATH?=${VENDOR}/$(shell basename $(CURDIR)) 5 | 6 | .PHONY: dev docs release 7 | 8 | # Start up the buildchain dev server 9 | dev: 10 | ${MAKE} -C buildchain/ dev 11 | # Start up the docs dev server 12 | docs: 13 | ${MAKE} -C docs/ dev 14 | # Run code quality tools, tests, and build the buildchain & docs in preparation for a release 15 | release: --code-quality --code-tests --buildchain-clean-build --docs-clean-build 16 | # The internal targets used by the dev & release targets 17 | --buildchain-clean-build: 18 | ${MAKE} -C buildchain/ clean 19 | ${MAKE} -C buildchain/ image-build 20 | ${MAKE} -C buildchain/ build 21 | --code-quality: 22 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- ecs check vendor/${PROJECT_PATH}/src --fix 23 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- phpstan analyze -c vendor/${PROJECT_PATH}/phpstan.neon 24 | --code-tests: 25 | --docs-clean-build: 26 | ${MAKE} -C docs/ clean 27 | ${MAKE} -C docs/ image-build 28 | ${MAKE} -C docs/ fix 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nystudio107/craft-imageoptimize/badges/quality-score.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-imageoptimize/?branch=v5) [![Code Coverage](https://scrutinizer-ci.com/g/nystudio107/craft-imageoptimize/badges/coverage.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-imageoptimize/?branch=v5) [![Build Status](https://scrutinizer-ci.com/g/nystudio107/craft-imageoptimize/badges/build.png?b=v5)](https://scrutinizer-ci.com/g/nystudio107/craft-imageoptimize/build-status/v5) [![Code Intelligence Status](https://scrutinizer-ci.com/g/nystudio107/craft-imageoptimize/badges/code-intelligence.svg?b=v5)](https://scrutinizer-ci.com/code-intelligence) 2 | 3 | # ImageOptimize plugin for Craft CMS 5.x 4 | 5 | Automatically create & optimize responsive image transforms, using either native Craft transforms or a service like imgix or Thumbor, with zero template changes. 6 | 7 | ![Screenshot](./docs/docs/resources/img/plugin-banner.jpg) 8 | 9 | **Note**: _The license fee for this plugin is $59.00 via the Craft Plugin Store._ 10 | 11 | ## Requirements 12 | 13 | This plugin requires Craft CMS 5.0.0 or later. 14 | 15 | ## Installation 16 | 17 | To install the plugin, follow these instructions. 18 | 19 | 1. Open your terminal and go to your Craft project: 20 | 21 | cd /path/to/project 22 | 23 | 2. Then tell Composer to load the plugin: 24 | 25 | composer require nystudio107/craft-imageoptimize 26 | 27 | 3. Install the plugin via `./craft install/plugin image-optimize` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Image Optimize. 28 | 29 | You can also install ImageOptimize via the **Plugin Store** in the Craft Control Panel. 30 | 31 | ## Documentation 32 | 33 | Click here -> [Image Optimize Documentation](https://nystudio107.com/plugins/imageoptimize/documentation) 34 | 35 | ## ImageOptimize Roadmap 36 | 37 | Some things to do, and ideas for potential features: 38 | 39 | * Consider supporting image optimization services like Cloudinary, TinyPNG, kraken.io, Uploadcare, and ImageOptim 40 | * Add support for additional image optimization tools 41 | 42 | Brought to you by [nystudio107](https://nystudio107.com) 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/craft-imageoptimize", 3 | "description": "Automatically create & optimize responsive image transforms, using either native Craft transforms or a service like imgix, with zero template changes.", 4 | "type": "craft-plugin", 5 | "version": "5.0.7", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "craft-plugin", 11 | "image", 12 | "optimize", 13 | "image-variants", 14 | "optimize-images", 15 | "imgix" 16 | ], 17 | "support": { 18 | "docs": "https://nystudio107.com/docs/image-optimize/", 19 | "issues": "https://nystudio107.com/plugins/imageoptimize/support", 20 | "source": "https://github.com/nystudio107/craft-imageoptimize" 21 | }, 22 | "license": "proprietary", 23 | "authors": [ 24 | { 25 | "name": "nystudio107", 26 | "homepage": "https://nystudio107.com" 27 | } 28 | ], 29 | "minimum-stability": "dev", 30 | "prefer-stable": true, 31 | "require": { 32 | "php": "^8.2", 33 | "craftcms/cms": "^5.0.0", 34 | "nystudio107/craft-plugin-vite": "^5.0.0", 35 | "nystudio107/craft-imageoptimize-imgix": "^5.0.0", 36 | "nystudio107/craft-imageoptimize-sharp": "^5.0.0", 37 | "nystudio107/craft-imageoptimize-thumbor": "^5.0.0", 38 | "ksubileau/color-thief-php": "^1.3", 39 | "mikehaertl/php-shellcommand": "~1.2" 40 | }, 41 | "require-dev": { 42 | "craftcms/ecs": "dev-main", 43 | "craftcms/phpstan": "dev-main", 44 | "craftcms/rector": "dev-main", 45 | "nystudio107/craft-minify": "^5.0.0" 46 | }, 47 | "scripts": { 48 | "phpstan": "phpstan --ansi --memory-limit=1G", 49 | "check-cs": "ecs check --ansi", 50 | "fix-cs": "ecs check --fix --ansi" 51 | }, 52 | "config": { 53 | "allow-plugins": { 54 | "craftcms/plugin-installer": true, 55 | "yiisoft/yii2-composer": true 56 | }, 57 | "optimize-autoloader": true, 58 | "platform": { 59 | "php": "8.2" 60 | }, 61 | "platform-check": false, 62 | "sort-packages": true 63 | }, 64 | "autoload": { 65 | "psr-4": { 66 | "nystudio107\\imageoptimize\\": "src/" 67 | } 68 | }, 69 | "extra": { 70 | "class": "nystudio107\\imageoptimize\\ImageOptimize", 71 | "handle": "image-optimize", 72 | "name": "ImageOptimize" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__ . '/src', 9 | __FILE__, 10 | ]); 11 | $ecsConfig->parallel(); 12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); 13 | }; 14 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | excludePaths: 9 | # Ignore library code 10 | - src/lib/ 11 | -------------------------------------------------------------------------------- /src/assetbundles/imageoptimize/ImageOptimizeAsset.php: -------------------------------------------------------------------------------- 1 | sourcePath = '@nystudio107/imageoptimize/web/assets/dist'; 33 | 34 | $this->depends = [ 35 | CpAsset::class, 36 | VueAsset::class, 37 | ]; 38 | 39 | parent::init(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | CraftImageTransform::class, 32 | 33 | /** 34 | * @var array Settings for the image transform components 35 | * 36 | * The data is stored in the following format, with the key being the class 37 | * of the image transform method: 38 | * 39 | * 'imageTransformTypeSettings' => [ 40 | * ImgixImageTransform::class => [ 41 | * 'domain' => 'XXXXX', 42 | * 'apiKey' => 'XXXXX', 43 | * 'securityToken' => 'XXXXX', 44 | * ] 45 | * ], 46 | */ 47 | 'imageTransformTypeSettings' => [], 48 | 49 | // Should the image variants in an Asset Volume be automatically re-saved when saving 50 | // an OptimizedImages field, saving an Asset Volume that has an OptimizedImages field 51 | // in its layout, or saving the ImageOptimized settings. Set this to false only if 52 | // you will be manually using the CLI console command to resave image variants 53 | 'automaticallyResaveImageVariants' => true, 54 | 55 | // Should image variant be created on Asset save (aka BeforePageLoad) 56 | 'generateTransformsBeforePageLoad' => true, 57 | 58 | // Set to false to disable all placeholder generation 59 | 'generatePlaceholders' => true, 60 | 61 | // Whether the placeholder silhouette SVGs should be capped at 32Kb in size 62 | 'capSilhouetteSvgSize' => true, 63 | 64 | // Controls whether a dominant color palette should be created for image variants 65 | // It takes a bit of time, so if you never plan to use it, you can turn it off 66 | 'createColorPalette' => true, 67 | 68 | // Controls whether SVG placeholder silhouettes should be created for image variants 69 | // It takes a bit of time, so if you never plan to use them, you can turn it off 70 | 'createPlaceholderSilhouettes' => false, 71 | 72 | // Controls whether retina images are automatically created with reduced quality 73 | // as per https://www.netvlies.nl/blogs/retina-revolutie-follow 74 | 'lowerQualityRetinaImageVariants' => true, 75 | 76 | // Controls whether Optimized Image Variants are created that would be up-scaled 77 | // to be larger than the original source image 78 | 'allowUpScaledImageVariants' => false, 79 | 80 | // Controls whether images scaled down >= 50% should be automatically sharpened 81 | 'autoSharpenScaledImages' => true, 82 | 83 | // The amount an image needs to be scaled down for automatic sharpening to be applied 84 | 'sharpenScaledImagePercentage' => 50, 85 | 86 | // Whether to allow limiting the creation of Optimized Image Variants for images by sub-folders 87 | 'assetVolumeSubFolders' => true, 88 | 89 | // The default Image Transform type classes 90 | 'defaultImageTransformTypes' => [ 91 | ], 92 | 93 | // Default aspect ratios 94 | 'defaultAspectRatios' => [ 95 | ['x' => 16, 'y' => 9], 96 | ['x' => 8, 'y' => 5], 97 | ['x' => 4, 'y' => 3], 98 | ['x' => 5, 'y' => 4], 99 | ['x' => 1, 'y' => 1], 100 | ['x' => 9, 'y' => 16], 101 | ['x' => 5, 'y' => 8], 102 | ['x' => 3, 'y' => 4], 103 | ['x' => 4, 'y' => 5], 104 | ], 105 | 106 | // Default image variants 107 | 'defaultVariants' => [ 108 | [ 109 | 'width' => 1200, 110 | 'useAspectRatio' => true, 111 | 'aspectRatioX' => 16.0, 112 | 'aspectRatioY' => 9.0, 113 | 'retinaSizes' => ['1'], 114 | 'quality' => 82, 115 | 'format' => 'jpg', 116 | ], 117 | [ 118 | 'width' => 992, 119 | 'useAspectRatio' => true, 120 | 'aspectRatioX' => 16.0, 121 | 'aspectRatioY' => 9.0, 122 | 'retinaSizes' => ['1'], 123 | 'quality' => 82, 124 | 'format' => 'jpg', 125 | ], 126 | [ 127 | 'width' => 768, 128 | 'useAspectRatio' => true, 129 | 'aspectRatioX' => 4.0, 130 | 'aspectRatioY' => 3.0, 131 | 'retinaSizes' => ['1'], 132 | 'quality' => 60, 133 | 'format' => 'jpg', 134 | ], 135 | [ 136 | 'width' => 576, 137 | 'useAspectRatio' => true, 138 | 'aspectRatioX' => 4.0, 139 | 'aspectRatioY' => 3.0, 140 | 'retinaSizes' => ['1'], 141 | 'quality' => 60, 142 | 'format' => 'jpg', 143 | ], 144 | ], 145 | 146 | // Active image processors 147 | 'activeImageProcessors' => [ 148 | 'jpg' => [ 149 | 'jpegoptim', 150 | ], 151 | 'png' => [ 152 | 'optipng', 153 | ], 154 | 'svg' => [ 155 | 'svgo', 156 | ], 157 | 'gif' => [ 158 | 'gifsicle', 159 | ], 160 | ], 161 | 162 | // Active image variant creators 163 | 'activeImageVariantCreators' => [ 164 | 'jpg' => [ 165 | 'cwebp', 166 | ], 167 | 'png' => [ 168 | 'cwebp', 169 | ], 170 | 'gif' => [ 171 | 'cwebp', 172 | ], 173 | ], 174 | 175 | // Preset image processors 176 | 'imageProcessors' => [ 177 | // jpeg optimizers 178 | 'jpegoptim' => [ 179 | 'commandPath' => '/usr/bin/jpegoptim', 180 | 'commandOptions' => '-s', 181 | 'commandOutputFileFlag' => '', 182 | ], 183 | 'mozjpeg' => [ 184 | 'commandPath' => '/usr/bin/mozjpeg', 185 | 'commandOptions' => '-optimize -copy none', 186 | 'commandOutputFileFlag' => '-outfile', 187 | ], 188 | 'jpegtran' => [ 189 | 'commandPath' => '/usr/bin/jpegtran', 190 | 'commandOptions' => '-optimize -copy none', 191 | 'commandOutputFileFlag' => '', 192 | ], 193 | // png optimizers 194 | 'optipng' => [ 195 | 'commandPath' => '/usr/bin/optipng', 196 | 'commandOptions' => '-o3 -strip all', 197 | 'commandOutputFileFlag' => '', 198 | ], 199 | 'pngcrush' => [ 200 | 'commandPath' => '/usr/bin/pngcrush', 201 | 'commandOptions' => '-brute -ow', 202 | 'commandOutputFileFlag' => '', 203 | ], 204 | 'pngquant' => [ 205 | 'commandPath' => '/usr/bin/pngquant', 206 | 'commandOptions' => '--strip--skip -if-larger', 207 | 'commandOutputFileFlag' => '', 208 | ], 209 | // svg optimizers 210 | 'svgo' => [ 211 | 'commandPath' => '/usr/bin/svgo', 212 | 'commandOptions' => '', 213 | 'commandOutputFileFlag' => '', 214 | ], 215 | // gif optimizers 216 | 'gifsicle' => [ 217 | 'commandPath' => '/usr/bin/gifsicle', 218 | 'commandOptions' => '-O3 -k 256', 219 | 'commandOutputFileFlag' => '', 220 | ], 221 | ], 222 | 223 | 'imageVariantCreators' => [ 224 | // webp variant creator 225 | 'cwebp' => [ 226 | 'commandPath' => '/usr/bin/cwebp', 227 | 'commandOptions' => '', 228 | 'commandOutputFileFlag' => '-o', 229 | 'commandQualityFlag' => '-q', 230 | 'imageVariantExtension' => 'webp', 231 | ], 232 | ], 233 | 234 | ]; 235 | -------------------------------------------------------------------------------- /src/console/controllers/OptimizeController.php: -------------------------------------------------------------------------------- 1 | force) { 78 | echo 'Forcing optimized image variants creation via --force' . PHP_EOL; 79 | } 80 | 81 | $fieldId = null; 82 | if ($this->field !== null) { 83 | /** @var ?Field $field */ 84 | $field = Craft::$app->getFields()->getFieldByHandle($this->field); 85 | $fieldId = $field?->id; 86 | } 87 | if ($volumeHandle === null) { 88 | // Re-save all the optimized image variants in all volumes 89 | ImageOptimize::$plugin->optimizedImages->resaveAllVolumesAssets($fieldId, $this->force); 90 | } else { 91 | // Re-save all the optimized image variants in a specific volume 92 | $volumes = Craft::$app->getVolumes(); 93 | $volume = $volumes->getVolumeByHandle($volumeHandle); 94 | if ($volume) { 95 | ImageOptimize::$plugin->optimizedImages->resaveVolumeAssets($volume, $fieldId, $this->force); 96 | } else { 97 | echo 'Unknown Asset Volume handle: ' . $volumeHandle . PHP_EOL; 98 | } 99 | } 100 | if (!$this->queue) { 101 | $this->runCraftQueue(); 102 | } 103 | } 104 | 105 | /** 106 | * Create a single OptimizedImage for the passed in Asset ID 107 | * 108 | * @param ?int $id 109 | */ 110 | public function actionCreateAsset(?int $id = null): void 111 | { 112 | echo 'Creating optimized image variants' . PHP_EOL; 113 | 114 | if ($id === null) { 115 | echo 'No Asset ID specified' . PHP_EOL; 116 | } else { 117 | // Re-save a single Asset ID 118 | ImageOptimize::$plugin->optimizedImages->resaveAsset($id, $this->force); 119 | } 120 | if (!$this->queue) { 121 | $this->runCraftQueue(); 122 | } 123 | } 124 | 125 | /** 126 | * 127 | */ 128 | private function runCraftQueue(): void 129 | { 130 | // This might take a while 131 | App::maxPowerCaptain(); 132 | $queue = Craft::$app->getQueue(); 133 | if ($queue instanceof QueueInterface) { 134 | $queue->run(); 135 | } elseif ($queue instanceof RedisQueue) { 136 | $queue->run(false); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/gql/types/OptimizedImagesType.php: -------------------------------------------------------------------------------- 1 | fieldName; 28 | 29 | switch ($fieldName) { 30 | // Special-case the `src` field with arguments 31 | case 'src': 32 | $width = $arguments['width'] ?? 0; 33 | return $source->src($width); 34 | 35 | // Special-case the `srcWebp` field with arguments 36 | case 'srcWebp': 37 | $width = $arguments['width'] ?? 0; 38 | return $source->srcWebp($width); 39 | 40 | // Special-case the `srcset` field with arguments 41 | case 'srcset': 42 | $dpr = $arguments['dpr'] ?? false; 43 | return $source->srcset($dpr); 44 | 45 | // Special-case the `srcsetMinWidth` field with arguments 46 | case 'srcsetMinWidth': 47 | $width = $arguments['width'] ?? 0; 48 | $dpr = $arguments['dpr'] ?? false; 49 | return $source->srcsetMinWidth($width, $dpr); 50 | 51 | // Special-case the `srcsetMaxWidth` field with arguments 52 | case 'srcsetMaxWidth': 53 | $width = $arguments['width'] ?? 0; 54 | $dpr = $arguments['dpr'] ?? false; 55 | return $source->srcsetMaxWidth($width, $dpr); 56 | 57 | // Special-case the `srcsetWebp` field with arguments 58 | case 'srcsetWebp': 59 | $dpr = $arguments['dpr'] ?? false; 60 | return $source->srcsetWebp($dpr); 61 | 62 | // Special-case the `srcsetMinWidthWebp` field with arguments 63 | case 'srcsetMinWidthWebp': 64 | $width = $arguments['width'] ?? 0; 65 | $dpr = $arguments['dpr'] ?? false; 66 | return $source->srcsetMinWidthWebp($width, $dpr); 67 | 68 | // Special-case the `srcsetMaxWidthWebp` field with arguments 69 | case 'srcsetMaxWidthWebp': 70 | $width = $arguments['width'] ?? 0; 71 | $dpr = $arguments['dpr'] ?? false; 72 | return $source->srcsetMaxWidthWebp($width, $dpr); 73 | 74 | // Special-case the `maxSrcsetWidth` field 75 | case 'maxSrcsetWidth': 76 | return $source->maxSrcsetWidth(); 77 | 78 | // Special-case the `placeholderImage` field 79 | case 'placeholderImage': 80 | return $source->placeholderImage(); 81 | 82 | // Special-case the `placeholderBox` field 83 | case 'placeholderBox': 84 | $color = $arguments['color'] ?? null; 85 | return $source->placeholderBox($color); 86 | 87 | // Special-case the `placeholderSilhouette` field 88 | case 'placeholderSilhouette': 89 | return $source->placeholderSilhouette(); 90 | 91 | // Special-case the `srcUrls` field 92 | case 'srcUrls': 93 | $result = []; 94 | foreach ($source->optimizedImageUrls as $width => $url) { 95 | $result[] = ['width' => $width, 'url' => $url]; 96 | } 97 | return $result; 98 | 99 | // Special-case the `colorPaletteRgb` field 100 | case 'colorPaletteRgb': 101 | return $source->colorPaletteRgb(); 102 | 103 | // Default to just returning the field value 104 | default: 105 | return $source[$fieldName]; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/gql/types/generators/OptimizedImagesGenerator.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'name' => 'optimizedImageUrls', 39 | 'description' => 'An array of optimized image variant URLs', 40 | 'type' => Type::listOf(Type::string()), 41 | ], 42 | 'optimizedWebPImageUrls' => [ 43 | 'name' => 'optimizedWebPImageUrls', 44 | 'description' => 'An array of optimized .webp image variant URLs', 45 | 'type' => Type::listOf(Type::string()), 46 | ], 47 | 'variantSourceWidths' => [ 48 | 'name' => 'variantSourceWidths', 49 | 'description' => 'An array of the widths of the optimized image variants', 50 | 'type' => Type::listOf(Type::int()), 51 | ], 52 | 'variantHeights' => [ 53 | 'name' => 'variantHeights', 54 | 'description' => 'An array of the heights of the optimized image variants', 55 | 'type' => Type::listOf(Type::int()), 56 | ], 57 | 'focalPoint' => [ 58 | 'name' => 'focalPoint', 59 | 'description' => 'An array of the x,y image focal point coords, ranging from 0.0 to 1.0', 60 | 'type' => Type::listOf(Type::float()), 61 | ], 62 | 'originalImageWidth' => [ 63 | 'name' => 'originalImageWidth', 64 | 'description' => 'The width of the original source image', 65 | 'type' => Type::int(), 66 | ], 67 | 'originalImageHeight' => [ 68 | 'name' => 'originalImageHeight', 69 | 'description' => 'The height of the original source image', 70 | 'type' => Type::int(), 71 | ], 72 | 'placeholder' => [ 73 | 'name' => 'placeholder', 74 | 'description' => 'The base64 encoded placeholder LQIP image', 75 | 'type' => Type::string(), 76 | ], 77 | 'placeholderSvg' => [ 78 | 'name' => 'placeholderSvg', 79 | 'description' => 'The base64 encoded placeholder LQIP SVG image', 80 | 'type' => Type::string(), 81 | ], 82 | 'colorPalette' => [ 83 | 'name' => 'colorPalette', 84 | 'description' => 'An array the 5 most dominant colors in the image', 85 | 'type' => Type::listOf(Type::string()), 86 | ], 87 | 'colorPaletteRgb' => [ 88 | 'name' => 'colorPaletteRgb', 89 | 'description' => 'An array the 5 most dominant colors in the image in RGB format', 90 | 'type' => Type::listOf(Type::listOf(Type::int())), 91 | ], 92 | 'lightness' => [ 93 | 'name' => 'lightness', 94 | 'description' => 'The overall lightness of the image, from 0..100', 95 | 'type' => Type::int(), 96 | ], 97 | 'placeholderWidth' => [ 98 | 'name' => 'placeholderWidth', 99 | 'description' => 'The width of the placeholder image', 100 | 'type' => Type::int(), 101 | ], 102 | 'placeholderHeight' => [ 103 | 'name' => 'placeholderHeight', 104 | 'description' => 'The height of the placeholder image', 105 | 'type' => Type::int(), 106 | ], 107 | // Dynamic fields 108 | 'srcUrls' => [ 109 | 'name' => 'srcUrls', 110 | 'description' => 'Return the first image variant URL or the specific one passed in via `width`', 111 | 'type' => Type::listOf(Type::listOf(Type::string())), 112 | ], 113 | 'maxSrcsetWidth' => [ 114 | 'name' => 'maxSrcsetWidth', 115 | 'description' => 'Work around issues with `` returning sizes larger than are available', 116 | 'type' => Type::int(), 117 | ], 118 | 'placeholderImage' => [ 119 | 'name' => 'placeholderImage', 120 | 'description' => 'Return a base64-encoded placeholder image', 121 | 'type' => Type::string(), 122 | ], 123 | 'placeholderSilhouette' => [ 124 | 'name' => 'placeholderSilhouette', 125 | 'description' => 'Return a silhouette of the image as an SVG placeholder', 126 | 'type' => Type::string(), 127 | ], 128 | // Dynamic fields with arguments 129 | 'src' => [ 130 | 'name' => 'src', 131 | 'description' => 'Return the first image variant URL or the specific one passed in via `width`', 132 | 'args' => [ 133 | 'width' => [ 134 | 'name' => 'width', 135 | 'type' => Type::int(), 136 | 'description' => 'Width of the image', 137 | ], 138 | ], 139 | 'type' => Type::string(), 140 | ], 141 | 'srcWebp' => [ 142 | 'name' => 'srcWebp', 143 | 'description' => 'Return the first webp image variant URL or the specific one passed in via `width`', 144 | 'args' => [ 145 | 'width' => [ 146 | 'name' => 'width', 147 | 'type' => Type::int(), 148 | 'description' => 'Width of the image', 149 | ], 150 | ], 151 | 'type' => Type::string(), 152 | ], 153 | 'srcset' => [ 154 | 'name' => 'srcset', 155 | 'description' => 'Return a string of image URLs and their sizes', 156 | 'args' => [ 157 | 'dpr' => [ 158 | 'name' => 'dpr', 159 | 'type' => Type::boolean(), 160 | 'description' => 'Include dpr images?', 161 | ], 162 | ], 163 | 'type' => Type::string(), 164 | ], 165 | 'srcsetMinWidth' => [ 166 | 'name' => 'srcsetMinWidth', 167 | 'description' => 'Return a string of image URLs and their sizes', 168 | 'args' => [ 169 | 'width' => [ 170 | 'name' => 'width', 171 | 'type' => Type::int(), 172 | 'description' => 'Width of the image', 173 | ], 174 | 'dpr' => [ 175 | 'name' => 'dpr', 176 | 'type' => Type::boolean(), 177 | 'description' => 'Include dpr images?', 178 | ], 179 | ], 180 | 'type' => Type::string(), 181 | ], 182 | 'srcsetMaxWidth' => [ 183 | 'name' => 'srcsetMaxWidth', 184 | 'description' => 'Return a string of image URLs and their sizes', 185 | 'args' => [ 186 | 'width' => [ 187 | 'name' => 'width', 188 | 'type' => Type::int(), 189 | 'description' => 'Width of the image', 190 | ], 191 | 'dpr' => [ 192 | 'name' => 'dpr', 193 | 'type' => Type::boolean(), 194 | 'description' => 'Include dpr images?', 195 | ], 196 | ], 197 | 'type' => Type::string(), 198 | ], 199 | 'srcsetWebp' => [ 200 | 'name' => 'srcsetWebp', 201 | 'description' => 'Return a string of webp image URLs and their sizes', 202 | 'args' => [ 203 | 'dpr' => [ 204 | 'name' => 'dpr', 205 | 'type' => Type::boolean(), 206 | 'description' => 'Include dpr images?', 207 | ], 208 | ], 209 | 'type' => Type::string(), 210 | ], 211 | 'srcsetMinWidthWebp' => [ 212 | 'name' => 'srcsetMinWidthWebp', 213 | 'description' => 'Return a string of webp image URLs and their sizes', 214 | 'args' => [ 215 | 'width' => [ 216 | 'name' => 'width', 217 | 'type' => Type::int(), 218 | 'description' => 'Width of the image', 219 | ], 220 | 'dpr' => [ 221 | 'name' => 'dpr', 222 | 'type' => Type::boolean(), 223 | 'description' => 'Include dpr images?', 224 | ], 225 | ], 226 | 'type' => Type::string(), 227 | ], 228 | 'srcsetMaxWidthWebp' => [ 229 | 'name' => 'srcsetMaxWidthWebp', 230 | 'description' => 'Return a string of webp image URLs and their sizes', 231 | 'args' => [ 232 | 'width' => [ 233 | 'name' => 'width', 234 | 'type' => Type::int(), 235 | 'description' => 'Width of the image', 236 | ], 237 | 'dpr' => [ 238 | 'name' => 'dpr', 239 | 'type' => Type::boolean(), 240 | 'description' => 'Include dpr images?', 241 | ], 242 | ], 243 | 'type' => Type::string(), 244 | ], 245 | 'placeholderBox' => [ 246 | 'name' => 'placeholderBox', 247 | 'description' => 'Return an SVG box as a placeholder image', 248 | 'args' => [ 249 | 'color' => [ 250 | 'name' => 'color', 251 | 'type' => Type::string(), 252 | 'description' => 'The color for the placeholder box', 253 | ], 254 | ], 255 | 'type' => Type::string(), 256 | ], 257 | ]; 258 | $optimizedImagesType = GqlEntityRegistry::getEntity($typeName) 259 | ?: GqlEntityRegistry::createEntity($typeName, new OptimizedImagesType([ 260 | 'name' => $typeName, 261 | 'description' => 'This entity has all the OptimizedImages properties', 262 | 'fields' => function() use ($optimizedImagesFields) { 263 | return $optimizedImagesFields; 264 | }, 265 | ])); 266 | 267 | TypeLoader::registerType($typeName, function() use ($optimizedImagesType) { 268 | return $optimizedImagesType; 269 | }); 270 | 271 | return [$optimizedImagesType]; 272 | } 273 | 274 | /** 275 | * @inheritdoc 276 | */ 277 | public static function getName($context = null): string 278 | { 279 | /** @var OptimizedImages $context */ 280 | return $context->handle . '_OptimizedImages'; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/helpers/Color.php: -------------------------------------------------------------------------------- 1 | $r, 'g' => $g, 'b' => $b]; 48 | } 49 | 50 | /** 51 | * Convert an RGB color array to a HSL color array 52 | * 53 | * @param array $rgb 54 | * 55 | * @return array 56 | */ 57 | public static function RGBToHSL(array $rgb): array 58 | { 59 | $r = ((float)$rgb['r']) / 255.0; 60 | $g = ((float)$rgb['g']) / 255.0; 61 | $b = ((float)$rgb['b']) / 255.0; 62 | 63 | $maxC = max($r, $g, $b); 64 | $minC = min($r, $g, $b); 65 | 66 | $l = ($maxC + $minC) / 2.0; 67 | 68 | $s = 0; 69 | $h = 0; 70 | if ($maxC !== $minC) { 71 | if ($l < .5) { 72 | $s = ($maxC - $minC) / ($maxC + $minC); 73 | } else { 74 | $s = ($maxC - $minC) / (2.0 - $maxC - $minC); 75 | } 76 | if ($r === $maxC) { 77 | $h = ($g - $b) / ($maxC - $minC); 78 | } 79 | if ($g === $maxC) { 80 | $h = 2.0 + ($b - $r) / ($maxC - $minC); 81 | } 82 | if ($b === $maxC) { 83 | $h = 4.0 + ($r - $g) / ($maxC - $minC); 84 | } 85 | 86 | $h /= 6.0; 87 | } 88 | 89 | $h = (int)round(360.0 * $h); 90 | $s = (int)round(100.0 * $s); 91 | $l = (int)round(100.0 * $l); 92 | 93 | return ['h' => $h, 's' => $s, 'l' => $l]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/helpers/Image.php: -------------------------------------------------------------------------------- 1 | getConfig()->getGeneral(); 42 | 43 | $extension = strtolower($generalConfig->imageDriver); 44 | 45 | // If it's explicitly set, take their word for it. 46 | if ($extension === 'gd') { 47 | $instance = new GdImagine(); 48 | } elseif ($extension === 'imagick') { 49 | $instance = new ImagickImagine(); 50 | } elseif (Craft::$app->getImages()->getIsGd()) { 51 | $instance = new GdImagine(); 52 | } else { 53 | $instance = new ImagickImagine(); 54 | } 55 | 56 | $imageService = Craft::$app->getImages(); 57 | if ($imageService->getIsGd()) { 58 | return false; 59 | } 60 | 61 | if (!is_file($path)) { 62 | Craft::error('Tried to load an image at ' . $path . ', but the file does not exist.', __METHOD__); 63 | throw new ImageException(Craft::t('app', 'No file exists at the given path.')); 64 | } 65 | 66 | if (!$imageService->checkMemoryForImage($path)) { 67 | throw new ImageException(Craft::t( 68 | 'app', 69 | 'Not enough memory available to perform this image operation.' 70 | )); 71 | } 72 | 73 | // Make sure the image says it's an image 74 | $mimeType = FileHelper::getMimeType($path, null, false); 75 | 76 | if ($mimeType !== null && !str_starts_with($mimeType, 'image/') && !str_starts_with($mimeType, 'application/pdf')) { 77 | throw new ImageException(Craft::t( 78 | 'app', 79 | 'The file “{name}” does not appear to be an image.', 80 | ['name' => pathinfo($path, PATHINFO_BASENAME)] 81 | )); 82 | } 83 | 84 | try { 85 | $image = $instance->open($path); 86 | } catch (Throwable $e) { 87 | throw new ImageException(Craft::t( 88 | 'app', 89 | 'The file “{path}” does not appear to be an image.', 90 | ['path' => $path] 91 | ), 0, $e); 92 | } 93 | 94 | $extension = pathinfo($path, PATHINFO_EXTENSION); 95 | 96 | return $extension === 'gif' && $image->layers()->count() > 1; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/helpers/PluginTemplate.php: -------------------------------------------------------------------------------- 1 | getView()->renderString($templateString, $params); 38 | } catch (\Exception $e) { 39 | $html = Craft::t( 40 | 'image-optimize', 41 | 'Error rendering template string -> {error}', 42 | ['error' => $e->getMessage()] 43 | ); 44 | Craft::error($html, __METHOD__); 45 | } 46 | 47 | return $html; 48 | } 49 | 50 | /** 51 | * Render a plugin template 52 | * 53 | * @param string $templatePath 54 | * @param array $params 55 | * @param string|null $minifier 56 | * 57 | * @return string 58 | */ 59 | public static function renderPluginTemplate( 60 | string $templatePath, 61 | array $params = [], 62 | string $minifier = null, 63 | ): string { 64 | $template = 'image-optimize/' . $templatePath; 65 | $oldMode = Craft::$app->view->getTemplateMode(); 66 | // Look for the template on the frontend first 67 | try { 68 | $templateMode = View::TEMPLATE_MODE_CP; 69 | if (Craft::$app->view->doesTemplateExist($template, View::TEMPLATE_MODE_SITE)) { 70 | $templateMode = View::TEMPLATE_MODE_SITE; 71 | } 72 | Craft::$app->view->setTemplateMode($templateMode); 73 | } catch (Exception $e) { 74 | Craft::error($e->getMessage(), __METHOD__); 75 | } 76 | 77 | // Render the template with our vars passed in 78 | try { 79 | $htmlText = Craft::$app->view->renderTemplate($template, $params); 80 | if ($minifier) { 81 | // If Minify is installed, use it to minify the template 82 | $minify = Craft::$app->getPlugins()->getPlugin(self::MINIFY_PLUGIN_HANDLE); 83 | if ($minify) { 84 | $htmlText = Minify::$plugin->minify->$minifier($htmlText); 85 | } 86 | } 87 | } catch (\Exception $e) { 88 | $htmlText = Craft::t( 89 | 'image-optimize', 90 | 'Error rendering `{template}` -> {error}', 91 | ['template' => $templatePath, 'error' => $e->getMessage()] 92 | ); 93 | Craft::error($htmlText, __METHOD__); 94 | } 95 | 96 | // Restore the old template mode 97 | try { 98 | Craft::$app->view->setTemplateMode($oldMode); 99 | } catch (Exception $e) { 100 | Craft::error($e->getMessage(), __METHOD__); 101 | } 102 | 103 | return Template::raw($htmlText); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/helpers/UrlHelper.php: -------------------------------------------------------------------------------- 1 | getMessage(), __METHOD__); 56 | } 57 | } else { 58 | try { 59 | $url = self::siteUrl($url, null, $protocol); 60 | if (self::isProtocolRelativeUrl($url)) { 61 | $url = self::urlWithScheme($url, $protocol); 62 | } 63 | } catch (Exception $e) { 64 | Craft::error($e->getMessage(), __METHOD__); 65 | } 66 | } 67 | } 68 | 69 | return $url; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/icon-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 23 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/imagetransforms/CraftImageTransform.php: -------------------------------------------------------------------------------- 1 | getSettings(); 55 | // Get our $generateTransformsBeforePageLoad setting 56 | $this->generateTransformsBeforePageLoad = $settings->generateTransformsBeforePageLoad ?? true; 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function getTransformUrl(Asset $asset, CraftImageTransformModel|string|array|null $transform): ?string 63 | { 64 | // Generate the URLs to the optimized images 65 | $oldValue = Craft::$app->getConfig()->getGeneral()->generateTransformsBeforePageLoad; 66 | 67 | if ($this->generateTransformsBeforePageLoad) { 68 | Craft::$app->getConfig()->getGeneral()->generateTransformsBeforePageLoad = true; 69 | } 70 | $url = $asset->getUrl($transform); 71 | Craft::$app->getConfig()->getGeneral()->generateTransformsBeforePageLoad = $oldValue; 72 | 73 | return $url; 74 | } 75 | 76 | /** 77 | * @inheritDoc 78 | */ 79 | public function getWebPUrl(string $url, Asset $asset, CraftImageTransformModel|string|array|null $transform): ?string 80 | { 81 | return $this->appendExtension($url, '.webp'); 82 | } 83 | 84 | /** 85 | * @inheritdoc 86 | */ 87 | public function getSettingsHtml(): ?string 88 | { 89 | $imageProcessors = ImageOptimize::$plugin->optimize->getActiveImageProcessors(); 90 | $variantCreators = ImageOptimize::$plugin->optimize->getActiveVariantCreators(); 91 | 92 | return Craft::$app->getView()->renderTemplate('craft-image-transform/settings/image-transforms/craft.twig', [ 93 | 'imageTransform' => $this, 94 | 'imageProcessors' => $imageProcessors, 95 | 'variantCreators' => $variantCreators, 96 | ]); 97 | } 98 | 99 | /** 100 | * No savable fields for this component 101 | * 102 | * @return array 103 | */ 104 | public function fields(): array 105 | { 106 | return []; 107 | } 108 | 109 | /** 110 | * @inheritdoc 111 | */ 112 | public function rules(): array 113 | { 114 | $rules = parent::rules(); 115 | return array_merge($rules, [ 116 | ]); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/imagetransforms/ImageTransform.php: -------------------------------------------------------------------------------- 1 | getFileName()) 53 | . '/../templates' 54 | ) 55 | . DIRECTORY_SEPARATOR; 56 | $id = StringHelper::toKebabCase($reflect->getShortName()); 57 | 58 | return [$id, $classPath]; 59 | } 60 | 61 | // Public Methods 62 | // ========================================================================= 63 | 64 | /** 65 | * @inheritdoc 66 | */ 67 | public function getTransformUrl(Asset $asset, CraftImageTransformModel|string|array|null $transform): ?string 68 | { 69 | return null; 70 | } 71 | 72 | /** 73 | * @inheritdoc 74 | */ 75 | public function getWebPUrl(string $url, Asset $asset, CraftImageTransformModel|string|array|null $transform): ?string 76 | { 77 | return $url; 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function getPurgeUrl(Asset $asset): ?string 84 | { 85 | return null; 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function purgeUrl(string $url): bool 92 | { 93 | return true; 94 | } 95 | 96 | /** 97 | * @inheritdoc 98 | */ 99 | public function getAssetUri(Asset $asset): ?string 100 | { 101 | $volume = $asset->getVolume(); 102 | $assetPath = $volume->getSubpath() . $asset->getPath(); 103 | 104 | // Account for volume types with a subfolder setting 105 | // e.g. craftcms/aws-s3, craftcms/google-cloud 106 | if ($volume->getFs()->subfolder ?? null) { 107 | $subfolder = $volume->getFs()->subfolder; 108 | $subfolder = Craft::parseEnv($subfolder); 109 | return rtrim($subfolder, '/') . '/' . $assetPath; 110 | } 111 | 112 | return $assetPath; 113 | } 114 | 115 | /** 116 | * @param string $url 117 | * @noinspection PhpComposerExtensionStubsInspection 118 | */ 119 | public function prefetchRemoteFile(string $url): void 120 | { 121 | // Get an absolute URL with protocol that curl will be happy with 122 | $url = UrlHelper::absoluteUrlWithProtocol($url); 123 | $ch = curl_init($url); 124 | curl_setopt_array($ch, [ 125 | CURLOPT_RETURNTRANSFER => 1, 126 | CURLOPT_FOLLOWLOCATION => 1, 127 | CURLOPT_SSL_VERIFYPEER => 0, 128 | CURLOPT_NOBODY => 1, 129 | ]); 130 | curl_exec($ch); 131 | curl_close($ch); 132 | } 133 | 134 | /** 135 | * Append an extension a passed url or path 136 | * 137 | * @param $pathOrUrl 138 | * @param $extension 139 | * 140 | * @return string 141 | */ 142 | public function appendExtension($pathOrUrl, $extension): string 143 | { 144 | $path = $this->decomposeUrl($pathOrUrl); 145 | $path_parts = pathinfo($path['path']); 146 | $new_path = ($path_parts['filename']) . '.' . ($path_parts['extension'] ?? '') . $extension; 147 | if (!empty($path_parts['dirname']) && $path_parts['dirname'] !== '.') { 148 | $dirname = $path_parts['dirname']; 149 | $dirname = $dirname === '/' ? '' : $dirname; 150 | $new_path = $dirname . DIRECTORY_SEPARATOR . $new_path; 151 | $new_path = preg_replace('/([^:])(\/{2,})/', '$1/', $new_path); 152 | } 153 | 154 | return $path['prefix'] . $new_path . $path['suffix']; 155 | } 156 | 157 | // Protected Methods 158 | // ========================================================================= 159 | 160 | /** 161 | * Decompose a URL into a prefix, path, and suffix 162 | * 163 | * @param $pathOrUrl 164 | * 165 | * @return array 166 | */ 167 | protected function decomposeUrl($pathOrUrl): array 168 | { 169 | $result = array(); 170 | 171 | if (filter_var($pathOrUrl, FILTER_VALIDATE_URL)) { 172 | $url_parts = parse_url($pathOrUrl); 173 | $result['prefix'] = $url_parts['scheme'] . '://' . $url_parts['host']; 174 | if (!empty($url_parts['port'])) { 175 | $result['prefix'] .= ':' . $url_parts['port']; 176 | } 177 | $result['path'] = $url_parts['path']; 178 | $result['suffix'] = ''; 179 | $result['suffix'] .= empty($url_parts['query']) ? '' : '?' . $url_parts['query']; 180 | $result['suffix'] .= empty($url_parts['fragment']) ? '' : '#' . $url_parts['fragment']; 181 | } else { 182 | $result['prefix'] = ''; 183 | $result['path'] = $pathOrUrl; 184 | $result['suffix'] = ''; 185 | } 186 | 187 | return $result; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/imagetransforms/ImageTransformInterface.php: -------------------------------------------------------------------------------- 1 | getElements()->invalidateCachesForElementType(Asset::class); 70 | 71 | // Now find the affected element IDs 72 | /** @var ElementQuery $query */ 73 | $query = Asset::find(); 74 | if (!empty($this->criteria)) { 75 | Craft::configure($query, $this->criteria); 76 | } 77 | if (Craft::$app instanceof ConsoleApplication) { 78 | echo $this->description . PHP_EOL; 79 | } 80 | // Use craft\db\Paginator to paginate the results so we don't exceed any memory limits 81 | // See batch() and each() discussion here: https://github.com/yiisoft/yii2/issues/8420 82 | // and here: https://github.com/craftcms/cms/issues/7338 83 | $paginator = new Paginator($query, [ 84 | 'pageSize' => self::ASSET_QUERY_PAGE_SIZE, 85 | ]); 86 | $currentElement = 0; 87 | $totalElements = $paginator->getTotalResults(); 88 | // Iterate through the paginated results 89 | while ($currentElement < $totalElements) { 90 | $elements = $paginator->getPageResults(); 91 | if (Craft::$app instanceof ConsoleApplication) { 92 | echo 'Query ' . $paginator->getCurrentPage() . '/' . $paginator->getTotalPages() 93 | . ' - assets: ' . $paginator->getTotalResults() 94 | . PHP_EOL; 95 | } 96 | /** @var ElementInterface $element */ 97 | foreach ($elements as $element) { 98 | $currentElement++; 99 | // Find each OptimizedImages field and process it 100 | $layout = $element->getFieldLayout(); 101 | if ($layout !== null) { 102 | $fields = $layout->getCustomFields(); 103 | /** @var Field $field */ 104 | foreach ($fields as $field) { 105 | if ($field instanceof OptimizedImagesField && $element instanceof Asset) { 106 | if ($this->fieldId === null || (int)$field->id === (int)$this->fieldId) { 107 | if (Craft::$app instanceof ConsoleApplication) { 108 | echo $currentElement . '/' . $totalElements 109 | . ' - processing asset: ' . $element->title 110 | . ' from field: ' . $field->name . PHP_EOL; 111 | } 112 | try { 113 | ImageOptimize::$plugin->optimizedImages->updateOptimizedImageFieldData($field, $element, $this->force); 114 | } catch (Exception $e) { 115 | Craft::error($e->getMessage(), __METHOD__); 116 | if (Craft::$app instanceof ConsoleApplication) { 117 | echo '[error]: ' 118 | . $e->getMessage() 119 | . ' while processing ' 120 | . $currentElement . '/' . $totalElements 121 | . ' - processing asset: ' . $element->title 122 | . ' from field: ' . $field->name . PHP_EOL; 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | $this->setProgress($queue, $currentElement / $totalElements); 130 | } 131 | $paginator->currentPage++; 132 | } 133 | } 134 | 135 | // Protected Methods 136 | // ========================================================================= 137 | 138 | /** 139 | * @inheritdoc 140 | */ 141 | protected function defaultDescription(): ?string 142 | { 143 | return Craft::t('app', 'Resaving {class} elements', [ 144 | 'class' => App::humanizeClass(Asset::class), 145 | ]); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/models/BaseImageTag.php: -------------------------------------------------------------------------------- 1 | getLazyLoadSrc($placeHolder) . ')'; 44 | $attrs['style']['background-size'] = 'cover'; 45 | } 46 | } 47 | // Handle attributes that lazy and lazySizesFallback have in common 48 | switch ($loading) { 49 | case 'lazy': 50 | case 'lazySizesFallback': 51 | if (isset($attrs['loading'])) { 52 | $attrs['loading'] = 'lazy'; 53 | } 54 | break; 55 | default: 56 | break; 57 | } 58 | // Handle attributes that lazySizes and lazySizesFallback have in common 59 | switch ($loading) { 60 | case 'lazySizes': 61 | case 'lazySizesFallback': 62 | // Only swap to data- attributes if they want the LazySizes fallback 63 | if (!empty($attrs['sizes'])) { 64 | $attrs['data-sizes'] = $attrs['sizes']; 65 | $attrs['sizes'] = ''; 66 | } 67 | if (!empty($attrs['srcset'])) { 68 | $attrs['data-srcset'] = $attrs['srcset']; 69 | $attrs['srcset'] = ''; 70 | } 71 | if (!empty($attrs['src'])) { 72 | $attrs['data-src'] = $attrs['src']; 73 | $attrs['src'] = $this->getLazyLoadSrc($placeHolder); 74 | } 75 | break; 76 | default: 77 | break; 78 | } 79 | 80 | return $attrs; 81 | } 82 | 83 | /** 84 | * Return a lazy loading placeholder image based on the passed in $lazyload setting 85 | * 86 | * @param string $lazyLoad 87 | * 88 | * @return string 89 | */ 90 | protected function getLazyLoadSrc(string $lazyLoad): string 91 | { 92 | $lazyLoad = strtolower($lazyLoad); 93 | return match ($lazyLoad) { 94 | 'image' => $this->optimizedImage->getPlaceholderImage(), 95 | 'silhouette' => $this->optimizedImage->getPlaceholderSilhouette(), 96 | 'color' => $this->optimizedImage->getPlaceholderBox($this->colorPalette[0] ?? null), 97 | default => $this->optimizedImage->getPlaceholderBox('#CCC'), 98 | }; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/models/BaseTag.php: -------------------------------------------------------------------------------- 1 | render(); 37 | } 38 | 39 | /** 40 | * Render the tag 41 | * 42 | * @return Markup 43 | */ 44 | public function render(): Markup 45 | { 46 | return Template::raw(''); 47 | } 48 | 49 | /** 50 | * Filter out attributes with empty values in them, so they don't get rendered 51 | * 52 | * @param array $attrs 53 | * @return array 54 | */ 55 | public function filterEmptyAttributes(array $attrs): array 56 | { 57 | // Keep certain attributes even if they are empty 58 | return array_filter($attrs, static fn($value, $key) => in_array($key, self::ALLOWED_EMPTY_ATTRS, true) || !empty($value), ARRAY_FILTER_USE_BOTH); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/models/ImgTag.php: -------------------------------------------------------------------------------- 1 | tag 36 | */ 37 | public array $imgAttrs = []; 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public function init(): void 43 | { 44 | parent::init(); 45 | // Populate the $imageAttrs 46 | $this->imgAttrs = [ 47 | 'class' => '', 48 | 'style' => '', 49 | 'width' => $this->optimizedImage->placeholderWidth, 50 | 'height' => $this->optimizedImage->placeholderHeight, 51 | 'src' => reset($this->optimizedImage->optimizedImageUrls), 52 | 'srcset' => $this->optimizedImage->getSrcsetFromArray($this->optimizedImage->optimizedImageUrls), 53 | 'sizes' => '100vw', 54 | 'loading' => '', 55 | ]; 56 | // If the original image is an SVG or gif, don't add the placeholder box CSS so that transparency works as intended 57 | $path = parse_url($this->imgAttrs['src'], PHP_URL_PATH); 58 | $extension = pathinfo($path, PATHINFO_EXTENSION); 59 | if ($extension === 'svg' || $extension === 'gif') { 60 | $this->placeholder = 'none'; 61 | } 62 | } 63 | 64 | /** 65 | * Set the $loading property 66 | * 67 | * @param string $value 68 | * @return $this 69 | */ 70 | public function loadingStrategy(string $value): ImgTag 71 | { 72 | $this->loadingStrategy = $value; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Set the $placeholder property 79 | * 80 | * @param string $value 81 | * @return $this 82 | */ 83 | public function placeholder(string $value): ImgTag 84 | { 85 | $this->placeholder = $value; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Merge the passed array of tag attributes into $imgAttrs 92 | * 93 | * @param array $value 94 | * @return $this 95 | */ 96 | public function imgAttrs(array $value): ImgTag 97 | { 98 | $this->imgAttrs = array_merge($this->imgAttrs, $value); 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Generate a complete tag for the $optimizedImage OptimizedImage model 105 | * 106 | * @return Markup 107 | */ 108 | public function render(): Markup 109 | { 110 | $attrs = $this->imgAttrs; 111 | // Handle lazy loading 112 | if ($this->loadingStrategy !== 'eager') { 113 | $attrs = $this->swapLazyLoadAttrs($this->loadingStrategy, $this->placeholder, $attrs); 114 | } 115 | // Remove any empty attributes 116 | $attrs = $this->filterEmptyAttributes($attrs); 117 | // Render the tag 118 | $tag = Html::tag('img', '', $attrs); 119 | 120 | return Template::raw($tag); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/models/LinkPreloadTag.php: -------------------------------------------------------------------------------- 1 | tag 26 | */ 27 | public array $linkAttrs = []; 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function init(): void 33 | { 34 | parent::init(); 35 | // Any web browser that supports link rel="preload" as="image" also supports webp, so prefer that 36 | $srcset = $this->optimizedImage->optimizedImageUrls; 37 | if (!empty($this->optimizedImage->optimizedWebPImageUrls)) { 38 | $srcset = $this->optimizedImage->optimizedWebPImageUrls; 39 | } 40 | // Populate the $imageAttrs 41 | $this->linkAttrs = [ 42 | 'rel' => 'preload', 43 | 'as' => 'image', 44 | 'href' => reset($srcset), 45 | 'imagesrcset' => $this->optimizedImage->getSrcsetFromArray($srcset), 46 | 'imagesizes' => '100vw', 47 | ]; 48 | } 49 | 50 | /** 51 | * Merge the passed array of tag attributes into $linkAttrs 52 | * 53 | * @param array $value 54 | * @return $this 55 | */ 56 | public function linkAttrs(array $value): LinkPreloadTag 57 | { 58 | $this->linkAttrs = array_merge($this->linkAttrs, $value); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Generate a complete tag for the $optimizedImage OptimizedImage model 65 | * ref: https://web.dev/preload-responsive-images/#imagesrcset-and-imagesizes 66 | * 67 | * @return Markup 68 | */ 69 | public function render(): Markup 70 | { 71 | $attrs = $this->linkAttrs; 72 | // Remove any empty attributes 73 | $attrs = $this->filterEmptyAttributes($attrs); 74 | // Render the tag 75 | $tag = Html::tag('link', '', $attrs); 76 | 77 | return Template::raw($tag); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/models/PictureTag.php: -------------------------------------------------------------------------------- 1 | tag 36 | */ 37 | public array $pictureAttrs = []; 38 | 39 | /** 40 | * @var array array of tag attributes for the tags 41 | */ 42 | public array $sourceAttrs = []; 43 | 44 | /** 45 | * @var array array of tag attributes for the tag 46 | */ 47 | public array $imgAttrs = []; 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function init(): void 53 | { 54 | parent::init(); 55 | // Populate the $imageAttrs 56 | $this->imgAttrs = [ 57 | 'class' => '', 58 | 'style' => '', 59 | 'width' => $this->optimizedImage->placeholderWidth, 60 | 'height' => $this->optimizedImage->placeholderHeight, 61 | 'src' => reset($this->optimizedImage->optimizedImageUrls), 62 | 'loading' => '', 63 | ]; 64 | // Populate the $sourceAttrs 65 | $this->populateSourceAttrs($this->optimizedImage, []); 66 | // Populate the $pictureAttrs 67 | $this->pictureAttrs = []; 68 | // If the original image is an SVG or gif, don't add the placeholder box CSS so that transparency works as intended 69 | $path = parse_url($this->imgAttrs['src'], PHP_URL_PATH); 70 | $extension = pathinfo($path, PATHINFO_EXTENSION); 71 | if ($extension === 'svg' || $extension === 'gif') { 72 | $this->placeholder = 'none'; 73 | } 74 | } 75 | 76 | /** 77 | * Set the $loading property 78 | * 79 | * @param string $value 80 | * @return $this 81 | */ 82 | public function loadingStrategy(string $value): PictureTag 83 | { 84 | $this->loadingStrategy = $value; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Set the $placeholder property 91 | * 92 | * @param string $value 93 | * @return $this 94 | */ 95 | public function placeholder(string $value): PictureTag 96 | { 97 | $this->placeholder = $value; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Merge the passed array of tag attributes into $pictureAttrs 104 | * 105 | * @param array $value 106 | * @return $this 107 | */ 108 | public function pictureAttrs(array $value): PictureTag 109 | { 110 | $this->pictureAttrs = array_merge($this->pictureAttrs, $value); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Merge the passed array of tag attributes into $sourceAttrs 117 | * 118 | * @param array $value 119 | * @return $this 120 | */ 121 | public function sourceAttrs(array $value): PictureTag 122 | { 123 | foreach ($this->sourceAttrs as &$attrs) { 124 | $attrs = array_merge($attrs, $value); 125 | } 126 | unset($attrs); 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Merge the passed array of tag attributes into $imgAttrs 133 | * 134 | * @param array $value 135 | * @return $this 136 | */ 137 | public function imgAttrs(array $value): PictureTag 138 | { 139 | $this->imgAttrs = array_merge($this->imgAttrs, $value); 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Add art direction sources to the $sourceAttrs 146 | * 147 | * @param OptimizedImage $optimizedImage 148 | * @param array $sourceAttrs 149 | * @return PictureTag 150 | */ 151 | public function addSourceFrom(OptimizedImage $optimizedImage, array $sourceAttrs = []): PictureTag 152 | { 153 | $this->populateSourceAttrs($optimizedImage, $sourceAttrs); 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * Generate a complete tag for the $optimizedImage OptimizedImage model 160 | * 161 | * @return Markup 162 | */ 163 | public function render(): Markup 164 | { 165 | $content = ''; 166 | // Handle the tag(s) 167 | foreach ($this->sourceAttrs as $attrs) { 168 | // Handle lazy loading 169 | if ($this->loadingStrategy !== 'eager') { 170 | $attrs = $this->swapLazyLoadAttrs($this->loadingStrategy, $this->placeholder, $attrs); 171 | } 172 | // Remove any empty attributes 173 | $attrs = array_filter($attrs); 174 | // Render the tag 175 | $content .= Html::tag('source', '', $attrs); 176 | } 177 | // Handle the tag 178 | $attrs = $this->imgAttrs; 179 | // Handle lazy loading 180 | if ($this->loadingStrategy !== 'eager') { 181 | $attrs = $this->swapLazyLoadAttrs($this->loadingStrategy, $this->placeholder, $attrs); 182 | } 183 | // Remove any empty attributes 184 | $attrs = $this->filterEmptyAttributes($attrs); 185 | // Render the tag 186 | $content .= Html::tag('img', '', $attrs); 187 | // Handle the tag 188 | $attrs = $this->pictureAttrs; 189 | // Remove any empty attributes 190 | $attrs = array_filter($attrs); 191 | // Render the tag 192 | $tag = Html::tag('picture', $content, $attrs); 193 | 194 | return Template::raw($tag); 195 | } 196 | 197 | /** 198 | * Populate the $sourceAttrs from the passed in $optimizedImage and $sizes 199 | * 200 | * @param OptimizedImage $optimizedImage 201 | * @param array $sourceAttrs attributes to add to the $sourceAttrs array 202 | * @return void 203 | */ 204 | protected function populateSourceAttrs(OptimizedImage $optimizedImage, array $sourceAttrs): void 205 | { 206 | if (!empty($optimizedImage->optimizedWebPImageUrls)) { 207 | $this->sourceAttrs[] = array_merge([ 208 | 'media' => '', 209 | 'srcset' => $optimizedImage->getSrcsetFromArray($optimizedImage->optimizedWebPImageUrls), 210 | 'type' => 'image/webp', 211 | 'sizes' => '100vw', 212 | 'width' => $optimizedImage->placeholderWidth, 213 | 'height' => $optimizedImage->placeholderHeight, 214 | ], $sourceAttrs); 215 | } 216 | $this->sourceAttrs[] = array_merge([ 217 | 'media' => '', 218 | 'srcset' => $optimizedImage->getSrcsetFromArray($optimizedImage->optimizedImageUrls), 219 | 'sizes' => '100vw', 220 | 'width' => $optimizedImage->placeholderWidth, 221 | 'height' => $optimizedImage->placeholderHeight, 222 | ], $sourceAttrs); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/models/Settings.php: -------------------------------------------------------------------------------- 1 | = 50% should be 111 | * automatically sharpened 112 | */ 113 | public bool $autoSharpenScaledImages = true; 114 | 115 | /** 116 | * @var int The amount an image needs to be scaled down for automatic sharpening 117 | * to be applied 118 | */ 119 | public int $sharpenScaledImagePercentage = 50; 120 | 121 | /** 122 | * @var bool Whether to allow limiting the creation of Optimized Image Variants 123 | * for images by sub-folders 124 | */ 125 | public bool $assetVolumeSubFolders = true; 126 | 127 | /** 128 | * @var ImageTransformInterface[] The default Image Transform type classes 129 | */ 130 | public array $defaultImageTransformTypes = [ 131 | ]; 132 | 133 | /** 134 | * @var array Default aspect ratios 135 | */ 136 | public array $defaultAspectRatios = [ 137 | ['x' => 16, 'y' => 9], 138 | ['x' => 8, 'y' => 5], 139 | ['x' => 4, 'y' => 3], 140 | ['x' => 5, 'y' => 4], 141 | ['x' => 1, 'y' => 1], 142 | ['x' => 9, 'y' => 16], 143 | ['x' => 5, 'y' => 8], 144 | ['x' => 3, 'y' => 4], 145 | ['x' => 4, 'y' => 5], 146 | ]; 147 | 148 | /** 149 | * @var array Default variants 150 | */ 151 | public array $defaultVariants = [ 152 | [ 153 | 'width' => 1200, 154 | 'useAspectRatio' => true, 155 | 'aspectRatioX' => 16.0, 156 | 'aspectRatioY' => 9.0, 157 | 'retinaSizes' => ['1'], 158 | 'quality' => 82, 159 | 'format' => 'jpg', 160 | ], 161 | [ 162 | 'width' => 992, 163 | 'useAspectRatio' => true, 164 | 'aspectRatioX' => 16.0, 165 | 'aspectRatioY' => 9.0, 166 | 'retinaSizes' => ['1'], 167 | 'quality' => 82, 168 | 'format' => 'jpg', 169 | ], 170 | [ 171 | 'width' => 768, 172 | 'useAspectRatio' => true, 173 | 'aspectRatioX' => 4.0, 174 | 'aspectRatioY' => 3.0, 175 | 'retinaSizes' => ['1'], 176 | 'quality' => 60, 177 | 'format' => 'jpg', 178 | ], 179 | [ 180 | 'width' => 576, 181 | 'useAspectRatio' => true, 182 | 'aspectRatioX' => 4.0, 183 | 'aspectRatioY' => 3.0, 184 | 'retinaSizes' => ['1'], 185 | 'quality' => 60, 186 | 'format' => 'jpg', 187 | ], 188 | ]; 189 | 190 | /** 191 | * @var array Active image processors 192 | */ 193 | public array $activeImageProcessors = [ 194 | 'jpg' => [ 195 | 'jpegoptim', 196 | ], 197 | 'png' => [ 198 | 'optipng', 199 | ], 200 | 'svg' => [ 201 | 'svgo', 202 | ], 203 | 'gif' => [ 204 | 'gifsicle', 205 | ], 206 | ]; 207 | 208 | /** 209 | * @var array Active image variant creators 210 | */ 211 | public array $activeImageVariantCreators = [ 212 | 'jpg' => [ 213 | 'cwebp', 214 | ], 215 | 'png' => [ 216 | 'cwebp', 217 | ], 218 | 'gif' => [ 219 | 'cwebp', 220 | ], 221 | ]; 222 | 223 | /** 224 | * @var array Preset image processors 225 | */ 226 | public array $imageProcessors = [ 227 | // jpeg optimizers 228 | 'jpegoptim' => [ 229 | 'commandPath' => '/usr/bin/jpegoptim', 230 | 'commandOptions' => '-s', 231 | 'commandOutputFileFlag' => '', 232 | ], 233 | 'mozjpeg' => [ 234 | 'commandPath' => '/usr/bin/mozjpeg', 235 | 'commandOptions' => '-optimize -copy none', 236 | 'commandOutputFileFlag' => '-outfile', 237 | ], 238 | 'jpegtran' => [ 239 | 'commandPath' => '/usr/bin/jpegtran', 240 | 'commandOptions' => '-optimize -copy none', 241 | 'commandOutputFileFlag' => '', 242 | ], 243 | // png optimizers 244 | 'optipng' => [ 245 | 'commandPath' => '/usr/bin/optipng', 246 | 'commandOptions' => '-o3 -strip all', 247 | 'commandOutputFileFlag' => '', 248 | ], 249 | 'pngcrush' => [ 250 | 'commandPath' => '/usr/bin/pngcrush', 251 | 'commandOptions' => '-brute -ow', 252 | 'commandOutputFileFlag' => '', 253 | ], 254 | 'pngquant' => [ 255 | 'commandPath' => '/usr/bin/pngquant', 256 | 'commandOptions' => '--strip --skip-if-larger', 257 | 'commandOutputFileFlag' => '', 258 | ], 259 | // svg optimizers 260 | 'svgo' => [ 261 | 'commandPath' => '/usr/bin/svgo', 262 | 'commandOptions' => '', 263 | 'commandOutputFileFlag' => '', 264 | ], 265 | // gif optimizers 266 | 'gifsicle' => [ 267 | 'commandPath' => '/usr/bin/gifsicle', 268 | 'commandOptions' => '-O3 -k 256', 269 | 'commandOutputFileFlag' => '', 270 | ], 271 | ]; 272 | 273 | public array $imageVariantCreators = [ 274 | // webp variant creator 275 | 'cwebp' => [ 276 | 'commandPath' => '/usr/bin/cwebp', 277 | 'commandOptions' => '', 278 | 'commandOutputFileFlag' => '-o', 279 | 'commandQualityFlag' => '-q', 280 | 'imageVariantExtension' => 'webp', 281 | ], 282 | ]; 283 | 284 | // Public Methods 285 | // ========================================================================= 286 | 287 | /** 288 | * @inheritdoc 289 | */ 290 | public function __construct(array $config = []) 291 | { 292 | // Unset any deprecated properties 293 | if (!empty($config)) { 294 | // Handle migrating old Imagix settings 295 | if (isset($config['imgixDomain'])) { 296 | $config['imageTransformTypeSettings'][ImgixImageTransform::class]['domain'] = $config['imgixDomain']; 297 | } 298 | if (isset($config['imgixApiKey'])) { 299 | $config['imageTransformTypeSettings'][ImgixImageTransform::class]['apiKey'] = $config['imgixApiKey']; 300 | } 301 | if (isset($config['imgixSecurityToken'])) { 302 | $config['imageTransformTypeSettings'][ImgixImageTransform::class]['securityToken'] = $config['imgixSecurityToken']; 303 | } 304 | // Handle migrating old Thumbor settings 305 | if (isset($config['thumborBaseUrl'])) { 306 | $config['imageTransformTypeSettings'][ThumborImageTransform::class]['baseUrl'] = $config['thumborBaseUrl']; 307 | } 308 | if (isset($config['thumborSecurityKey'])) { 309 | $config['imageTransformTypeSettings'][ThumborImageTransform::class]['securityKey'] = $config['thumborSecurityKey']; 310 | } 311 | // Remove deprecated properties 312 | foreach (self::DEPRECATED_PROPERTIES as $prop) { 313 | unset($config[$prop]); 314 | } 315 | } 316 | 317 | parent::__construct($config); 318 | } 319 | 320 | /** 321 | * @inheritdoc 322 | */ 323 | public function rules(): array 324 | { 325 | return [ 326 | ['transformClass', 'string'], 327 | ['transformClass', 'default', 'value' => CraftImageTransform::class], 328 | [ 329 | [ 330 | 'automaticallyResaveImageVariants', 331 | 'generateTransformsBeforePageLoad', 332 | 'createColorPalette', 333 | 'createPlaceholderSilhouettes', 334 | 'capSilhouetteSvgSize', 335 | 'lowerQualityRetinaImageVariants', 336 | 'allowUpScaledImageVariants', 337 | 'autoSharpenScaledImages', 338 | 'assetVolumeSubFolders', 339 | ], 340 | 'boolean', 341 | ], 342 | ['sharpenScaledImagePercentage', 'integer', 'min' => 0, 'max' => 100], 343 | [ 344 | [ 345 | 'defaultVariants', 346 | 'activeImageProcessors', 347 | 'activeImageVariantCreators', 348 | 'imageProcessors', 349 | 'imageVariantCreators', 350 | ], 351 | 'required', 352 | ], 353 | [ 354 | [ 355 | 'imageTransformTypeSettings', 356 | 'defaultImageTransformTypes', 357 | 'defaultVariants', 358 | 'activeImageProcessors', 359 | 'activeImageVariantCreators', 360 | 'imageProcessors', 361 | 'imageVariantCreators', 362 | ], 363 | ArrayValidator::class, 364 | ], 365 | ]; 366 | } 367 | 368 | /** 369 | * @inheritdoc 370 | */ 371 | public function fields(): array 372 | { 373 | // Only return user-editable settings 374 | return [ 375 | 'transformClass', 376 | 'imageTransformTypeSettings', 377 | 'createColorPalette', 378 | 'createPlaceholderSilhouettes', 379 | 'capSilhouetteSvgSize', 380 | 'lowerQualityRetinaImageVariants', 381 | 'allowUpScaledImageVariants', 382 | 'autoSharpenScaledImages', 383 | 'sharpenScaledImagePercentage', 384 | 'assetVolumeSubFolders', 385 | ]; 386 | } 387 | 388 | /** 389 | * @return array 390 | */ 391 | public function behaviors(): array 392 | { 393 | return [ 394 | 'typecast' => [ 395 | 'class' => AttributeTypecastBehavior::class, 396 | // 'attributeTypes' will be composed automatically according to `rules()` 397 | ], 398 | 'parser' => [ 399 | 'class' => EnvAttributeParserBehavior::class, 400 | 'attributes' => [ 401 | ], 402 | ], 403 | ]; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/models/TagInterface.php: -------------------------------------------------------------------------------- 1 | "; 72 | 73 | return $header . ImageOptimize::$plugin->optimizedImages->encodeOptimizedSVGDataUri($content); 74 | } 75 | 76 | /** 77 | * Generate a base64-encoded placeholder image 78 | * 79 | * @param string $tempPath 80 | * @param float $aspectRatio 81 | * @param mixed|string|null $position 82 | * 83 | * @return string 84 | */ 85 | public function generatePlaceholderImage(string $tempPath, float $aspectRatio, mixed $position): string 86 | { 87 | Craft::beginProfile('generatePlaceholderImage', __METHOD__); 88 | Craft::info( 89 | 'Generating placeholder image for asset', 90 | __METHOD__ 91 | ); 92 | $result = ''; 93 | $width = self::PLACEHOLDER_WIDTH; 94 | $height = (int)($width / $aspectRatio); 95 | $placeholderPath = $this->createImageFromPath($tempPath, $width, $height, self::PLACEHOLDER_QUALITY, $position); 96 | if (!empty($placeholderPath)) { 97 | $result = base64_encode(file_get_contents($placeholderPath)); 98 | unlink($placeholderPath); 99 | } 100 | Craft::endProfile('generatePlaceholderImage', __METHOD__); 101 | 102 | return $result; 103 | } 104 | 105 | /** 106 | * Generate a color palette from the image 107 | * 108 | * @param string $tempPath 109 | * 110 | * @return array 111 | */ 112 | public function generateColorPalette(string $tempPath): array 113 | { 114 | Craft::beginProfile('generateColorPalette', __METHOD__); 115 | Craft::info( 116 | 'Generating color palette for: ' . $tempPath, 117 | __METHOD__ 118 | ); 119 | $colorPalette = []; 120 | if (!empty($tempPath)) { 121 | // Extract the color palette 122 | try { 123 | $palette = ColorThief::getPalette($tempPath, 5); 124 | } catch (Exception $e) { 125 | Craft::error($e->getMessage(), __METHOD__); 126 | 127 | return []; 128 | } 129 | // Convert RGB to hex color 130 | foreach ($palette as $colors) { 131 | $colorPalette[] = sprintf('#%02x%02x%02x', $colors[0], $colors[1], $colors[2]); 132 | } 133 | } 134 | Craft::endProfile('generateColorPalette', __METHOD__); 135 | 136 | return $colorPalette; 137 | } 138 | 139 | /** 140 | * @param array $colors 141 | * 142 | * @return float|int|null 143 | */ 144 | public function calculateLightness(array $colors): float|int|null 145 | { 146 | $lightness = null; 147 | if (!empty($colors)) { 148 | $lightness = 0; 149 | $colorWeight = count($colors); 150 | $colorCount = 0; 151 | foreach ($colors as $color) { 152 | $rgb = ColorHelper::HTMLToRGB($color); 153 | $hsl = ColorHelper::RGBToHSL($rgb); 154 | $lightness += $hsl['l'] * $colorWeight; 155 | $colorCount += $colorWeight; 156 | $colorWeight--; 157 | } 158 | 159 | $lightness /= $colorCount; 160 | } 161 | 162 | return $lightness === null ? $lightness : (int)$lightness; 163 | } 164 | 165 | /** 166 | * Generate an SVG image via Potrace 167 | * 168 | * @param string $tempPath 169 | * 170 | * @return string 171 | */ 172 | public function generatePlaceholderSvg(string $tempPath): string 173 | { 174 | Craft::beginProfile('generatePlaceholderSvg', __METHOD__); 175 | $result = ''; 176 | 177 | if (!empty($tempPath)) { 178 | // Potracio depends on `gd` being installed 179 | if (function_exists('imagecreatefromjpeg')) { 180 | $pot = new Potracio(); 181 | $pot->loadImageFromFile($tempPath); 182 | $pot->process(); 183 | 184 | $result = $pot->getSVG(1); 185 | 186 | // Optimize the result if we got one 187 | if (!empty($result)) { 188 | $result = ImageOptimize::$plugin->optimizedImages->encodeOptimizedSVGDataUri($result); 189 | } 190 | } 191 | /** @var Settings $settings */ 192 | $settings = ImageOptimize::$plugin->getSettings(); 193 | /** 194 | * If Potracio failed or gd isn't installed, or this is larger 195 | * than MAX_SILHOUETTE_SIZE bytes, just return a box 196 | */ 197 | if (empty($result) || ((strlen($result) > self::MAX_SILHOUETTE_SIZE) && $settings->capSilhouetteSvgSize)) { 198 | $size = getimagesize($tempPath); 199 | if ($size !== false) { 200 | [$width, $height] = $size; 201 | $result = $this->generatePlaceholderBox($width, $height); 202 | } 203 | } 204 | } 205 | Craft::endProfile('generatePlaceholderSvg', __METHOD__); 206 | 207 | return $result; 208 | } 209 | 210 | /** 211 | * Create a small placeholder image file that the various placerholder 212 | * generators can use 213 | * 214 | * @param Asset $asset 215 | * @param float $aspectRatio 216 | * @param mixed|string|null $position 217 | * 218 | * @return string 219 | */ 220 | public function createTempPlaceholderImage(Asset $asset, float $aspectRatio, mixed $position): string 221 | { 222 | Craft::beginProfile('createTempPlaceholderImage', __METHOD__); 223 | Craft::info( 224 | 'Creating temporary placeholder image for asset', 225 | __METHOD__ 226 | ); 227 | $width = self::TEMP_PLACEHOLDER_WIDTH; 228 | $height = (int)($width / $aspectRatio); 229 | $tempPath = $this->createImageFromAsset($asset, $width, $height, self::TEMP_PLACEHOLDER_QUALITY, $position); 230 | Craft::endProfile('createTempPlaceholderImage', __METHOD__); 231 | 232 | return $tempPath; 233 | } 234 | 235 | /** 236 | * @param Asset $asset 237 | * @param int $width 238 | * @param int $height 239 | * @param int $quality 240 | * @param mixed|string|null $position 241 | * 242 | * @return string 243 | */ 244 | public function createImageFromAsset(Asset $asset, int $width, int $height, int $quality, mixed $position): string 245 | { 246 | $tempPath = ''; 247 | if (Image::canManipulateAsImage($asset->getExtension())) { 248 | $imageSource = TransformHelper::getLocalImageSource($asset); 249 | // Scale and crop the placeholder image 250 | $tempPath = $this->createImageFromPath($imageSource, $width, $height, $quality, $position); 251 | } 252 | 253 | return $tempPath; 254 | } 255 | 256 | /** 257 | * @param string $filePath 258 | * @param int $width 259 | * @param int $height 260 | * @param int $quality 261 | * @param mixed|string|null $position 262 | * 263 | * @return string 264 | */ 265 | public function createImageFromPath( 266 | string $filePath, 267 | int $width, 268 | int $height, 269 | int $quality, 270 | mixed $position, 271 | ): string { 272 | $images = Craft::$app->getImages(); 273 | $pathParts = pathinfo($filePath); 274 | try { 275 | if (StringHelper::toLowerCase($pathParts['extension']) === 'svg') { 276 | $image = $images->loadImage($filePath, true, $width); 277 | } else { 278 | $image = $images->loadImage($filePath); 279 | } 280 | } catch (Throwable $e) { 281 | Craft::error( 282 | 'Error creating temporary image: ' . $e->getMessage(), 283 | __METHOD__ 284 | ); 285 | 286 | return ''; 287 | } 288 | 289 | if ($image instanceof Raster) { 290 | $image->setQuality($quality); 291 | } 292 | 293 | // Resize the image 294 | $image->scaleAndCrop($width, $height, true, $position); 295 | 296 | // Strip any EXIF data from the image before trying to save it 297 | if ($image instanceof Raster) { 298 | $imagineImage = $image->getImagineImage(); 299 | if ($imagineImage) { 300 | $imagineImage->strip(); 301 | } 302 | } 303 | 304 | 305 | // Save the image out to a temp file, then return its contents 306 | $tempFilename = uniqid(pathinfo($pathParts['filename'], PATHINFO_FILENAME), true) . '.' . 'jpg'; 307 | $tempPath = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . $tempFilename; 308 | clearstatcache(true, $tempPath); 309 | try { 310 | $image->saveAs($tempPath); 311 | } catch (Throwable $e) { 312 | Craft::error( 313 | 'Error saving temporary image: ' . $e->getMessage(), 314 | __METHOD__ 315 | ); 316 | } 317 | 318 | return $tempPath; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/services/ServicesTrait.php: -------------------------------------------------------------------------------- 1 | = 8.2, and config() is called before __construct(), 41 | // so we can't extract it from the passed in $config 42 | $majorVersion = '5'; 43 | // Dev server container name & port are based on the major version of this plugin 44 | $devPort = 3000 + (int)$majorVersion; 45 | $versionName = 'v' . $majorVersion; 46 | return [ 47 | 'components' => [ 48 | 'optimize' => OptimizeService::class, 49 | 'optimizedImages' => OptimizedImagesService::class, 50 | 'placeholder' => PlaceholderService::class, 51 | // Register the vite service 52 | 'vite' => [ 53 | 'assetClass' => ImageOptimizeAsset::class, 54 | 'checkDevServer' => true, 55 | 'class' => VitePluginService::class, 56 | 'devServerInternal' => 'http://craft-imageoptimize-' . $versionName . '-buildchain-dev:' . $devPort, 57 | 'devServerPublic' => 'http://localhost:' . $devPort, 58 | 'errorEntry' => 'src/js/ImageOptimize.js', 59 | 'useDevServer' => true, 60 | ], 61 | ], 62 | ]; 63 | } 64 | 65 | // Public Methods 66 | // ========================================================================= 67 | 68 | /** 69 | * Returns the optimize service 70 | * 71 | * @return OptimizeService The optimize service 72 | * @throws InvalidConfigException 73 | */ 74 | public function getOptimize(): OptimizeService 75 | { 76 | return $this->get('optimize'); 77 | } 78 | 79 | /** 80 | * Returns the optimizedImages service 81 | * 82 | * @return OptimizedImagesService The optimizedImages service 83 | * @throws InvalidConfigException 84 | */ 85 | public function getOptimizedImages(): OptimizedImagesService 86 | { 87 | return $this->get('optimizedImages'); 88 | } 89 | 90 | /** 91 | * Returns the placeholder service 92 | * 93 | * @return PlaceholderService The placeholder service 94 | * @throws InvalidConfigException 95 | */ 96 | public function getPlaceholder(): PlaceholderService 97 | { 98 | return $this->get('placeholder'); 99 | } 100 | 101 | /** 102 | * Returns the vite service 103 | * 104 | * @return VitePluginService The vite service 105 | * @throws InvalidConfigException 106 | */ 107 | public function getVite(): VitePluginService 108 | { 109 | return $this->get('vite'); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/templates/_components/fields/OptimizedImages_error.twig: -------------------------------------------------------------------------------- 1 | {# @var craft \craft\web\twig\variables\CraftVariable #} 2 | {# 3 | /** 4 | * Image Optimize plugin for Craft CMS 5 | * 6 | * OptimizedImages Field Input 7 | * 8 | * @author nystudio107 9 | * @copyright Copyright (c) 2017 nystudio107 10 | * @link https://nystudio107.com 11 | * @package ImageOptimize 12 | * @since 1.4.10 13 | */ 14 | #} 15 | 16 | {% import "_includes/forms" as forms %} 17 | 18 |
19 |
20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /src/templates/_components/fields/focal-point.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/templates/_components/utilities/ImageOptimizeUtility_content.twig: -------------------------------------------------------------------------------- 1 | {# @var craft \craft\web\twig\variables\CraftVariable #} 2 | {# 3 | /** 4 | * ImageOptimize plugin for Craft CMS 5 | * 6 | * ImageOptimizeUtility Utility Content 7 | * 8 | * @author nystudio107 9 | * @copyright Copyright (c) 2020 nystudio107 10 | * @link https://nystudio107.com 11 | * @package ImageOptimize 12 | * @since 1.0.0 13 | */ 14 | #} 15 | 16 |

ImageOptimize Info

17 | 18 | {% include "image-optimize/settings/image-transforms/craft.twig" %} 19 | -------------------------------------------------------------------------------- /src/templates/_includes/checkboxGroup.twig: -------------------------------------------------------------------------------- 1 | {# @var craft \craft\web\twig\variables\CraftVariable #} 2 | {# 3 | /** 4 | * Image Optimize plugin for Craft CMS 5 | * 6 | * Custom `checkboxGroup` input. Checkbox group doesn't allow access to checkbox, therefore we can't control the ID. 7 | * For this reason we use create the a custom `checkboxGroup` that will use Craft native checkboxes. 8 | * 9 | * @author nystudio107 10 | * @copyright Copyright (c) 2017 nystudio107 11 | * @link https://nystudio107.com 12 | * @package ImageOptimize 13 | * @since 1.5.6 14 | */ 15 | #} 16 | 17 | {% import "_includes/forms" as forms %} 18 | 19 | {% if name is defined and name %} 20 | 21 | {% endif -%} 22 | 23 | {%- set options = (options is defined ? options : []) %} 24 | {%- set values = (values is defined ? values : []) %} 25 | {%- set name = (name is defined and name ? name~'[]' : null) %} 26 | 27 |
29 | {%- for key, option in options %} 30 | {%- if option is not iterable %} 31 | {%- set option = {label: option, value: key} %} 32 | {%- endif %} 33 |
34 | {{ forms.checkbox({ 35 | id: (id is defined ? id ~ '-' ~ key : null), 36 | name: name, 37 | checked: (option.value is defined and option.value in values), 38 | autofocus: (autofocus is defined and autofocus and loop.first and not craft.app.request.isMobileBrowser(true)) 39 | }|merge(option)) }} 40 |
41 | {%- endfor %} 42 |
43 | -------------------------------------------------------------------------------- /src/templates/_includes/macros.twig: -------------------------------------------------------------------------------- 1 | {% macro configWarning(setting, file) -%} 2 | {%- set configArray = craft.app.config.getConfigFromFile(file) -%} 3 | {%- if configArray[setting] is defined -%} 4 | {{- "This is being overridden by the `#{setting}` setting in the `config/#{file}.php` file." |raw }} 5 | {%- else -%} 6 | {{ false }} 7 | {%- endif -%} 8 | {%- endmacro %} 9 | 10 | {% macro checkboxGroupField(config) %} 11 | {% import "_includes/forms" as forms %} 12 | {% import _self as macros %} 13 | {{ forms.field(config, macros.checkboxGroup(config)) }} 14 | {% endmacro %} 15 | 16 | {% macro checkboxGroup(config) %} 17 | {% include "image-optimize/_includes/checkboxGroup" with config only %} 18 | {% endmacro %} 19 | -------------------------------------------------------------------------------- /src/templates/_layouts/imageoptimize-cp.twig: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/cp" %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | {% set tagOptions = { 6 | 'depends': [ 7 | 'nystudio107\\imageoptimize\\assetbundles\\imageoptimize\\ImageOptimizeAsset' 8 | ], 9 | } %} 10 | {{ craft.imageOptimize.register('src/js/ImageOptimize.js', false, tagOptions, tagOptions) }} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /src/templates/frontend/lazysizes-fallback.twig.js: -------------------------------------------------------------------------------- 1 | // ref: https://web.dev/articles/browser-level-image-lazy-loading#how-do-i-handle-browsers-that-don't-yet-support-native-lazy-loading 2 | if ('loading' in HTMLImageElement.prototype) { 3 | // Replace the img.src with what is in the data-src property 4 | const images = document.querySelectorAll('img[loading="lazy"]'); 5 | images.forEach(img => { 6 | if (img.dataset.src) { 7 | img.src = img.dataset.src; 8 | img.removeAttribute('data-src'); 9 | } 10 | if (img.dataset.srcset) { 11 | img.srcset = img.dataset.srcset; 12 | img.removeAttribute('data-srcset'); 13 | } 14 | }); 15 | // Replace the source.srcset with what is in the data-srcset property 16 | const sources = document.querySelectorAll('source[data-srcset]') 17 | sources.forEach(source => { 18 | if (source.dataset.srcset) { 19 | source.srcset = source.dataset.srcset; 20 | source.removeAttribute('data-srcset'); 21 | } 22 | if (source.dataset.sizes) { 23 | source.sizes = source.dataset.sizes; 24 | source.removeAttribute('data-sizes'); 25 | } 26 | }); 27 | } else { 28 | // Dynamically import the LazySizes library 29 | const script = document.createElement('script'); 30 | script.src = '{{ scriptSrc }}'; 31 | document.body.appendChild(script); 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/frontend/lazysizes.twig.js: -------------------------------------------------------------------------------- 1 | // Dynamically import the LazySizes library 2 | const script = document.createElement('script'); 3 | script.src = '{{ scriptSrc }}'; 4 | document.body.appendChild(script); 5 | -------------------------------------------------------------------------------- /src/templates/settings/_settings.twig: -------------------------------------------------------------------------------- 1 | {# @var craft \craft\web\twig\variables\CraftVariable #} 2 | {# 3 | /** 4 | * Image Optimize plugin for Craft CMS 5 | * 6 | * Image Optimize Settings.twig 7 | * 8 | * @author nystudio107 9 | * @copyright Copyright (c) 2017 nystudio107 10 | * @link https://nystudio107.com 11 | * @package ImageOptimize 12 | * @since 1.5.0 13 | */ 14 | #} 15 | 16 | {% from 'image-optimize/_includes/macros' import configWarning %} 17 | 18 | {% import "_includes/forms" as forms %} 19 | 20 | {% do view.registerAssetBundle("nystudio107\\imageoptimize\\assetbundles\\imageoptimize\\ImageOptimizeAsset") %} 21 | 22 |
23 | 24 | 25 | {{ forms.selectField({ 26 | label: "Transform Method"|t('image-optimize'), 27 | instructions: "Choose from Craft native transforms or an image transform service to handle your image transforms site-wide."|t('image-optimize'), 28 | id: 'transformClass', 29 | name: 'transformClass', 30 | value: settings.transformClass, 31 | options: imageTransformTypeOptions, 32 | class: 'io-transform-method', 33 | warning: configWarning('transformClass', 'image-optimize'), 34 | }) }} 35 | 36 | {% for type in allImageTransformTypes %} 37 | {% set isCurrent = (type == className(imageTransform)) %} 38 | 45 | {% endfor %} 46 | 47 |
48 |
49 |

OptimizedImages Field Settings

50 |
51 |

ImageOptimize also comes with an OptimizedImages Field that you can add to an Asset 52 | Volume's layout. The OptimizedImages Field makes creating responsive image sizes for 53 | {{ '' |escape }} or {{ '' |escape }} elements 54 | sublimely easy.

55 |

These responsive image transforms are created when an asset is saved, rather than at page 56 | load time, to ensure that frontend performance is optimal.

57 |
58 |
59 |
60 | 61 | {{ forms.lightswitchField({ 62 | label: "Create Color Palette"|t('image-optimize'), 63 | instructions: "Controls whether a dominant color palette should be created for image variants. It takes a bit of time, so if you never plan to use it, you can turn it off."|t('image-optimize'), 64 | 'id': 'createColorPalette', 65 | 'name': 'createColorPalette', 66 | 'on': settings.createColorPalette, 67 | 'warning': configWarning('createColorPalette', 'image-optimize') 68 | }) }} 69 | 70 | {% if not gdInstalled %} 71 |
72 |

You do not have the GD PHP extension installed, so placeholder silhouettes 73 | cannot be generated. An SVG box will be used instead.

74 |
75 | {% endif %} 76 | {{ forms.lightswitchField({ 77 | label: "Create Placeholder Silhouettes"|t('image-optimize'), 78 | instructions: "Controls whether SVG placeholder silhouettes should be created for image variants. It takes a bit of time, so if you never plan to use them, you can turn it off."|t('image-optimize'), 79 | 'id': 'createPlaceholderSilhouettes', 80 | 'name': 'createPlaceholderSilhouettes', 81 | 'on': settings.createPlaceholderSilhouettes, 82 | 'warning': configWarning('createPlaceholderSilhouettes', 'image-optimize') 83 | }) }} 84 | 85 | {{ forms.lightswitchField({ 86 | label: "Cap Placeholder Silhouette Size"|t('image-optimize'), 87 | instructions: "This option caps the placeholder silhouette SVG size to 32kB. If it's larger than that, a default SVG box is returned."|t('image-optimize'), 88 | 'id': 'capSilhouetteSvgSize', 89 | 'name': 'capSilhouetteSvgSize', 90 | 'on': settings.capSilhouetteSvgSize, 91 | 'warning': configWarning('capSilhouetteSvgSize', 'image-optimize') 92 | }) }} 93 | 94 | {{ forms.lightswitchField({ 95 | label: "Lower Quality Retina Image Variants"|t('image-optimize'), 96 | instructions: "Controls whether retina images are automatically created with reduced quality. Learn more."|t('image-optimize') |raw, 97 | 'id': 'lowerQualityRetinaImageVariants', 98 | 'name': 'lowerQualityRetinaImageVariants', 99 | 'on': settings.lowerQualityRetinaImageVariants, 100 | 'warning': configWarning('lowerQualityRetinaImageVariants', 'image-optimize') 101 | }) }} 102 | 103 | {{ forms.lightswitchField({ 104 | label: "Allow Up-Scaled Image Variants"|t('image-optimize'), 105 | instructions: "Controls whether Optimized Image Variants are created that would be up-scaled to be larger than the original source image."|t('image-optimize'), 106 | 'id': 'allowUpScaledImageVariants', 107 | 'name': 'allowUpScaledImageVariants', 108 | 'on': settings.allowUpScaledImageVariants, 109 | 'warning': configWarning('allowUpScaledImageVariants', 'image-optimize') 110 | }) }} 111 | 112 | {{ forms.lightswitchField({ 113 | label: "Auto Sharpen Scaled Images"|t('image-optimize'), 114 | instructions: "Controls whether images scaled down >= 50% should be automatically sharpened."|t('image-optimize'), 115 | 'id': 'autoSharpenScaledImages', 116 | 'name': 'autoSharpenScaledImages', 117 | 'on': settings.autoSharpenScaledImages, 118 | 'warning': configWarning('autoSharpenScaledImages', 'image-optimize') 119 | }) }} 120 | 121 | {{ forms.textField({ 122 | label: "Sharpen Image Percentage"|t("image-optimize"), 123 | instructions: "The amount an image needs to be scaled down for automatic sharpening to be applied."|t("image-optimize"), 124 | id: "sharpenScaledImagePercentage", 125 | name: "sharpenScaledImagePercentage", 126 | size: 5, 127 | maxlength: 5, 128 | value: settings.sharpenScaledImagePercentage, 129 | warning: configWarning("sharpenScaledImagePercentage", "image-optimize"), 130 | errors: settings.getErrors("sharpenScaledImagePercentage"), 131 | }) }} 132 | 133 | {{ forms.lightswitchField({ 134 | label: "Limit by Sub-Folder"|t('image-optimize'), 135 | instructions: "Whether to allow limiting the creation of Optimized Image Variants for images by sub-folders."|t('image-optimize'), 136 | 'id': 'assetVolumeSubFolders', 137 | 'name': 'assetVolumeSubFolders', 138 | 'on': settings.assetVolumeSubFolders, 139 | 'warning': configWarning('assetVolumeSubFolders', 'image-optimize') 140 | }) }} 141 |
142 | 143 | {% js %} 144 | new Craft.AdminTable({ 145 | tableSelector: '#imageProcessors', 146 | }); 147 | new Craft.AdminTable({ 148 | tableSelector: '#variantCreators', 149 | }); 150 | $('.io-transform-method').change(function(ev) { 151 | $('.io-method-settings').hide(); 152 | var value = 'io-' + Craft.formatInputId($(ev.target).val()) + '-method'; 153 | $('.' + value).slideDown(); 154 | }); 155 | {% endjs %} 156 | -------------------------------------------------------------------------------- /src/templates/settings/image-transforms/craft.twig: -------------------------------------------------------------------------------- 1 | {% from 'image-optimize/_includes/macros' import configWarning %} 2 | 3 | {% import "_includes/forms" as forms %} 4 | 5 |
6 |

{{ 'Image Processors & Variant Creators' }}

7 |
8 |

9 | The following ImageOptimize settings are not editable here; this is 10 | just an informational display. 11 | Instead copy the imageoptimize/config.php file to 12 | Craft's config/ directory, renaming it to image-optimize.php, 13 | and make your changes there to override default settings. 14 |

15 |
16 |
17 | {% if imageProcessors is defined and imageProcessors |length %} 18 |

{{ 'Active Image Processors' }}

19 | 20 | 21 | 22 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | {% for imageProcessor in imageProcessors %} 32 | 34 | 37 | 40 | 43 | 54 | 55 | {% endfor %} 56 | 57 |
{{ "File Format"|t('image-optimize') }}{{ "Image Processor"|t('image-optimize') }}{{ "Command"|t('image-optimize') }}{{ "Installed"|t('image-optimize') }}
35 | {{ imageProcessor.format }} 36 | 38 | {{ imageProcessor.creator }} 39 | 41 | {{ imageProcessor.command }} 42 | 44 | {% if imageProcessor.installed %} 45 | 48 | {% else %} 49 | 52 | {% endif %} 53 |
58 |
59 |
60 |

61 | Image Processors optimize Craft's Image Transforms by losslessly 62 | removing metadata and cruft from the images. 63 |

64 |
65 |
66 | {% endif %} 67 | 68 | {% if variantCreators is defined and variantCreators |length %} 69 |

{{ 'Active Image Variant Creators' }}

70 | 71 | 72 | 73 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | {% for variantCreator in variantCreators %} 83 | 85 | 88 | 91 | 94 | 105 | 106 | {% endfor %} 107 | 108 |
{{ "File Format"|t('image-optimize') }}{{ "Variant Creator"|t('image-optimize') }}{{ "Command"|t('image-optimize') }}{{ "Installed"|t('image-optimize') }}
86 | {{ variantCreator.format }} 87 | 89 | {{ variantCreator.creator }} 90 | 92 | {{ variantCreator.command }} 93 | 95 | {% if variantCreator.installed %} 96 | 99 | {% else %} 100 | 103 | {% endif %} 104 |
109 |
110 |
111 |

112 | Image Variant Creators make new image variants from Craft's 113 | Image Transforms. 114 |

115 |
116 |
117 | {% endif %} 118 | -------------------------------------------------------------------------------- /src/templates/settings/index.twig: -------------------------------------------------------------------------------- 1 | {% requireAdmin %} 2 | 3 | {% set crumbs = [ 4 | { label: "Settings"|t('app'), url: url('settings') }, 5 | { label: "Plugins"|t('app'), url: url('settings/plugins') } 6 | ] %} 7 | 8 | {% set fullPageForm = true %} 9 | 10 | {% extends "image-optimize/_layouts/imageoptimize-cp.twig" %} 11 | {% set title = plugin.name %} 12 | {% set docTitle = title ~ ' - ' ~ "Plugins"|t('app') %} 13 | 14 | {% block content %} 15 | 16 | 17 | {{ redirectInput('settings/plugins') }} 18 | {% namespace 'settings' %} 19 | {{ settingsHtml|raw }} 20 | {% endnamespace %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /src/templates/welcome.twig: -------------------------------------------------------------------------------- 1 | {# @var craft \craft\web\twig\variables\CraftVariable #} 2 | {% extends 'image-optimize/_layouts/imageoptimize-cp.twig' %} 3 | 4 | {% set title = 'Welcome to ImageOptimize!' %} 5 | 6 | {% set linkGetStarted = url('settings/plugins/image-optimize') %} 7 | {% set docsUrl = "https://github.com/nystudio107/craft-imageoptimize/blob/v1/README.md" %} 8 | 9 | {% do view.registerAssetBundle("nystudio107\\imageoptimize\\assetbundles\\imageoptimize\\ImageOptimizeAsset") %} 10 | {% set baseAssetsUrl = view.getAssetManager().getPublishedUrl('@nystudio107/imageoptimize/web/assets/dist', true) %} 11 | 12 | {% set crumbs = [ 13 | { label: "ImageOptimize", url: url('image-optimize') }, 14 | { label: "Welcome"|t, url: url('image-optimize/welcome') }, 15 | ] %} 16 | 17 | {% block content %} 18 |
19 | 20 | 21 | 22 |

Thanks for using ImageOptimize!

23 |

ImageOptimize allows you to automatically create & optimize responsive image transforms from your Craft CMS 24 | assets.

25 |

It works equally well with native Craft image transforms, and image services like imgix, 28 | with zero template changes.

29 | 30 |

We hope you love it! For more information, please see 31 | the documentation.

32 |

33 |   34 |

35 |

36 | 37 | 38 | 39 |

40 |
41 |
42 |

43 | Brought to you by nystudio107 44 |

45 |
46 | {% endblock %} 47 | 48 | {% block foot %} 49 | {# include our JavaScript modules #} 50 | {{ parent() }} 51 | {% set tagOptions = { 52 | 'depends': [ 53 | 'nystudio107\\imageoptimize\\assetbundles\\imageoptimize\\ImageOptimizeAsset' 54 | ], 55 | } %} 56 | {{ craft.imageOptimize.register('src/js/Welcome.js', false, tagOptions, tagOptions) }} 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /src/translations/en/image-optimize.php: -------------------------------------------------------------------------------- 1 | 'does not exist', 18 | 'Original' => 'Original', 19 | 'Optimized' => 'Optimized', 20 | 'Savings' => 'Savings', 21 | 'Not Installed' => 'Not Installed', 22 | 'The aspectRatioX of the transformed images.' => 'The aspectRatioX of the transformed images.', 23 | 'The source domain to use for the imgix transforms.' => 'The source domain to use for the imgix transforms.', 24 | 'The additional retina sizes that should be created for this variant.' => 'The additional retina sizes that should be created for this variant.', 25 | 'Variant Creator' => 'Variant Creator', 26 | 'The width of the optimized image variant in pixels. This should match your `@media` query breakpoints.' => 'The width of the optimized image variant in pixels. This should match your `@media` query breakpoints.', 27 | 'Focal Point not set' => 'Focal Point not set', 28 | 'Failed to create image variant at: ' => 'Failed to create image variant at: ', 29 | 'The aspectRatioY of the transformed images.' => 'The aspectRatioY of the transformed images.', 30 | 'This image is upscaled' => 'This image is upscaled', 31 | 'Retina Sizes' => 'Retina Sizes', 32 | 'Choose from Craft native transforms or an image transform service to handle your image transforms site-wide.' => 'Choose from Craft native transforms or an image transform service to handle your image transforms site-wide.', 33 | 'Craft' => 'Craft', 34 | 'imgix' => 'imgix', 35 | '{name} plugin loaded' => '{name} plugin loaded', 36 | 'Transform Method' => 'Transform Method', 37 | 'Variant' => 'Variant', 38 | 'File Format' => 'File Format', 39 | 'Image Processor' => 'Image Processor', 40 | 'The quality of the optimized image variant.' => 'The quality of the optimized image variant.', 41 | 'Command' => 'Command', 42 | 'Installed' => 'Installed', 43 | 'Focal Point set' => 'Focal Point set', 44 | 'Started resaveAsset queue job id: {jobId} Element id: {elementId}' => 'Started resaveAsset queue job id: {jobId} Element id: {elementId}', 45 | 'Started resaveVolumeAssets queue job id: {jobId}' => 'Started resaveVolumeAssets queue job id: {jobId}', 46 | 'Controls whether Optimized Image Variants are created that would be up-scaled to be larger than the original source image.' => 'Controls whether Optimized Image Variants are created that would be up-scaled to be larger than the original source image.', 47 | 'Optimizing images in {name}' => 'Optimizing images in {name}', 48 | 'The optional [security token](https://docs.imgix.com/setup/securing-images) used to sign image URLs from imgix.' => 'The optional [security token](https://docs.imgix.com/setup/securing-images) used to sign image URLs from imgix.', 49 | 'Controls whether retina images are automatically created with reduced quality as per here.' => 'Controls whether retina images are automatically created with reduced quality as per here.', 50 | 'Display Optimized Image Variants' => 'Display Optimized Image Variants', 51 | 'Controls whether images scaled down >= 50% should be automatically sharpened.' => 'Controls whether images scaled down >= 50% should be automatically sharpened.', 52 | 'Controls whether SVG placeholder silhouettes should be created for image variants. It takes a bit of time, so if you never plan to use them, you can turn it off.' => 'Controls whether SVG placeholder silhouettes should be created for image variants. It takes a bit of time, so if you never plan to use them, you can turn it off.', 53 | 'Controls whether the optimized image variants will be displayed in the Edit Asset HUD.' => 'Controls whether the optimized image variants will be displayed in the Edit Asset HUD.', 54 | 'Optimizing image id {id}' => 'Optimizing image id {id}', 55 | 'Auto Sharpen Scaled Images' => 'Auto Sharpen Scaled Images', 56 | 'Display Dominant Color Palette' => 'Display Dominant Color Palette', 57 | 'Image transform >= 50%, sharpened the transformed image: {name}' => 'Image transform >= 50%, sharpened the transformed image: {name}', 58 | 'Controls whether the dominant color palette will be displayed in the Edit Asset HUD.' => 'Controls whether the dominant color palette will be displayed in the Edit Asset HUD.', 59 | 'Create Placeholder Silhouettes' => 'Create Placeholder Silhouettes', 60 | 'Display LazyLoad Placeholder Images' => 'Display LazyLoad Placeholder Images', 61 | 'Controls whether a dominant color palette should be created for image variants. It takes a bit of time, so if you never plan to use it, you can turn it off.' => 'Controls whether a dominant color palette should be created for image variants. It takes a bit of time, so if you never plan to use it, you can turn it off.', 62 | 'Create Color Palette' => 'Create Color Palette', 63 | 'The API key to use for the imgix transforms (needed for auto-purging changed assets).' => 'The API key to use for the imgix transforms (needed for auto-purging changed assets).', 64 | 'Controls whether the lazy load placeholder images will be displayed in the Edit Asset HUD.' => 'Controls whether the lazy load placeholder images will be displayed in the Edit Asset HUD.', 65 | 'Lower Quality Retina Image Variants' => 'Lower Quality Retina Image Variants', 66 | 'Allow Up-Scaled Image Variants' => 'Allow Up-Scaled Image Variants', 67 | 'Manifest file not found at: {manifestPath}' => 'Manifest file not found at: {manifestPath}', 68 | 'Don\'t create image transforms for files that are of the following types:' => 'Don\'t create image transforms for files that are of the following types:', 69 | 'Ignore Files' => 'Ignore Files', 70 | 'GIF' => 'GIF', 71 | 'The optional [security key](https://thumbor.readthedocs.io/en/latest/security.html) used by Thumbor to create secure image urls.' => 'The optional [security key](https://thumbor.readthedocs.io/en/latest/security.html) used by Thumbor to create secure image urls.', 72 | 'SVG' => 'SVG', 73 | 'The base URL to use for the Thumbor transforms.' => 'The base URL to use for the Thumbor transforms.', 74 | 'Module does not exist in the manifest: {moduleName}' => 'Module does not exist in the manifest: {moduleName}', 75 | 'Thumbor' => 'Thumbor', 76 | 'Optionally prefix your asset path with the bucket name. This is useful if your Thumbor configuration does not specify an explicit bucket. Only relevant for AWS S3 volumes at this time.' => 'Optionally prefix your asset path with the bucket name. This is useful if your Thumbor configuration does not specify an explicit bucket. Only relevant for AWS S3 volumes at this time.', 77 | 'Object failed to validate' => 'Object failed to validate', 78 | 'Is not a Model object.' => 'Is not a Model object.', 79 | 'Generic Transform' => 'Generic Transform', 80 | 'Placeholder Silhouette' => 'Placeholder Silhouette', 81 | 'OptimizedImages fields only work when added to an Asset Volume\'s layout.' => 'OptimizedImages fields only work when added to an Asset Volume\'s layout.', 82 | 'Sharp' => 'Sharp', 83 | 'Placeholder Box' => 'Placeholder Box', 84 | 'Create Optimized Image Variants for images in the sub-folders:' => 'Create Optimized Image Variants for images in the sub-folders:', 85 | 'LazyLoad Placeholder Images:' => 'LazyLoad Placeholder Images:', 86 | 'Dominant Color Palette:' => 'Dominant Color Palette:', 87 | 'Color Palette not extracted' => 'Color Palette not extracted', 88 | 'Optimized Image Variants:' => 'Optimized Image Variants:', 89 | 'Placeholder Image' => 'Placeholder Image', 90 | 'The base URL to use for the Sharp transform service. If Sharp is being used via [AWS Serverless Image Handler](https://aws.amazon.com/solutions/serverless-image-handler/), this is your \'Image handler distribution\' CloudFront distribution URL.' => 'The base URL to use for the Sharp transform service. If Sharp is being used via [AWS Serverless Image Handler](https://aws.amazon.com/solutions/serverless-image-handler/), this is your \'Image handler distribution\' CloudFront distribution URL.', 91 | 'Lightness:' => 'Lightness:', 92 | 'Add Variant' => 'Add Variant', 93 | 'Imgix' => 'Imgix', 94 | 'The source domain to use for the Imgix transforms.' => 'The source domain to use for the Imgix transforms.', 95 | 'Whether to allow limiting the creation of Optimized Image Variants for images by sub-folders.' => 'Whether to allow limiting the creation of Optimized Image Variants for images by sub-folders.', 96 | 'The API key to use for the Imgix transforms (needed for auto-purging changed assets).' => 'The API key to use for the Imgix transforms (needed for auto-purging changed assets).', 97 | 'The optional [security token](https://docs.imgix.com/setup/securing-images) used to sign image URLs from Imgix.' => 'The optional [security token](https://docs.imgix.com/setup/securing-images) used to sign image URLs from Imgix.', 98 | 'ImageOptimize Info' => 'ImageOptimize Info', 99 | 'Limit by Sub-Folder' => 'Limit by Sub-Folder', 100 | 'Cap Placeholder Silhouette Size' => 'Cap Placeholder Silhouette Size', 101 | 'The amount of sharpening that should be applied if an image is scaled down more than 50% via the [USM parameter](https://docs.imgix.com/apis/rendering/adjustment/usm).' => 'The amount of sharpening that should be applied if an image is scaled down more than 50% via the [USM parameter](https://docs.imgix.com/apis/rendering/adjustment/usm).', 102 | 'Error rendering template string -> {error}' => 'Error rendering template string -> {error}', 103 | 'The amount an image needs to be scaled down for automatic sharpening to be applied.' => 'The amount an image needs to be scaled down for automatic sharpening to be applied.', 104 | 'Error rendering `{template}` -> {error}' => 'Error rendering `{template}` -> {error}', 105 | 'This option caps the placeholder silhouette SVG size to 32Kb. If it\'s larger than that, a default SVG box is returned.' => 'This option caps the placeholder silhouette SVG size to 32Kb. If it\'s larger than that, a default SVG box is returned.', 106 | 'This option caps the placeholder silhouette SVG size to 32kB. If it\'s larger than that, a default SVG box is returned.' => 'This option caps the placeholder silhouette SVG size to 32kB. If it\'s larger than that, a default SVG box is returned.', 107 | 'Controls whether retina images are automatically created with reduced quality. Learn more.' => 'Controls whether retina images are automatically created with reduced quality. Learn more.', 108 | 'Sharpen Image Percentage' => 'Sharpen Image Percentage', 109 | ]; 110 | -------------------------------------------------------------------------------- /src/utilities/ImageOptimizeUtility.php: -------------------------------------------------------------------------------- 1 | optimize->getActiveImageProcessors(); 68 | $variantCreators = ImageOptimize::$plugin->optimize->getActiveVariantCreators(); 69 | 70 | return Craft::$app->getView()->renderTemplate( 71 | 'image-optimize/_components/utilities/ImageOptimizeUtility_content', 72 | [ 73 | 'imageProcessors' => $imageProcessors, 74 | 'variantCreators' => $variantCreators, 75 | ] 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/validators/EmbeddedModelValidator.php: -------------------------------------------------------------------------------- 1 | $attribute; 34 | 35 | if (!empty($value) && $value instanceof Model) { 36 | if (!$value->validate()) { 37 | $errors = $value->getErrors(); 38 | foreach ($errors as $attributeError => $valueErrors) { 39 | /** @var array $valueErrors */ 40 | foreach ($valueErrors as $valueError) { 41 | $model->addError( 42 | $attribute, 43 | Craft::t('image-optimize', 'Object failed to validate') 44 | . '-' . $attributeError . ' - ' . $valueError 45 | ); 46 | } 47 | } 48 | } 49 | } else { 50 | $model->addError($attribute, Craft::t('image-optimize', 'Is not a Model object.')); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/variables/ImageOptimizeVariable.php: -------------------------------------------------------------------------------- 1 | optimize->renderLazySizesFallbackJs($scriptAttrs, $variables)); 45 | } 46 | 47 | /** 48 | * Render the LazySizes JS 49 | * 50 | * @param array $scriptAttrs 51 | * @param array $variables 52 | * @return Markup 53 | */ 54 | public function renderLazySizesJs(array $scriptAttrs = [], array $variables = []): Markup 55 | { 56 | return Template::raw(ImageOptimize::$plugin->optimize->renderLazySizesJs($scriptAttrs, $variables)); 57 | } 58 | 59 | /** 60 | * Return an SVG box as a placeholder image 61 | * 62 | * @param $width 63 | * @param $height 64 | * @param ?string $color 65 | * 66 | * @return Markup 67 | */ 68 | public function placeholderBox($width, $height, ?string $color = null): Markup 69 | { 70 | return Template::raw(ImageOptimize::$plugin->placeholder->generatePlaceholderBox($width, $height, $color)); 71 | } 72 | 73 | /** 74 | * @param Asset $asset 75 | * @param ?array $variants 76 | * @param bool $generatePlaceholders 77 | * 78 | * @return ?OptimizedImage 79 | * @throws InvalidConfigException 80 | */ 81 | public function createOptimizedImages( 82 | Asset $asset, 83 | ?array $variants = null, 84 | bool $generatePlaceholders = false, 85 | ): ?OptimizedImage { 86 | // Override our settings for lengthy operations, since we're doing this via Twig 87 | ImageOptimize::$generatePlaceholders = $generatePlaceholders; 88 | 89 | return ImageOptimize::$plugin->optimizedImages->createOptimizedImages($asset, $variants); 90 | } 91 | 92 | /** 93 | * Returns whether `.webp` is a format supported by the server 94 | * 95 | * @return bool 96 | */ 97 | public function serverSupportsWebP(): bool 98 | { 99 | return ImageOptimize::$plugin->optimize->serverSupportsWebP(); 100 | } 101 | 102 | /** 103 | * Creates an Image Transform with a given config. 104 | * 105 | * @param mixed $config The Image Transform’s class name, or its config, 106 | * with a `type` value and optionally a `settings` value 107 | * 108 | * @return ?ImageTransformInterface The Image Transform 109 | */ 110 | public function createImageTransformType($config): ?ImageTransformInterface 111 | { 112 | return ImageOptimize::$plugin->optimize->createImageTransformType($config); 113 | } 114 | 115 | /** 116 | * Return whether we are running Craft 3.1 or later 117 | * 118 | * @return bool 119 | */ 120 | public function craft31(): bool 121 | { 122 | return true; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/field-D_XkB3eu.js: -------------------------------------------------------------------------------- 1 | function u(t,e=!1,n=1){const o=e?1e3:1024;if(Math.abs(t)=o&&s0){const n=performance.getEntriesByName(e)[0];if(typeof n<"u"){const o=t.parentNode.parentNode.parentNode.nextElementSibling.querySelector(".io-file-size");o&&(o.innerHTML=u(n.decodedBodySize,!0))}}}(function(t,e,n){const o="ImageOptimizeOptimizedImages",a={};function s(i,r){this.element=i,this.options=t.extend({},a,r),this._defaults=a,this._name=o,this.init()}s.prototype={init:function(){t(function(){const i=n.querySelectorAll("img.io-preview-image");for(const r of i)r.complete?d(r):r.addEventListener("load",c=>{d(c.target)})})}},t.fn[o]=function(i){return this.each(function(){t.data(this,"plugin_"+o)||t.data(this,"plugin_"+o,new s(this,i))})}})($,window,document);Craft.OptimizedImagesInput=Garnish.Base.extend({id:null,inputNamePrefix:null,inputIdPrefix:null,$container:null,$blockContainer:null,$addBlockBtnContainer:null,$addBlockBtnGroup:null,$addBlockBtnGroupBtns:null,blockSort:null,blockSelect:null,init:function(t,e){this.id=t,this.inputNamePrefix=e,this.inputIdPrefix=Craft.formatInputId(this.inputNamePrefix),this.$container=$("#"+this.id),this.$blockContainer=this.$container.children(".variant-blocks"),this.$addBlockBtnContainer=this.$container.children(".buttons"),this.$addBlockBtnGroup=this.$addBlockBtnContainer.children(".btngroup"),this.$addBlockBtnGroupBtns=this.$addBlockBtnGroup.children(".btn"),this.$blockContainer.find("> > .actions > .settings").each((o,a)=>{const s=$(a);let i;s.data("menubtn")?i=s.data("menubtn"):i=new Garnish.MenuBtn(a),i.menu.settings.onOptionSelect=$.proxy(function(r){this.onMenuOptionSelect(r,i)},this)});const n=this.$blockContainer.children();this.blockSort=new Garnish.DragSort(n,{handle:"> .actions > .move",axis:"y",collapseDraggees:!0,magnetStrength:4,helperLagBase:1.5,helperOpacity:.9,onSortChange:$.proxy(function(){this.resetVariantBlockOrder()},this)}),this.addListener(this.$addBlockBtnGroupBtns,"click",function(){this.addVariantBlock(null)}),this.addAspectRatioHandlers(),this.reIndexVariants()},onMenuOptionSelect:function(t,e){const n=$(t),o=e.$btn.closest(".matrixblock");switch(n.data("action")){case"add":{this.addVariantBlock(o);break}case"delete":{n.hasClass("disabled")||this.deleteVariantBlock(o);break}}},getHiddenBlockCss:function(t){return{opacity:0,marginBottom:-t.outerHeight()}},reIndexVariants:function(){this.$blockContainer=this.$container.children(".variant-blocks");const t=this.$blockContainer.children();t.each(function(n,o){const a=n,i=$(o).find("div .field, label, input, select");$(i).each(function(r,c){let l=$(c).attr("id");l&&(l=l.replace(/-([0-9]+)-/g,"-"+a+"-"),$(c).attr("id",l)),l=$(c).attr("for"),l&&(l=l.replace(/-([0-9]+)-/g,"-"+a+"-"),$(c).attr("for",l)),l=$(c).attr("name"),l&&(l=l.replace(/\[([0-9]+)]/g,"["+a+"]"),$(c).attr("name",l))})});let e=!1;t.length==1&&(e=!0),t.find("> .actions > .settings").each(function(n,o){const a=$(o);let s;if(a.data("menubtn")&&(s=a.data("menubtn"),typeof s.menu.$options<"u")){let i=$(s.menu.$options[1]);typeof i<"u"&&(e?i.addClass("disabled").disable():i.removeClass("disabled").enable())}})},addAspectRatioHandlers:function(){this.addListener($(".lightswitch"),"click",function(t){$(t.target).closest(".matrixblock").find(".io-aspect-ratio-wrapper").slideToggle()}),this.addListener($(".io-select-ar-box"),"click",function(t){const e=$(t.target);let n=$(t.target).data("x"),o=$(t.target).data("y"),a=$(t.target).data("custom"),s,i;i=e.closest(".matrixblock"),i.find(".io-select-ar-box").each(function(r,c){$(c).removeClass("io-selected-ar-box")}),e.addClass("io-selected-ar-box"),a?i.find(".io-custom-ar-wrapper").slideDown():(i.find(".io-custom-ar-wrapper").slideUp(),s=i.find("input")[2],$(s).val(n),s=i.find("input")[3],$(s).val(o))})},addVariantBlock:function(t){let e=$(this.$blockContainer.children()[0]).clone();e.find(".io-select-ar-box").each((o,a)=>{o===0?$(a).addClass("io-selected-ar-box"):$(a).removeClass("io-selected-ar-box")}),e.find(".io-custom-ar-wrapper").hide();let n=e.find("input")[0];$(n).val(1200),n=e.find("input")[1],$(n).val(1),n=e.find("input")[2],$(n).val(16),n=e.find("input")[3],$(n).val(9),n=e.find("select")[0],$(n).val(82),n=e.find("select")[1],$(n).val("jpg"),e.css(this.getHiddenBlockCss(e)).velocity({opacity:1,"margin-bottom":10},"fast",$.proxy(function(){t?e.insertBefore(t):e.appendTo(this.$blockContainer),this.blockSort.addItems(e),this.addAspectRatioHandlers(),e.find(".settings").each((o,a)=>{let s=$(a),i,r;r=this.$container.find(".io-menu-clone > .menu").clone(),$(r).insertAfter(s),i=new Garnish.MenuBtn(a),i.menu.settings.onOptionSelect=$.proxy(function(c){this.onMenuOptionSelect(c,i)},this)}),this.reIndexVariants()},this))},deleteVariantBlock:function(t){t.velocity(this.getHiddenBlockCss(t),"fast",$.proxy(()=>{t.remove(),this.reIndexVariants()},this))},resetVariantBlockOrder:function(){this.reIndexVariants()}});$(document).ready(function(){const t=new CustomEvent("vite-script-loaded",{detail:{path:"../src/web/assets/src/js/OptimizedImagesField.js"}});document.dispatchEvent(t)}); 2 | //# sourceMappingURL=field-D_XkB3eu.js.map 3 | -------------------------------------------------------------------------------- /src/web/assets/dist/assets/field-D_XkB3eu.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/craft-imageoptimize/77cfa09fc779ee77be376603f66f92e1283df0eb/src/web/assets/dist/assets/field-D_XkB3eu.js.gz -------------------------------------------------------------------------------- /src/web/assets/dist/assets/field-D_XkB3eu.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"field-D_XkB3eu.js","sources":["../../../../../buildchain/src/js/OptimizedImagesField.js"],"sourcesContent":["/* global $ */\n/* global Craft */\n/* global Garnish */\n\n/**\n * Image Optimize plugin for Craft CMS\n *\n * OptimizedImages Field JS\n *\n * @author nystudio107\n * @copyright Copyright (c) 2017 nystudio107\n * @link https://nystudio107.com\n * @package ImageOptimize\n * @since 1.2.0\n */\n\n/**\n * Convert the passed in bytes into a human readable format\n *\n * @param bytes\n * @param si\n * @param dp\n * @returns {string}\n */\nfunction humanFileSize(bytes, si = false, dp = 1) {\n const thresh = si ? 1000 : 1024;\n\n if (Math.abs(bytes) < thresh) {\n return bytes + ' B';\n }\n\n const units = si\n ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];\n let u = -1;\n const r = 10 ** dp;\n\n do {\n bytes /= thresh;\n ++u;\n } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);\n\n\n return bytes.toFixed(dp) + ' ' + units[u];\n}\n\n/**\n * After an image has loaded, query the performance API for the decodedBodySize\n *\n * @param image\n */\nfunction imageLoaded(image) {\n const url = image.src || image.href;\n if (url && url.length > 0) {\n const iTime = performance.getEntriesByName(url)[0];\n if (typeof iTime !== \"undefined\") {\n const elem = image.parentNode.parentNode.parentNode.nextElementSibling.querySelector('.io-file-size');\n if (elem) {\n elem.innerHTML = humanFileSize(iTime.decodedBodySize, true);\n }\n }\n }\n}\n\n(function ($, window, document) {\n\n const pluginName = \"ImageOptimizeOptimizedImages\",\n defaults = {};\n\n // Plugin constructor\n function Plugin(element, options) {\n this.element = element;\n\n this.options = $.extend({}, defaults, options);\n\n this._defaults = defaults;\n this._name = pluginName;\n\n this.init();\n }\n\n Plugin.prototype = {\n\n init: function () {\n $(function () {\n\n /* -- _this.options gives us access to the $jsonVars that our FieldType passed down to us */\n\n const images = document.querySelectorAll(\"img.io-preview-image\");\n for (const image of images) {\n if (image.complete) {\n imageLoaded(image);\n } else {\n image.addEventListener('load', (event) => {\n imageLoaded(event.target);\n });\n }\n }\n });\n }\n };\n\n // A really lightweight plugin wrapper around the constructor,\n // preventing against multiple instantiations\n $.fn[pluginName] = function (options) {\n return this.each(function () {\n if (!$.data(this, \"plugin_\" + pluginName)) {\n $.data(this, \"plugin_\" + pluginName,\n new Plugin(this, options));\n }\n });\n };\n\n})($, window, document);\n\nCraft.OptimizedImagesInput = Garnish.Base.extend(\n {\n id: null,\n inputNamePrefix: null,\n inputIdPrefix: null,\n\n $container: null,\n $blockContainer: null,\n $addBlockBtnContainer: null,\n $addBlockBtnGroup: null,\n $addBlockBtnGroupBtns: null,\n\n blockSort: null,\n blockSelect: null,\n\n init: function (id, inputNamePrefix) {\n this.id = id;\n this.inputNamePrefix = inputNamePrefix;\n this.inputIdPrefix = Craft.formatInputId(this.inputNamePrefix);\n\n this.$container = $('#' + this.id);\n this.$blockContainer = this.$container.children('.variant-blocks');\n this.$addBlockBtnContainer = this.$container.children('.buttons');\n this.$addBlockBtnGroup = this.$addBlockBtnContainer.children('.btngroup');\n this.$addBlockBtnGroupBtns = this.$addBlockBtnGroup.children('.btn');\n\n // Create our action button menus\n this.$blockContainer.find('> > .actions > .settings').each((index, value) => {\n const $value = $(value);\n let menuBtn;\n if ($value.data('menubtn')) {\n menuBtn = $value.data('menubtn');\n } else {\n menuBtn = new Garnish.MenuBtn(value);\n }\n menuBtn.menu.settings.onOptionSelect = $.proxy(function (option) {\n this.onMenuOptionSelect(option, menuBtn);\n }, this);\n });\n\n const $blocks = this.$blockContainer.children();\n\n this.blockSort = new Garnish.DragSort($blocks, {\n handle: '> .actions > .move',\n axis: 'y',\n collapseDraggees: true,\n magnetStrength: 4,\n helperLagBase: 1.5,\n helperOpacity: 0.9,\n onSortChange: $.proxy(function () {\n this.resetVariantBlockOrder();\n }, this)\n });\n\n this.addListener(this.$addBlockBtnGroupBtns, 'click', function () {\n this.addVariantBlock(null);\n });\n\n this.addAspectRatioHandlers();\n this.reIndexVariants();\n },\n\n onMenuOptionSelect: function (option, menuBtn) {\n const $option = $(option);\n const container = menuBtn.$btn.closest('.matrixblock');\n\n switch ($option.data('action')) {\n case 'add': {\n this.addVariantBlock(container);\n break;\n }\n case 'delete': {\n if (!$option.hasClass('disabled')) {\n this.deleteVariantBlock(container);\n }\n break;\n }\n }\n },\n\n getHiddenBlockCss: function ($block) {\n return {\n opacity: 0,\n marginBottom: -($block.outerHeight())\n };\n },\n\n // Re-index all of the variant blocks\n reIndexVariants: function () {\n this.$blockContainer = this.$container.children('.variant-blocks');\n const $blocks = this.$blockContainer.children();\n $blocks.each(function (index, value) {\n const variantIndex = index;\n const $value = $(value);\n const elements = $value.find('div .field, label, input, select');\n\n // Re-index all of the element attributes\n $(elements).each(function (index, value) {\n // id attributes\n let str = $(value).attr('id');\n if (str) {\n str = str.replace(/-([0-9]+)-/g, \"-\" + variantIndex + \"-\");\n $(value).attr('id', str);\n }\n // for attributes\n str = $(value).attr('for');\n if (str) {\n str = str.replace(/-([0-9]+)-/g, \"-\" + variantIndex + \"-\");\n $(value).attr('for', str);\n }\n // Name attributes\n str = $(value).attr('name');\n if (str) {\n str = str.replace(/\\[([0-9]+)]/g, \"[\" + variantIndex + \"]\");\n $(value).attr('name', str);\n }\n });\n });\n let disabledDeleteItem = false;\n // If we only have one block, don't allow it to be deleted\n if ($blocks.length == 1) {\n disabledDeleteItem = true;\n }\n $blocks.find('> .actions > .settings').each(function (index, value) {\n const $value = $(value);\n let menuBtn;\n if ($value.data('menubtn')) {\n menuBtn = $value.data('menubtn');\n if (typeof menuBtn.menu.$options !== \"undefined\") {\n let menuItem = $(menuBtn.menu.$options[1]);\n if (typeof menuItem !== \"undefined\") {\n if (disabledDeleteItem) {\n menuItem.addClass('disabled').disable();\n } else {\n menuItem.removeClass('disabled').enable();\n }\n }\n }\n }\n });\n },\n\n addAspectRatioHandlers: function () {\n this.addListener($('.lightswitch'), 'click', function (ev) {\n const $target = $(ev.target);\n const $block = $target.closest('.matrixblock');\n $block.find('.io-aspect-ratio-wrapper').slideToggle();\n });\n this.addListener($('.io-select-ar-box'), 'click', function (ev) {\n const $target = $(ev.target);\n let x = $(ev.target).data('x'),\n y = $(ev.target).data('y'),\n custom = $(ev.target).data('custom'),\n field, $block;\n // Select the appropriate aspect ratio\n $block = $target.closest('.matrixblock');\n $block.find('.io-select-ar-box').each(function (index, value) {\n $(value).removeClass('io-selected-ar-box');\n });\n $target.addClass('io-selected-ar-box');\n\n // Handle setting the actual field values\n if (custom) {\n $block.find('.io-custom-ar-wrapper').slideDown();\n } else {\n $block.find('.io-custom-ar-wrapper').slideUp();\n field = $block.find('input')[2];\n $(field).val(x);\n field = $block.find('input')[3];\n $(field).val(y);\n }\n });\n },\n\n addVariantBlock: function (container) {\n let $block = $(this.$blockContainer.children()[0]).clone();\n // Reset to default values\n $block.find('.io-select-ar-box').each((index, value) => {\n if (index === 0) {\n $(value).addClass('io-selected-ar-box');\n } else {\n $(value).removeClass('io-selected-ar-box');\n }\n });\n $block.find('.io-custom-ar-wrapper').hide();\n let field = $block.find('input')[0];\n $(field).val(1200);\n field = $block.find('input')[1];\n $(field).val(1);\n field = $block.find('input')[2];\n $(field).val(16);\n field = $block.find('input')[3];\n $(field).val(9);\n field = $block.find('select')[0];\n $(field).val(82);\n field = $block.find('select')[1];\n $(field).val('jpg');\n $block.css(this.getHiddenBlockCss($block)).velocity({\n opacity: 1,\n 'margin-bottom': 10\n }, 'fast', $.proxy(function () {\n // Insert the block in the right place\n if (container) {\n $block.insertBefore(container);\n } else {\n $block.appendTo(this.$blockContainer);\n }\n // Update the Garnish UI controls\n this.blockSort.addItems($block);\n this.addAspectRatioHandlers();\n $block.find('.settings').each((index, value) => {\n let $value = $(value),\n menuBtn,\n menu;\n\n menu = this.$container.find('.io-menu-clone > .menu').clone();\n $(menu).insertAfter($value);\n menuBtn = new Garnish.MenuBtn(value);\n\n menuBtn.menu.settings.onOptionSelect = $.proxy(function (option) {\n this.onMenuOptionSelect(option, menuBtn);\n }, this);\n });\n this.reIndexVariants();\n }, this));\n },\n\n deleteVariantBlock: function (container) {\n container.velocity(this.getHiddenBlockCss(container), 'fast', $.proxy(() => {\n container.remove();\n this.reIndexVariants();\n }, this));\n },\n\n resetVariantBlockOrder: function () {\n this.reIndexVariants();\n }\n });\n\n// Accept HMR as per: https://vitejs.dev/guide/api-hmr.html\nif (import.meta.hot) {\n import.meta.hot.accept(() => {\n console.log(\"HMR\")\n });\n}\n\n// Re-broadcast the custom `vite-script-loaded` event so that we know that this module has loaded\n// Needed because when \n","import Vue from 'vue';\nimport ConfettiParty from '@/vue/ConfettiParty.vue';\n\nnew Vue({\n el: \"#cp-nav-content\",\n components: {\n ConfettiParty,\n },\n});\n\n// Accept HMR as per: https://vitejs.dev/guide/api-hmr.html\nif (import.meta.hot) {\n import.meta.hot.accept(() => {\n console.log(\"HMR\")\n });\n}\n"],"names":["t","i","module","this","n","e","s","a","o","r","h","c","Vue","VueConfetti","_sfc_main","ConfettiParty"],"mappings":"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","x_google_ignoreList":[0]} -------------------------------------------------------------------------------- /src/web/assets/dist/assets/welcome-2KWkXHu8.js.map.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/craft-imageoptimize/77cfa09fc779ee77be376603f66f92e1283df0eb/src/web/assets/dist/assets/welcome-2KWkXHu8.js.map.gz -------------------------------------------------------------------------------- /src/web/assets/dist/img/ImageOptimize-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 23 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/web/assets/dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/js/ImageOptimize.js": { 3 | "file": "assets/imageoptimize-Cb5BVZk9.js", 4 | "name": "imageoptimize", 5 | "src": "src/js/ImageOptimize.js", 6 | "isEntry": true, 7 | "css": [ 8 | "assets/imageoptimize-B4gebLDH.css" 9 | ] 10 | }, 11 | "src/js/OptimizedImagesField.js": { 12 | "file": "assets/field-D_XkB3eu.js", 13 | "name": "field", 14 | "src": "src/js/OptimizedImagesField.js", 15 | "isEntry": true 16 | }, 17 | "src/js/Welcome.js": { 18 | "file": "assets/welcome-2KWkXHu8.js", 19 | "name": "welcome", 20 | "src": "src/js/Welcome.js", 21 | "isEntry": true 22 | } 23 | } --------------------------------------------------------------------------------