Issues (231)

src/helpers/ImageTransform.php (1 issue)

Severity
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\helpers;
13
14
use Craft;
15
use craft\elements\Asset;
16
use craft\elements\db\ElementQuery;
17
use craft\helpers\StringHelper;
18
use craft\models\AssetTransform;
19
use craft\volumes\Local;
20
use DateTime;
21
use Exception;
22
use nystudio107\seomatic\helpers\Environment as EnvironmentHelper;
23
use nystudio107\seomatic\Seomatic;
24
use yii\base\InvalidConfigException;
25
use function in_array;
26
use function is_array;
27
28
/**
29
 * @author    nystudio107
30
 * @package   Seomatic
31
 * @since     3.0.0
32
 */
33
class ImageTransform
34
{
35
    // Constants
36
    // =========================================================================
37
38
    const SOCIAL_TRANSFORM_QUALITY = 82;
39
40
    const ALLOWED_SOCIAL_MIME_TYPES = [
41
        'image/jpeg',
42
        'image/png',
43
        'image/webp',
44
        'image/gif',
45
    ];
46
47
    const DEFAULT_SOCIAL_FORMAT = 'jpg';
48
49
    // Static Public Properties
50
    // =========================================================================
51
52
    /**
53
     * @var bool
54
     */
55
    public static $pendingImageTransforms = false;
56
57
    // Static Private Properties
58
    // =========================================================================
59
60
    private static $transforms = [
61
        'base' => [
62
            'format' => null,
63
            'quality' => self::SOCIAL_TRANSFORM_QUALITY,
64
            'width' => 1200,
65
            'height' => 630,
66
            'mode' => 'crop',
67
        ],
68
        'facebook' => [
69
            'width' => 1200,
70
            'height' => 630,
71
        ],
72
        'twitter-summary' => [
73
            'width' => 800,
74
            'height' => 800,
75
        ],
76
        'twitter-large' => [
77
            'width' => 800,
78
            'height' => 418,
79
        ],
80
        'schema-logo' => [
81
            'format' => 'png',
82
            'width' => 600,
83
            'height' => 60,
84
            'mode' => 'fit',
85
        ],
86
    ];
87
88
    private static $cachedAssetsElements = [];
89
90
    // Static Methods
91
    // =========================================================================
92
93
    /**
94
     * Transform the $asset for social media sites in $transformName and
95
     * optional $siteId
96
     *
97
     * @param int|Asset $asset the Asset or Asset ID
98
     * @param string $transformName the name of the transform to apply
99
     * @param int|null $siteId
100
     * @param string $transformMode
101
     *
102
     * @return string URL to the transformed image
103
     */
104
    public static function socialTransform(
105
        $asset,
106
        $transformName = '',
107
        $siteId = null,
108
        $transformMode = null
109
    ): string {
110
        $url = '';
111
        $transform = self::createSocialTransform($transformName);
112
        // Let them override the mode
113
        if (empty($transformMode)) {
114
            $transformMode = $transform->mode ?? 'crop';
115
        }
116
        if ($transform !== null) {
117
            $transform->mode = $transformMode;
118
        }
119
        $asset = self::assetFromAssetOrIdOrQuery($asset, $siteId);
120
        if (($asset !== null) && ($asset instanceof Asset)) {
121
            // Make sure the format is an allowed format, otherwise explicitly change it
122
            $mimeType = $asset->getMimeType();
123
            if ($transform !== null && !in_array($mimeType, self::ALLOWED_SOCIAL_MIME_TYPES, false)) {
124
                $transform->format = self::DEFAULT_SOCIAL_FORMAT;
125
            }
126
            // Generate a transformed image
127
            $assets = Craft::$app->getAssets();
128
            try {
129
                $volume = $asset->getVolume();
130
            } catch (InvalidConfigException $e) {
131
                $volume = null;
132
            }
133
            // If we're not in local dev, tell it to generate the transform immediately so that
134
            // urls like `actions/assets/generate-transform` don't get cached
135
            $generateNow = Seomatic::$environment === EnvironmentHelper::SEOMATIC_DEV_ENV ? null : true;
136
            if ($volume instanceof Local) {
137
                // Preflight to ensure that the source asset actually exists to avoid Craft hanging
138
                if (!$volume->fileExists($asset->getPath())) {
139
                    $generateNow = false;
140
                }
141
            } else {
142
                // If this is not a local volume, avoid a potentially long round-trip by
143
                // being paranoid, and defaulting to not generating the image now
144
                // if we're in local dev
145
                if (Seomatic::$environment === EnvironmentHelper::SEOMATIC_DEV_ENV) {
146
                    $generateNow = false;
147
                }
148
            }
149
            try {
150
                if ($asset->getHasFocalPoint()) {
151
                    $transform->position = $asset->getFocalPoint(true);
152
                }
153
                $url = $assets->getAssetUrl($asset, $transform, $generateNow);
154
            } catch (Exception $e) {
155
                $url = $asset->getUrl();
156
            }
157
            if ($url === null) {
158
                $url = '';
159
            }
160
            // If we have a url, add an `mtime` param to cache bust
161
            if (!empty($url) && empty(parse_url($url, PHP_URL_QUERY))) {
162
                $now = new DateTime();
0 ignored issues
show
The assignment to $now is dead and can be removed.
Loading history...
163
                $newestChange = max($asset->dateModified, $asset->dateUpdated);
164
                $url = UrlHelper::url($url, [
165
                    'mtime' => $newestChange->getTimestamp(),
166
                ]);
167
            }
168
        }
169
        // Check to see if the $url contains a pending image transform
170
        if (!empty($url) && StringHelper::contains($url, 'assets/generate-transform')) {
171
            self::$pendingImageTransforms = true;
172
        }
173
174
        return $url;
175
    }
176
177
    /**
178
     * @param int|Asset $asset the Asset or Asset ID
179
     * @param string $transformName the name of the transform to apply
180
     * @param int|null $siteId
181
     * @param string $transformMode
182
     *
183
     * @return string width of the transformed image
184
     */
185
    public static function socialTransformWidth(
186
        $asset,
187
        $transformName = '',
188
        $siteId = null,
189
        $transformMode = null
190
    ): string {
191
        $width = '';
192
        $transform = self::createSocialTransform($transformName);
193
        // Let them override the mode
194
        if ($transform !== null) {
195
            $transform->mode = $transformMode ?? $transform->mode;
196
        }
197
        $asset = self::assetFromAssetOrIdOrQuery($asset, $siteId);
198
        if ($asset instanceof Asset) {
199
            $width = $asset->getWidth($transform);
200
            if ($width === null) {
201
                $width = '';
202
            }
203
            $width = (string)$width;
204
        }
205
206
        return $width;
207
    }
208
209
    /**
210
     * @param int|Asset $asset the Asset or Asset ID
211
     * @param string $transformName the name of the transform to apply
212
     * @param int|null $siteId
213
     * @param string $transformMode
214
     *
215
     * @return string width of the transformed image
216
     */
217
    public static function socialTransformHeight(
218
        $asset,
219
        $transformName = '',
220
        $siteId = null,
221
        $transformMode = null
222
    ): string {
223
        $height = '';
224
        $transform = self::createSocialTransform($transformName);
225
        // Let them override the mode
226
        if ($transform !== null) {
227
            $transform->mode = $transformMode ?? $transform->mode;
228
        }
229
        $asset = self::assetFromAssetOrIdOrQuery($asset, $siteId);
230
        if ($asset instanceof Asset) {
231
            $height = $asset->getHeight($transform);
232
            if ($height === null) {
233
                $height = '';
234
            }
235
            $height = (string)$height;
236
        }
237
238
        return $height;
239
    }
240
241
    /**
242
     * Return an array of Asset elements from an array of element IDs
243
     *
244
     * @param array|string $assetIds
245
     * @param int|null $siteId
246
     *
247
     * @return array
248
     */
249
    public static function assetElementsFromIds($assetIds, $siteId = null): array
250
    {
251
        $elements = Craft::$app->getElements();
252
        $assets = [];
253
        if (!empty($assetIds)) {
254
            if (is_array($assetIds)) {
255
                foreach ($assetIds as $assetId) {
256
                    if (!empty($assetId)) {
257
                        $assets[] = $elements->getElementById((int)$assetId, Asset::class, $siteId);
258
                    }
259
                }
260
            } else {
261
                $assetId = $assetIds;
262
                $assets[] = $elements->getElementById((int)$assetId, Asset::class, $siteId);
263
            }
264
        }
265
266
        return array_filter($assets);
267
    }
268
269
    // Protected Static Methods
270
    // =========================================================================
271
272
    /**
273
     * Return an asset from either an id or an asset
274
     *
275
     * @param int|array|Asset|ElementQuery $asset the Asset or Asset ID or ElementQuery
276
     * @param int|null $siteId
277
     *
278
     * @return Asset|array|null
279
     */
280
    protected static function assetFromAssetOrIdOrQuery($asset, $siteId = null)
281
    {
282
        if (empty($asset)) {
283
            return null;
284
        }
285
        // If it's an array (eager loaded Element query), return the first element
286
        if (is_array($asset)) {
287
            return reset($asset);
288
        }
289
        // If it's an asset already, just return it
290
        if ($asset instanceof Asset) {
291
            return $asset;
292
        }
293
        // If it is an ElementQuery, resolve that to an asset
294
        if ($asset instanceof ElementQuery) {
295
            return $asset->one();
296
        }
297
298
        $resolvedAssetId = (int)$asset;
299
        $resolvedSiteId = $siteId ?? 0;
300
        if (isset(self::$cachedAssetsElements[$resolvedAssetId][$resolvedSiteId])) {
301
            return self::$cachedAssetsElements[$resolvedAssetId][$resolvedSiteId];
302
        }
303
304
        $asset = Craft::$app->getAssets()->getAssetById($resolvedAssetId, $siteId);
305
        self::$cachedAssetsElements[$resolvedAssetId][$resolvedSiteId] = $asset;
306
307
        return $asset;
308
    }
309
310
    /**
311
     * Create a transform from the passed in $transformName
312
     *
313
     * @param string $transformName the name of the transform to apply
314
     *
315
     * @return AssetTransform|null
316
     */
317
    protected static function createSocialTransform($transformName = 'base')
318
    {
319
        $transform = null;
320
        if (!empty($transformName)) {
321
            $config = array_merge(
322
                self::$transforms['base'],
323
                self::$transforms[$transformName] ?? self::$transforms['base']
324
            );
325
            $transform = new AssetTransform($config);
326
        }
327
328
        return $transform;
329
    }
330
}
331