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

322 lines
9.1 KiB
PHP

<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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\Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Exceptions\Part_Files_Not_Found_Exception;
use Avatar_Privacy\Tools\Number_Generator;
/**
* A base class for parts-based icon generators independent of the used image
* format. Part definitions are chached using WordPress' transient mechanism.
*
* The algorithm for building icons is conceptually based on Andreas Gohr's
* MonsterId library.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type PartType string
* @phpstan-type PartsTemplate array<PartType, array{}>
* @phpstan-type AllPossibleParts array<PartType, string[]>
* @phpstan-type RandomizedParts array<PartType, string>
* @phpstan-type AdditionalArguments array<string, mixed>
*/
abstract class Parts_Generator implements Generator {
/**
* The path to the monster parts image files.
*
* @var string
*/
protected string $parts_dir;
/**
* An array of part types.
*
* @var string[]
*/
protected array $part_types;
/**
* Lists of files, indexed by part types.
*
* @since 2.4.0 Property renamed to $all_parts, visibility changed to private.
*
* @var array {
* @type string[] $type An array of files.
* }
*
* @phpstan-var array<string, string[]>
*/
private array $all_parts;
/**
* The random number generator.
*
* @var Number_Generator
*/
protected Number_Generator $number_generator;
/**
* The site transients handler.
*
* @var Site_Transients
*/
private Site_Transients $site_transients;
/**
* 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 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,
Number_Generator $number_generator,
Site_Transients $site_transients
) {
$this->parts_dir = $parts_dir;
$this->part_types = $part_types;
$this->number_generator = $number_generator;
$this->site_transients = $site_transients;
}
/**
* Builds an icon based on the given seed returns the image data.
*
* @param string $seed The seed data (hash).
* @param int $size The size in pixels.
*
* @return string|false
*/
public function build( $seed, $size ) {
try {
// Set randomness from seed.
$this->number_generator->seed( $seed );
// Throw the dice for avatar parts.
$parts = $this->get_randomized_parts();
// Prepare any additional arguments needed.
$args = $this->get_additional_arguments( $seed, $size, $parts );
// Build the avatar image in its final size.
return $this->get_avatar( $size, $parts, $args );
} catch ( \Exception $e ) {
// Something went wrong but don't want to mess up blog layout.
return false;
} finally {
// Reset randomness to something unknonwn.
$this->number_generator->reset();
}
}
/**
* Retrieves the "randomized" parts for the avatar being built.
*
* @return array A simple array of files, indexed by part.
*
* @throws Part_Files_Not_Found_Exception The part files could not be found.
*
* @phpstan-return RandomizedParts
*/
protected function get_randomized_parts() {
return $this->randomize_parts( $this->get_parts() );
}
/**
* Prepares any additional arguments needed for rendering the avatar image.
*
* The arguments will be passed to `render_avatar()`.
*
* @param string $seed The seed data (hash).
* @param int $size The size in pixels.
* @param array $parts The (randomized) avatar parts.
*
* @return array
*
* @phpstan-param RandomizedParts $parts
* @phpstan-return AdditionalArguments
*/
protected function get_additional_arguments( $seed, $size, array $parts ) {
return [];
}
/**
* 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
*/
abstract protected function get_avatar( $size, array $parts, array $args );
/**
* Throws the dice for parts.
*
* @param array $parts An array of arrays containing all parts definitions.
*
* @return array A simple array of part definitions, indexed by type.
*
* @phpstan-param AllPossibleParts $parts
* @phpstan-return RandomizedParts
*/
protected function randomize_parts( array $parts ) {
$randomized_parts = [];
// Throw the dice for every part type.
foreach ( $parts as $type => $list ) {
$randomized_parts[ $type ] = $list[ $this->get_random_part_index( $type, \count( $list ) ) ];
}
return $randomized_parts;
}
/**
* Generates a random but valid part index based on the type and number of parts.
*
* @param string $type The part type.
* @param int $count The number of different parts of the type.
*
* @return int
*/
protected function get_random_part_index( $type, $count ) {
return $this->number_generator->get( 0, $count - 1 );
}
/**
* Retrieves the avatar parts image files.
*
* @return array {
* An array of file lists indexed by the part name.
*
* @type string[] $part An array of part definitions (the exact content
* is determined by the subclasses).
* }
*
* @throws Part_Files_Not_Found_Exception The part files could not be found.
*
* @phpstan-return AllPossibleParts
*/
protected function get_parts() {
if ( empty( $this->all_parts ) ) {
// Calculate transient key.
$basename = \basename( $this->parts_dir );
$key = "avatar_privacy_{$basename}_parts";
// Check existence of transient first.
$cached_parts = $this->site_transients->get( $key );
if ( \is_array( $cached_parts ) && ! empty( $cached_parts ) ) {
/**
* The cached parts look good, let's use those.
*
* @phpstan-var AllPossibleParts $cached_parts
*/
$this->all_parts = $cached_parts;
} else {
// Look at the actual filesystem.
$this->all_parts = $this->build_parts_array();
// Only store transient if we got a result.
if ( ! empty( $this->all_parts ) ) {
$this->site_transients->set( $key, $this->all_parts, \YEAR_IN_SECONDS );
}
}
}
return $this->all_parts;
}
/**
* Builds a sorted array of parts.
*
* @return array {
* An array of part type definitions.
*
* @type string $type The part definition list, indexed by type.
* }
*
* @throws Part_Files_Not_Found_Exception The part files could not be found.
*
* @phpstan-return AllPossibleParts
*/
protected function build_parts_array() {
// Make sure the keys are in the correct order.
$empty_parts = \array_fill_keys( $this->part_types, [] );
// Read part definitions.
$parts = $this->read_parts_from_filesystem( $empty_parts );
// Raise an exception if there were no files found.
if ( $parts === $empty_parts ) {
throw new Part_Files_Not_Found_Exception( "Could not find parts images in {$this->parts_dir}" );
}
return $this->sort_parts( $parts );
}
/**
* 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 array<PartType, array{}> $parts
* @phpstan-return AllPossibleParts
*/
abstract protected function read_parts_from_filesystem( array $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
*/
abstract protected function sort_parts( array $parts );
}