From d34ca5311cac1e269d09674524916719bac8f339 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 3 Mar 2026 16:22:57 -0600 Subject: [PATCH 1/5] Get embedded alt text from images Based on https://gist.github.com/adamsilverstein/e9c9993d938eae80e559d9eda8c0fdcc, fixes https://core.trac.wordpress.org/ticket/63895 and https://core.trac.wordpress.org/ticket/55535 --- src/wp-admin/includes/image.php | 70 ++++++++++++++++++++++++++++++++- src/wp-admin/includes/media.php | 9 +++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 2553f68434659..8ef9fa3a2918d 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -813,7 +813,7 @@ function wp_exif_date2ts( $str ) { * created_timestamp, focal_length, shutter_speed, and title. * * The IPTC metadata that is retrieved is APP13, credit, byline, created date - * and time, caption, copyright, and title. Also includes FNumber, Model, + * and time, caption, copyright, alt, and title. Also includes FNumber, Model, * DateTimeDigitized, FocalLength, ISOSpeedRatings, and ExposureTime. * * @todo Try other exif libraries if available. @@ -854,6 +854,7 @@ function wp_read_image_metadata( $file ) { 'title' => '', 'orientation' => 0, 'keywords' => array(), + 'alt' => '', ); $iptc = array(); @@ -926,6 +927,8 @@ function wp_read_image_metadata( $file ) { } } + $meta['alt'] = wp_get_image_alttext( $file ); + $exif = array(); /** @@ -1074,6 +1077,71 @@ function wp_read_image_metadata( $file ) { return apply_filters( 'wp_read_image_metadata', $meta, $file, $image_type, $iptc, $exif ); } +/** + * Get the alt text from image meta data. + * + * @param string $file File path to the image. + * + * @return string Embedded alternative text. + */ +function wp_get_image_alttext( $file ) { + + $img_contents = file_get_contents( $file ); + // Find the start and end positions of the XMP metadata. + $xmp_start = strpos( $img_contents, ''); + + if ( ! $xmp_start || ! $xmp_end ) { + // No XMP metadata found. + return ''; + } + + // Extract the XMP metadata from the JPEG contents + $xmp_data = substr( $img_contents, $xmp_start, $xmp_end - $xmp_start + 12 ); + + // Parse the XMP metadata using DOMDocument. + $doc = new DOMDocument(); + if ( false === $doc->loadXML( $xmp_data ) ) { + // Invalid XML in metadata. + return ''; + } + + // Instantiate an XPath object, used to extract portions of the XMP. + $xpath = new DOMXPath( $doc ); + + // Register the relevant XML namespaces. + $xpath->registerNamespace( 'x', 'adobe:ns:meta/' ); + $xpath->registerNamespace( 'rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' ); + $xpath->registerNamespace( 'Iptc4xmpCore', 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' ); + + $node_list = $xpath->query( '/x:xmpmeta/rdf:RDF/rdf:Description/Iptc4xmpCore:AltTextAccessibility' ); + if ( $node_list && $node_list->count() ) { + + $node = $node_list->item( 0 ); + + // Get the site's locale. + $locale = get_locale(); + + // Get the alt text accessibility alternative most appropriate for the site language. + // There are 3 possibilities: + // + // 1. there is an rdf:li with an exact match on the site locale. + // 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"). + // 3. there is an rdf:li with an "x-default" lang. + // + // Evaluate in that order, stopping when we have a match. + $value = $xpath->evaluate( "string( rdf:Alt/rdf:li[ @xml:lang = '{$locale}' ] )", $node ); + if ( ! $value ) { + $value = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "' . substr( $locale, 0, 2 ) . '" ] )', $node ); + if ( ! $value ) { + $value = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "x-default" ] )', $node ); + } + } + } + + return $value; +} + /** * Validates that file is an image. * diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 1d45224f5b7e4..cc728b0efd9bc 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -319,6 +319,7 @@ function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrid $title = sanitize_text_field( $name ); $content = ''; $excerpt = ''; + $alt = ''; if ( preg_match( '#^audio#', $type ) ) { $meta = wp_read_audio_metadata( $file ); @@ -399,6 +400,10 @@ function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrid if ( trim( $image_meta['caption'] ) ) { $excerpt = $image_meta['caption']; } + + if ( trim( $image_meta['alt'] ) ) { + $alt = $image_meta['alt']; + } } } @@ -421,6 +426,10 @@ function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrid // Save the data. $attachment_id = wp_insert_attachment( $attachment, $file, $post_id, true ); + if ( trim( $alt ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt ); + } + if ( ! is_wp_error( $attachment_id ) ) { /* * Set a custom header with the attachment_id. From f4981faa546d37d689ae2493f1e8b597c2efa658 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 3 Mar 2026 16:25:56 -0600 Subject: [PATCH 2/5] Update image.php --- src/wp-admin/includes/image.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 8ef9fa3a2918d..b8b8ec010a859 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -1089,7 +1089,7 @@ function wp_get_image_alttext( $file ) { $img_contents = file_get_contents( $file ); // Find the start and end positions of the XMP metadata. $xmp_start = strpos( $img_contents, ''); + $xmp_end = strpos( $img_contents, '' ); if ( ! $xmp_start || ! $xmp_end ) { // No XMP metadata found. @@ -1132,10 +1132,10 @@ function wp_get_image_alttext( $file ) { // Evaluate in that order, stopping when we have a match. $value = $xpath->evaluate( "string( rdf:Alt/rdf:li[ @xml:lang = '{$locale}' ] )", $node ); if ( ! $value ) { - $value = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "' . substr( $locale, 0, 2 ) . '" ] )', $node ); - if ( ! $value ) { - $value = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "x-default" ] )', $node ); - } + $value = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "' . substr( $locale, 0, 2 ) . '" ] )', $node ); + if ( ! $value ) { + $value = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "x-default" ] )', $node ); + } } } From a271bc6110b0bbd78842d198732ddc947f2d3a3a Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 3 Mar 2026 19:03:38 -0600 Subject: [PATCH 3/5] Add a placeholder since tag. --- src/wp-admin/includes/image.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index b8b8ec010a859..177663d8ce5ba 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -1080,8 +1080,9 @@ function wp_read_image_metadata( $file ) { /** * Get the alt text from image meta data. * - * @param string $file File path to the image. + * @since x.x.x * + * @param string $file File path to the image. * @return string Embedded alternative text. */ function wp_get_image_alttext( $file ) { From 76c8a69efba45ff7b904ba49db71477da1ee40e9 Mon Sep 17 00:00:00 2001 From: "Stephen A. Bernhardt" Date: Wed, 4 Mar 2026 06:52:21 -0600 Subject: [PATCH 4/5] Define `$value` variable --- src/wp-admin/includes/image.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 177663d8ce5ba..f321e597093e0 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -1115,6 +1115,7 @@ function wp_get_image_alttext( $file ) { $xpath->registerNamespace( 'rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' ); $xpath->registerNamespace( 'Iptc4xmpCore', 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' ); + $value = ''; $node_list = $xpath->query( '/x:xmpmeta/rdf:RDF/rdf:Description/Iptc4xmpCore:AltTextAccessibility' ); if ( $node_list && $node_list->count() ) { From ea11b44e1218adc6c7eb30dfa04ebac94039552e Mon Sep 17 00:00:00 2001 From: "Stephen A. Bernhardt" Date: Wed, 4 Mar 2026 06:58:13 -0600 Subject: [PATCH 5/5] Add `alt` to `Tests_Image_Meta::data_stream()` metadata --- tests/phpunit/tests/image/meta.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/phpunit/tests/image/meta.php b/tests/phpunit/tests/image/meta.php index 88b2cbcef1e40..9bc7ad398db2c 100644 --- a/tests/phpunit/tests/image/meta.php +++ b/tests/phpunit/tests/image/meta.php @@ -200,6 +200,7 @@ public function data_stream() { 'title' => '', 'orientation' => '3', 'keywords' => array(), + 'alt' => '', ), ), 'Exif from a Nikon D70 with IPTC data added later' => array( @@ -217,6 +218,7 @@ public function data_stream() { 'title' => 'IPTC Headline', 'orientation' => '0', 'keywords' => array(), + 'alt' => '', ), ), 'Exif from a DMC-LX2 camera with keywords' => array( @@ -234,6 +236,7 @@ public function data_stream() { 'title' => 'Photoshop Document Ttitle', 'orientation' => '1', 'keywords' => array( 'beach', 'baywatch', 'LA', 'sunset' ), + 'alt' => '', ), ), );