-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
ImgixImageTransform.php
342 lines (308 loc) · 11.1 KB
/
ImgixImageTransform.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
<?php
/**
* ImageOptimize plugin for Craft CMS 3.x
*
* Automatically optimize images after they've been transformed
*
* @link https://nystudio107.com
* @copyright Copyright (c) 2017 nystudio107
*/
namespace nystudio107\imageoptimizeimgix\imagetransforms;
use Craft;
use craft\elements\Asset;
use craft\errors\DeprecationException;
use craft\fs\Local;
use craft\helpers\App;
use craft\helpers\ArrayHelper;
use craft\helpers\Assets as AssetsHelper;
use craft\helpers\UrlHelper;
use craft\models\ImageTransform as CraftImageTransformModel;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use Imgix\UrlBuilder;
use nystudio107\imageoptimize\ImageOptimize;
use nystudio107\imageoptimize\imagetransforms\ImageTransform;
/**
* @author nystudio107
* @package ImageOptimize
* @since 1.1.0
*/
class ImgixImageTransform extends ImageTransform
{
// Constants
// =========================================================================
protected const TRANSFORM_ATTRIBUTES_MAP = [
'width' => 'w',
'height' => 'h',
'quality' => 'q',
'format' => 'fm',
];
protected const IMGIX_PURGE_ENDPOINT = 'https://api.imgix.com/api/v1/purge';
// Public Properties
// =========================================================================
/**
* @var string
*/
public string $domain = '';
/**
* @var string
*/
public string $apiKey = '';
/**
* @var string
*/
public string $securityToken = '';
/**
* @var int The amount that should be sent to the USM parameter
*/
public int $unsharpMask = 20;
/**
* @inheritdoc
*/
public static function displayName(): string
{
return Craft::t('image-optimize', 'Imgix');
}
// Public Methods
// =========================================================================
public function init(): void
{
parent::init();
}
/**
* @inheritdoc
*/
public function getTransformUrl(Asset $asset, CraftImageTransformModel|string|array|null $transform): ?string
{
$params = [];
$settings = ImageOptimize::$plugin->getSettings();
$domain = $this->domain ?? 'demos.imgix.net';
$securityToken = $this->securityToken;
$domain = App::parseEnv($domain);
$securityToken = App::parseEnv($securityToken);
$params['domain'] = $domain;
$builder = new UrlBuilder($domain);
$builder->setUseHttps(true);
if ($transform) {
// Map the transform properties
foreach (self::TRANSFORM_ATTRIBUTES_MAP as $key => $value) {
if (!empty($transform[$key])) {
$params[$value] = $transform[$key];
}
}
// Remove any 'AUTO' settings
ArrayHelper::removeValue($params, 'AUTO');
// Handle the Imgix auto setting for compression/format
$autoParams = [];
if (empty($params['q'])) {
$autoParams[] = 'compress';
}
if (empty($params['fm'])) {
$autoParams[] = 'format';
}
if (!empty($autoParams)) {
$params['auto'] = implode(',', $autoParams);
}
// Handle interlaced images
if (property_exists($transform, 'interlace') && ($transform->interlace !== 'none')
&& (!empty($params['fm']))
&& ($params['fm'] === 'jpg')) {
$params['fm'] = 'pjpg';
}
if ($settings->autoSharpenScaledImages && $asset->getWidth() && $asset->getHeight()) {
// See if the image has been scaled >= 50%
$widthScale = (int)((($transform->width ?? $asset->getWidth()) / $asset->getWidth()) * 100);
$heightScale = (int)((($transform->height ?? $asset->getHeight()) / $asset->getHeight()) * 100);
if (($widthScale >= $settings->sharpenScaledImagePercentage) || ($heightScale >= $settings->sharpenScaledImagePercentage)) {
$params['usm'] = ($this->unsharpMask ?? 25);
}
}
// Handle the mode
switch ($transform->mode) {
case 'fit':
$params['fit'] = 'clip';
break;
case 'stretch':
$params['fit'] = 'scale';
break;
default:
// Set a sane default
if (empty($transform->position)) {
$transform->position = 'center-center';
}
// Fit mode
$params['fit'] = 'crop';
$cropParams = [];
// Handle the focal point
$focalPoint = $asset->getFocalPoint();
if (!empty($focalPoint)) {
$params['fp-x'] = $focalPoint['x'];
$params['fp-y'] = $focalPoint['y'];
$cropParams[] = 'focalpoint';
$params['crop'] = implode(',', $cropParams);
} elseif (preg_match('/(top|center|bottom)-(left|center|right)/', $transform->position)) {
// Imgix defaults to 'center' if no param is present
$filteredCropParams = explode('-', $transform->position);
$filteredCropParams = array_diff($filteredCropParams, ['center']);
$cropParams[] = $filteredCropParams;
// Imgix
if (!empty($cropParams) && $transform->position !== 'center-center') {
$params['crop'] = implode(',', $cropParams);
}
}
break;
}
} else {
// No transform was passed in; so just auto all the things
$params['auto'] = 'format,compress';
}
// Apply the Security Token, if set
if (!empty($securityToken)) {
$builder->setSignKey($securityToken);
}
// Finally, create the Imgix URL for this transformed image
$assetUri = $this->getAssetUri($asset);
$url = $builder->createURL($assetUri, $params);
Craft::debug(
'Imgix transform created for: ' . $assetUri . ' - Params: ' . print_r($params, true) . ' - URL: ' . $url,
__METHOD__
);
return $url;
}
/**
* @inheritdoc
*/
public function getWebPUrl(string $url, Asset $asset, CraftImageTransformModel|string|array|null $transform): ?string
{
if ($transform) {
$transform->format = 'webp';
}
try {
$webPUrl = $this->getTransformUrl($asset, $transform);
} catch (Exception $e) {
Craft::error($e->getMessage(), __METHOD__);
}
return $webPUrl ?? '';
}
/**
* @inheritdoc
*/
public function getPurgeUrl(Asset $asset): ?string
{
$domain = $this->domain ?? 'demos.imgix.net';
$apiKey = $this->apiKey;
$securityToken = $this->securityToken;
$domain = App::parseEnv($domain);
$apiKey = App::parseEnv($apiKey);
$securityToken = App::parseEnv($securityToken);
$builder = new UrlBuilder($domain);
$builder->setUseHttps(true);
// Create the Imgix URL for purging this image
$assetUri = $this->getAssetUri($asset);
$url = $builder->createURL($assetUri, [
'domain' => $domain,
'api-key' => $apiKey,
'security-token' => $securityToken,
]);
// Strip the query string, so we just pass in the raw URL
return UrlHelper::stripQueryString($url);
}
/**
* @inheritdoc
*/
public function purgeUrl(string $url): bool
{
$result = false;
$apiKey = $this->apiKey;
if ($apiKey === '') {
Craft::error(
'Imgix API key is not set',
__METHOD__
);
return false;
}
$apiKey = App::parseEnv($apiKey);
// Check the API key to see if it is deprecated or not
if (strlen($this->apiKey) < 50) {
try {
Craft::$app->deprecator->log(__METHOD__, 'You are using a deprecated API key. Obtain a new API key to use the purging API. More info: https://blog.imgix.com/2020/10/16/api-deprecation');
} catch (DeprecationException $e) {
Craft::error($e->getMessage(), __METHOD__);
}
}
// create new guzzle client
$guzzleClient = Craft::createGuzzleClient(['timeout' => 120, 'connect_timeout' => 120]);
// Submit the sitemap index to each search engine
try {
$response = $guzzleClient->post(self::IMGIX_PURGE_ENDPOINT, [
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
],
'form_params' => [
'data' => [
'attributes' => [
'url' => $url
],
'type' => 'purges'
]
],
]);
// See if it succeeded
if (($response->getStatusCode() >= 200)
&& ($response->getStatusCode() < 400)
) {
$result = true;
}
Craft::info(
'URL purged: ' . $url . ' - Response code: ' . $response->getStatusCode(),
__METHOD__
);
} catch (GuzzleException $e) {
Craft::error(
'Error purging URL: ' . $url . ' - ' . $e->getMessage(),
__METHOD__
);
}
return $result;
}
/**
* @inheritdoc
*/
public function getAssetUri(Asset $asset): ?string
{
$volume = $asset->getVolume();
// If this is a local volume, it implies your are using a "Web Folder"
// source in Imgix. We can then also infer that:
// - This volume has URLs
// - The "Base URL" in Imgix is set to your domain root, per the ImageOptimize docs.
//
// Therefore, we need to parse the path from the full URL, so that it
// includes the path of the volume.
$fs = $volume->getFs();
if ($fs instanceof Local) {
$assetUrl = AssetsHelper::generateUrl($fs, $asset);
return parse_url(rawurldecode($assetUrl), PHP_URL_PATH);
}
return parent::getAssetUri($asset);
}
/**
* @inheritdoc
*/
public function getSettingsHtml(): ?string
{
return Craft::$app->getView()->renderTemplate('imgix-image-transform/settings/image-transforms/imgix.twig', [
'imageTransform' => $this,
]);
}
/**
* @inheritdoc
*/
public function rules(): array
{
$rules = parent::rules();
return array_merge($rules, [
[['domain', 'apiKey', 'securityToken'], 'default', 'value' => ''],
[['domain', 'apiKey', 'securityToken'], 'string'],
]);
}
}