Skip to content

Commit 0ffe400

Browse files
committed
Fix image transform editing in the control panel
1 parent 2d116dc commit 0ffe400

4 files changed

Lines changed: 101 additions & 70 deletions

File tree

src/Module.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
use craft\base\Event;
77
use craft\base\Model;
88
use craft\cloud\fs\AssetsFs;
9-
use craft\cloud\imagetransforms\ImageTransform;
9+
use craft\cloud\imagetransforms\ImageTransformBehavior;
1010
use craft\cloud\imagetransforms\ImageTransformer;
1111
use craft\cloud\twig\TwigExtension;
1212
use craft\cloud\web\assets\uploader\UploaderAsset;
1313
use craft\cloud\web\ResponseEventHandler;
1414
use craft\console\Application as ConsoleApplication;
1515
use craft\elements\Asset;
16+
use craft\events\DefineBehaviorsEvent;
1617
use craft\events\DefineRulesEvent;
1718
use craft\events\GenerateTransformEvent;
1819
use craft\events\RegisterComponentTypesEvent;
@@ -85,10 +86,6 @@ public function bootstrap($app): void
8586
),
8687
]);
8788

88-
// Replace ImageTransform with cloud ImageTransform via DI
89-
// We do this here and not in AppConfig, because non-Cloud envs need it to support non-standard transform props
90-
Craft::$container->set(CraftImageTransform::class, ImageTransform::class);
91-
9289
if (Helper::isCraftCloud()) {
9390
$this->bootstrapCloud($app);
9491
}
@@ -176,6 +173,14 @@ protected function bootstrapCloud(ConsoleApplication|WebApplication $app): void
176173

177174
protected function registerGlobalEventHandlers(): void
178175
{
176+
Event::on(
177+
CraftImageTransform::class,
178+
Model::EVENT_DEFINE_BEHAVIORS,
179+
static function(DefineBehaviorsEvent $event) {
180+
$event->behaviors['cloud'] = ImageTransformBehavior::class;
181+
}
182+
);
183+
179184
Event::on(
180185
ImageTransforms::class,
181186
ImageTransforms::EVENT_REGISTER_IMAGE_TRANSFORMERS,

src/imagetransforms/ImageTransform.php renamed to src/imagetransforms/ImageTransformBehavior.php

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
namespace craft\cloud\imagetransforms;
44

55
use Craft;
6+
use craft\models\ImageTransform;
67
use Illuminate\Support\Collection;
8+
use yii\base\Behavior;
79

810
/**
911
* @see https://developers.cloudflare.com/images/transform-images/transform-via-workers/#fetch-options
1012
* @see https://github.com/cloudflare/workerd/blob/main/types/defines/cf.d.ts
13+
*
14+
* @property ImageTransform $owner
1115
*/
12-
class ImageTransform extends \craft\models\ImageTransform
16+
class ImageTransformBehavior extends Behavior
1317
{
1418
public ?bool $anim = null;
1519
public ?string $background = null;
@@ -59,11 +63,6 @@ class ImageTransform extends \craft\models\ImageTransform
5963
*/
6064
public ?string $flip = null;
6165

62-
/**
63-
* @var 'auto'|'avif'|'webp'|'jpeg'|'baseline-jpeg'|'json'|string|null
64-
*/
65-
public ?string $format = null;
66-
6766
/**
6867
* @var float|null
6968
*/
@@ -74,8 +73,6 @@ class ImageTransform extends \craft\models\ImageTransform
7473
*/
7574
public string|array|null $gravity = null;
7675

77-
public ?int $height = null;
78-
7976
/**
8077
* @var 'keep'|'copyright'|'none'|null
8178
*/
@@ -106,11 +103,9 @@ class ImageTransform extends \craft\models\ImageTransform
106103
*/
107104
public null|string|array $trim = null;
108105

109-
public ?int $width = null;
110-
111106
public ?float $zoom = null;
112107

113-
public function toOptions(): array
108+
public function toOptions(array|string|null $gravity = null): array
114109
{
115110
$reflection = new \ReflectionClass($this);
116111

@@ -124,69 +119,56 @@ public function toOptions(): array
124119
$options['format'] = $this->computeFormat();
125120
$options['fit'] = $this->computeFit();
126121
$options['background'] = $this->computeBackground();
127-
$options['gravity'] ??= $this->computeGravity();
122+
$options['gravity'] ??= $gravity ?? $this->computeGravity();
123+
$options['height'] = $this->owner->height;
124+
$options['width'] = $this->owner->width;
128125

129126
return Collection::make($options)
130127
->filter(fn($value) => $value !== null)
131128
->all();
132129
}
133130

134-
/**
135-
* Compute the Cloudflare format from the base format and interlace settings.
136-
*
137-
* @return string|null
138-
*/
139131
private function computeFormat(): ?string
140132
{
141-
if ($this->format === 'jpg' && $this->interlace === 'none') {
133+
if ($this->owner->format === 'jpg' && $this->owner->interlace === 'none') {
142134
return 'baseline-jpeg';
143135
}
144136

145-
return match ($this->format) {
137+
return match ($this->owner->format) {
146138
'jpg' => 'jpeg',
147-
default => $this->format,
139+
default => $this->owner->format,
148140
};
149141
}
150142

151143
/**
152-
* Compute the Cloudflare fit mode from the base mode and upscale settings.
153-
*
154144
* @see https://developers.cloudflare.com/images/transform-images/transform-via-url/#fit
155-
* @return string
156145
*/
157146
private function computeFit(): string
158147
{
159148
if ($this->fit !== null) {
160149
return $this->fit;
161150
}
162151

163-
return match ($this->mode) {
164-
'fit' => $this->upscale ? 'contain' : 'scale-down',
152+
return match ($this->owner->mode) {
153+
'fit' => $this->owner->upscale ? 'contain' : 'scale-down',
165154
'stretch' => 'squeeze',
166155
'letterbox' => 'pad',
167-
default => $this->upscale ? 'cover' : 'crop',
156+
default => $this->owner->upscale ? 'cover' : 'crop',
168157
};
169158
}
170159

171-
/**
172-
* Compute the Cloudflare background color from the base mode and fill settings.
173-
*
174-
* @return string|null
175-
*/
176160
private function computeBackground(): ?string
177161
{
178162
if ($this->background !== null) {
179163
return $this->background;
180164
}
181165

182-
return $this->mode === 'letterbox'
183-
? $this->fill ?? '#FFFFFF'
166+
return $this->owner->mode === 'letterbox'
167+
? $this->owner->fill ?? '#FFFFFF'
184168
: null;
185169
}
186170

187171
/**
188-
* Compute the Cloudflare gravity from the base position setting.
189-
*
190172
* @return array{x: float, y: float}|null|'face'
191173
*/
192174
private function computeGravity(): array|null|string
@@ -195,12 +177,11 @@ private function computeGravity(): array|null|string
195177
return $this->gravity;
196178
}
197179

198-
if ($this->position === 'center-center') {
180+
if ($this->owner->position === 'center-center') {
199181
return null;
200182
}
201183

202-
// TODO: maybe just do this in Craft
203-
$parts = explode('-', $this->position);
184+
$parts = explode('-', $this->owner->position);
204185

205186
try {
206187
$x = match ($parts[1] ?? null) {
@@ -214,7 +195,7 @@ private function computeGravity(): array|null|string
214195
'bottom' => 1,
215196
};
216197
} catch (\UnhandledMatchError $e) {
217-
Craft::warning("Invalid position value: `{$this->position}`", __METHOD__);
198+
Craft::warning("Invalid position value: `{$this->owner->position}`", __METHOD__);
218199
return null;
219200
}
220201

src/imagetransforms/ImageTransformer.php

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use craft\elements\Asset;
1010
use craft\helpers\Assets;
1111
use craft\helpers\Html;
12+
use craft\models\ImageTransform;
1213
use League\Uri\Components\Query;
1314
use League\Uri\Contracts\UriInterface;
1415
use League\Uri\Modifier;
@@ -23,7 +24,7 @@ class ImageTransformer extends Component implements ImageTransformerInterface
2324
public const SUPPORTED_IMAGE_FORMATS = ['jpg', 'jpeg', 'gif', 'png', 'avif', 'webp'];
2425
private const SIGNING_PARAM = 's';
2526

26-
public function getTransformUrl(Asset $asset, \craft\models\ImageTransform $imageTransform, bool $immediately): string
27+
public function getTransformUrl(Asset $asset, ImageTransform $imageTransform, bool $immediately): string
2728
{
2829
if (version_compare(Craft::$app->version, '5.0', '>=')) {
2930
// @phpstan-ignore argument.type, arguments.count (Craft 5 compatibility)
@@ -44,13 +45,9 @@ public function getTransformUrl(Asset $asset, \craft\models\ImageTransform $imag
4445
throw new NotSupportedException('SVG files shouldn’t be transformed.');
4546
}
4647

47-
// ImageTransform DI will not work on Craft 4, so we convert the object.
48-
// @see https://github.com/craftcms/cms/pull/15646
49-
$imageTransform = Craft::createObject(ImageTransform::class, [$imageTransform->toArray()]);
48+
$gravity = $this->applyAssetFocalPointGravity($asset, $imageTransform);
5049

51-
$this->applyAssetFocalPointGravity($asset, $imageTransform);
52-
53-
$query = Query::fromVariable($imageTransform->toOptions());
50+
$query = Query::fromVariable($this->behavior($imageTransform)->toOptions($gravity));
5451
$uri = Modifier::wrap(Uri::new($assetUrl))
5552
->mergeQuery($query)
5653
->unwrap();
@@ -62,13 +59,26 @@ public function invalidateAssetTransforms(Asset $asset): void
6259
{
6360
}
6461

65-
protected function applyAssetFocalPointGravity(Asset $asset, ImageTransform $imageTransform): void
62+
protected function applyAssetFocalPointGravity(Asset $asset, ImageTransform $imageTransform): array|string|null
63+
{
64+
$behavior = $this->behavior($imageTransform);
65+
66+
if (!$asset->getHasFocalPoint() || isset($behavior->gravity)) {
67+
return null;
68+
}
69+
70+
return $asset->getFocalPoint();
71+
}
72+
73+
private function behavior(ImageTransform $imageTransform): ImageTransformBehavior
6674
{
67-
if (!$asset->getHasFocalPoint() || isset($imageTransform->gravity)) {
68-
return;
75+
$behavior = $imageTransform->getBehavior('cloud');
76+
77+
if (!$behavior instanceof ImageTransformBehavior) {
78+
throw new \RuntimeException('Cloud image transform behavior is not attached.');
6979
}
7080

71-
$imageTransform->gravity = $asset->getFocalPoint();
81+
return $behavior;
7282
}
7383

7484
private function sign(UriInterface $uri): UriInterface

tests/unit/ImageTransformTest.php

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@
33
namespace craft\cloud\tests\unit;
44

55
use Codeception\Test\Unit;
6-
use craft\cloud\imagetransforms\ImageTransform;
6+
use Craft;
7+
use craft\cloud\Module as CloudModule;
8+
use craft\cloud\imagetransforms\ImageTransformBehavior;
79
use craft\cloud\imagetransforms\ImageTransformer;
810
use craft\elements\Asset;
11+
use craft\models\ImageTransform;
912

1013
class ImageTransformTest extends Unit
1114
{
12-
/**
13-
* @var \UnitTester
14-
*/
15-
protected $tester;
15+
protected function _before(): void
16+
{
17+
parent::_before();
18+
19+
if (CloudModule::getInstance() === null) {
20+
$module = new CloudModule('cloud');
21+
$module->bootstrap(Craft::$app);
22+
}
23+
}
1624

1725
public function testCropModeWithExplicitGravityPreservesItInOptions(): void
1826
{
@@ -34,7 +42,7 @@ public function testCropModeWithExplicitGravityPreservesItInOptions(): void
3442
],
3543
'height' => 750,
3644
'width' => 1200,
37-
], $transform->toOptions());
45+
], $this->behavior($transform)->toOptions());
3846
}
3947

4048
public function testCropModeWithoutGravityUsesPositionMapping(): void
@@ -54,7 +62,7 @@ public function testCropModeWithoutGravityUsesPositionMapping(): void
5462
],
5563
'height' => 750,
5664
'width' => 1200,
57-
], $transform->toOptions());
65+
], $this->behavior($transform)->toOptions());
5866
}
5967

6068
public function testFocalPointGravityPassesThroughUnchanged(): void
@@ -67,12 +75,28 @@ public function testFocalPointGravityPassesThroughUnchanged(): void
6775
'height' => 750,
6876
]);
6977

70-
(new TestImageTransformer())->applyFocalPointGravity($asset, $transform);
78+
$gravity = (new TestImageTransformer())->applyFocalPointGravity($asset, $transform);
7179

7280
$this->assertSame([
7381
'x' => 0.474,
7482
'y' => 0.3064,
75-
], $transform->gravity);
83+
], $gravity);
84+
$this->assertNull($this->behavior($transform)->gravity);
85+
}
86+
87+
public function testInlineCloudPropertyDoesNotBreakBaseTransform(): void
88+
{
89+
$transform = new ImageTransform([
90+
'width' => 200,
91+
'blur' => 5,
92+
]);
93+
94+
$this->assertSame(5, $this->behavior($transform)->blur);
95+
$this->assertSame([
96+
'blur' => 5,
97+
'fit' => 'cover',
98+
'width' => 200,
99+
], $this->behavior($transform)->toOptions());
76100
}
77101

78102
public function testGetTransformUrlDoesNotLeakGravityBetweenAssets(): void
@@ -179,23 +203,34 @@ public function getMimeType(mixed $transform = null): ?string
179203
};
180204
}
181205

206+
private function behavior(ImageTransform $transform): ImageTransformBehavior
207+
{
208+
$behavior = $transform->getBehavior('cloud');
209+
210+
$this->assertInstanceOf(ImageTransformBehavior::class, $behavior);
211+
212+
return $behavior;
213+
}
214+
182215
}
183216

184217
class TestImageTransformer extends ImageTransformer
185218
{
186-
public function applyFocalPointGravity(Asset $asset, ImageTransform $imageTransform): void
219+
public function applyFocalPointGravity(Asset $asset, ImageTransform $imageTransform): array|string|null
187220
{
188-
$this->applyAssetFocalPointGravity($asset, $imageTransform);
221+
return $this->applyAssetFocalPointGravity($asset, $imageTransform);
189222
}
190223
}
191224

192225
class UrlTestImageTransformer extends ImageTransformer
193226
{
194-
public function buildTransformQuery(Asset $asset, \craft\models\ImageTransform $imageTransform): string
227+
public function buildTransformQuery(Asset $asset, ImageTransform $imageTransform): string
195228
{
196-
$imageTransform = \Craft::createObject(ImageTransform::class, [$imageTransform->toArray()]);
197-
$this->applyAssetFocalPointGravity($asset, $imageTransform);
229+
$gravity = $this->applyAssetFocalPointGravity($asset, $imageTransform);
230+
231+
/** @var ImageTransformBehavior $behavior */
232+
$behavior = $imageTransform->getBehavior('cloud');
198233

199-
return (string) \League\Uri\Components\Query::fromVariable($imageTransform->toOptions());
234+
return (string) \League\Uri\Components\Query::fromVariable($behavior->toOptions($gravity));
200235
}
201236
}

0 commit comments

Comments
 (0)