*/ use Exception; use Imagick; use ImagickPixel; use MediaWiki\Shell\Shell; use SimpleXMLElement; use UnexpectedValueException; /** * RandomImageGenerator: does what it says on the tin. * Can fetch a random image, or also write a number of them to disk with random filenames. */ class RandomImageGenerator { private $minWidth = 16; private $maxWidth = 16; private $minHeight = 16; private $maxHeight = 16; public function __construct( $options = [] ) { foreach ( [ 'minWidth', 'minHeight', 'maxWidth', 'maxHeight' ] as $property ) { if ( isset( $options[$property] ) ) { $this->$property = $options[$property]; } } } /** * Writes random images with random filenames to disk in the directory you * specify, or current working directory. * * @param int $number Number of filenames to write * @param string $format Optional, must be understood by ImageMagick, such as 'jpg' or 'gif' * @param string|null $dir Directory, optional (will default to current working directory) * @return array Filenames we just wrote */ public function writeImages( $number, $format = 'svg', $dir = null ) { $filenames = $this->getRandomFilenames( $number, $format, $dir ); $imageWriteMethod = $this->getImageWriteMethod( $format ); foreach ( $filenames as $filename ) { $this->{$imageWriteMethod}( $this->getImageSpec(), $format, $filename ); } return $filenames; } /** * Figure out how we write images. This is a factor of both format and the local system * * @param string $format (a typical extension like 'svg', 'jpg', etc.) * * @throws Exception * @return string */ public function getImageWriteMethod( $format ) { global $wgUseImageMagick, $wgImageMagickConvertCommand; if ( $format === 'svg' ) { return 'writeSvg'; } else { // figure out how to write images global $wgExiv2Command; if ( class_exists( Imagick::class ) && $wgExiv2Command && is_executable( $wgExiv2Command ) ) { return 'writeImageWithApi'; } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) { return 'writeImageWithCommandLine'; } } throw new Exception( "RandomImageGenerator: could not find a suitable " . "method to write images in '$format' format" ); } /** * Return a number of randomly-generated filenames. * * Each filename uses follows the pattern "hex_timestamp_1.jpg". * * @param int $number Number of filenames to generate * @param string $extension * @param string|null $dir Optional, defaults to current working directory * @return string[] */ private function getRandomFilenames( $number, $extension = 'svg', $dir = null ) { $dir ??= getcwd(); $filenames = []; $prefix = wfRandomString( 3 ) . '_' . gmdate( 'YmdHis' ) . '_'; foreach ( range( 1, $number ) as $offset ) { $filename = $prefix . $offset; if ( $extension !== null ) { $filename .= '.' . $extension; } $filenames[] = "$dir/$filename"; } return $filenames; } /** * Generate data representing an image of random size (within limits), * consisting of randomly colored and sized upward pointing triangles * against a random background color. (This data is used in the * writeImage* methods). * * @return array */ public function getImageSpec() { return [ 'width' => mt_rand( $this->minWidth, $this->maxWidth ), 'height' => mt_rand( $this->minHeight, $this->maxHeight ), 'fill' => '#f0f', ]; } /** * Based on image specification, write a very simple SVG file to disk. * Ignores the background spec because transparency is cool. :) * * @param array $spec Spec describing background and shapes to draw * @param string $format File format to write (which is obviously always svg here) * @param string $filename Filename to write to * * @throws Exception */ public function writeSvg( $spec, $format, $filename ) { $svg = new SimpleXmlElement( '' ); $svg->addAttribute( 'xmlns', 'http://www.w3.org/2000/svg' ); $svg->addAttribute( 'width', $spec['width'] ); $svg->addAttribute( 'height', $spec['height'] ); $fh = fopen( $filename, 'w' ); if ( !$fh ) { throw new UnexpectedValueException( "couldn't open $filename for writing" ); } fwrite( $fh, $svg->asXML() ); if ( !fclose( $fh ) ) { throw new UnexpectedValueException( "couldn't close $filename" ); } } /** * Based on an image specification, write such an image to disk, using Imagick PHP extension * @param array $spec Spec describing background and circles to draw * @param string $format File format to write * @param string $filename Filename to write to */ public function writeImageWithApi( $spec, $format, $filename ) { $image = new Imagick(); $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) ); $image->setImageFormat( $format ); $image->writeImage( $filename ); } /** * Based on an image specification, write such an image to disk, using the * command line ImageMagick program ('convert'). * * Sample command line: * $ convert -size 100x60 xc:rgb(90,87,45) \ * -draw 'fill rgb(12,34,56) polygon 41,39 44,57 50,57 41,39' \ * -draw 'fill rgb(99,123,231) circle 59,39 56,57' \ * -draw 'fill rgb(240,12,32) circle 50,21 50,3' filename.png * * @param array $spec Spec describing background and shapes to draw * @param string $format File format to write (unused by this method but * kept so it has the same signature as writeImageWithApi). * @param string $filename Filename to write to * * @return bool */ public function writeImageWithCommandLine( $spec, $format, $filename ) { global $wgImageMagickConvertCommand; $args = [ $wgImageMagickConvertCommand, '-size', $spec['width'] . 'x' . $spec['height'], "xc:{$spec['fill']}", $filename, ]; Shell::command( $args )->execute(); } } class_alias( RandomImageGenerator::class, 'RandomImageGenerator' );