Media: Support Stream Wrappers In WP_Image_Editor_Imagick
Since `WP_Image_Editor`'s introduction, stream wrappers have functioned in `WP_Image_Editor_GD`, but haven't been properly supported in `WP_Image_Editor_Imagick`. - Detects stream wrappers and uses `file_put_contents()` along with `Imagick::read/getImageBlob()` for handling when necessary. - Introduces private method, `WP_Image_Editor_Imagick::write_image` to handle detection and proper saving. - Introduces `WP_Test_Stream` class for testing stream wrappers, along with new tests for Imagick's stream handling and a stream filename test. Adds requirement for `Imagick::readImageBlob()`, available in Imagick >= 2.0.0, which aligns with the current requirement of Imagick >= 2.2.0. Props p00ya, calin, joemcgill, pputzer, jimyaghi, mikeschroder. Fixes #42663. git-svn-id: https://develop.svn.wordpress.org/trunk@49230 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
parent
fc378e2fe5
commit
7021f5f4b5
@ -71,6 +71,7 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
|
||||
'flipimage',
|
||||
'flopimage',
|
||||
'readimage',
|
||||
'readimageblob',
|
||||
);
|
||||
|
||||
// Now, test for deep requirements within Imagick.
|
||||
@ -127,7 +128,7 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) ) {
|
||||
if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) {
|
||||
return new WP_Error( 'error_loading_image', __( 'File doesn’t exist?' ), $this->file );
|
||||
}
|
||||
|
||||
@ -147,9 +148,14 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
|
||||
if ( is_wp_error( $pdf_loaded ) ) {
|
||||
return $pdf_loaded;
|
||||
}
|
||||
} else {
|
||||
if ( wp_is_stream( $this->file ) ) {
|
||||
// Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead.
|
||||
$this->image->readImageBlob( file_get_contents( $this->file ), $this->file );
|
||||
} else {
|
||||
$this->image->readImage( $this->file );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $this->image->valid() ) {
|
||||
return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
|
||||
@ -682,8 +688,16 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
|
||||
$orig_format = $this->image->getImageFormat();
|
||||
|
||||
$this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
|
||||
$this->make_image( $filename, array( $image, 'writeImage' ), array( $filename ) );
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
|
||||
}
|
||||
|
||||
$write_image_result = $this->write_image( $this->image, $filename );
|
||||
if ( is_wp_error( $write_image_result ) ) {
|
||||
return $write_image_result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Reset original format.
|
||||
$this->image->setImageFormat( $orig_format );
|
||||
} catch ( Exception $e ) {
|
||||
@ -705,6 +719,37 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an image to a file or stream.
|
||||
*
|
||||
* @since 5.6
|
||||
*
|
||||
* @param Imagick $image
|
||||
* @param string $filename The destination filename or stream URL.
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
private function write_image( $image, $filename ) {
|
||||
if ( wp_is_stream( $filename ) ) {
|
||||
/*
|
||||
* Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead.
|
||||
* Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php
|
||||
*/
|
||||
if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) {
|
||||
/* translators: %s: PHP function name. */
|
||||
return new WP_Error( 'image_save_error', sprintf( __( '%s failed while writing image to stream.' ), '<code>file_put_contents()</code>' ), $filename );
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return $image->writeImage( $filename );
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams current image to browser.
|
||||
*
|
||||
|
@ -365,10 +365,14 @@ abstract class WP_Image_Editor {
|
||||
$new_ext = strtolower( $extension ? $extension : $ext );
|
||||
|
||||
if ( ! is_null( $dest_path ) ) {
|
||||
if ( ! wp_is_stream( $dest_path ) ) {
|
||||
$_dest_path = realpath( $dest_path );
|
||||
if ( $_dest_path ) {
|
||||
$dir = $_dest_path;
|
||||
}
|
||||
} else {
|
||||
$dir = $dest_path;
|
||||
}
|
||||
}
|
||||
|
||||
return trailingslashit( $dir ) . "{$name}-{$suffix}.{$new_ext}";
|
||||
|
248
tests/phpunit/includes/class-wp-test-stream.php
Normal file
248
tests/phpunit/includes/class-wp-test-stream.php
Normal file
@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class WP_Test_Stream.
|
||||
*
|
||||
* An in-memory streamWrapper implementation for testing streams. Writes to a
|
||||
* stream URL like "protocol://bucket/foo" will be stored in the static
|
||||
* variable WP_Test_Stream::$data['bucket']['/foo'].
|
||||
*
|
||||
* Creating a directory at "protocol://bucket/foo" will store the string
|
||||
* 'DIRECTORY' to the static variable WP_Test_Stream::$data['bucket']['/foo/']
|
||||
* (note the trailing slash).
|
||||
*
|
||||
* This class can be used to test that code works with basic read/write streams,
|
||||
* as such, operations such as seeking are not supported.
|
||||
*
|
||||
* This class does not register itself as a stream handler: test fixtures
|
||||
* should make the appropriate call to stream_wrapper_register().
|
||||
*/
|
||||
class WP_Test_Stream {
|
||||
const FILE_MODE = 0100666;
|
||||
const DIRECTORY_MODE = 040777;
|
||||
|
||||
/**
|
||||
* In-memory storage for files and directories simulated by this wrapper.
|
||||
*/
|
||||
static $data = array();
|
||||
|
||||
var $position;
|
||||
var $file;
|
||||
var $bucket;
|
||||
var $data_ref;
|
||||
|
||||
/**
|
||||
* Initializes internal state for reading the given URL.
|
||||
*
|
||||
* @param string $url A URL of the form "protocol://bucket/path".
|
||||
*/
|
||||
private function open( $url ) {
|
||||
$components = array_merge(
|
||||
array(
|
||||
'host' => '',
|
||||
'path' => '',
|
||||
),
|
||||
parse_url( $url )
|
||||
);
|
||||
|
||||
$this->bucket = $components['host'];
|
||||
$this->file = $components['path'] ? $components['path'] : '/';
|
||||
|
||||
if ( empty( $this->bucket ) ) {
|
||||
trigger_error( 'Cannot use an empty bucket name', E_USER_ERROR );
|
||||
}
|
||||
|
||||
if ( ! isset( WP_Test_Stream::$data[ $this->bucket ] ) ) {
|
||||
WP_Test_Stream::$data[ $this->bucket ] = array();
|
||||
}
|
||||
|
||||
$this->data_ref =& WP_Test_Stream::$data[ $this->bucket ][ $this->file ];
|
||||
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a URL.
|
||||
*
|
||||
* @see streamWrapper::stream_open
|
||||
*/
|
||||
function stream_open( $path, $mode, $options, &$opened_path ) {
|
||||
$this->open( $path );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from a stream.
|
||||
*
|
||||
* @see streamWrapper::stream_read
|
||||
*/
|
||||
function stream_read( $count ) {
|
||||
if ( ! isset( $this->data_ref ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$ret = substr( $this->data_ref, $this->position, $count );
|
||||
|
||||
$this->position += strlen( $ret );
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes to a stream.
|
||||
*
|
||||
* @see streamWrapper::stream_write
|
||||
*/
|
||||
function stream_write( $data ) {
|
||||
if ( ! isset( $this->data_ref ) ) {
|
||||
$this->data_ref = '';
|
||||
}
|
||||
|
||||
$left = substr( $this->data_ref, 0, $this->position );
|
||||
$right = substr( $this->data_ref, $this->position + strlen( $data ) );
|
||||
|
||||
WP_Test_Stream::$data[ $this->bucket ][ $this->file ] = $left . $data . $right;
|
||||
|
||||
$this->position += strlen( $data );
|
||||
return strlen( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current position of a stream.
|
||||
*
|
||||
* @see streamWrapper::stream_tell
|
||||
*/
|
||||
function stream_tell() {
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for end-of-file.
|
||||
*
|
||||
* @see streamWrapper::stream_eof
|
||||
*/
|
||||
function stream_eof() {
|
||||
if ( ! isset( $this->data_ref ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->position >= strlen( $this->data_ref );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change stream metadata.
|
||||
*
|
||||
* @see streamWrapper::stream_metadata
|
||||
*/
|
||||
function stream_metadata( $path, $option, $var ) {
|
||||
$this->open( $path );
|
||||
if ( STREAM_META_TOUCH === $option ) {
|
||||
if ( ! isset( $this->data_ref ) ) {
|
||||
$this->data_ref = '';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory.
|
||||
*
|
||||
* @see streamWrapper::mkdir
|
||||
*/
|
||||
function mkdir( $path, $mode, $options ) {
|
||||
$this->open( $path );
|
||||
$plainfile = rtrim( $this->file, '/' );
|
||||
|
||||
if ( isset( WP_Test_Stream::$data[ $this->bucket ][ $file ] ) ) {
|
||||
return false;
|
||||
}
|
||||
$dir_ref = & $this->get_directory_ref();
|
||||
$dir_ref = 'DIRECTORY';
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file metadata object, with defaults.
|
||||
*
|
||||
* @param array $stats Partial file metadata.
|
||||
* @return array Complete file metadata.
|
||||
*/
|
||||
private function make_stat( $stats ) {
|
||||
$defaults = array(
|
||||
'dev' => 0,
|
||||
'ino' => 0,
|
||||
'mode' => 0,
|
||||
'nlink' => 0,
|
||||
'uid' => 0,
|
||||
'gid' => 0,
|
||||
'rdev' => 0,
|
||||
'size' => 0,
|
||||
'atime' => 0,
|
||||
'mtime' => 0,
|
||||
'ctime' => 0,
|
||||
'blksize' => 0,
|
||||
'blocks' => 0,
|
||||
);
|
||||
|
||||
return array_merge( $defaults, $stats );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about a file.
|
||||
*
|
||||
* @see streamWrapper::stream_stat
|
||||
*/
|
||||
public function stream_stat() {
|
||||
$dir_ref = & $this->get_directory_ref();
|
||||
if ( substr( $this->file, -1 ) === '/' || isset( $dir_ref ) ) {
|
||||
return $this->make_stat(
|
||||
array(
|
||||
'mode' => WP_Test_Stream::DIRECTORY_MODE,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! isset( $this->data_ref ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->make_stat(
|
||||
array(
|
||||
'size' => strlen( $this->data_ref ),
|
||||
'mode' => WP_Test_Stream::FILE_MODE,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about a file.
|
||||
*
|
||||
* @see streamWrapper::url_stat
|
||||
*/
|
||||
public function url_stat( $path, $flags ) {
|
||||
$this->open( $path );
|
||||
return $this->stream_stat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file.
|
||||
*
|
||||
* @see streamWrapper::unlink
|
||||
*/
|
||||
public function unlink( $path ) {
|
||||
if ( ! isset( $this->data_ref ) ) {
|
||||
return false;
|
||||
}
|
||||
unset( WP_Test_Stream::$data[ $this->bucket ][ $this->file ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interprets this stream's path as a directory, and returns the entry.
|
||||
*
|
||||
* @return A reference to the data entry for the directory.
|
||||
*/
|
||||
private function &get_directory_ref() {
|
||||
return WP_Test_Stream::$data[ $this->bucket ][ rtrim( $this->file, '/' ) . '/' ];
|
||||
}
|
||||
}
|
@ -143,6 +143,9 @@ class Tests_Image_Editor extends WP_Image_UnitTestCase {
|
||||
|
||||
// Combo!
|
||||
$this->assertSame( trailingslashit( realpath( get_temp_dir() ) ) . 'canola-new.png', $editor->generate_filename( 'new', realpath( get_temp_dir() ), 'png' ) );
|
||||
|
||||
// Test with a stream destination.
|
||||
$this->assertSame( 'file://testing/path/canola-100x50.jpg', $editor->generate_filename( null, 'file://testing/path' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,6 +16,7 @@ class Tests_Image_Editor_Imagick extends WP_Image_UnitTestCase {
|
||||
public function setUp() {
|
||||
require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
|
||||
require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php';
|
||||
require_once DIR_TESTROOT . '/includes/class-wp-test-stream.php';
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
@ -574,4 +575,33 @@ class Tests_Image_Editor_Imagick extends WP_Image_UnitTestCase {
|
||||
unlink( $ret['path'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that images can be loaded and written over streams
|
||||
*/
|
||||
public function test_streams() {
|
||||
stream_wrapper_register( 'wptest', 'WP_Test_Stream' );
|
||||
WP_Test_Stream::$data = array(
|
||||
'Tests_Image_Editor_Imagick' => array(
|
||||
'/read.jpg' => file_get_contents( DIR_TESTDATA . '/images/waffles.jpg' ),
|
||||
),
|
||||
);
|
||||
|
||||
$file = 'wptest://Tests_Image_Editor_Imagick/read.jpg';
|
||||
$imagick_image_editor = new WP_Image_Editor_Imagick( $file );
|
||||
|
||||
$ret = $imagick_image_editor->load();
|
||||
$this->assertNotWPError( $ret );
|
||||
|
||||
$temp_file = 'wptest://Tests_Image_Editor_Imagick/write.jpg';
|
||||
|
||||
$ret = $imagick_image_editor->save( $temp_file );
|
||||
$this->assertNotWPError( $ret );
|
||||
|
||||
$this->assertSame( $temp_file, $ret['path'] );
|
||||
|
||||
if ( $temp_file !== $ret['path'] ) {
|
||||
unlink( $ret['path'] );
|
||||
}
|
||||
unlink( $temp_file );
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user