Skip to content
71 changes: 70 additions & 1 deletion src/wp-admin/includes/image.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -854,6 +854,7 @@ function wp_read_image_metadata( $file ) {
'title' => '',
'orientation' => 0,
'keywords' => array(),
'alt' => '',
);

$iptc = array();
Expand Down Expand Up @@ -926,6 +927,8 @@ function wp_read_image_metadata( $file ) {
}
}

$meta['alt'] = wp_get_image_alttext( $file );

$exif = array();

/**
Expand Down Expand Up @@ -1074,6 +1077,72 @@ 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.
*
* @since x.x.x
*
* @param string $file File path to the image.
* @return string Embedded alternative text.
*/
function wp_get_image_alttext( $file ) {
$alt_text = '';
$img_contents = file_get_contents( $file );
// Find the start and end positions of the XMP metadata.
$xmp_start = strpos( $img_contents, '<x:xmpmeta' );
$xmp_end = strpos( $img_contents, '</x:xmpmeta>' );

if ( ! $xmp_start || ! $xmp_end ) {
// No XMP metadata found.
return $alt_text;
}

// 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 $alt_text;
}

// 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.
$alt_text = $xpath->evaluate( "string( rdf:Alt/rdf:li[ @xml:lang = '{$locale}' ] )", $node );
if ( ! $alt_text ) {
$alt_text = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "' . substr( $locale, 0, 2 ) . '" ] )', $node );
if ( ! $alt_text ) {
$alt_text = $xpath->evaluate( 'string( rdf:Alt/rdf:li[ @xml:lang = "x-default" ] )', $node );
}
}
}

return $alt_text;
}

/**
* Validates that file is an image.
*
Expand Down
18 changes: 18 additions & 0 deletions src/wp-admin/includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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'];
}
}
}

Expand All @@ -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', sanitize_text_field( $alt ) );
}

if ( ! is_wp_error( $attachment_id ) ) {
/*
* Set a custom header with the attachment_id.
Expand Down Expand Up @@ -477,6 +486,7 @@ function media_handle_sideload( $file_array, $post_id = 0, $desc = null, $post_d
$file = $file['file'];
$title = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
$content = '';
$alt = '';

// Use image exif/iptc data for title and caption defaults if possible.
$image_meta = wp_read_image_metadata( $file );
Expand All @@ -489,6 +499,10 @@ function media_handle_sideload( $file_array, $post_id = 0, $desc = null, $post_d
if ( trim( $image_meta['caption'] ) ) {
$content = $image_meta['caption'];
}

if ( trim( $image_meta['alt'] ) ) {
$alt = $image_meta['alt'];
}
}

if ( isset( $desc ) ) {
Expand All @@ -513,6 +527,10 @@ function media_handle_sideload( $file_array, $post_id = 0, $desc = null, $post_d
// Save the attachment metadata.
$attachment_id = wp_insert_attachment( $attachment, $file, $post_id, true );

if ( trim( $alt ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) );
}

if ( ! is_wp_error( $attachment_id ) ) {
wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ protected function insert_attachment( $request ) {
$url = $file['url'];
$type = $file['type'];
$file = $file['file'];
$alt = '';

// Include image functions to get access to wp_read_image_metadata().
require_once ABSPATH . 'wp-admin/includes/image.php';
Expand All @@ -452,6 +453,10 @@ protected function insert_attachment( $request ) {
if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
$request['caption'] = $image_meta['caption'];
}

if ( empty( $request['alt'] ) && trim( $image_meta['alt'] ) ) {
$alt = $image_meta['alt'];
}
}

$attachment = $this->prepare_item_for_database( $request );
Expand All @@ -477,6 +482,10 @@ protected function insert_attachment( $request ) {
// $post_parent is inherited from $attachment['post_parent'].
$id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false );

if ( trim( $alt ) ) {
update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) );
}

if ( is_wp_error( $id ) ) {
if ( 'db_update_error' === $id->get_error_code() ) {
$id->add_data( array( 'status' => 500 ) );
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions tests/phpunit/tests/image/meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ public function test_exif_no_data() {
$this->assertSame( '', $out['title'], 'Title value not the same' );
}

/**
* @ticket 63895
*/
public function test_iptc_alt() {
// Image tests alt text from the IPTC photo metadata standard 2025.1.
$out = wp_read_image_metadata( DIR_TESTDATA . '/images/IPTC-PhotometadataRef-Std2025.1.jpg' );

$this->assertSame( 'This is the Alt Text description to support accessibility in 2025.1', $out['alt'], 'Alt text does not match source.' );
}

/**
* @ticket 9417
*/
Expand Down Expand Up @@ -200,6 +210,7 @@ public function data_stream() {
'title' => '',
'orientation' => '3',
'keywords' => array(),
'alt' => '',
),
),
'Exif from a Nikon D70 with IPTC data added later' => array(
Expand All @@ -217,6 +228,7 @@ public function data_stream() {
'title' => 'IPTC Headline',
'orientation' => '0',
'keywords' => array(),
'alt' => '',
),
),
'Exif from a DMC-LX2 camera with keywords' => array(
Expand All @@ -234,6 +246,7 @@ public function data_stream() {
'title' => 'Photoshop Document Ttitle',
'orientation' => '1',
'keywords' => array( 'beach', 'baywatch', 'LA', 'sunset' ),
'alt' => '',
),
),
);
Expand Down
Loading