Make WordPress Core

Changeset 61841


Ignore:
Timestamp:
03/05/2026 10:17:34 AM (7 months ago)
Author:
audrasjb
Message:

Media: Import images Alt text IPTC metadata.

This changeset introduce a new wp_get_image_alttext() function that extracts Image Alt text metadata from image IPTC metadata.

Props jhmonroe, rishabhwpn, adamsilverstein, sajjad67, ozgursar, joedolson, audrasjb, huzaifaalmesbah, sabernhardt, valentingrenier, louischan, penelopeadrian, mathiaspeguet.
Fixes #63895.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/image.php

    r61291 r61841  
    814814 *
    815815 * The IPTC metadata that is retrieved is APP13, credit, byline, created date
    816  * and time, caption, copyright, and title. Also includes FNumber, Model,
     816 * and time, caption, copyright, alt, and title. Also includes FNumber, Model,
    817817 * DateTimeDigitized, FocalLength, ISOSpeedRatings, and ExposureTime.
    818818 *
     
    855855                'orientation'       => 0,
    856856                'keywords'          => array(),
     857                'alt'               => '',
    857858        );
    858859
     
    926927                }
    927928        }
     929
     930        $meta['alt'] = wp_get_image_alttext( $file );
    928931
    929932        $exif = array();
     
    10731076         */
    10741077        return apply_filters( 'wp_read_image_metadata', $meta, $file, $image_type, $iptc, $exif );
     1078}
     1079
     1080/**
     1081 * Gets the alt text from image meta data.
     1082 *
     1083 * @since 7.0.0
     1084 *
     1085 * @param string $file File path to the image.
     1086 * @return string Embedded alternative text.
     1087 */
     1088function wp_get_image_alttext( $file ) {
     1089        $alt_text     = '';
     1090        $img_contents = file_get_contents( $file );
     1091        // Find the start and end positions of the XMP metadata.
     1092        $xmp_start = strpos( $img_contents, '<x:xmpmeta' );
     1093        $xmp_end   = strpos( $img_contents, '</x:xmpmeta>' );
     1094
     1095        if ( ! $xmp_start || ! $xmp_end ) {
     1096                // No XMP metadata found.
     1097                return $alt_text;
     1098        }
     1099
     1100        // Extract the XMP metadata from the JPEG contents
     1101        $xmp_data = substr( $img_contents, $xmp_start, $xmp_end - $xmp_start + 12 );
     1102
     1103        // Parse the XMP metadata using DOMDocument.
     1104        $doc = new DOMDocument();
     1105        if ( false === $doc->loadXML( $xmp_data ) ) {
     1106                // Invalid XML in metadata.
     1107                return $alt_text;
     1108        }
     1109
     1110        // Instantiate an XPath object, used to extract portions of the XMP.
     1111        $xpath = new DOMXPath( $doc );
     1112
     1113        // Register the relevant XML namespaces.
     1114        $xpath->registerNamespace( 'x', 'adobe:ns:meta/' );
     1115        $xpath->registerNamespace( 'rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' );
     1116        $xpath->registerNamespace( 'Iptc4xmpCore', 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' );
     1117
     1118        $node_list = $xpath->query( '/x:xmpmeta/rdf:RDF/rdf:Description/Iptc4xmpCore:AltTextAccessibility' );
     1119        if ( $node_list && $node_list->count() ) {
     1120
     1121                $node = $node_list->item( 0 );
     1122
     1123                // Get the site's locale.
     1124                $locale = get_locale();
     1125
     1126                // Get the alt text accessibility alternative most appropriate for the site language.
     1127                // There are 3 possibilities:
     1128                //
     1129                // 1. there is an rdf:li with an exact match on the site locale.
     1130                // 2. there is an rdf:li with a partial match on the site locale (e.g., site locale is en_US and rdf:li has @xml:lang="en").
     1131                // 3. there is an rdf:li with an "x-default" lang.
     1132                //
     1133                // Evaluate in that order, stopping when we have a match.
     1134                $alt_text = $xpath->evaluate( "string( rdf:Alt/rdf:li[ @xml:lang = '{$locale}' ] )", $node );
     1135                if ( ! $alt_text ) {
     1136                        $alt_text = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "' . substr( $locale, 0, 2 ) . '" ] )', $node );
     1137                        if ( ! $alt_text ) {
     1138                                $alt_text = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "x-default" ] )', $node );
     1139                        }
     1140                }
     1141        }
     1142
     1143        return $alt_text;
    10751144}
    10761145
  • trunk/src/wp-admin/includes/media.php

    r61681 r61841  
    320320        $content = '';
    321321        $excerpt = '';
     322        $alt     = '';
    322323
    323324        if ( preg_match( '#^audio#', $type ) ) {
     
    400401                                $excerpt = $image_meta['caption'];
    401402                        }
     403
     404                        if ( trim( $image_meta['alt'] ) ) {
     405                                $alt = $image_meta['alt'];
     406                        }
    402407                }
    403408        }
     
    422427        $attachment_id = wp_insert_attachment( $attachment, $file, $post_id, true );
    423428
     429        if ( trim( $alt ) ) {
     430                update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) );
     431        }
     432
    424433        if ( ! is_wp_error( $attachment_id ) ) {
    425434                /*
     
    478487        $title   = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
    479488        $content = '';
     489        $alt     = '';
    480490
    481491        // Use image exif/iptc data for title and caption defaults if possible.
     
    489499                if ( trim( $image_meta['caption'] ) ) {
    490500                        $content = $image_meta['caption'];
     501                }
     502                if ( trim( $image_meta['alt'] ) ) {
     503                        $alt = $image_meta['alt'];
    491504                }
    492505        }
     
    513526        // Save the attachment metadata.
    514527        $attachment_id = wp_insert_attachment( $attachment, $file, $post_id, true );
     528
     529        if ( trim( $alt ) ) {
     530                update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) );
     531        }
    515532
    516533        if ( ! is_wp_error( $attachment_id ) ) {
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

    r61809 r61841  
    438438                $type = $file['type'];
    439439                $file = $file['file'];
     440                $alt  = '';
    440441
    441442                // Include image functions to get access to wp_read_image_metadata().
     
    452453                        if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
    453454                                $request['caption'] = $image_meta['caption'];
     455                        }
     456
     457                        if ( empty( $request['alt'] ) && trim( $image_meta['alt'] ) ) {
     458                                $alt = $image_meta['alt'];
    454459                        }
    455460                }
     
    477482                // $post_parent is inherited from $attachment['post_parent'].
    478483                $id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false );
     484
     485                if ( trim( $alt ) ) {
     486                        update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) );
     487                }
    479488
    480489                if ( is_wp_error( $id ) ) {
  • trunk/tests/phpunit/tests/image/meta.php

    r57987 r61841  
    128128                $this->assertSame( '0', $out['shutter_speed'], 'Shutter speed value not equivalent' );
    129129                $this->assertSame( '', $out['title'], 'Title value not the same' );
     130        }
     131
     132        /**
     133         * @ticket 63895
     134         */
     135        public function test_iptc_alt() {
     136                // Image tests alt text from the IPTC photo metadata standard 2025.1.
     137                $out = wp_read_image_metadata( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' );
     138
     139                $this->assertSame( 'This is the Alt Text description to support accessibility in 2025.1', $out['alt'], 'Alt text does not match source.' );
    130140        }
    131141
     
    201211                                        'orientation'       => '3',
    202212                                        'keywords'          => array(),
     213                                        'alt'               => '',
    203214                                ),
    204215                        ),
     
    218229                                        'orientation'       => '0',
    219230                                        'keywords'          => array(),
     231                                        'alt'               => '',
    220232                                ),
    221233                        ),
     
    235247                                        'orientation'       => '1',
    236248                                        'keywords'          => array( 'beach', 'baywatch', 'LA', 'sunset' ),
     249                                        'alt'               => '',
    237250                                ),
    238251                        ),
Note: See TracChangeset for help on using the changeset viewer.