-
Notifications
You must be signed in to change notification settings - Fork 382
Expand file tree
/
Copy pathclass-amp-story-media.php
More file actions
381 lines (326 loc) · 11.9 KB
/
class-amp-story-media.php
File metadata and controls
381 lines (326 loc) · 11.9 KB
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
<?php
/**
* Class AMP_Story_Media
*
* @package AMP
*/
/**
* Class AMP_Story_Media
*/
class AMP_Story_Media {
/**
* The image size for the AMP story card, used in an embed and the Latest Stories block.
*
* @var string
*/
const STORY_CARD_IMAGE_SIZE = 'amp-story-poster-portrait';
/**
* The image size for the poster-landscape-src.
*
* @var string
*/
const STORY_LANDSCAPE_IMAGE_SIZE = 'amp-story-poster-landscape';
/**
* The image size for the poster-square-src.
*
* @var string
*/
const STORY_SQUARE_IMAGE_SIZE = 'amp-story-poster-square';
/**
* The slug of the largest image size allowed in an AMP Story page.
*
* @var string
*/
const MAX_IMAGE_SIZE_SLUG = 'amp_story_page';
/**
* The large dimension of the AMP Story poster images.
*
* @var int
*/
const STORY_LARGE_IMAGE_DIMENSION = 928;
/**
* The small dimension of the AMP Story poster images.
*
* @var int
*/
const STORY_SMALL_IMAGE_DIMENSION = 696;
/**
* The poster post meta key.
*
* @var string
*/
const POSTER_POST_META_KEY = 'amp_is_poster';
/**
* Init.
*/
public static function init() {
register_meta(
'post',
self::POSTER_POST_META_KEY,
[
'sanitize_callback' => 'rest_sanitize_boolean',
'type' => 'boolean',
'description' => __( 'Whether the attachment is a poster image.', 'amp' ),
'show_in_rest' => true,
'single' => true,
'object_subtype' => 'attachment',
]
);
// Used for amp-story[poster-portrait-src]: The story poster in portrait format (3x4 aspect ratio).
add_image_size( self::STORY_CARD_IMAGE_SIZE, self::STORY_SMALL_IMAGE_DIMENSION, self::STORY_LARGE_IMAGE_DIMENSION, true );
// Used for amp-story[poster-square-src]: The story poster in square format (1x1 aspect ratio).
add_image_size( self::STORY_SQUARE_IMAGE_SIZE, self::STORY_LARGE_IMAGE_DIMENSION, self::STORY_LARGE_IMAGE_DIMENSION, true );
// Used for amp-story[poster-landscape-src]: The story poster in square format (1x1 aspect ratio).
add_image_size( self::STORY_LANDSCAPE_IMAGE_SIZE, self::STORY_LARGE_IMAGE_DIMENSION, self::STORY_SMALL_IMAGE_DIMENSION, true );
// The default image size for AMP Story image block and background media image.
add_image_size( self::MAX_IMAGE_SIZE_SLUG, 99999, 1440 );
// Include additional story image sizes in Schema.org metadata.
add_filter( 'amp_schemaorg_metadata', [ __CLASS__, 'filter_schemaorg_metadata_images' ], 100 );
// In case there is no featured image for the poster-portrait-src, add a fallback image.
add_filter( 'wp_get_attachment_image_src', [ __CLASS__, 'poster_portrait_fallback' ], 10, 3 );
// If the image is for a poster-square-src or poster-landscape-src, this ensures that it's not too small.
add_filter( 'wp_get_attachment_image_src', [ __CLASS__, 'ensure_correct_poster_size' ], 10, 3 );
add_filter( 'image_size_names_choose', [ __CLASS__, 'add_new_max_image_size' ] );
// The AJAX handler for when an image is cropped and sent via POST.
add_action( 'wp_ajax_custom-header-crop', [ __CLASS__, 'crop_featured_image' ] );
add_action( 'pre_get_posts', [ __CLASS__, 'filter_poster_attachments' ] );
add_action( 'rest_api_init', [ __CLASS__, 'rest_api_init' ] );
}
/**
* Get story meta images.
*
* There is a fallback poster-portrait image added via a filter, in case there's no featured image.
*
* @since 1.2.1
* @see AMP_Story_Media::poster_portrait_fallback()
*
* @param int|WP_Post|null $post Post.
* @return string[] Images.
*/
public static function get_story_meta_images( $post = null ) {
$thumbnail_id = get_post_thumbnail_id( $post );
$images = [
'poster-portrait' => wp_get_attachment_image_url( $thumbnail_id, self::STORY_CARD_IMAGE_SIZE ),
'poster-square' => wp_get_attachment_image_url( $thumbnail_id, self::STORY_SQUARE_IMAGE_SIZE ),
'poster-landscape' => wp_get_attachment_image_url( $thumbnail_id, self::STORY_LANDSCAPE_IMAGE_SIZE ),
];
return array_filter( $images );
}
/**
* Include additional story image sizes in Schema.org metadata for AMP Stories.
*
* @since 1.2.1
*
* @param array $data Metadata.
* @return array Metadata.
*/
public static function filter_schemaorg_metadata_images( $data ) {
if ( ! is_singular( AMP_Story_Post_Type::POST_TYPE_SLUG ) ) {
return $data;
}
if ( ! isset( $data['image'] ) ) {
$data['image'] = [];
} elseif ( is_string( $data['image'] ) ) {
$data['image'] = [ $data['image'] ];
} elseif ( isset( $data['image']['@type'] ) ) {
$data['image'] = [ $data['image'] ];
}
$data['image'] = array_merge(
array_values( self::get_story_meta_images() ),
$data['image']
);
return $data;
}
/**
* If there's no featured image for the poster-portrait-src, this adds a fallback.
*
* @param array|false $image The featured image, or false.
* @param int $attachment_id The ID of the image.
* @param string|array $size The size of the image.
* @return array|false The featured image, or false.
*/
public static function poster_portrait_fallback( $image, $attachment_id, $size ) {
if ( ! $image && self::STORY_CARD_IMAGE_SIZE === $size ) {
return [
amp_get_asset_url( 'images/story-fallback-poster.jpg' ),
self::STORY_LARGE_IMAGE_DIMENSION,
self::STORY_SMALL_IMAGE_DIMENSION,
];
}
return $image;
}
/**
* Helps to ensure that the poster-square-src and poster-landscape-src images aren't too small.
*
* These values come from the featured image.
* But the featured image is often cropped down to 696 x 928.
* So from that, it's not possible to get a 928 x 928 image, for example.
* So instead, use the source image that was cropped, instead of the cropped image.
* This is more likely to produce the right size image.
*
* @param array|false $image The featured image, or false.
* @param int $attachment_id The ID of the image.
* @param string|array $size The size of the image.
* @return array|false The featured image, or false.
*/
public static function ensure_correct_poster_size( $image, $attachment_id, $size ) {
if ( self::STORY_LANDSCAPE_IMAGE_SIZE === $size || self::STORY_SQUARE_IMAGE_SIZE === $size ) {
$attachment_meta = wp_get_attachment_metadata( $attachment_id );
// The source image that was cropped.
if ( ! empty( $attachment_meta['attachment_parent'] ) ) {
return wp_get_attachment_image_src( $attachment_meta['attachment_parent'], $size );
}
}
return $image;
}
/**
* Adds a new max image size to the image sizes available.
*
* In the AMP story editor, when selecting Background Media,
* it will use this custom image size.
* This filter will also make it available in the Image block's 'Image Size' <select> element.
*
* @param array $image_sizes {
* An associative array of image sizes.
*
* @type string $slug Image size slug, like 'medium'.
* @type string $name Image size name, like 'Medium'.
* }
* @return array $image_sizes The filtered image sizes.
*/
public static function add_new_max_image_size( $image_sizes ) {
$full_size_name = __( 'Story Max Size', 'amp' );
if ( isset( $_POST['action'] ) && ( 'query-attachments' === $_POST['action'] || 'upload-attachment' === $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$image_sizes[ self::MAX_IMAGE_SIZE_SLUG ] = $full_size_name;
} elseif ( get_post_type() && AMP_Story_Post_Type::POST_TYPE_SLUG === get_post_type() ) {
$image_sizes[ self::MAX_IMAGE_SIZE_SLUG ] = $full_size_name;
unset( $image_sizes['full'] );
}
return $image_sizes;
}
/**
* Create an attachment 'object'.
*
* Forked from Custom_Image_Header::create_attachment_object() in Core.
*
* @param string $cropped Cropped image URL.
* @param int $parent_attachment_id Attachment ID of parent image.
* @return array Attachment object.
*/
public static function create_attachment_object( $cropped, $parent_attachment_id ) {
$parent = get_post( $parent_attachment_id );
$parent_url = wp_get_attachment_url( $parent->ID );
$url = str_replace( basename( $parent_url ), basename( $cropped ), $parent_url );
$size = null;
try {
$size = getimagesize( $cropped );
} catch ( Exception $error ) {
unset( $error );
}
$image_type = $size ? $size['mime'] : 'image/jpeg';
$object = [
'ID' => $parent_attachment_id,
'post_title' => basename( $cropped ),
'post_mime_type' => $image_type,
'guid' => $url,
'context' => 'amp-story-poster',
'post_parent' => $parent_attachment_id,
];
return $object;
}
/**
* Insert an attachment and its metadata.
*
* Forked from Custom_Image_Header::insert_attachment() in Core.
*
* @param array $object Attachment object.
* @param string $cropped Cropped image URL.
* @return int Attachment ID.
*/
public static function insert_attachment( $object, $cropped ) {
$parent_id = isset( $object['post_parent'] ) ? $object['post_parent'] : null;
unset( $object['post_parent'] );
$attachment_id = wp_insert_attachment( $object, $cropped );
$metadata = wp_generate_attachment_metadata( $attachment_id, $cropped );
// If this is a crop, save the original attachment ID as metadata.
if ( $parent_id ) {
$metadata['attachment_parent'] = $parent_id;
}
wp_update_attachment_metadata( $attachment_id, $metadata );
return $attachment_id;
}
/**
* Crops the image and returns the object as JSON.
*
* Forked from Custom_Image_Header::ajax_header_crop().
*/
public static function crop_featured_image() {
check_ajax_referer( 'image_editor-' . $_POST['id'], 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error();
}
$crop_details = $_POST['cropDetails'];
$dimensions = [
'dst_width' => self::STORY_SMALL_IMAGE_DIMENSION,
'dst_height' => self::STORY_LARGE_IMAGE_DIMENSION,
];
$attachment_id = absint( $_POST['id'] );
$cropped = wp_crop_image(
$attachment_id,
(int) $crop_details['x1'],
(int) $crop_details['y1'],
(int) $crop_details['width'],
(int) $crop_details['height'],
(int) $dimensions['dst_width'],
(int) $dimensions['dst_height']
);
if ( ! $cropped || is_wp_error( $cropped ) ) {
wp_send_json_error( [ 'message' => __( 'Image could not be processed. Please go back and try again.', 'default' ) ] );
}
/** This filter is documented in wp-admin/custom-header.php */
$cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.
$object = self::create_attachment_object( $cropped, $attachment_id );
unset( $object['ID'] );
$new_attachment_id = self::insert_attachment( $object, $cropped );
$object['attachment_id'] = $new_attachment_id;
$object['url'] = wp_get_attachment_url( $new_attachment_id );
$object['width'] = $dimensions['dst_width'];
$object['height'] = $dimensions['dst_height'];
wp_send_json_success( $object );
}
/**
* Filters the current query to hide all automatically extracted poster image attachments.
*
* Reduces unnecessary noise in the media library.
*
* @param WP_Query $query WP_Query instance, passed by reference.
*/
public static function filter_poster_attachments( &$query ) {
$post_type = (array) $query->get( 'post_type' );
if ( ! in_array( 'any', $post_type, true ) && ! in_array( 'attachment', $post_type, true ) ) {
return;
}
$meta_query = (array) $query->get( 'meta_query' );
$meta_query[] = [
'key' => self::POSTER_POST_META_KEY,
'compare' => 'NOT EXISTS',
];
$query->set( 'meta_query', $meta_query );
}
/**
* Registers additional REST API fields upon API initialization.
*/
public static function rest_api_init() {
register_rest_field(
'attachment',
'featured_media',
[
'schema' => [
'description' => __( 'The ID of the featured media for the object.', 'amp' ),
'type' => 'integer',
'context' => [ 'view', 'edit', 'embed' ],
],
]
);
}
}