Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/wp-includes/class-wp-image-editor-gd.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function __destruct() {
* Checks to see if current environment supports GD.
*
* @since 3.5.0
* @since 7.2.0 Added support for testing the 'mask' method capability via the 'methods' argument.
*
* @param array $args
* @return bool
Expand All @@ -51,6 +52,17 @@ public static function test( $args = array() ) {
return false;
}

if ( isset( $args['methods'] ) && in_array( 'mask', $args['methods'], true ) ) {
return (
self::supports_mime_type( 'image/png' ) &&
function_exists( 'imagealphablending' ) &&
function_exists( 'imagecolorallocatealpha' ) &&
function_exists( 'imagefilledrectangle' ) &&
function_exists( 'imagepng' ) &&
function_exists( 'imagesavealpha' )
);
}

return true;
}

Expand Down Expand Up @@ -470,6 +482,81 @@ public function flip( $horz, $vert ) {
return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file );
}

/**
* Applies a mask to the current image.
*
* The mask introduces transparency, so the caller must save the result in an
* alpha-capable format (PNG, WebP, or AVIF). This method mutates the pixels
* only and leaves the output format to the caller, like the other editor
* transforms.
*
* @since 7.2.0
*
* @param array $args {
* Mask arguments.
*
* @type string $shape Mask shape. Accepts 'circle'.
* }
* @return true|WP_Error True on success, WP_Error object on failure.
*/
public function mask( $args ) {
if ( ! is_array( $args ) || 'circle' !== ( $args['shape'] ?? null ) ) {
return new WP_Error(
'image_mask_unsupported',
__( 'Unsupported image mask.' ),
$this->file
);
}

if ( function_exists( 'imagepalettetotruecolor' ) ) {
imagepalettetotruecolor( $this->image );
}

imagealphablending( $this->image, false );
imagesavealpha( $this->image, true );

$width = imagesx( $this->image );
$height = imagesy( $this->image );
$transparent = imagecolorallocatealpha( $this->image, 0, 0, 0, 127 );
$center_x = ( $width - 1 ) / 2;
$center_y = ( $height - 1 ) / 2;
$radius = min( $width, $height ) / 2;
$radius_squared = $radius * $radius;

Comment thread
ramonjd marked this conversation as resolved.
/*
* Clear the pixels outside the inscribed circle one scanline at a time.
* For each row the circle spans the horizontal range
* [center_x - span, center_x + span]; everything outside that span is
* filled transparent with at most two rectangle fills. This keeps the
* cost at O(height) rectangle fills rather than O(width * height)
* per-pixel writes. `ceil()`/`floor()` preserve the same boundary as the
* `dx^2 + dy^2 <= radius^2` test, so a pixel exactly on the radius stays
* opaque and no seam appears at the rim.
*/
for ( $y = 0; $y < $height; $y++ ) {
$dy = $y - $center_y;

if ( ( $dy * $dy ) > $radius_squared ) {
imagefilledrectangle( $this->image, 0, $y, $width - 1, $y, $transparent );
continue;
}

$span = sqrt( $radius_squared - ( $dy * $dy ) );
$left = max( 0, (int) ceil( $center_x - $span ) );
$right = min( $width - 1, (int) floor( $center_x + $span ) );

if ( $left > 0 ) {
imagefilledrectangle( $this->image, 0, $y, $left - 1, $y, $transparent );
}

if ( $right < $width - 1 ) {
imagefilledrectangle( $this->image, $right + 1, $y, $width - 1, $y, $transparent );
}
}

return true;
}

/**
* Saves current in-memory image to file.
*
Expand Down
79 changes: 79 additions & 0 deletions src/wp-includes/class-wp-image-editor-imagick.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public function __destruct() {
* method can be called statically.
*
* @since 3.5.0
* @since 7.2.0 Added support for testing the 'mask' method capability via the 'methods' argument.
*
* @param array $args
* @return bool
Expand Down Expand Up @@ -84,6 +85,21 @@ public static function test( $args = array() ) {
return false;
}

if ( isset( $args['methods'] ) && in_array( 'mask', $args['methods'], true ) ) {
return (
class_exists( 'ImagickDraw', false ) &&
( defined( 'Imagick::ALPHACHANNEL_SET' ) || defined( 'Imagick::ALPHACHANNEL_ACTIVATE' ) ) &&
defined( 'Imagick::COMPOSITE_DSTIN' ) &&
method_exists( 'ImagickDraw', 'ellipse' ) &&
method_exists( 'ImagickDraw', 'setFillColor' ) &&
method_exists( 'Imagick', 'compositeImage' ) &&
method_exists( 'Imagick', 'drawImage' ) &&
method_exists( 'Imagick', 'getImageGeometry' ) &&
method_exists( 'Imagick', 'newImage' ) &&
method_exists( 'Imagick', 'setImageAlphaChannel' )
);
}

return true;
}

Expand Down Expand Up @@ -826,6 +842,69 @@ public function flip( $horz, $vert ) {
return true;
}

/**
* Applies a mask to the current image.
*
* The mask introduces transparency, so the caller must save the result in an
* alpha-capable format (PNG, WebP, or AVIF). This method mutates the pixels
* only and leaves the output format to the caller, like the other editor
* transforms.
*
* @since 7.2.0
*
* @param array $args {
* Mask arguments.
*
* @type string $shape Mask shape. Accepts 'circle'.
* }
* @return true|WP_Error True on success, WP_Error object on failure.
*/
public function mask( $args ) {
if ( ! is_array( $args ) || 'circle' !== ( $args['shape'] ?? null ) ) {
return new WP_Error(
'image_mask_unsupported',
__( 'Unsupported image mask.' ),
$this->file
);
}

try {
if ( defined( 'Imagick::ALPHACHANNEL_SET' ) ) {
$this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_SET );
} elseif ( defined( 'Imagick::ALPHACHANNEL_ACTIVATE' ) ) {
$this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_ACTIVATE );
}

$geometry = $this->image->getImageGeometry();
$width = (int) $geometry['width'];
$height = (int) $geometry['height'];

$mask = new Imagick();
$mask->newImage( $width, $height, new ImagickPixel( 'transparent' ), 'png' );

$draw = new ImagickDraw();
$draw->setFillColor( new ImagickPixel( 'white' ) );
$draw->ellipse(
( $width - 1 ) / 2,
( $height - 1 ) / 2,
min( $width, $height ) / 2,
min( $width, $height ) / 2,
0,
360
);
$mask->drawImage( $draw );

$this->image->compositeImage( $mask, Imagick::COMPOSITE_DSTIN, 0, 0 );

$mask->clear();
$mask->destroy();
} catch ( Exception $e ) {
return new WP_Error( 'image_mask_error', $e->getMessage(), $this->file );
}

return true;
}

/**
* Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
*
Expand Down
6 changes: 3 additions & 3 deletions src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -4344,6 +4344,7 @@ function wp_max_upload_size() {
* Returns a WP_Image_Editor instance and loads file into it.
*
* @since 3.5.0
* @since 7.2.0 A specified 'output_mime_type' argument is no longer overridden by the image_editor_output_format mapping.
*
* @param string $path Path to the file to load.
* @param array $args Optional. Additional arguments for retrieving the image editor.
Expand All @@ -4367,8 +4368,8 @@ function wp_get_image_editor( $path, $args = array() ) {
}
}

// Check and set the output mime type mapped to the input type.
if ( isset( $args['mime_type'] ) ) {
// Check and set the output mime type mapped to the input type unless a specific output mime type was requested.
if ( isset( $args['mime_type'] ) && ! isset( $args['output_mime_type'] ) ) {
$output_format = wp_get_image_editor_output_format( $path, $args['mime_type'] );
if ( isset( $output_format[ $args['mime_type'] ] ) ) {
$args['output_mime_type'] = $output_format[ $args['mime_type'] ];
Expand Down Expand Up @@ -6798,4 +6799,3 @@ function wp_add_crossorigin_attributes( string $html ): string {

return $processor->get_updated_html();
}

Loading
Loading