PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-publicize / src / class-focal-point.php

class-focal-point.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-publicize/src/class-focal-point.php

433 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Focal point crop helpers.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8 namespace Automattic\Jetpack\Publicize;
9
10 use Automattic\Jetpack\Image_CDN\Image_CDN;
11 use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
12 use Automattic\Jetpack\Status;
13
14 /**
15 * Shared focal point crop helpers for Jetpack Social images.
16 */
17 class Focal_Point {
18
19 /**
20 * Target Open Graph image width.
21 *
22 * @var int
23 */
24 const OG_IMAGE_WIDTH = 1200;
25
26 /**
27 * Target Open Graph image height.
28 *
29 * @var int
30 */
31 const OG_IMAGE_HEIGHT = 630;
32
33 /**
34 * Get the stored focal point for an image.
35 *
36 * @param int $attachment_id Attachment ID.
37 * @return array|null {
38 * Focal point, or null when not set or invalid.
39 *
40 * @type float $x X axis, 0-1.
41 * @type float $y Y axis, 0-1.
42 * }
43 */
44 public static function get_for_image( $attachment_id ) {
45 $attachment_id = absint( $attachment_id );
46
47 if ( ! $attachment_id ) {
48 return null;
49 }
50
51 $focal_point = get_metadata_raw( 'post', $attachment_id, Publicize_Base::ATTACHMENT_IMAGE_FOCAL_POINT, true );
52
53 if ( ! self::is_valid_focal_point( $focal_point ) ) {
54 return null;
55 }
56
57 if ( self::is_default_focal_point( $focal_point ) && ! self::has_stored_focal_point_meta( $attachment_id ) ) {
58 return null;
59 }
60
61 return array(
62 'x' => (float) $focal_point['x'],
63 'y' => (float) $focal_point['y'],
64 );
65 }
66
67 /**
68 * Get a focal-point cropped image for an attachment.
69 *
70 * @param int $attachment_id Attachment ID.
71 * @param int $target_width Target width.
72 * @param int $target_height Target height.
73 * @return array|null {
74 * Image data, or null when a cropped image cannot be generated.
75 *
76 * @type string $url Image source URL.
77 * @type int $width Image width in pixels.
78 * @type int $height Image height in pixels.
79 * }
80 */
81 public static function get_cropped_image( $attachment_id, $target_width = self::OG_IMAGE_WIDTH, $target_height = self::OG_IMAGE_HEIGHT ) {
82 $focal_point = self::get_for_image( $attachment_id );
83
84 if ( ! $focal_point ) {
85 return null;
86 }
87
88 $crop_data = self::get_crop_data( $attachment_id, $focal_point['x'], $focal_point['y'], $target_width, $target_height );
89
90 if ( ! $crop_data ) {
91 return null;
92 }
93
94 return array(
95 'url' => $crop_data['url'],
96 'width' => $crop_data['width'],
97 'height' => $crop_data['height'],
98 );
99 }
100
101 /**
102 * Get a focal-point cropped URL for an attachment.
103 *
104 * @param int $attachment_id Attachment ID.
105 * @param float $focal_x Focal point x axis, 0-1.
106 * @param float $focal_y Focal point y axis, 0-1.
107 * @param int $target_width Target width.
108 * @param int $target_height Target height.
109 * @return string|null Cropped URL, or null when one cannot be generated.
110 */
111 public static function get_cropped_url( $attachment_id, $focal_x, $focal_y, $target_width = self::OG_IMAGE_WIDTH, $target_height = self::OG_IMAGE_HEIGHT ) {
112 $crop_data = self::get_crop_data( $attachment_id, $focal_x, $focal_y, $target_width, $target_height );
113
114 return $crop_data ? $crop_data['url'] : null;
115 }
116
117 /**
118 * Calculate a source crop rectangle for a focal point and target aspect ratio.
119 *
120 * The crop model matches the Social previews: center the crop on the focal
121 * point, then clamp the crop rectangle to the source image edges.
122 *
123 * @param int $source_width Source image width.
124 * @param int $source_height Source image height.
125 * @param float $focal_x Focal point x axis, 0-1.
126 * @param float $focal_y Focal point y axis, 0-1.
127 * @param float $aspect Target aspect ratio.
128 * @return array|null {
129 * Crop rectangle, or null when inputs are invalid.
130 *
131 * @type int $x Source x coordinate.
132 * @type int $y Source y coordinate.
133 * @type int $width Crop width.
134 * @type int $height Crop height.
135 * }
136 */
137 public static function crop_rect( $source_width, $source_height, $focal_x, $focal_y, $aspect ) {
138 $source_width = absint( $source_width );
139 $source_height = absint( $source_height );
140 $aspect = (float) $aspect;
141
142 if ( ! $source_width || ! $source_height || $aspect <= 0 ) {
143 return null;
144 }
145
146 $focal_x = self::clamp( (float) $focal_x, 0, 1 );
147 $focal_y = self::clamp( (float) $focal_y, 0, 1 );
148
149 $crop_width = min( $source_width, $source_height * $aspect );
150 $crop_height = $crop_width / $aspect;
151
152 if ( $crop_height > $source_height ) {
153 $crop_height = $source_height;
154 $crop_width = $crop_height * $aspect;
155 }
156
157 $crop_width = max( 1, min( $source_width, (int) round( $crop_width ) ) );
158 $crop_height = max( 1, min( $source_height, (int) round( $crop_height ) ) );
159 $crop_x = (int) self::clamp( round( $focal_x * $source_width - $crop_width / 2 ), 0, $source_width - $crop_width );
160 $crop_y = (int) self::clamp( round( $focal_y * $source_height - $crop_height / 2 ), 0, $source_height - $crop_height );
161
162 return array(
163 'x' => $crop_x,
164 'y' => $crop_y,
165 'width' => $crop_width,
166 'height' => $crop_height,
167 );
168 }
169
170 /**
171 * Get all crop data needed for a Photon URL and dimensions.
172 *
173 * @param int $attachment_id Attachment ID.
174 * @param float $focal_x Focal point x axis, 0-1.
175 * @param float $focal_y Focal point y axis, 0-1.
176 * @param int $target_width Target width.
177 * @param int $target_height Target height.
178 * @return array|null Crop data, or null.
179 */
180 private static function get_crop_data( $attachment_id, $focal_x, $focal_y, $target_width, $target_height ) {
181 $attachment_id = absint( $attachment_id );
182 $target_width = absint( $target_width );
183 $target_height = absint( $target_height );
184
185 if ( ! $attachment_id || ! $target_width || ! $target_height || ! wp_attachment_is_image( $attachment_id ) ) {
186 return null;
187 }
188
189 if (
190 ! class_exists( Image_CDN_Core::class )
191 || ! method_exists( Image_CDN_Core::class, 'cdn_url' )
192 || ! method_exists( Image_CDN_Core::class, 'is_cdn_url' )
193 ) {
194 return null;
195 }
196
197 if ( ( new Status() )->is_private_site() ) {
198 return null;
199 }
200
201 $source_url = wp_get_attachment_url( $attachment_id );
202
203 if ( ! $source_url || ! self::is_supported_image_url( $source_url ) ) {
204 return null;
205 }
206
207 $dimensions = self::get_dimensions( $attachment_id );
208
209 if ( ! $dimensions ) {
210 return null;
211 }
212
213 $aspect = $target_width / $target_height;
214 $crop_rect = self::crop_rect( $dimensions['width'], $dimensions['height'], $focal_x, $focal_y, $aspect );
215
216 if ( ! $crop_rect ) {
217 return null;
218 }
219
220 $args = array();
221 $needs_crop = self::needs_crop( $crop_rect, $dimensions );
222 $resize_width = $crop_rect['width'];
223 $resize_height = $crop_rect['height'];
224
225 if ( $needs_crop ) {
226 $args['crop'] = sprintf(
227 '%dpx,%dpx,%dpx,%dpx',
228 $crop_rect['x'],
229 $crop_rect['y'],
230 $crop_rect['width'],
231 $crop_rect['height']
232 );
233 }
234
235 if ( $crop_rect['width'] > $target_width || $crop_rect['height'] > $target_height ) {
236 $scale = min( $target_width / $crop_rect['width'], $target_height / $crop_rect['height'] );
237 $resize_width = max( 1, (int) round( $crop_rect['width'] * $scale ) );
238 $resize_height = max( 1, (int) round( $crop_rect['height'] * $scale ) );
239 $args['resize'] = $resize_width . ',' . $resize_height;
240 }
241
242 if ( ! $args ) {
243 return array(
244 'url' => $source_url,
245 'width' => $dimensions['width'],
246 'height' => $dimensions['height'],
247 );
248 }
249
250 if ( ! self::can_preserve_source_query_string( $source_url ) ) {
251 return null;
252 }
253
254 $cropped_url = Image_CDN_Core::cdn_url(
255 $source_url,
256 $args
257 );
258
259 if ( ! $cropped_url || $cropped_url === $source_url ) {
260 return null;
261 }
262
263 return array(
264 'url' => $cropped_url,
265 'width' => $resize_width,
266 'height' => $resize_height,
267 );
268 }
269
270 /**
271 * Check whether the crop rectangle changes the source image.
272 *
273 * @param array $crop_rect Crop rectangle.
274 * @param array $dimensions Source dimensions.
275 * @return bool Whether the source image needs a crop operation.
276 */
277 private static function needs_crop( $crop_rect, $dimensions ) {
278 return 0 !== $crop_rect['x']
279 || 0 !== $crop_rect['y']
280 || $crop_rect['width'] !== $dimensions['width']
281 || $crop_rect['height'] !== $dimensions['height'];
282 }
283
284 /**
285 * Get image dimensions from attachment metadata.
286 *
287 * @param int $attachment_id Attachment ID.
288 * @return array|null {
289 * Dimensions, or null.
290 *
291 * @type int $width Image width.
292 * @type int $height Image height.
293 * }
294 */
295 private static function get_dimensions( $attachment_id ) {
296 $metadata = wp_get_attachment_metadata( $attachment_id );
297
298 if (
299 ! is_array( $metadata )
300 || empty( $metadata['width'] )
301 || empty( $metadata['height'] )
302 || ! is_numeric( $metadata['width'] )
303 || ! is_numeric( $metadata['height'] )
304 || $metadata['width'] <= 0
305 || $metadata['height'] <= 0
306 ) {
307 return null;
308 }
309
310 return array(
311 'width' => absint( $metadata['width'] ),
312 'height' => absint( $metadata['height'] ),
313 );
314 }
315
316 /**
317 * Check whether a URL has an Image CDN supported extension.
318 *
319 * @param string $url Image URL.
320 * @return bool Whether Image CDN supports the URL extension.
321 */
322 private static function is_supported_image_url( $url ) {
323 if ( ! class_exists( Image_CDN::class ) || ! method_exists( Image_CDN::class, 'get_supported_extensions' ) ) {
324 return false;
325 }
326
327 $path = wp_parse_url( $url, PHP_URL_PATH );
328
329 if ( ! $path ) {
330 return false;
331 }
332
333 return in_array( strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ), Image_CDN::get_supported_extensions(), true );
334 }
335
336 /**
337 * Check whether Photon will preserve the source URL query string.
338 *
339 * Photon ignores source query strings by default. Some attachment providers put
340 * required signatures in the query string, while transformed CDN URLs can already
341 * contain ordered image manipulation args. Skip focal crops unless the domain opts
342 * in to query string preservation.
343 *
344 * @param string $url Image URL.
345 * @return bool Whether the source query string can be preserved.
346 */
347 private static function can_preserve_source_query_string( $url ) {
348 $url_parts = wp_parse_url( $url );
349
350 if ( ! is_array( $url_parts ) || empty( $url_parts['query'] ) ) {
351 return true;
352 }
353
354 $host = strtolower( $url_parts['host'] ?? '' );
355
356 if ( '' === $host ) {
357 return false;
358 }
359
360 if ( Image_CDN_Core::is_cdn_url( $url ) ) {
361 return false;
362 }
363
364 /**
365 * Allow Photon to add source query strings for opted-in domains.
366 *
367 * @module photon
368 *
369 * @param bool false Should query strings be added to the image URL. Default is false.
370 * @param string $host Image URL's host.
371 */
372 return (bool) apply_filters( 'jetpack_photon_add_query_string_to_domain', false, $host );
373 }
374
375 /**
376 * Validate focal point shape.
377 *
378 * @param mixed $value Value to validate.
379 * @return bool Whether the value is a valid focal point.
380 */
381 private static function is_valid_focal_point( $value ) {
382 if ( ! is_array( $value ) || ! array_key_exists( 'x', $value ) || ! array_key_exists( 'y', $value ) ) {
383 return false;
384 }
385
386 return is_numeric( $value['x'] )
387 && is_numeric( $value['y'] )
388 && $value['x'] >= 0
389 && $value['x'] <= 1
390 && $value['y'] >= 0
391 && $value['y'] <= 1;
392 }
393
394 /**
395 * Check whether the focal point is the registered default.
396 *
397 * @param array $value Focal point.
398 * @return bool Whether the value is the default center point.
399 */
400 private static function is_default_focal_point( $value ) {
401 return 0.5 === (float) $value['x'] && 0.5 === (float) $value['y'];
402 }
403
404 /**
405 * Check whether the focal point meta key is actually stored.
406 *
407 * Registered meta defaults can appear through the metadata API even when no
408 * row has been saved. Only use the default center point when the key exists.
409 *
410 * @param int $attachment_id Attachment ID.
411 * @return bool Whether the focal point key is stored on the attachment.
412 */
413 private static function has_stored_focal_point_meta( $attachment_id ) {
414 // metadata_exists() applies metadata filters, so registered defaults can look stored.
415 $stored_meta_keys = get_post_custom_keys( $attachment_id );
416
417 return is_array( $stored_meta_keys )
418 && in_array( Publicize_Base::ATTACHMENT_IMAGE_FOCAL_POINT, $stored_meta_keys, true );
419 }
420
421 /**
422 * Clamp a numeric value.
423 *
424 * @param float $value Value to clamp.
425 * @param float $min Minimum value.
426 * @param float $max Maximum value.
427 * @return float Clamped value.
428 */
429 private static function clamp( $value, $min, $max ) {
430 return min( max( $value, $min ), $max );
431 }
432 }
433