Skip to content

Commit 1aa6f73

Browse files
authored
Merge branch 'trunk' into add/ap-post-purge
2 parents 44b8748 + 4231124 commit 1aa6f73

File tree

3 files changed

+400
-19
lines changed

3 files changed

+400
-19
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Add image optimization for imported attachments (resize to 1200px max, convert to WebP).

includes/class-attachments.php

Lines changed: 144 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ class Attachments {
2727
*/
2828
public static $comments_dir = '/activitypub/comments/';
2929

30+
/**
31+
* Maximum width for imported images.
32+
*
33+
* @var int
34+
*/
35+
const MAX_IMAGE_DIMENSION = 1200;
36+
3037
/**
3138
* Initialize the class and set up filters.
3239
*/
@@ -420,13 +427,14 @@ private static function save_attachment( $attachment_data, $post_id, $author_id
420427
require_once ABSPATH . 'wp-admin/includes/image.php';
421428
}
422429

430+
// Initialize filesystem.
431+
\WP_Filesystem();
432+
global $wp_filesystem;
433+
423434
$is_local = ! preg_match( '#^https?://#i', $attachment_data['url'] );
424435

425436
if ( $is_local ) {
426437
// Read local file from disk.
427-
\WP_Filesystem();
428-
global $wp_filesystem;
429-
430438
if ( ! $wp_filesystem->exists( $attachment_data['url'] ) ) {
431439
/* translators: %s: file path */
432440
return new \WP_Error( 'file_not_found', sprintf( \__( 'File not found: %s', 'activitypub' ), $attachment_data['url'] ) );
@@ -444,27 +452,47 @@ private static function save_attachment( $attachment_data, $post_id, $author_id
444452
}
445453
}
446454

447-
// Prepare file array for WordPress.
455+
// Get original filename from URL.
456+
$original_name = \basename( \wp_parse_url( $attachment_data['url'], PHP_URL_PATH ) );
457+
458+
// Rename temp file to have proper extension for optimize_image to detect mime type.
459+
$original_ext = \pathinfo( $original_name, PATHINFO_EXTENSION );
460+
if ( $original_ext ) {
461+
$renamed_tmp = $tmp_file . '.' . $original_ext;
462+
if ( $wp_filesystem->move( $tmp_file, $renamed_tmp, true ) ) {
463+
$tmp_file = $renamed_tmp;
464+
}
465+
}
466+
467+
// Optimize images before sideloading (resize and convert to WebP).
468+
$tmp_file = self::optimize_image( $tmp_file, self::MAX_IMAGE_DIMENSION );
469+
470+
// Update filename extension to match optimized file.
471+
$new_ext = \pathinfo( $tmp_file, PATHINFO_EXTENSION );
472+
if ( $new_ext ) {
473+
$original_name = \preg_replace( '/\.[^.]+$/', '.' . $new_ext, $original_name );
474+
}
475+
448476
$file_array = array(
449-
'name' => \basename( \wp_parse_url( $attachment_data['url'], PHP_URL_PATH ) ),
477+
'name' => $original_name,
450478
'tmp_name' => $tmp_file,
451479
);
452480

453481
// Prepare attachment post data.
482+
// Let WordPress auto-detect the mime type from the file.
454483
$post_data = array(
455-
'post_mime_type' => $attachment_data['mediaType'] ?? '',
456-
'post_title' => $attachment_data['name'] ?? '',
457-
'post_content' => $attachment_data['name'] ?? '',
458-
'post_author' => $author_id,
459-
'meta_input' => array(
484+
'post_title' => $attachment_data['name'] ?? '',
485+
'post_content' => $attachment_data['name'] ?? '',
486+
'post_author' => $author_id,
487+
'meta_input' => array(
460488
'_source_url' => $attachment_data['url'],
461489
),
462490
);
463491

464492
// Add alt text for images.
465493
if ( ! empty( $attachment_data['name'] ) ) {
466-
$mime_type = $attachment_data['mediaType'] ?? '';
467-
if ( 'image' === strtok( $mime_type, '/' ) ) {
494+
$original_mime = $attachment_data['mediaType'] ?? '';
495+
if ( 'image' === strtok( $original_mime, '/' ) ) {
468496
$post_data['meta_input']['_wp_attachment_image_alt'] = $attachment_data['name'];
469497
}
470498
}
@@ -489,6 +517,7 @@ private static function save_attachment( $attachment_data, $post_id, $author_id
489517
* @param array $attachment_data The normalized attachment data.
490518
* @param int $object_id The post or comment ID to attach to.
491519
* @param string $object_type The object type ('post' or 'comment').
520+
* @param int $max_dimension Optional. Maximum image dimension in pixels. Default MAX_IMAGE_DIMENSION.
492521
*
493522
* @return array|\WP_Error {
494523
* Array of file data on success, WP_Error on failure.
@@ -498,7 +527,7 @@ private static function save_attachment( $attachment_data, $post_id, $author_id
498527
* @type string $alt Alt text from attachment name field.
499528
* }
500529
*/
501-
private static function save_file( $attachment_data, $object_id, $object_type ) {
530+
private static function save_file( $attachment_data, $object_id, $object_type, $max_dimension = self::MAX_IMAGE_DIMENSION ) {
502531
$mime_type = $attachment_data['mediaType'] ?? '';
503532

504533
// Skip download for video and audio files - use remote URL directly.
@@ -554,6 +583,10 @@ private static function save_file( $attachment_data, $object_id, $object_type )
554583
return new \WP_Error( 'file_move_failed', \__( 'Failed to move file to destination.', 'activitypub' ) );
555584
}
556585

586+
// Optimize images (resize and convert to WebP).
587+
$file_path = self::optimize_image( $file_path, $max_dimension );
588+
$file_name = \basename( $file_path );
589+
557590
// Get mime type and validate file.
558591
$file_info = \wp_check_filetype_and_ext( $file_path, $file_name );
559592
$mime_type = $file_info['type'] ?? $attachment_data['mediaType'] ?? '';
@@ -565,6 +598,104 @@ private static function save_file( $attachment_data, $object_id, $object_type )
565598
);
566599
}
567600

601+
/**
602+
* Get a unique file path by appending a counter if the file already exists.
603+
*
604+
* @param string $file_path The desired file path.
605+
*
606+
* @return string A unique file path that doesn't exist.
607+
*/
608+
private static function get_unique_path( $file_path ) {
609+
if ( ! \file_exists( $file_path ) ) {
610+
return $file_path;
611+
}
612+
613+
$path_info = \pathinfo( $file_path );
614+
$dir = $path_info['dirname'];
615+
$base_name = $path_info['filename'];
616+
$extension = isset( $path_info['extension'] ) ? '.' . $path_info['extension'] : '';
617+
$counter = 1;
618+
619+
do {
620+
$new_path = $dir . '/' . $base_name . '-' . $counter . $extension;
621+
++$counter;
622+
} while ( \file_exists( $new_path ) );
623+
624+
return $new_path;
625+
}
626+
627+
/**
628+
* Optimize an image file by resizing and converting to WebP.
629+
*
630+
* Uses WordPress image editor to resize large images and convert them
631+
* to WebP format for better compression while maintaining quality.
632+
*
633+
* @param string $file_path Path to the image file.
634+
* @param int $max_dimension Maximum width/height in pixels.
635+
*
636+
* @return string The optimized file path.
637+
*/
638+
private static function optimize_image( $file_path, $max_dimension ) {
639+
// Check if it's an image.
640+
$mime_type = \wp_check_filetype( $file_path )['type'] ?? '';
641+
if ( ! $mime_type || ! \str_starts_with( $mime_type, 'image/' ) ) {
642+
return $file_path;
643+
}
644+
645+
// Skip SVG and GIF files (GIFs may be animated).
646+
if ( \in_array( $mime_type, array( 'image/svg+xml', 'image/gif' ), true ) ) {
647+
return $file_path;
648+
}
649+
650+
$editor = \wp_get_image_editor( $file_path );
651+
if ( \is_wp_error( $editor ) ) {
652+
return $file_path;
653+
}
654+
655+
$size = $editor->get_size();
656+
$needs_resize = $size['width'] > $max_dimension || $size['height'] > $max_dimension;
657+
658+
// Resize if needed.
659+
if ( $needs_resize ) {
660+
$editor->resize( $max_dimension, $max_dimension, false );
661+
}
662+
663+
// Check if WebP is supported.
664+
$can_webp = $editor->supports_mime_type( 'image/webp' );
665+
666+
// Determine output format and save.
667+
if ( $can_webp ) {
668+
// Convert to WebP.
669+
$new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.webp', $file_path ) );
670+
$result = $editor->save( $new_path, 'image/webp' );
671+
} elseif ( \in_array( $mime_type, array( 'image/png', 'image/webp' ), true ) ) {
672+
// Keep original format for potentially transparent images when WebP not available.
673+
if ( ! $needs_resize ) {
674+
// No changes needed.
675+
return $file_path;
676+
}
677+
$result = $editor->save( $file_path );
678+
} else {
679+
// Convert to JPEG when WebP not available.
680+
$new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.jpg', $file_path ) );
681+
$result = $editor->save( $new_path, 'image/jpeg' );
682+
}
683+
684+
if ( \is_wp_error( $result ) ) {
685+
return $file_path;
686+
}
687+
688+
// Handle result - $result is always an array from $editor->save().
689+
$result_path = $result['path'] ?? $file_path;
690+
691+
// If path changed (format conversion), delete the original file.
692+
if ( $result_path !== $file_path ) {
693+
\wp_delete_file( $file_path );
694+
}
695+
696+
return $result_path;
697+
}
698+
568699
/**
569700
* Append media to post content.
570701
*

0 commit comments

Comments
 (0)