close

Make WordPress Core

Changeset 61841


Ignore:
Timestamp:
03/05/2026 10:17:34 AM (8 days 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.