PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.2
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.2
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-publicize / src / class-focal-point.php
jetpack / jetpack_vendor / automattic / jetpack-publicize / src Last commit date
jetpack-social-settings 3 months ago rest-api 3 weeks ago social-image-generator 3 weeks ago class-connections.php 3 weeks ago class-focal-point.php 2 months ago class-keyring-helper.php 2 months ago class-keyring-result-handler.php 2 months ago class-message-templates-placeholders.php 3 months ago class-publicize-assets.php 2 months ago class-publicize-base.php 4 weeks ago class-publicize-script-data.php 1 month ago class-publicize-setup.php 4 weeks ago class-publicize-ui.php 4 weeks ago class-publicize-utils.php 4 weeks ago class-publicize.php 2 months ago class-services.php 3 weeks ago class-share-status.php 9 months ago class-social-admin-page.php 3 weeks ago
class-focal-point.php
433 lines
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