├── .gitignore ├── imagesize.png ├── filemanager.png ├── imagesizeitem.png ├── translations ├── ContaoImageAlternativesBundle.de.yaml └── ContaoImageAlternativesBundle.en.yaml ├── contao ├── languages │ ├── en │ │ ├── tl_image_size.php │ │ ├── tl_image_size_item.php │ │ └── tl_files.php │ └── de │ │ ├── tl_image_size.php │ │ ├── tl_image_size_item.php │ │ └── tl_files.php ├── dca │ ├── tl_image_size.php │ ├── tl_image_size_item.php │ └── tl_files.php └── templates │ └── be_importantPartSwitch.html5 ├── public ├── backend.css └── importantParts.js ├── src ├── ContaoImageAlternativesBundle.php ├── ContaoManager │ └── Plugin.php ├── EventListener │ ├── AddBackendAssetsListener.php │ └── DataContainer │ │ ├── ImportantPartsListener.php │ │ └── ImageAlternativesListener.php ├── DataContainer │ └── FolderDriver.php ├── DependencyInjection │ ├── Configuration.php │ └── ContaoImageAlternativesExtension.php └── Image │ └── PictureFactory.php ├── rector.php ├── .github └── FUNDING.yml ├── ecs.php ├── config └── services.yaml ├── composer.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor/ 3 | /node_modules/ 4 | -------------------------------------------------------------------------------- /imagesize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspiredminds/contao-image-alternatives/HEAD/imagesize.png -------------------------------------------------------------------------------- /filemanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspiredminds/contao-image-alternatives/HEAD/filemanager.png -------------------------------------------------------------------------------- /imagesizeitem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspiredminds/contao-image-alternatives/HEAD/imagesizeitem.png -------------------------------------------------------------------------------- /translations/ContaoImageAlternativesBundle.de.yaml: -------------------------------------------------------------------------------- 1 | alternative_description: 'Alternatives Bild für: %alternative%' 2 | -------------------------------------------------------------------------------- /translations/ContaoImageAlternativesBundle.en.yaml: -------------------------------------------------------------------------------- 1 | alternative_description: 'Alternative image for: %alternative%' 2 | -------------------------------------------------------------------------------- /contao/languages/en/tl_image_size.php: -------------------------------------------------------------------------------- 1 | true, 13 | 'inputType' => 'checkbox', 14 | 'eval' => ['tl_class' => 'w50'], 15 | 'sql' => ['type' => 'boolean', 'default' => false], 16 | ]; 17 | 18 | PaletteManipulator::create() 19 | ->addField('preCrop', 'expert_legend', PaletteManipulator::POSITION_APPEND) 20 | ->applyToPalette('default', 'tl_image_size') 21 | ; 22 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withSets([SetList::CONTAO]) 12 | ->withPaths([ 13 | __DIR__.'/contao', 14 | __DIR__.'/src', 15 | ]) 16 | ->withSkip([ 17 | FirstClassCallableRector::class, 18 | RemoveParentCallWithoutParentRector::class, 19 | ]) 20 | ->withParallel() 21 | ->withCache(sys_get_temp_dir().'/rector_cache') 22 | ; 23 | -------------------------------------------------------------------------------- /contao/dca/tl_image_size_item.php: -------------------------------------------------------------------------------- 1 | true, 13 | 'inputType' => 'select', 14 | 'eval' => ['includeBlankOption' => true, 'tl_class' => 'w50'], 15 | 'sql' => ['type' => 'string', 'length' => 32, 'default' => ''], 16 | ]; 17 | 18 | PaletteManipulator::create() 19 | ->addField('alternative', 'source_legend', PaletteManipulator::POSITION_APPEND) 20 | ->applyToPalette('default', 'tl_image_size_item') 21 | ; 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: fritzmg 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/ContaoManager/Plugin.php: -------------------------------------------------------------------------------- 1 | setLoadAfter([ContaoCoreBundle::class]), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/EventListener/AddBackendAssetsListener.php: -------------------------------------------------------------------------------- 1 | scopeMatcher->isBackendMainRequest($event)) { 19 | return; 20 | } 21 | 22 | $GLOBALS['TL_JAVASCRIPT'][] = 'bundles/contaoimagealternatives/importantParts.js|static|async'; 23 | $GLOBALS['TL_CSS'][] = 'bundles/contaoimagealternatives/backend.css|static'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | withSets([SetList::CONTAO]) 13 | ->withPaths([ 14 | __DIR__.'/contao', 15 | __DIR__.'/src', 16 | ]) 17 | ->withSkip([ 18 | CommentLengthFixer::class, 19 | MethodChainingIndentationFixer::class => ['*/DependencyInjection/Configuration.php'], 20 | ]) 21 | ->withConfiguredRule(HeaderCommentFixer::class, [ 22 | 'header' => "(c) INSPIRED MINDS", 23 | ]) 24 | ->withParallel() 25 | ->withSpacing(lineEnding: "\n") 26 | ->withCache(sys_get_temp_dir().'/ecs_default_cache') 27 | ; 28 | -------------------------------------------------------------------------------- /contao/dca/tl_files.php: -------------------------------------------------------------------------------- 1 | 'textarea', 16 | 'eval' => ['tl_class' => 'clr', 'decodeEntities' => true], 17 | 'sql' => ['type' => 'blob', 'notnull' => false, 'length' => 65535], // MySqlPlatform::LENGTH_LIMIT_BLOB 18 | ]; 19 | 20 | $GLOBALS['TL_DCA']['tl_files']['fields']['alternatives'] = [ 21 | 'sql' => ['type' => 'blob', 'notnull' => false, 'length' => 65535], // MySqlPlatform::LENGTH_LIMIT_BLOB 22 | ]; 23 | 24 | PaletteManipulator::create() 25 | ->removeField('importantPartX') 26 | ->removeField('importantPartY') 27 | ->removeField('importantPartWidth') 28 | ->removeField('importantPartHeight') 29 | ->applyToPalette('default', 'tl_files') 30 | ; 31 | -------------------------------------------------------------------------------- /contao/templates/be_importantPartSwitch.html5: -------------------------------------------------------------------------------- 1 |
10 | 23 | -------------------------------------------------------------------------------- /src/DataContainer/FolderDriver.php: -------------------------------------------------------------------------------- 1 | strField && null !== $this->objActiveRecord && 'file' === $this->objActiveRecord->type) { 33 | $row = preg_replace('~~', '', $row, -1, $count); 34 | 35 | if ($count > 0) { 36 | $template = new BackendTemplate('be_importantPartSwitch'); 37 | $template->alternatives = System::getContainer()->getParameter('contao_image_alternatives.alternatives'); 38 | $row = $template->parse().$row; 39 | } 40 | } 41 | 42 | return $row; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autoconfigure: true 4 | 5 | contao_image_alternatives.image.picture_factory: 6 | class: InspiredMinds\ContaoImageAlternatives\Image\PictureFactory 7 | decorates: contao.image.picture_factory 8 | arguments: 9 | - '@contao_image_alternatives.image.picture_factory.inner' 10 | - '@contao.image.factory' 11 | - '@contao.image.resizer' 12 | - '%contao_image_alternatives.alternative_sizes%' 13 | - '%contao_image_alternatives.predefined_sizes%' 14 | - '%kernel.project_dir%' 15 | 16 | contao_image_alternatives.data_container.image_alternatives: 17 | class: InspiredMinds\ContaoImageAlternatives\EventListener\DataContainer\ImageAlternativesListener 18 | arguments: 19 | - '@request_stack' 20 | - '@translator' 21 | - '%contao_image_alternatives.alternatives%' 22 | - '%kernel.project_dir%' 23 | - '%contao.image.valid_extensions%' 24 | 25 | contao_image_alternatives.data_container.important_parts: 26 | class: InspiredMinds\ContaoImageAlternatives\EventListener\DataContainer\ImportantPartsListener 27 | arguments: 28 | - '@request_stack' 29 | - '%kernel.project_dir%' 30 | - '%contao.image.valid_extensions%' 31 | 32 | InspiredMinds\ContaoImageAlternatives\EventListener\AddBackendAssetsListener: 33 | autowire: true 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inspiredminds/contao-image-alternatives", 3 | "description": "Contao extension to provide the possibility of defining alternative images to be used on different output devices.", 4 | "type": "contao-bundle", 5 | "license": "LGPL-3.0-or-later", 6 | "homepage": "https://github.com/inspiredminds/contao-image-alternatives", 7 | "authors": [ 8 | { 9 | "name": "Fritz Michael Gschwantner", 10 | "email": "fmg@inspiredminds.at", 11 | "role": "Developer" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/inspiredminds/contao-image-alternatives/issues", 16 | "source": "https://github.com/inspiredminds/contao-image-alternatives", 17 | "forum": "https://community.contao.org/de" 18 | }, 19 | "funding": [ 20 | { 21 | "type": "github", 22 | "url": "https://github.com/sponsors/fritzmg" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=8.2", 27 | "contao/core-bundle": "^5.3.40 || ^5.6.3", 28 | "symfony/config": "^6.4 || ^7.3", 29 | "symfony/dependency-injection": "^6.4 || ^7.3", 30 | "symfony/http-foundation": "^6.4 || ^7.3", 31 | "symfony/http-kernel": "^6.4 || ^7.3", 32 | "symfony/translation": "^6.4 || ^7.3" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "InspiredMinds\\ContaoImageAlternatives\\": "src/" 37 | } 38 | }, 39 | "extra": { 40 | "contao-manager-plugin": "InspiredMinds\\ContaoImageAlternatives\\ContaoManager\\Plugin" 41 | }, 42 | "require-dev": { 43 | "contao/easy-coding-standard": "^6.0", 44 | "contao/rector": "^1.0" 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "contao-components/installer": true, 49 | "contao/manager-plugin": true, 50 | "php-http/discovery": false, 51 | "dealerdirect/phpcodesniffer-composer-installer": true 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode() 21 | ->children() 22 | ->arrayNode('alternatives') 23 | ->info('The available image alternatives.') 24 | ->example(['tablet', 'mobile']) 25 | ->prototype('scalar')->end() 26 | ->defaultValue([]) 27 | ->end() 28 | ->arrayNode('sizes') 29 | ->info('Allows to define the usage of alternatives for existing contao.image.sizes entries. Also allows you to set whether to pre-crop images to the important part.') 30 | ->useAttributeAsKey('name') 31 | ->arrayPrototype() 32 | ->children() 33 | ->booleanNode('pre_crop') 34 | ->info('Pre-crops the image to the important part before processing the rest of the pipeline.') 35 | ->end() 36 | ->arrayNode('items') 37 | ->arrayPrototype() 38 | ->children() 39 | ->scalarNode('media') 40 | ->info('The media query this item applies too (needs to be the same as in the contao.image.sizes entry).') 41 | ->end() 42 | ->scalarNode('alternative') 43 | ->info('Defines which image alternative should automatically be used for this item.') 44 | ->end() 45 | ->end() 46 | ->end() 47 | ->end() 48 | ->end() 49 | ->end() 50 | ->end() 51 | ->end() 52 | ; 53 | 54 | return $treeBuilder; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DependencyInjection/ContaoImageAlternativesExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yaml'); 25 | 26 | $configuration = new Configuration(); 27 | 28 | $config = $this->processConfiguration($configuration, $configs); 29 | 30 | $container->setParameter('contao_image_alternatives.alternatives', $config['alternatives']); 31 | $container->setParameter('contao_image_alternatives.alternative_sizes', $this->processImageSizes($config['sizes'] ?? [])); 32 | } 33 | 34 | public function prepend(ContainerBuilder $container): void 35 | { 36 | if (class_exists(PreviewLinkController::class)) { 37 | $contaoConfig = new ContaoConfig((string) $container->getParameter('kernel.project_dir')); 38 | } else { 39 | $contaoConfig = new ContaoConfig((string) $container->getParameter('kernel.project_dir'), ''); 40 | } 41 | 42 | $config = $this->processConfiguration($contaoConfig, $container->getExtensionConfig('contao')); 43 | 44 | $imageSizes = []; 45 | 46 | // Do not add a size with the special name "_defaults" but merge its values into all other definitions instead. 47 | foreach ($config['image']['sizes'] as $name => $value) { 48 | if ('_defaults' === $name) { 49 | continue; 50 | } 51 | 52 | if (isset($config['image']['sizes']['_defaults'])) { 53 | // Make sure that arrays defined under _defaults will take precedence over empty arrays (see #2783) 54 | $value = array_merge( 55 | $config['image']['sizes']['_defaults'], 56 | array_filter($value, static fn ($v) => !\is_array($v) || [] !== $v), 57 | ); 58 | } 59 | 60 | $imageSizes[$name] = $value; 61 | } 62 | 63 | $container->setParameter('contao_image_alternatives.predefined_sizes', $this->processImageSizes($imageSizes)); 64 | } 65 | 66 | private function processImageSizes(array $sizes): array 67 | { 68 | $imageSizes = []; 69 | 70 | foreach ($sizes as $name => $value) { 71 | $imageSizes['_'.$name] = $this->camelizeKeys($value); 72 | } 73 | 74 | return $imageSizes; 75 | } 76 | 77 | /** 78 | * Camelizes keys so "resize_mode" becomes "resizeMode". 79 | */ 80 | private function camelizeKeys(array $config): array 81 | { 82 | $keys = array_keys($config); 83 | 84 | foreach ($keys as &$key) { 85 | if (\is_array($config[$key])) { 86 | $config[$key] = $this->camelizeKeys($config[$key]); 87 | } 88 | 89 | if (\is_string($key)) { 90 | $key = lcfirst(Container::camelize($key)); 91 | } 92 | } 93 | 94 | unset($key); 95 | 96 | return array_combine($keys, $config); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/EventListener/DataContainer/ImportantPartsListener.php: -------------------------------------------------------------------------------- 1 | requestStack->getCurrentRequest(); 32 | 33 | if ('edit' !== $request->query->get('act')) { 34 | return; 35 | } 36 | 37 | if (!file_exists(Path::join($this->projectDir, $dc->id))) { 38 | return; 39 | } 40 | 41 | $file = Dbafs::addResource($dc->id); 42 | 43 | if (null === $file) { 44 | return; 45 | } 46 | 47 | if ('file' !== $file->type || !\in_array($file->extension, $this->validExtensions, true)) { 48 | return; 49 | } 50 | 51 | PaletteManipulator::create() 52 | ->addField('importantParts', null) 53 | ->applyToPalette('default', 'tl_files') 54 | ; 55 | } 56 | 57 | #[AsCallback('tl_files', 'fields.importantParts.load')] 58 | public function importantPartsLoadCallback($value, DataContainer $dc): string 59 | { 60 | try { 61 | $importantParts = $value ? json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR) : []; 62 | } catch (\JsonException) { 63 | $importantParts = []; 64 | } 65 | 66 | $file = FilesModel::findByPath($dc->id); 67 | 68 | if ($file->importantPartWidth > 0 && $file->importantPartHeight > 0) { 69 | $importantParts['default'] = [ 70 | 'x' => $file->importantPartX, 71 | 'y' => $file->importantPartY, 72 | 'width' => $file->importantPartWidth, 73 | 'height' => $file->importantPartHeight, 74 | ]; 75 | } 76 | 77 | return json_encode((object) $importantParts, JSON_PRETTY_PRINT); 78 | } 79 | 80 | #[AsCallback('tl_files', 'fields.importantParts.save')] 81 | public function importantPartsSaveCallback($value, DataContainer $dc): string 82 | { 83 | try { 84 | $importantParts = $value ? json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR) : []; 85 | } catch (\JsonException) { 86 | $importantParts = []; 87 | } 88 | 89 | $file = FilesModel::findByPath($dc->id); 90 | 91 | $file->importantPartX = $importantParts['default']['x'] ?? 0; 92 | $file->importantPartY = $importantParts['default']['y'] ?? 0; 93 | $file->importantPartWidth = $importantParts['default']['width'] ?? 0; 94 | $file->importantPartHeight = $importantParts['default']['height'] ?? 0; 95 | 96 | $file->save(); 97 | 98 | // Remove any invalid entries 99 | $importantParts = array_filter($importantParts, static fn (array $importantPart): bool => (float) $importantPart['width'] > 0 && (float) $importantPart['height'] > 0); 100 | 101 | // "compress" JSON 102 | return json_encode((object) $importantParts, JSON_THROW_ON_ERROR); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://packagist.org/packages/inspiredminds/contao-image-alternatives) 2 | [](https://packagist.org/packages/inspiredminds/contao-image-alternatives) 3 | 4 | Contao Image Alternatives 5 | ========================= 6 | 7 | This extensions expands the capabilities of using responsive images with art direction in Contao. You will have the possibility to define a set of alternatives which will then allow you to define the respective alternatives per image in the file manager. 8 | 9 | Suppose you want to use different images for a certain image size depending on the output device, i.e. a specific image for desktop, for tablets and for mobile. You can define a set of alternatives called `mobile` and `tablet` for example (desktop will be the fallback - the original image): 10 | 11 | ```yaml 12 | # config/config.yaml 13 | contao_image_alternatives: 14 | alternatives: 15 | - tablet 16 | - mobile 17 | ``` 18 | 19 | Now you can choose the alternatives for each image separately in Contao's file manager: 20 | 21 |
22 |
23 | The back end labels for each alternative can be translated via the `image_alternatives` translation domain:
24 |
25 | ```yaml
26 | # translations/image_alternatives.en.yaml
27 | tablet: Tablet
28 | mobile: Mobile
29 | ```
30 |
31 | Within your image size settings you can then choose per media query item, whether an alternative image should be chosen for this particular image size:
32 |
33 |
34 |
35 |
36 |
37 | Alternatively you can also set the alternative in your config via `contao_image_alternatives.sizes.*.items`. Note that the name of the size must be same name as in `contao.image.sizes` and the media query for each item must match with the ones in `contao.image.sizes.*.items`:
38 |
39 | ```yaml
40 | # config/config.yaml
41 | contao_image_alternatives:
42 | alternatives:
43 | - tablet
44 | - mobile
45 | sizes:
46 | example:
47 | items:
48 | -
49 | media: '(max-width: 480px)'
50 | alternative: mobile
51 | -
52 | media: '(max-width: 800px)'
53 | alternative: tablet
54 | ```
55 |
56 | When you choose the configured image size in your content element or module, the generated images will automatically use the alternative versions for each source image for the particular image size media query item.
57 |
58 |
59 | ## Alternative Important Parts
60 |
61 | It is also possible to set different important parts for each image alternative. When editing an image in the file manager, there will be a an **Important part alternative** selection at the top, with which you can switch between the different important parts. For example if you have set the default important part for an image, plus the important part for the `mobile` alternative, then latter will be used for your `mobile` media query image size item (if configured) and otherwise the default. This allows you to crop the image to different parts for different output devices within the same image.
62 |
63 |
64 | ## Pre-Crop
65 |
66 | It is also possible to to pre-crop the image to the important part _before_ the image is resized according to the resize settings. This allows you to crop the image to the important part for certain image sizes while otherwise only resizing to a specific width or height. This setting can be set per image size in the database and also via the bundle configuration:
67 |
68 | ```yaml
69 | # config/config.yaml
70 | contao_image_alternatives:
71 | sizes:
72 | example:
73 | pre_crop: true
74 | ```
75 |
76 |
77 | ## Attributions
78 |
79 | Development funded by:
80 |
81 | * [ncm](https://www.ncm.at/) - Net Communication Management GmbH
82 | * [nickname . Büro für visuelle Kommunikation](https://www.hofff.com/)
83 |
--------------------------------------------------------------------------------
/src/EventListener/DataContainer/ImageAlternativesListener.php:
--------------------------------------------------------------------------------
1 | alternatives) {
41 | return;
42 | }
43 |
44 | $request = $this->requestStack->getCurrentRequest();
45 |
46 | if ('edit' !== $request->query->get('act')) {
47 | return;
48 | }
49 |
50 | if (!file_exists(Path::join($this->projectDir, $dc->id))) {
51 | return;
52 | }
53 |
54 | $file = Dbafs::addResource($dc->id);
55 |
56 | if (null === $file) {
57 | return;
58 | }
59 |
60 | if ('folder' === $file->type || !\in_array($file->extension, $this->validExtensions, true)) {
61 | return;
62 | }
63 |
64 | $pm = PaletteManipulator::create();
65 | $pm->addLegend('image_alternatives', null);
66 |
67 | foreach ($this->alternatives as $alternative) {
68 | $fieldName = 'alternative_'.StringUtil::standardize($alternative);
69 | $GLOBALS['TL_DCA']['tl_files']['fields'][$fieldName] = [
70 | 'label' => [$this->translator->trans($alternative, [], 'image_alternatives'), $this->translator->trans('alternative_description', ['%alternative%' => $alternative], 'ContaoImageAlternativesBundle')],
71 | 'inputType' => 'fileTree',
72 | 'eval' => [
73 | 'extensions' => Config::get('validImageTypes'),
74 | 'filesOnly' => true,
75 | 'fieldType' => 'radio',
76 | 'tl_class' => 'clr',
77 | 'doNotSaveEmpty' => true,
78 | ],
79 | 'load_callback' => [['contao_image_alternatives.data_container.image_alternatives', 'alternativeLoadCallback']],
80 | 'save_callback' => [['contao_image_alternatives.data_container.image_alternatives', 'alternativeSaveCallback']],
81 | 'alternativeName' => $alternative,
82 | ];
83 |
84 | $pm->addField($fieldName, 'image_alternatives', PaletteManipulator::POSITION_APPEND);
85 | }
86 |
87 | $pm->applyToPalette('default', 'tl_files');
88 | }
89 |
90 | public function alternativeLoadCallback(mixed $value, DataContainer $dc): string|null
91 | {
92 | $file = FilesModel::findByPath($dc->id);
93 |
94 | $alternatives = StringUtil::deserialize($file->alternatives, true);
95 |
96 | $alternativeName = $GLOBALS['TL_DCA']['tl_files']['fields'][$dc->field]['alternativeName'];
97 |
98 | return $alternatives[$alternativeName] ?? null;
99 | }
100 |
101 | public function alternativeSaveCallback(mixed $value, DataContainer $dc): string|null
102 | {
103 | $file = FilesModel::findByPath($dc->id);
104 |
105 | $alternativeName = $GLOBALS['TL_DCA']['tl_files']['fields'][$dc->field]['alternativeName'];
106 |
107 | $alternatives = StringUtil::deserialize($file->alternatives, true);
108 | $alternatives[$alternativeName] = $value;
109 |
110 | $file->alternatives = serialize($alternatives);
111 | $file->save();
112 |
113 | return null;
114 | }
115 |
116 | #[AsCallback('tl_image_size_item', 'fields.alternative.options')]
117 | public function alternativeOptionsCallback(): array
118 | {
119 | $options = [];
120 |
121 | foreach ($this->alternatives as $alternative) {
122 | $options[$alternative] = $this->translator->trans($alternative, [], 'image_alternatives');
123 | }
124 |
125 | return $options;
126 | }
127 |
128 | #[AsCallback('tl_image_size_item', 'list.sorting.child_record')]
129 | public function imageSizeItemChildRecordCallback(array $row): string
130 | {
131 | $original = (new \tl_image_size_item())->listImageSizeItem($row);
132 |
133 | if ($row['alternative']) {
134 | $alternative = $this->translator->trans($row['alternative'], [], 'image_alternatives');
135 | $original = str_replace('', ' ['.$alternative.']', $original);
136 | }
137 |
138 | return $original;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/public/importantParts.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | let initialized = new WeakMap();
5 | let importantPartsInput = null;
6 |
7 | const getImportantPartData = (alternative) => {
8 | if (null === importantPartsInput) {
9 | return {};
10 | }
11 |
12 | const importantParts = JSON.parse(importantPartsInput.value);
13 |
14 | if (typeof importantParts[alternative] === 'undefined') {
15 | return {};
16 | }
17 |
18 | return importantParts[alternative];
19 | };
20 |
21 | const updateImportantPartData = (alternative, data) => {
22 | if (null === importantPartsInput) {
23 | return;
24 | }
25 |
26 | let importantParts = JSON.parse(importantPartsInput.value);
27 | importantParts[alternative] = data;
28 | importantPartsInput.value = JSON.stringify(importantParts, null, 4);
29 | };
30 |
31 | const getCurrentAlternative = () => {
32 | const select = document.querySelector('select[name="importantPartSwitch"]');
33 |
34 | if (null === select) {
35 | return 'default';
36 | }
37 |
38 | return select.value;
39 | };
40 |
41 | const editPreviewWizard = () => {
42 | let el = document.querySelector('.tl_edit_preview'),
43 | imageElement = el.querySelector('img'),
44 | inputElements = {},
45 | isDrawing = false,
46 | partElement, startPos,
47 | getComputedSize = function() {
48 | let style = getComputedStyle(imageElement);
49 | let paddingLeft = parseFloat(style['padding-left']);
50 | let paddingRight = parseFloat(style['padding-right']);
51 | let paddingTop = parseFloat(style['padding-top']);
52 | let paddingBottom = parseFloat(style['padding-bottom']);
53 | return {
54 | width: imageElement.clientWidth - paddingLeft - paddingRight,
55 | height: imageElement.clientHeight - paddingTop - paddingBottom,
56 | computedTop: paddingTop,
57 | computedLeft: paddingLeft
58 | }
59 | },
60 | getScale = function() {
61 | let size = getComputedSize();
62 | return {
63 | x: size.width,
64 | y: size.height,
65 | };
66 | },
67 | updateImage = function() {
68 | var scale = getScale(),
69 | imageSize = getComputedSize();
70 | partElement.setStyles({
71 | top: imageSize.computedTop + Math.round(inputElements.y * scale.y) + 'px',
72 | left: imageSize.computedLeft + Math.round(inputElements.x * scale.x) + 'px',
73 | width: Math.round(inputElements.width * scale.x) + 'px',
74 | height: Math.round(inputElements.height * scale.y) + 'px'
75 | });
76 | if (!parseFloat(inputElements.width) || !parseFloat(inputElements.height)) {
77 | partElement.style.display = 'none';
78 | } else {
79 | partElement.style.removeProperty('display')
80 | }
81 | },
82 | updateValues = function() {
83 | var scale = getScale(),
84 | styles = {
85 | top: partElement.style.top,
86 | left: partElement.style.left,
87 | width: partElement.style.width,
88 | height: partElement.style.height,
89 | },
90 | imageSize = getComputedSize(),
91 | values = {
92 | x: Math.max(0, Math.min(1, (parseFloat(styles.left) - imageSize.computedLeft) / scale.x)),
93 | y: Math.max(0, Math.min(1, (parseFloat(styles.top) - imageSize.computedTop) / scale.y))
94 | };
95 | values.width = Math.min(1 - values.x, styles.width.toFloat() / scale.x);
96 | values.height = Math.min(1 - values.y, styles.height.toFloat() / scale.y);
97 | if (!values.width || !values.height) {
98 | values.x = values.y = values.width = values.height = 0;
99 | partElement.style.display = 'none';
100 | } else {
101 | partElement.style.removeProperty('display')
102 | }
103 | Object.each(values, function(value, key) {
104 | inputElements[key] = parseFloat(value.toFixed(15));
105 | });
106 | updateImportantPartData(getCurrentAlternative(), inputElements);
107 | },
108 | start = function(event) {
109 | event.preventDefault();
110 | if (isDrawing) {
111 | return;
112 | }
113 | isDrawing = true;
114 | var imageSize = getComputedSize();
115 | startPos = {
116 | x: event.clientX - el.getBoundingClientRect().x - imageSize.computedLeft,
117 | y: event.clientY - el.getBoundingClientRect().y - imageSize.computedTop
118 | };
119 | move(event);
120 | },
121 | move = function(event) {
122 | if (!isDrawing) {
123 | return;
124 | }
125 | event.preventDefault();
126 | var imageSize = getComputedSize();
127 | var aspectRatioSelect = document.querySelector('select[name="aspectRatioSwitch"]');
128 | var aspectRatio = null;
129 |
130 | if (null !== aspectRatioSelect) {
131 | var aspectRatioValue = aspectRatioSelect.value;
132 |
133 | if (aspectRatioValue) {
134 | var aspectRatioArray = aspectRatioValue.split(':');
135 | aspectRatio = aspectRatioArray[1] / aspectRatioArray[0];
136 | }
137 | }
138 |
139 | var rect = {
140 | x: [
141 | Math.max(0, Math.min(imageSize.width, startPos.x)),
142 | Math.max(0, Math.min(imageSize.width, event.clientX - el.getBoundingClientRect().x - imageSize.computedLeft))
143 | ],
144 | y: [
145 | Math.max(0, Math.min(imageSize.height, startPos.y)),
146 | Math.max(0, Math.min(imageSize.height, event.clientY - el.getBoundingClientRect().y - imageSize.computedTop))
147 | ]
148 | };
149 |
150 | if (null !== aspectRatio) {
151 | if (startPos.y < rect.y[1]) {
152 | rect.y[1] = rect.y[0] + (Math.abs(rect.x[1] - rect.x[0]) * aspectRatio);
153 | } else {
154 | rect.y[1] = rect.y[0] - (Math.abs(rect.x[1] - rect.x[0]) * aspectRatio);
155 | }
156 |
157 | if (rect.y[1] >= imageSize.height) {
158 | rect.x[1] = Math.max(0, Math.min(imageSize.width, rect.x[0] + ((imageSize.height - rect.y[0]) * (1 / aspectRatio))));
159 | rect.y[1] = imageSize.height;
160 | }
161 |
162 | if (rect.y[1] <= 0) {
163 | if (startPos.x < rect.x[1]) {
164 | rect.x[1] = Math.max(0, Math.min(imageSize.width, rect.x[0] + (rect.y[0] * (1 / aspectRatio))));
165 | } else {
166 | rect.x[1] = Math.max(0, Math.min(imageSize.width, rect.x[0] - (rect.y[0] * (1 / aspectRatio))));
167 | }
168 | rect.y[1] = 0;
169 | }
170 | }
171 |
172 | partElement.style.top = Math.min(rect.y[0], rect.y[1]) + imageSize.computedTop + 'px',
173 | partElement.style.left = Math.min(rect.x[0], rect.x[1]) + imageSize.computedLeft + 'px',
174 | partElement.style.width = Math.abs(rect.x[0] - rect.x[1]) + 'px',
175 | partElement.style.height = Math.abs(rect.y[0] - rect.y[1]) + 'px'
176 | updateValues();
177 | },
178 | stop = function(event) {
179 | move(event);
180 | isDrawing = false;
181 | },
182 | init = function() {
183 | inputElements = getImportantPartData(getCurrentAlternative());
184 | el.classList.add('tl_edit_preview_enabled');
185 | partElement = document.createElement('div');
186 | partElement.setAttribute('data-turbo-temporary', '');
187 | partElement.classList.add('tl_edit_preview_important_part');
188 | el.appendChild(partElement);
189 | updateImage();
190 | imageElement.addEventListener('load', updateImage);
191 | el.addEventListener('mousedown', start);
192 | el.addEventListener('touchstart', start);
193 | document.addEventListener('mousemove', move);
194 | document.addEventListener('touchmove', move);
195 | document.addEventListener('mouseup', stop);
196 | document.addEventListener('touchend', stop);
197 | document.addEventListener('touchcancel', stop);
198 | document.addEventListener('resize', updateImage);
199 | document.querySelector('select[name="importantPartSwitch"]').addEventListener('change', () => {
200 | inputElements = getImportantPartData(getCurrentAlternative());
201 | updateImage();
202 | });
203 | }
204 | ;
205 |
206 | init();
207 | };
208 |
209 | const mainInit = (element) => {
210 | if (initialized.has(element)) {
211 | return;
212 | }
213 |
214 | initialized.set(element, true);
215 |
216 | importantPartsInput = element;
217 | importantPartsInput.closest('.widget').style.display = 'none';
218 | editPreviewWizard();
219 | };
220 |
221 | const selector = '#ctrl_importantParts';
222 |
223 | document.querySelectorAll(selector).forEach(element => mainInit(element));
224 |
225 | new MutationObserver(function (mutationsList) {
226 | for (const mutation of mutationsList) {
227 | if (mutation.type === 'childList') {
228 | mutation.addedNodes.forEach(function (element) {
229 | if (element.matches && element.matches(selector)) {
230 | mainInit(element);
231 | }
232 |
233 | if (element.querySelectorAll) {
234 | element.querySelectorAll(selector).forEach(element => mainInit(element));
235 | }
236 | })
237 | }
238 | }
239 | }).observe(document, {
240 | attributes: false,
241 | childList: true,
242 | subtree: true
243 | });
244 | })();
245 |
--------------------------------------------------------------------------------
/src/Image/PictureFactory.php:
--------------------------------------------------------------------------------
1 | 1,
43 | 'avif' => 2,
44 | 'heic' => 3,
45 | 'webp' => 4,
46 | 'png' => 5,
47 | 'jpg' => 6,
48 | 'jpeg' => 7,
49 | 'gif' => 8,
50 | ];
51 |
52 | private $predefinedSizes;
53 |
54 | public function __construct(
55 | private readonly ContaoPictureFactory $inner,
56 | private readonly ImageFactoryInterface $imageFactory,
57 | private readonly ResizerInterface $resizer,
58 | private array $alternativeSizes,
59 | array $predefinedSizes,
60 | private readonly string $projectDir,
61 | ) {
62 | $this->predefinedSizes = $this->mergeImageSizes($predefinedSizes, $this->alternativeSizes);
63 | }
64 |
65 | public function setDefaultDensities(string $densities): static
66 | {
67 | $this->inner->setDefaultDensities($densities);
68 |
69 | return $this;
70 | }
71 |
72 | public function create(ImageInterface|string $path, PictureConfiguration|array|int|string|null $size = null, ResizeOptions|null $options = null): PictureInterface
73 | {
74 | $size = StringUtil::deserialize($size);
75 |
76 | if (\is_int($size) || \is_string($size)) {
77 | $size = [0, 0, $size];
78 | }
79 |
80 | if (!\is_array($size) || !isset($size[2])) {
81 | return $this->inner->create($path, $size);
82 | }
83 |
84 | $file = FilesModel::findByPath($path);
85 |
86 | if (null === $file) {
87 | return $this->inner->create($path, $size);
88 | }
89 |
90 | if (!is_numeric($size[2]) && isset($this->alternativeSizes[$size[2]])) {
91 | $predefinedSize = $this->predefinedSizes[$size[2]] ?? [];
92 |
93 | if (!empty($predefinedSize['items'])) {
94 | $useAlternatives = false;
95 |
96 | foreach ($predefinedSize['items'] as $item) {
97 | if (!empty($item['alternative'])) {
98 | $alternativeFile = $this->getAlternative($file, $item['alternative']);
99 | $importantParts = $this->getImportantParts($alternativeFile ?? $file);
100 |
101 | if ($alternativeFile || isset($importantParts[$item['alternative']])) {
102 | $useAlternatives = true;
103 | break;
104 | }
105 | }
106 | }
107 |
108 | if ($useAlternatives) {
109 | $index = 0;
110 | $sources = [];
111 |
112 | foreach ($predefinedSize['items'] as $item) {
113 | $alternativeSizeName = $size[2].'_alternative_item_'.$index;
114 | $alternativeSize = $item;
115 | $alternativeSize['formats'] = $predefinedSize['formats'] ?? null;
116 | $alternativeSize['items'] = [];
117 | $itemPath = $path;
118 | $alternativeFile = null;
119 |
120 | if (!empty($item['alternative']) && $alternativeFile = $this->getAlternative($file, $item['alternative'])) {
121 | // Do not use Path::join here (https://github.com/contao/contao/pull/4596)
122 | $itemPath = $this->projectDir.'/'.$alternativeFile->path;
123 | }
124 |
125 | $this->predefinedSizes[$alternativeSizeName] = $alternativeSize;
126 | $this->inner->setPredefinedSizes($this->predefinedSizes);
127 |
128 | if ($itemPath instanceof ImageInterface) {
129 | $itemImage = $itemPath;
130 | } else {
131 | $itemImage = $this->imageFactory->create($itemPath);
132 | }
133 |
134 | $importantParts = $this->getImportantParts($alternativeFile ?? $file);
135 |
136 | // Override the important part
137 | if (!empty($item['alternative']) && isset($importantParts[$item['alternative']])) {
138 | $importantPart = $importantParts[$item['alternative']];
139 |
140 | if ((float) $importantPart['width'] > 0 && (float) $importantPart['height'] > 0) {
141 | $itemImage->setImportantPart(new ImportantPart(
142 | (float) $importantPart['x'],
143 | (float) $importantPart['y'],
144 | (float) $importantPart['width'],
145 | (float) $importantPart['height'],
146 | ));
147 | }
148 | }
149 |
150 | if ($this->alternativeSizes[$size[2]]['preCrop'] ?? false) {
151 | $itemImage = $this->cropToImportantPart($itemImage);
152 | }
153 |
154 | $picture = $this->inner->create($itemImage, [0, 0, $alternativeSizeName]);
155 | $sources = [...$sources, ...$picture->getSources()];
156 | $sources[] = $picture->getImg();
157 |
158 | ++$index;
159 | }
160 |
161 | $alternativeSizeName = $size[2].'_alternative';
162 | $alternativeSize = $predefinedSize;
163 | $alternativeSize['items'] = [];
164 |
165 | $this->predefinedSizes[$alternativeSizeName] = $alternativeSize;
166 | $this->inner->setPredefinedSizes($this->predefinedSizes);
167 |
168 | if ($this->alternativeSizes[$size[2]]['preCrop'] ?? false) {
169 | $path = $this->cropToImportantPart($path);
170 | }
171 |
172 | $picture = $this->inner->create($path, [0, 0, $alternativeSizeName]);
173 | $sources = [...$sources, ...$picture->getSources()];
174 |
175 | return new Picture($picture->getImg(), $sources);
176 | }
177 | }
178 |
179 | if ($this->alternativeSizes[$size[2]]['preCrop'] ?? false) {
180 | $path = $this->cropToImportantPart($path);
181 | }
182 | } elseif (is_numeric($size[2]) && null !== ($imageSize = ImageSizeModel::findById($size[2]))) {
183 | if (null !== ($sizeItems = ImageSizeItemModel::findVisibleByPid($imageSize->id, ['order' => 'sorting ASC']))) {
184 | $useAlternatives = false;
185 |
186 | foreach ($sizeItems as $sizeItem) {
187 | if (!empty($sizeItem->alternative)) {
188 | $alternativeFile = $this->getAlternative($file, $sizeItem->alternative);
189 | $importantParts = $this->getImportantParts($alternativeFile ?? $file);
190 |
191 | if ($alternativeFile || isset($importantParts[$sizeItem->alternative])) {
192 | $useAlternatives = true;
193 | break;
194 | }
195 | }
196 | }
197 |
198 | if ($useAlternatives) {
199 | $sources = [];
200 |
201 | foreach ($sizeItems as $sizeItem) {
202 | $sizeItem->preCrop = $imageSize->preCrop;
203 | $picture = $this->getPicture($file, $sizeItem);
204 | $sources = [...$sources, ...$picture->getSources()];
205 | $sources[] = $picture->getImg();
206 | }
207 |
208 | $picture = $this->getPicture($file, $imageSize);
209 | $sources = [...$sources, ...$picture->getSources()];
210 |
211 | $img = $picture->getImg();
212 |
213 | if ($imageSize->cssClass) {
214 | $img['class'] = $imageSize->cssClass;
215 | }
216 |
217 | if ($imageSize->lazyLoading) {
218 | $img['loading'] = 'lazy';
219 | }
220 |
221 | return new Picture($img, $sources);
222 | }
223 | }
224 |
225 | if ($imageSize->preCrop) {
226 | $path = $this->cropToImportantPart($path);
227 | }
228 | }
229 |
230 | return $this->inner->create($path, $size);
231 | }
232 |
233 | private function getAlternative(FilesModel $file, string $alternative): FilesModel|null
234 | {
235 | $alternatives = StringUtil::deserialize($file->alternatives, true);
236 |
237 | if (empty($alternatives[$alternative])) {
238 | return null;
239 | }
240 |
241 | $alternativeFile = FilesModel::findByUuid($alternatives[$alternative]);
242 |
243 | if (null === $alternativeFile) {
244 | return null;
245 | }
246 |
247 | if (!file_exists(Path::join($this->projectDir, $alternativeFile->path))) {
248 | return null;
249 | }
250 |
251 | return $alternativeFile;
252 | }
253 |
254 | /**
255 | * Copy of Contao\CoreBundle\Image\PictureFactory::createConfigItem.
256 | */
257 | private function createConfigItem(array|null $imageSize = null): PictureConfigurationItem
258 | {
259 | $configItem = new PictureConfigurationItem();
260 | $resizeConfig = new ResizeConfiguration();
261 |
262 | if (null !== $imageSize) {
263 | if (isset($imageSize['width'])) {
264 | $resizeConfig->setWidth((int) $imageSize['width']);
265 | }
266 |
267 | if (isset($imageSize['height'])) {
268 | $resizeConfig->setHeight((int) $imageSize['height']);
269 | }
270 |
271 | if (isset($imageSize['zoom'])) {
272 | $resizeConfig->setZoomLevel((int) $imageSize['zoom']);
273 | }
274 |
275 | if (isset($imageSize['resizeMode'])) {
276 | $resizeConfig->setMode((string) $imageSize['resizeMode']);
277 | }
278 |
279 | $configItem->setResizeConfig($resizeConfig);
280 |
281 | if (isset($imageSize['sizes'])) {
282 | $configItem->setSizes((string) $imageSize['sizes']);
283 | }
284 |
285 | if (isset($imageSize['densities'])) {
286 | $configItem->setDensities((string) $imageSize['densities']);
287 | }
288 |
289 | if (isset($imageSize['media'])) {
290 | $configItem->setMedia((string) $imageSize['media']);
291 | }
292 | }
293 |
294 | return $configItem;
295 | }
296 |
297 | /**
298 | * Copy of Contao\CoreBundle\Image\PictureFactory::createConfig.
299 | *
300 | * @param ImageSizeModel|ImageSizeItemModel $sizeModel
301 | */
302 | private function getFormats(Model $sizeModel): array
303 | {
304 | if ($sizeModel instanceof ImageSizeItemModel) {
305 | $sizeModel = $sizeModel->getRelated('pid');
306 | }
307 |
308 | if (!$sizeModel instanceof ImageSizeModel) {
309 | throw new \InvalidArgumentException('$sizeModel is not an instance of ImageSizeModel');
310 | }
311 |
312 | $formats = [];
313 |
314 | if (empty($sizeModel->formats)) {
315 | return $formats;
316 | }
317 |
318 | $formatsString = implode(';', StringUtil::deserialize($sizeModel->formats, true));
319 |
320 | foreach (explode(';', $formatsString) as $format) {
321 | [$source, $targets] = explode(':', $format, 2);
322 | $targets = explode(',', $targets);
323 |
324 | if (!isset($formats[$source])) {
325 | $formats[$source] = $targets;
326 | continue;
327 | }
328 |
329 | $formats[$source] = array_unique(array_merge($formats[$source], $targets));
330 |
331 | usort(
332 | $formats[$source],
333 | static fn ($a, $b) => (self::FORMATS_ORDER[$a] ?? $a) <=> (self::FORMATS_ORDER[$b] ?? $b),
334 | );
335 | }
336 |
337 | return $formats;
338 | }
339 |
340 | /**
341 | * @param ImageSizeModel|ImageSizeItemModel $sizeModel
342 | */
343 | private function getPicture(FilesModel $file, Model $sizeModel): Picture
344 | {
345 | $path = $this->projectDir.'/'.$file->path;
346 | $alternativeFile = null;
347 |
348 | if (!empty($sizeModel->alternative) && $alternativeFile = $this->getAlternative($file, $sizeModel->alternative)) {
349 | // Do not use Path::join here (https://github.com/contao/contao/pull/4596)
350 | $path = $this->projectDir.'/'.$alternativeFile->path;
351 | }
352 |
353 | $options = new ResizeOptions();
354 | $options->setSkipIfDimensionsMatch((bool) $sizeModel->skipIfDimensionsMatch);
355 |
356 | $config = new PictureConfiguration();
357 | $config->setFormats($this->getFormats($sizeModel));
358 | $config->setSize($this->createConfigItem($sizeModel->row()));
359 |
360 | $image = $this->imageFactory->create($path);
361 | $importantParts = $this->getImportantParts($alternativeFile ?? $file);
362 |
363 | // Override the important part
364 | if (!empty($sizeModel->alternative) && isset($importantParts[$sizeModel->alternative])) {
365 | $importantPart = $importantParts[$sizeModel->alternative];
366 |
367 | if ((float) $importantPart['width'] > 0 && (float) $importantPart['height'] > 0) {
368 | $image->setImportantPart(new ImportantPart(
369 | (float) $importantPart['x'],
370 | (float) $importantPart['y'],
371 | (float) $importantPart['width'],
372 | (float) $importantPart['height'],
373 | ));
374 | }
375 | }
376 |
377 | if ($sizeModel->preCrop) {
378 | $image = $this->cropToImportantPart($image);
379 | }
380 |
381 | return $this->inner->create($image, $config, $options);
382 | }
383 |
384 | private function mergeImageSizes(array $predefinedSizes, array $alternativeSizes): array
385 | {
386 | foreach ($predefinedSizes as $name => &$config) {
387 | if (isset($alternativeSizes[$name]['items'], $config['items'])) {
388 | foreach ($alternativeSizes[$name]['items'] as $alternativeItem) {
389 | foreach ($config['items'] as &$item) {
390 | if (isset($alternativeItem['alternative']) && $item['media'] === $alternativeItem['media']) {
391 | $item['alternative'] = $alternativeItem['alternative'];
392 | }
393 | }
394 | }
395 | }
396 | }
397 |
398 | return $predefinedSizes;
399 | }
400 |
401 | private function getImportantParts(FilesModel $file): array
402 | {
403 | if (empty($file->importantParts)) {
404 | return [];
405 | }
406 |
407 | try {
408 | return json_decode((string) $file->importantParts, true, 512, JSON_THROW_ON_ERROR);
409 | } catch (\JsonException) {
410 | return [];
411 | }
412 | }
413 |
414 | /**
415 | * @param string|ImageInterface $image
416 | */
417 | private function cropToImportantPart($image): ImageInterface
418 | {
419 | if (!$image instanceof ImageInterface) {
420 | $image = $this->imageFactory->create($image);
421 | }
422 |
423 | $importantPart = $image->getImportantPart();
424 |
425 | if ($importantPart->getWidth() <= 0 || $importantPart->getHeight() <= 0) {
426 | return $image;
427 | }
428 |
429 | $imageSize = $image->getDimensions()->getSize();
430 |
431 | $config = (new ResizeConfiguration())
432 | ->setMode(ResizeConfiguration::MODE_CROP)
433 | ->setWidth((int) ($imageSize->getWidth() * $importantPart->getWidth()))
434 | ->setHeight((int) ($imageSize->getHeight() * $importantPart->getHeight()))
435 | ->setZoomLevel(100)
436 | ;
437 |
438 | $image = $this->resizer
439 | ->resize($image, $config, new ResizeOptions())
440 | ->setImportantPart(null)
441 | ;
442 |
443 | // If there are legacy hooks in use, the image needs to be generated inline before
444 | if ($this->hasExecuteResizeHook() || $this->hasGetImageHook()) {
445 | if ($image instanceof DeferredImageInterface && $this->resizer instanceof DeferredResizerInterface) {
446 | $image = $this->resizer->resizeDeferredImage($image);
447 | }
448 | }
449 |
450 | return $image;
451 | }
452 |
453 | private function hasExecuteResizeHook(): bool
454 | {
455 | return !empty($GLOBALS['TL_HOOKS']['executeResize']) && \is_array($GLOBALS['TL_HOOKS']['executeResize']);
456 | }
457 |
458 | private function hasGetImageHook(): bool
459 | {
460 | return !empty($GLOBALS['TL_HOOKS']['getImage']) && \is_array($GLOBALS['TL_HOOKS']['getImage']);
461 | }
462 | }
463 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.