avatar-privacy/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-png-parts-generator.php

384 lines
11 KiB
PHP

<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2007-2014 Scott Sherrill-Mix.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators\Parts_Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Number_Generator;
use GdImage; // phpcs:ignore ImportDetection.Imports -- PHP 8.0 compatibility.
/**
* A base class for parts-based PNG icon generators.
*
* The class includes some functions created for Scott Sherrill-Mix' WordPress
* plugin of MonsterID.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
* @author Scott Sherrill-Mix
*
* @phpstan-import-type PartsTemplate from Parts_Generator
* @phpstan-import-type AllPossibleParts from Parts_Generator
* @phpstan-import-type RandomizedParts from Parts_Generator
* @phpstan-import-type AdditionalArguments from Parts_Generator
*
* @phpstan-type BoundsTuple array{ 0: int, 1: int }
*/
abstract class PNG_Parts_Generator extends Parts_Generator {
// Units used in HSL colors.
/**
* Use Image\Color::MAX_PERCENT instead.
*
* @deprecated 2.7.0
*/
const PERCENT = Images\Color::MAX_PERCENT;
/**
* Use Image\Color::MAX_DEGREE instead.
*
* @deprecated 2.7.0
*/
const DEGREE = Images\Color::MAX_DEGREE;
/**
* The base size of the generated avatar.
*
* @var int
*/
protected int $size;
/**
* The image editor support class.
*
* @var Images\Editor
*/
private Images\Editor $editor;
/**
* The PNG image helper.
*
* @var Images\PNG
*/
protected Images\PNG $png;
/**
* Creates a new generator.
*
* @param string $parts_dir The directory containing our image parts.
* @param string[] $part_types The valid part types for this generator.
* @param int $size The width and height of the generated image (in pixels).
* @param Images\Editor $editor The image editing handler.
* @param Images\PNG $png The PNG image helper.
* @param Number_Generator $number_generator A pseudo-random number generator.
* @param Site_Transients $site_transients The site transients handler.
*/
public function __construct(
$parts_dir,
array $part_types,
$size,
Images\Editor $editor,
Images\PNG $png,
Number_Generator $number_generator,
Site_Transients $site_transients
) {
$this->size = $size;
$this->editor = $editor;
$this->png = $png;
parent::__construct( $parts_dir, $part_types, $number_generator, $site_transients );
}
/**
* Renders the avatar from its parts in the given size, using any of the
* optional additional arguments.
*
* @param int $size The target image size in pixels.
* @param array $parts The (randomized) avatar parts.
* @param array $args Any additional arguments defined by the subclass.
*
* @return string The image data (bytes).
*
* @phpstan-param RandomizedParts $parts
* @phpstan-param AdditionalArguments $args
*/
protected function get_avatar( $size, array $parts, array $args ) {
// Build the avatar image in its native size.
$avatar = $this->render_avatar( $parts, $args );
// Resize if necessary.
return $this->get_resized_image_data( $avatar, $size );
}
/**
* Renders the avatar from its parts, using any of the given additional arguments.
*
* @since 2.7.0 Return type amended to include `GdImage` on PHP 8.x
*
* @param array $parts The (randomized) avatar parts.
* @param array $args Any additional arguments defined by the subclass.
*
* @return resource|GdImage
*
* @phpstan-param RandomizedParts $parts
* @phpstan-param AdditionalArguments $args
*/
abstract protected function render_avatar( array $parts, array $args );
/**
* Resizes the image and returns the raw data.
*
* @since 2.5.0 Parameter $image can now also be a GdImage.
*
* @param resource|GdImage $image The image resource.
* @param int $size The size in pixels.
*
* @return string The image data (or the empty string on error).
*/
protected function get_resized_image_data( $image, $size ) {
return $this->editor->get_resized_image_data(
$this->editor->create_from_image_resource( $image ), $size, $size, 'image/png'
);
}
/**
* Retrieves an array of SVG part type definitions.
*
* @param array $parts An array of empty arrays indexed by part type.
*
* @return array The same array, but now containing the part type definitions.
*
* @phpstan-param PartsTemplate $parts
* @phpstan-return AllPossibleParts
*/
protected function read_parts_from_filesystem( array $parts ) {
// Iterate over the files in the parts directory.
$dir = new \FilesystemIterator(
$this->parts_dir,
\FilesystemIterator::KEY_AS_FILENAME |
\FilesystemIterator::CURRENT_AS_PATHNAME |
\FilesystemIterator::SKIP_DOTS
);
foreach ( $dir as $file => $path ) {
list( $partname, ) = \explode( '_', $file );
if ( isset( $parts[ $partname ] ) ) {
$parts[ $partname ][] = $file;
}
}
return $parts;
}
/**
* Sorts the parts array to be independent of filesystem sort order.
*
* @param array $parts {
* An array of part type definitions.
*
* @type string $type The part definition list, indexed by type.
* }
*
* @return array
*
* @phpstan-param AllPossibleParts $parts
* @phpstan-return AllPossibleParts
*/
protected function sort_parts( array $parts ) {
foreach ( $parts as $key => $value ) {
\sort( $parts[ $key ], \SORT_NATURAL );
}
return $parts;
}
/**
* Creates a GD image of the chosen type with the set avatar size for width
* and height.
*
* @since 2.3.0
* @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
*
* @param string $type The type of background to create. Valid: 'white', 'black', 'transparent'.
*
* @return resource|GdImage
*
* @throws \RuntimeException The image could not be copied.
*/
protected function create_image( $type ) {
return $this->png->create( $type, $this->size, $this->size );
}
/**
* Copies an image onto an existing base image. Image parts are loaded from
* the parts directory if a filename is given, assuming the avatar size for
* width and height.
*
* The GD image (resource) is freed after copying.
*
* @since 2.5.0 Parameters $base and $image can now also be GdImage instances.
*
* @param resource|GdImage $base The avatar image resource.
* @param string|resource|GdImage $image The image to be copied onto the base. Can
* be either the name of the image file
* relative to the parts directory, or an
* existing image resource.
*
* @return void
*
* @throws \RuntimeException The image could not be copied.
*/
protected function combine_images( $base, $image ) {
// Load image if we are given a filename.
if ( \is_string( $image ) ) {
$image = $this->png->create_from_file( "{$this->parts_dir}/{$image}" );
}
$this->png->combine( $base, $image, $this->size, $this->size );
}
/**
* Determines exact dimensions for individual parts. Mainly useful for subclasses
* exchanging the provided images.
*
* @since 2.1.0 Visibility changed to protected.
* @since 2.3.0 Moved to PNG_Parts_Generator class and paramter $text removed.
* Use new method `get_parts_dimensions_as_text` to retrieve the
* human-readable array definition.
*
* @author Peter Putzer
* @author Scott Sherrill-Mix
*
* @return array {
* An array of boundary coordinates indexed by filename.
*
* @type array $file {
* The boundary coordinates for a single file.
*
* @type int[] $xbounds The low and high boundary on the X axis.
* @type int[] $ybounds The low and high boundary on the Y axis.
* }
* }
*
* @phpstan-return array<string, array{ 0: BoundsTuple, 1: BoundsTuple }>
*/
protected function get_parts_dimensions() {
/**
* An array of boundary coordinates indexed by filename.
*
* @phpstan-var array<string, array{ 0: BoundsTuple, 1: BoundsTuple }> $bounds
*/
$bounds = [];
foreach ( $this->get_parts() as $file_list ) {
foreach ( $file_list as $file ) {
$im = @\imageCreateFromPNG( "{$this->parts_dir}/{$file}" );
if ( false === $im ) {
// Not a valid image file.
continue;
}
$bounds[ $file ] = $this->get_image_bounds( $im );
}
}
return $bounds;
}
/**
* Determines exact dimensions for an image (i.e. not including very light or
* transparent pixels).
*
* @since 2.4.0 Extracted from ::get_parts_dimensions.
* @since 2.5.0 Parameter $im can now also be a GdImage.
*
* @author Peter Putzer
* @author Scott Sherrill-Mix
*
* @param resource|GdImage $im The image resource.
*
* @return array {
* The boundary coordinates for the image.
*
* @type int[] $xbounds The low and high boundary on the X axis.
* @type int[] $ybounds The low and high boundary on the Y axis.
* }
*
* @phpstan-return array{ 0: BoundsTuple, 1: BoundsTuple }
*/
protected function get_image_bounds( $im ) {
$imgw = \imageSX( $im );
$imgh = \imageSY( $im );
$xbounds = [ 999999, 0 ];
$ybounds = [ 999999, 0 ];
for ( $i = 0;$i < $imgw;$i++ ) {
for ( $j = 0;$j < $imgh;$j++ ) {
$rgb = \imageColorAt( $im, $i, $j );
$r = ( $rgb >> 16 ) & 0xFF;
$g = ( $rgb >> 8 ) & 0xFF;
$b = $rgb & 0xFF;
$alpha = ( $rgb & 0x7F000000 ) >> 24;
$lightness = ( $r + $g + $b ) / 3 / Images\Color::MAX_RGB * Images\Color::MAX_PERCENT;
if ( $lightness > 10 && $lightness < 99 && $alpha < 115 ) {
$xbounds[0] = \min( $xbounds[0],$i );
$xbounds[1] = \max( $xbounds[1],$i );
$ybounds[0] = \min( $ybounds[0],$j );
$ybounds[1] = \max( $ybounds[1],$j );
}
}
}
return [ $xbounds, $ybounds ];
}
/**
* Prints the exact dimensions for individual parts as human-readable PHP
* array definitions.
*
* Mainly useful for subclasses exchanging the provided images.
*
* @since 2.3.0
*
* @return string
*/
protected function get_parts_dimensions_as_text() {
$result = '';
foreach ( $this->get_parts_dimensions() as $part => $bounds ) {
list( $xbounds, $ybounds ) = $bounds;
$result .= "'$part' => [ [ {$xbounds[0]}, {$xbounds[1]} ], [ {$ybounds[0]}, {$ybounds[1]} ] ],\n";
}
return $result;
}
}