* @since Class available since Release 2.0.0 */ abstract class AbstractConverter { use AutoQualityTrait; use OptionsTrait; use WarningLoggerTrait; use DestinationPreparationTrait; use LoggerTrait; /** * The actual conversion is be done by a concrete converter extending this class. * * At the stage this method is called, the abstract converter has taken preparational steps. * - It has created the destination folder (if neccesary) * - It has checked the input (valid mime type) * - It has set up an error handler, mostly in order to catch and log warnings during the doConvert fase * * Note: This method is not meant to be called from the outside. Use the static *convert* method for converting * or, if you wish, create an instance with ::createInstance() and then call ::doConvert() * * @throws ConversionFailedException in case conversion failed in an antipiciated way (or subclass) * @throws \Exception in case conversion failed in an unantipiciated way */ abstract protected function doActualConvert(); /** * Whether or not the converter supports lossless encoding (even for jpegs) * * PS: Converters that supports lossless encoding all use the EncodingAutoTrait, which * overrides this function. * * @return boolean Whether the converter supports lossless encoding (even for jpegs). */ public function supportsLossless() { return false; } /** @var string The filename of the image to convert (complete path) */ protected $source; /** @var string Where to save the webp (complete path) */ protected $destination; /** * Check basis operationality * * Converters may override this method for the purpose of performing basic operationaly checks. It is for * running general operation checks for a conversion method. * If some requirement is not met, it should throw a ConverterNotOperationalException (or subtype) * * The method is called internally right before calling doActualConvert() method. * - It SHOULD take options into account when relevant. For example, a missing api key for a * cloud converter should be detected here * - It should NOT take the actual filename into consideration, as the purpose is *general* * For that pupose, converters should override checkConvertability * Also note that doConvert method is allowed to throw ConverterNotOperationalException too. * * @return void */ public function checkOperationality() { } /** * Converters may override this for the purpose of performing checks on the concrete file. * * This can for example be used for rejecting big uploads in cloud converters or rejecting unsupported * image types. * * @return void */ public function checkConvertability() { } /** * Constructor. * * @param string $source path to source file * @param string $destination path to destination * @param array $options (optional) options for conversion * @param BaseLogger $logger (optional) */ final public function __construct($source = '', $destination = '', $options = [], $logger = null) { if ($source == '') { return; } InputValidator::checkSourceAndDestination($source, $destination); $this->source = $source; $this->destination = $destination; $this->setLogger($logger); $this->setProvidedOptions($options); if (!isset($this->options['_skip_input_check'])) { $this->logLn('WebP Convert 2.9.0 ignited', 'bold'); $this->logLn('PHP version: ' . phpversion()); if (isset($_SERVER['SERVER_SOFTWARE'])) { $this->logLn('Server software: ' . $_SERVER['SERVER_SOFTWARE']); } $this->logLn(''); if (isset($this->options['log-call-arguments']) && $this->options['log-call-arguments']) { $this->logLn('source: ' . $this->source); $this->logLn('destination: ' . $this->destination); $this->logLn(''); } $this->logLn(self::getConverterDisplayName() . ' converter ignited', 'bold'); } } /** * Get source. * * @return string The source. */ public function getSource() { return $this->source; } /** * Get destination. * * @return string The destination. */ public function getDestination() { return $this->destination; } /** * Set destination. * * @param string $destination path to destination * @return void */ public function setDestination($destination) { $this->destination = $destination; } /** * Get converter name for display (defaults to the class name (short)). * * Converters can override this. * * @return string A display name, ie "Gd" */ protected static function getConverterDisplayName() { // https://stackoverflow.com/questions/19901850/how-do-i-get-an-objects-unqualified-short-class-name/25308464 return substr(strrchr('\\' . static::class, '\\'), 1); } /** * Get converter id (defaults to the class name lowercased) * * Converters can override this. * * @return string A display name, ie "Gd" */ protected static function getConverterId() { return strtolower(self::getConverterDisplayName()); } /** * Create an instance of this class * * @param string $source The path to the file to convert * @param string $destination The path to save the converted file to * @param array $options (optional) * @param \WebPConvert\Loggers\BaseLogger $logger (optional) * * @return static */ public static function createInstance($source, $destination, $options = [], $logger = null) { return new static($source, $destination, $options, $logger); } protected function logReduction($source, $destination) { $sourceSize = filesize($source); $destSize = filesize($destination); $this->log(round(($sourceSize - $destSize) / $sourceSize * 100) . '% '); if ($sourceSize < 10000) { $this->logLn('(went from ' . strval($sourceSize) . ' bytes to ' . strval($destSize) . ' bytes)'); } else { $this->logLn('(went from ' . round($sourceSize / 1024) . ' kb to ' . round($destSize / 1024) . ' kb)'); } } /** * Run conversion. * * @return void */ private function doConvertImplementation() { $beginTime = microtime(true); $this->activateWarningLogger(); $this->checkOptions(); // Prepare destination folder $this->createWritableDestinationFolder(); $this->removeExistingDestinationIfExists(); if (!isset($this->options['_skip_input_check'])) { // Check that a file can be written to destination $this->checkDestinationWritable(); } $this->checkOperationality(); $this->checkConvertability(); if ($this->options['log-call-arguments']) { $this->logOptions(); $this->logLn(''); } $this->runActualConvert(); $source = $this->source; $destination = $this->destination; if (!@file_exists($destination)) { throw new ConversionFailedException('Destination file is not there: ' . $destination); } elseif (@filesize($destination) === 0) { @unlink($destination); throw new ConversionFailedException('Destination file was completely empty'); } else { if (!isset($this->options['_suppress_success_message'])) { $this->ln(); $this->log('Converted image in ' . round((microtime(true) - $beginTime) * 1000) . ' ms'); $sourceSize = @filesize($source); if ($sourceSize !== false) { $this->log(', reducing file size with '); $this->logReduction($source, $destination); } } } $this->deactivateWarningLogger(); } //private function logEx /** * Start conversion. * * Usually it would be more convenience to call the static convert method, but alternatively you can call * call ::createInstance to get an instance and then ::doConvert(). * * @return void */ public function doConvert() { try { //trigger_error('hello', E_USER_ERROR); $this->doConvertImplementation(); } catch (WebPConvertException $e) { $this->logLn(''); /* if (isset($e->description) && ($e->description != '')) { $this->log('Error: ' . $e->description . '. ', 'bold'); } else { $this->log('Error: ', 'bold'); } */ $this->log('Error: ', 'bold'); $this->logLn($e->getMessage(), 'bold'); throw $e; } catch (\Exception $e) { $className = get_class($e); $classNameParts = explode("\\", $className); $shortClassName = array_pop($classNameParts); $this->logLn(''); $this->logLn($shortClassName . ' thrown in ' . $e->getFile() . ':' . $e->getLine(), 'bold'); $this->logLn('Message: "' . $e->getMessage() . '"', 'bold'); $this->logLn('Trace:'); foreach ($e->getTrace() as $trace) { //$this->logLn(print_r($trace, true)); if (isset($trace['file']) && isset($trace['line'])) { $this->logLn( $trace['file'] . ':' . $trace['line'] ); } } throw $e; } catch (\Throwable $e) { $className = get_class($e); $classNameParts = explode("\\", $className); $shortClassName = array_pop($classNameParts); $this->logLn(''); $this->logLn($shortClassName . ' thrown in ' . $e->getFile() . ':' . $e->getLine(), 'bold'); $this->logLn('Message: "' . $e->getMessage() . '"', 'bold'); throw $e; } } /** * Runs the actual conversion (after setup and checks) * Simply calls the doActualConvert() of the actual converter. * However, in the EncodingAutoTrait, this method is overridden to make two conversions * and select the smallest. * * @return void */ protected function runActualConvert() { $this->doActualConvert(); } /** * Convert an image to webp. * * @param string $source path to source file * @param string $destination path to destination * @param array $options (optional) options for conversion * @param BaseLogger $logger (optional) * * @throws ConversionFailedException in case conversion fails in an antipiciated way * @throws \Exception in case conversion fails in an unantipiciated way * @return void */ public static function convert($source, $destination, $options = [], $logger = null) { $c = self::createInstance($source, $destination, $options, $logger); $c->doConvert(); //echo $instance->id; } /** * Get mime type for image (best guess). * * It falls back to using file extension. If that fails too, false is returned * * PS: Is it a security risk to fall back on file extension? * - By setting file extension to "jpg", one can lure our library into trying to convert a file, which isn't a jpg. * hmm, seems very unlikely, though not unthinkable that one of the converters could be exploited * * @return string|false|null mimetype (if it is an image, and type could be determined / guessed), * false (if it is not an image type that the server knowns about) * or null (if nothing can be determined) */ public function getMimeTypeOfSource() { return MimeType::getMimeTypeDetectionResult($this->source); } }