@@ -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