From 42001d3edf8b2d59473d2fb1518305805862e409 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 20 Sep 2014 18:30:48 +0200 Subject: [PATCH] Refactored thumbnail system --- TODO | 5 - public_html/data/.gitignore | 1 - public_html/data/thumbnails/.gitignore | 3 + public_html/data/thumbnails/blank.png | Bin 0 -> 1811 bytes src/Controllers/PostContentController.php | 15 +- src/Controllers/UserAvatarController.php | 18 ++- src/Helpers/ProgramExecutor.php | 26 ++++ .../ImageManipulation/GdImageManipulator.php | 93 ++++++++++++ .../ImageManipulation/IImageManipulator.php | 20 +++ .../ImageManipulation/ImageManipulator.php | 55 +++++++ .../ImagickImageManipulator.php | 60 ++++++++ src/Services/PostService.php | 11 +- .../FlashThumbnailGenerator.php | 59 +++++--- .../IThumbnailGenerator.php | 5 +- .../ImageGdThumbnailGenerator.php | 98 ------------- .../ImageImagickThumbnailGenerator.php | 78 ---------- .../ImageThumbnailGenerator.php | 81 +++++++++-- .../SmartThumbnailGenerator.php | 15 +- .../VideoThumbnailGenerator.php | 59 ++++---- src/Services/ThumbnailService.php | 74 ++++++---- tests/Helpers/ProgramExecutorTest.php | 10 ++ tests/Services/ImageManipulatorTest.php | 114 +++++++++++++++ tests/Services/PostServiceTest.php | 16 +-- tests/Services/ThumbnailGeneratorTest.php | 99 +++++++++++++ tests/Services/ThumbnailServiceTest.php | 135 ++++++++++++++---- tests/test_files/text.txt | 1 + 26 files changed, 817 insertions(+), 334 deletions(-) create mode 100644 public_html/data/thumbnails/.gitignore create mode 100644 public_html/data/thumbnails/blank.png create mode 100644 src/Helpers/ProgramExecutor.php create mode 100644 src/Services/ImageManipulation/GdImageManipulator.php create mode 100644 src/Services/ImageManipulation/IImageManipulator.php create mode 100644 src/Services/ImageManipulation/ImageManipulator.php create mode 100644 src/Services/ImageManipulation/ImagickImageManipulator.php delete mode 100644 src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php delete mode 100644 src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php create mode 100644 tests/Helpers/ProgramExecutorTest.php create mode 100644 tests/Services/ImageManipulatorTest.php create mode 100644 tests/Services/ThumbnailGeneratorTest.php create mode 100644 tests/test_files/text.txt diff --git a/TODO b/TODO index a0e8da58..c872aa4a 100644 --- a/TODO +++ b/TODO @@ -6,11 +6,6 @@ everything related to posts: - fav count - score - comment count - - fix broken thumbnails if no external software is installed - - - uploading post - - remove hard dependency on gd2 in PostService::setContentFromString - (getimagesizefromstring) - single post view - post content diff --git a/public_html/data/.gitignore b/public_html/data/.gitignore index fed7cebe..20c21237 100644 --- a/public_html/data/.gitignore +++ b/public_html/data/.gitignore @@ -1,2 +1 @@ posts -thumbnails diff --git a/public_html/data/thumbnails/.gitignore b/public_html/data/thumbnails/.gitignore new file mode 100644 index 00000000..4da33e18 --- /dev/null +++ b/public_html/data/thumbnails/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!blank.png diff --git a/public_html/data/thumbnails/blank.png b/public_html/data/thumbnails/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..767dcce562da6488bf5aad6a448bc73cf2e36e99 GIT binary patch literal 1811 zcmXw)dsGu=7QmwxG+M-MTs$Sb8z#FYf;MwWs!Tigv4dAZYF_K*ABbMHCd@7(X+ z``ugeo;1pCyW@5SgJB0mhhk}6@vCgxOrLEj)9tj{lo1;xWavknW@+P%)CfrggVB0^ z$EwnXwwWhIA7wBYJNti?O+zQXJ<4F%ehUsoe2|@FF)7n0_`8el{gfa7k$ffD^+s1K z2lI&7ynEz&X*hU8ohn#cSQ!jAy1lvR4)MbLGKW9-c)hM=X>#22oyP`$<;#~%{?p)= zOVQ}h;18UG&%mlU6z1u?&Hc|GMslkbCC;)IJ-{>6UyeSdEO{L2Ad%hDNKVrMYpq4WL7 zz$iyWUHo1`rG(4mt8%ij)?mUz+mH2`xfowHEyNbF29`s34FPY7t0rtdq{I1wY^*fg z!S}X()@ODHx)0OyK>VK;kbx^GfA6#z?HKf8IoOLSqQEPLiuRGp5Ab2*``%vg+|4lM z%XwU4Z)n7C9yKBZzd`wV&(ETZSDRT5TLGo%zt2$>Izzm37%^KI11SEASnl*Ld}1_{ zqiTCh`zwgRX%DocIF!rVSw-BK+KzT$wOro0Y9jFYKd_F=YqVcS+<4ZGbzFm34pCBy zJ>Lf=;;%8aggib^|$`A6vh{bcdmh4X4?)x;3WmmCO-knU^PT8PYeiWHX$V%-{|ROf)S!4%Zgpy%@R`(V55 zgJkag6=dKrq>4K&fSuV#`pg2XgB5;>=qdA6s++*X@Th0=seZ9MK}GWCBLTZt#B$hE zLsTx6;C9I^G+jzX_tX&T(mzmH5&jfHzeM{=L4N7?1n~}mytaqb;Vry>o*Z`+_%g%D5 zEz`H&3n&7eyx?yH`NGNz#7IyAIg;@&MDM=QaKp^RgQQ^kYY@^8vvTQ{993gkh78C` z0yYxKwaB^RnM{noJ4!~KIafEPg;WQV+~BBSjqrLEq0A}6oy`X3%>>f+$4^kxlL%IB zGoE-hE?SWiC7vllv)q$-@ZSBG$F6ZK>D@`>B{-zMQbZlj`3`TtcTzYcqZ)GVs z3%ANPz9zRf>S>gGuOmqqiuqkbjjw|yNams zQmvE(9#) w{m8OgTx)MrO5^fdtmV7op@kQp-g<5GdBv{sJ*Ju-dXX`}Vd){g@W|Q!07;5&{r~^~ literal 0 HcmV?d00001 diff --git a/src/Controllers/PostContentController.php b/src/Controllers/PostContentController.php index 9aacabed..86fdaf4d 100644 --- a/src/Controllers/PostContentController.php +++ b/src/Controllers/PostContentController.php @@ -29,18 +29,19 @@ final class PostContentController extends AbstractController public function getPostContent($postName) { $post = $this->postService->getByName($postName); - $source = $post->getContentPath(); - $this->fileService->serve($source); + $this->fileService->serve($post->getContentPath()); } public function getPostThumbnail($postName, $size) { $post = $this->postService->getByName($postName); - $source = $post->getThumbnailSourceContentPath(); - if (!$this->fileService->exists($source)) - $source = $post->getContentPath(); - $sizedSource = $this->thumbnailService->getOrGenerate($source, $size, $size); - $this->fileService->serve($sizedSource); + $sourceName = $post->getThumbnailSourceContentPath(); + if (!$this->fileService->exists($sourceName)) + $sourceName = $post->getContentPath(); + + $this->thumbnailService->generateIfNeeded($sourceName, $size, $size); + $thumbnailName = $this->thumbnailService->getThumbnailName($sourceName, $size, $size); + $this->fileService->serve($thumbnailName); } } diff --git a/src/Controllers/UserAvatarController.php b/src/Controllers/UserAvatarController.php index fe750c4f..1311fcaa 100644 --- a/src/Controllers/UserAvatarController.php +++ b/src/Controllers/UserAvatarController.php @@ -38,7 +38,7 @@ final class UserAvatarController extends AbstractController break; case \Szurubooru\Entities\User::AVATAR_STYLE_BLANK: - $this->serveFromFile($this->getBlankAvatarSourcePath(), $size); + $this->serveFromFile($this->getBlankAvatarSourceContentPath(), $size); break; case \Szurubooru\Entities\User::AVATAR_STYLE_MANUAL: @@ -46,7 +46,7 @@ final class UserAvatarController extends AbstractController break; default: - $this->serveFromFile($this->getBlankAvatarSourcePath(), $size); + $this->serveFromFile($this->getBlankAvatarSourceContentPath(), $size); break; } } @@ -56,17 +56,15 @@ final class UserAvatarController extends AbstractController $this->httpHelper->redirect($url); } - private function serveFromFile($file, $size) + private function serveFromFile($sourceName, $size) { - if (!$this->fileService->exists($file)) - $file = $this->getBlankAvatarSourcePath(); - - $sizedFile = $this->thumbnailService->getOrGenerate($file, $size, $size); - $this->fileService->serve($sizedFile); + $this->thumbnailService->generateIfNeeded($sourceName, $size, $size); + $thumbnailName = $this->thumbnailService->getThumbnailName($sourceName, $size, $size); + $this->fileService->serve($thumbnailName); } - private function getBlankAvatarSourcePath() + private function getBlankAvatarSourceContentPath() { - return 'avatars' . DIRECTORY_SEPARATOR . 'blank.png'; + return 'avatars/blank.png'; } } diff --git a/src/Helpers/ProgramExecutor.php b/src/Helpers/ProgramExecutor.php new file mode 100644 index 00000000..87aa0ffa --- /dev/null +++ b/src/Helpers/ProgramExecutor.php @@ -0,0 +1,26 @@ +&1', $programName, implode(' ', $quotedArguments)); + return exec($cmd); + } + + public static function isProgramAvailable($programName) + { + if (PHP_OS === 'WINNT') + { + exec('where "' . $programName . '" 2>&1 >nul', $trash, $exitCode); + } + else + { + exec('command -v "' . $programName . '" >/dev/null 2>&1', $trash, $exitCode); + } + return $exitCode === 0; + } +} diff --git a/src/Services/ImageManipulation/GdImageManipulator.php b/src/Services/ImageManipulation/GdImageManipulator.php new file mode 100644 index 00000000..276355b1 --- /dev/null +++ b/src/Services/ImageManipulation/GdImageManipulator.php @@ -0,0 +1,93 @@ + imagesx($imageResource)) + $width = imagesx($imageResource) - $originX; + if ($originY + $height > imagesy($imageResource)) + $height = imagesy($imageResource) - $originY; + + if ($originX < 0) + $width = $width + $originX; + if ($originY < 0) + $height = $height + $originY; + + if ($width < 1) + $width = 1; + if ($height < 1) + $height = 1; + + $imageResource = imagecrop($imageResource, [ + 'x' => $originX, + 'y' => $originY, + 'width' => $width, + 'height' => $height]); + } + + public function saveToBuffer($imageResource, $format) + { + ob_start(); + + switch ($format) + { + case self::FORMAT_JPEG: + imagejpeg($imageResource); + break; + + case self::FORMAT_PNG: + imagepng($imageResource); + break; + + default: + } + + $buffer = ob_get_contents(); + ob_end_clean(); + + if (!$buffer) + throw new \InvalidArgumentException('Not supported'); + return $buffer; + } +} diff --git a/src/Services/ImageManipulation/IImageManipulator.php b/src/Services/ImageManipulation/IImageManipulator.php new file mode 100644 index 00000000..c0a638ca --- /dev/null +++ b/src/Services/ImageManipulation/IImageManipulator.php @@ -0,0 +1,20 @@ +strategy = $imagickImageManipulator; + } + else if (extension_loaded('gd')) + { + $this->strategy = $gdImageManipulator; + } + else + { + throw new \RuntimeException('Neither imagick or gd image extensions are enabled'); + } + } + + public function loadFromBuffer($source) + { + return $this->strategy->loadFromBuffer($source); + } + + public function getImageWidth($imageResource) + { + return $this->strategy->getImageWidth($imageResource); + } + + public function getImageHeight($imageResource) + { + return $this->strategy->getImageHeight($imageResource); + } + + public function resizeImage(&$imageResource, $width, $height) + { + return $this->strategy->resizeImage($imageResource, $width, $height); + } + + public function cropImage(&$imageResource, $width, $height, $originX, $originY) + { + return $this->strategy->cropImage($imageResource, $width, $height, $originX, $originY); + } + + public function saveToBuffer($source, $format) + { + return $this->strategy->saveToBuffer($source, $format); + } +} diff --git a/src/Services/ImageManipulation/ImagickImageManipulator.php b/src/Services/ImageManipulation/ImagickImageManipulator.php new file mode 100644 index 00000000..0469227d --- /dev/null +++ b/src/Services/ImageManipulation/ImagickImageManipulator.php @@ -0,0 +1,60 @@ +readImageBlob($source); + $image = $image->coalesceImages(); + return $image; + } + catch (\Exception $e) + { + return null; + } + } + + public function getImageWidth($imageResource) + { + return $imageResource->getImageWidth(); + } + + public function getImageHeight($imageResource) + { + return $imageResource->getImageHeight(); + } + + public function resizeImage(&$imageResource, $width, $height) + { + $imageResource->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 0.9); + } + + public function cropImage(&$imageResource, $width, $height, $originX, $originY) + { + $imageResource->cropImage($width, $height, $originX, $originY); + $imageResource->setImagePage(0, 0, 0, 0); + } + + public function saveToBuffer($imageResource, $format) + { + switch ($format) + { + case self::FORMAT_JPEG: + $imageResource->setImageFormat('jpeg'); + break; + + case self::FORMAT_PNG: + $imageResource->setImageFormat('png'); + break; + + default: + throw new \InvalidArgumentException('Not supported'); + } + + return $imageResource->getImageBlob(); + } +} diff --git a/src/Services/PostService.php b/src/Services/PostService.php index cba99731..24217131 100644 --- a/src/Services/PostService.php +++ b/src/Services/PostService.php @@ -11,6 +11,7 @@ class PostService private $timeService; private $authService; private $fileService; + private $imageManipulator; public function __construct( \Szurubooru\Config $config, @@ -20,7 +21,8 @@ class PostService \Szurubooru\Dao\Services\PostSearchService $postSearchService, \Szurubooru\Services\AuthService $authService, \Szurubooru\Services\TimeService $timeService, - \Szurubooru\Services\FileService $fileService) + \Szurubooru\Services\FileService $fileService, + \Szurubooru\Services\ImageManipulation\ImageManipulator $imageManipulator) { $this->config = $config; $this->validator = $validator; @@ -30,6 +32,7 @@ class PostService $this->timeService = $timeService; $this->authService = $authService; $this->fileService = $fileService; + $this->imageManipulator = $imageManipulator; } public function getByNameOrId($postNameOrId) @@ -137,9 +140,9 @@ class PostService $post->setContent($content); - list ($imageWidth, $imageHeight) = getimagesizefromstring($content); - $post->setImageWidth($imageWidth); - $post->setImageHeight($imageHeight); + $image = $this->imageManipulator->loadFromBuffer($content); + $post->setImageWidth($this->imageManipulator->getImageWidth($image)); + $post->setImageHeight($this->imageManipulator->getImageHeight($image)); $post->setOriginalFileSize(strlen($content)); } diff --git a/src/Services/ThumbnailGenerators/FlashThumbnailGenerator.php b/src/Services/ThumbnailGenerators/FlashThumbnailGenerator.php index f12bf6e3..4ff913b7 100644 --- a/src/Services/ThumbnailGenerators/FlashThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/FlashThumbnailGenerator.php @@ -3,6 +3,9 @@ namespace Szurubooru\Services\ThumbnailGenerators; class FlashThumbnailGenerator implements IThumbnailGenerator { + const PROGRAM_NAME_DUMP_GNASH = 'dump-gnash'; + const PROGRAM_NAME_SWFRENDER = 'swfrender'; + private $imageThumbnailGenerator; public function __construct(ImageThumbnailGenerator $imageThumbnailGenerator) @@ -10,35 +13,47 @@ class FlashThumbnailGenerator implements IThumbnailGenerator $this->imageThumbnailGenerator = $imageThumbnailGenerator; } - public function generate($srcPath, $dstPath, $width, $height) + public function generate($source, $width, $height, $cropStyle) { - if (!file_exists($srcPath)) - throw new \InvalidArgumentException($srcPath . ' does not exist'); + $tmpSourcePath = tempnam(sys_get_temp_dir(), 'thumb') . '.dat'; + $tmpTargetPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; + file_put_contents($tmpSourcePath, $source); - $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; - - $cmd = sprintf( - 'dump-gnash --screenshot last --screenshot-file "%s" -1 -r1 --max-advances 15 "%s"', - $tmpPath, - $srcPath); - exec($cmd); - - if (file_exists($tmpPath)) + if (\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_DUMP_GNASH)) { - $this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); - unlink($tmpPath); - return; + \Szurubooru\Helpers\ProgramExecutor::run( + self::PROGRAM_NAME_DUMP_GNASH, + [ + '--screenshot', 'last', + '--screenshot-file', $tmpTargetPath, + '-1', + '-r1', + '--max-advances', '15', + $tmpSourcePath, + ]); } - exec('swfrender ' . $srcPath . ' -o ' . $tmpPath); - - if (file_exists($tmpPath)) + if (!file_exists($tmpTargetPath) and \Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_SWFRENDER)) { - $this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); - unlink($tmpPath); - return; + \Szurubooru\Helpers\ProgramExecutor::run( + self::PROGRAM_NAME_SWFRENDER, + [ + 'swfrender', + $tmpSourcePath, + '-o', + $tmpTargetPath, + ]); } - throw new \RuntimeException('Failed to generate thumbnail'); + if (!file_exists($tmpTargetPath)) + { + unlink($tmpSourcePath); + return null; + } + + $ret = $this->imageThumbnailGenerator->generate(file_get_contents($tmpTargetPath), $width, $height, $cropStyle); + unlink($tmpSourcePath); + unlink($tmpTargetPath); + return $ret; } } diff --git a/src/Services/ThumbnailGenerators/IThumbnailGenerator.php b/src/Services/ThumbnailGenerators/IThumbnailGenerator.php index 8581457b..9ebe9880 100644 --- a/src/Services/ThumbnailGenerators/IThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/IThumbnailGenerator.php @@ -3,5 +3,8 @@ namespace Szurubooru\Services\ThumbnailGenerators; interface IThumbnailGenerator { - public function generate($srcPath, $dstPath, $width, $height); + const CROP_OUTSIDE = 0; + const CROP_INSIDE = 1; + + public function generate($sourceString, $width, $height, $cropStyle); } diff --git a/src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php deleted file mode 100644 index c8f5ff80..00000000 --- a/src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php +++ /dev/null @@ -1,98 +0,0 @@ -config = $config; - } - - public function generate($srcPath, $dstPath, $width, $height) - { - if (!file_exists($srcPath)) - throw new \InvalidArgumentException($srcPath . ' does not exist'); - - $mime = mime_content_type($srcPath); - - switch ($mime) - { - case 'image/jpeg': - $srcImage = imagecreatefromjpeg($srcPath); - break; - - case 'image/png': - $srcImage = imagecreatefrompng($srcPath); - break; - - case 'image/gif': - $srcImage = imagecreatefromgif($srcPath); - break; - - default: - throw new \Exception('Invalid thumbnail image type'); - } - - switch ($this->config->misc->thumbnailCropStyle) - { - case 'outside': - $dstImage = $this->cropOutside($srcImage, $width, $height); - break; - case 'inside': - $dstImage = $this->cropInside($srcImage, $width, $height); - break; - default: - throw new \Exception('Unknown thumbnail crop style'); - } - - imagejpeg($dstImage, $dstPath); - imagedestroy($srcImage); - imagedestroy($dstImage); - } - - private function cropOutside($srcImage, $dstWidth, $dstHeight) - { - $srcWidth = imagesx($srcImage); - $srcHeight = imagesy($srcImage); - - if (($dstHeight / $dstWidth) > ($srcHeight / $srcWidth)) - { - $cropHeight = $srcHeight; - $cropWidth = $srcHeight * $dstWidth / $dstHeight; - } - else - { - $cropWidth = $srcWidth; - $cropHeight = $srcWidth * $dstHeight / $dstWidth; - } - $cropX = ($srcWidth - $cropWidth) / 2; - $cropY = ($srcHeight - $cropHeight) / 2; - - $dstImage = imagecreatetruecolor($dstWidth, $dstHeight); - imagecopyresampled($dstImage, $srcImage, 0, 0, $cropX, $cropY, $dstWidth, $dstHeight, $cropWidth, $cropHeight); - return $dstImage; - } - - private function cropInside($srcImage, $dstWidth, $dstHeight) - { - $srcWidth = imagesx($srcImage); - $srcHeight = imagesy($srcImage); - - if (($dstHeight / $dstWidth) < ($srcHeight / $srcWidth)) - { - $cropHeight = $dstHeight; - $cropWidth = $dstHeight * $srcWidth / $srcHeight; - } - else - { - $cropWidth = $dstWidth; - $cropHeight = $dstWidth * $srcHeight / $srcWidth; - } - - $dstImage = imagecreatetruecolor($cropWidth, $cropHeight); - imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $cropWidth, $cropHeight, $srcWidth, $srcHeight); - return $dstImage; - } -} diff --git a/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php deleted file mode 100644 index 6df5dccf..00000000 --- a/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php +++ /dev/null @@ -1,78 +0,0 @@ -config = $config; - } - - public function generate($srcPath, $dstPath, $width, $height) - { - if (!file_exists($srcPath)) - throw new \InvalidArgumentException($srcPath . ' does not exist'); - - $image = new \Imagick($srcPath); - $image = $image->coalesceImages(); - - switch ($this->config->misc->thumbnailCropStyle) - { - case 'outside': - $this->cropOutside($image, $width, $height); - break; - case 'inside': - $this->cropInside($image, $width, $height); - break; - default: - throw new \Exception('Unknown thumbnail crop style'); - } - - $image->writeImage($dstPath); - $image->destroy(); - } - - private function cropOutside($srcImage, $dstWidth, $dstHeight) - { - $srcWidth = $srcImage->getImageWidth(); - $srcHeight = $srcImage->getImageHeight(); - - if (($dstHeight / $dstWidth) > ($srcHeight / $srcWidth)) - { - $cropHeight = $dstHeight; - $cropWidth = $dstHeight * $srcWidth / $srcHeight; - } - else - { - $cropWidth = $dstWidth; - $cropHeight = $dstWidth * $srcHeight / $srcWidth; - } - $cropX = ($cropWidth - $dstWidth) >> 1; - $cropY = ($cropHeight - $dstHeight) >> 1; - - $srcImage->resizeImage($cropWidth, $cropHeight, \imagick::FILTER_LANCZOS, 0.9); - $srcImage->cropImage($dstWidth, $dstHeight, $cropX, $cropY); - $srcImage->setImagePage(0, 0, 0, 0); - } - - private function cropInside($srcImage, $dstWidth, $dstHeight) - { - $srcWidth = $srcImage->getImageWidth(); - $srcHeight = $srcImage->getImageHeight(); - - if (($dstHeight / $dstWidth) < ($srcHeight / $srcWidth)) - { - $cropHeight = $dstHeight; - $cropWidth = $dstHeight * $srcWidth / $srcHeight; - } - else - { - $cropWidth = $dstWidth; - $cropHeight = $dstWidth * $srcHeight / $srcWidth; - } - - $srcImage->resizeImage($cropWidth, $cropHeight, \imagick::FILTER_LANCZOS, 0.9); - } -} diff --git a/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php index b6a3a7e0..a5b8e91b 100644 --- a/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php @@ -3,26 +3,77 @@ namespace Szurubooru\Services\ThumbnailGenerators; class ImageThumbnailGenerator implements IThumbnailGenerator { - private $imageImagickThumbnailGenerator; - private $imageGdThumbnailGenerator; + private $imageManipulator; - public function __construct( - ImageImagickThumbnailGenerator $imageImagickThumbnailGenerator, - ImageGdThumbnailGenerator $imageGdThumbnailGenerator) + public function __construct(\Szurubooru\Services\ImageManipulation\ImageManipulator $imageManipulator) { - $this->imageImagickThumbnailGenerator = $imageImagickThumbnailGenerator; - $this->imageGdThumbnailGenerator = $imageGdThumbnailGenerator; + $this->imageManipulator = $imageManipulator; } - public function generate($srcPath, $dstPath, $width, $height) + public function generate($source, $width, $height, $cropStyle) { - if (extension_loaded('imagick')) - $strategy = $this->imageImagickThumbnailGenerator; - elseif (extension_loaded('gd')) - $strategy = $this->imageGdThumbnailGenerator; - else - throw new \Exception('Both imagick and gd extensions are disabled'); + try + { + $image = $this->imageManipulator->loadFromBuffer($source); + $srcWidth = $this->imageManipulator->getImageWidth($image); + $srcHeight = $this->imageManipulator->getImageHeight($image); - return $strategy->generate($srcPath, $dstPath, $width, $height); + switch ($cropStyle) + { + case IThumbnailGenerator::CROP_OUTSIDE: + $this->cropOutside($image, $srcWidth, $srcHeight, $width, $height); + break; + + case IThumbnailGenerator::CROP_INSIDE: + $this->cropInside($image, $srcWidth, $srcHeight, $width, $height); + break; + + default: + throw new \InvalidArgumentException('Unknown thumbnail crop style'); + } + + return $this->imageManipulator->saveToBuffer( + $image, + \Szurubooru\Services\ImageManipulation\IImageManipulator::FORMAT_JPEG); + } + catch (\Exception $e) + { + return null; + } + } + + private function cropOutside($image, $srcWidth, $srcHeight, $dstWidth, $dstHeight) + { + if (($dstHeight / $dstWidth) > ($srcHeight / $srcWidth)) + { + $cropHeight = $dstHeight; + $cropWidth = $dstHeight * $srcWidth / $srcHeight; + } + else + { + $cropWidth = $dstWidth; + $cropHeight = $dstWidth * $srcHeight / $srcWidth; + } + $cropX = ($cropWidth - $dstWidth) >> 1; + $cropY = ($cropHeight - $dstHeight) >> 1; + + $this->imageManipulator->resizeImage($image, $cropWidth, $cropHeight); + $this->imageManipulator->cropImage($image, $dstWidth, $dstHeight, $cropX, $cropY); + } + + private function cropInside($image, $srcWidth, $srcHeight, $dstWidth, $dstHeight) + { + if (($dstHeight / $dstWidth) < ($srcHeight / $srcWidth)) + { + $cropHeight = $dstHeight; + $cropWidth = $dstHeight * $srcWidth / $srcHeight; + } + else + { + $cropWidth = $dstWidth; + $cropHeight = $dstWidth * $srcHeight / $srcWidth; + } + + $this->imageManipulator->resizeImage($image, $cropWidth, $cropHeight); } } diff --git a/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php b/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php index 429b1d95..fe0e4759 100644 --- a/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/SmartThumbnailGenerator.php @@ -17,22 +17,19 @@ class SmartThumbnailGenerator implements IThumbnailGenerator $this->imageThumbnailGenerator = $imageThumbnailGenerator; } - public function generate($srcPath, $dstPath, $width, $height) + public function generate($source, $width, $height, $cropStyle) { - if (!file_exists($srcPath)) - throw new \InvalidArgumentException($srcPath . ' does not exist'); - - $mime = \Szurubooru\Helpers\MimeHelper::getMimeTypeFromFile($srcPath); + $mime = \Szurubooru\Helpers\MimeHelper::getMimeTypeFromBuffer($source); if (\Szurubooru\Helpers\MimeHelper::isFlash($mime)) - return $this->flashThumbnailGenerator->generate($srcPath, $dstPath, $width, $height); + return $this->flashThumbnailGenerator->generate($source, $width, $height, $cropStyle); if (\Szurubooru\Helpers\MimeHelper::isVideo($mime)) - return $this->videoThumbnailGenerator->generate($srcPath, $dstPath, $width, $height); + return $this->videoThumbnailGenerator->generate($source, $width, $height, $cropStyle); if (\Szurubooru\Helpers\MimeHelper::isImage($mime)) - return $this->imageThumbnailGenerator->generate($srcPath, $dstPath, $width, $height); + return $this->imageThumbnailGenerator->generate($source, $width, $height, $cropStyle); - throw new \InvalidArgumentException('Invalid thumbnail file type: ' . $mime); + return null; } } diff --git a/src/Services/ThumbnailGenerators/VideoThumbnailGenerator.php b/src/Services/ThumbnailGenerators/VideoThumbnailGenerator.php index 621d9f23..12907d2e 100644 --- a/src/Services/ThumbnailGenerators/VideoThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/VideoThumbnailGenerator.php @@ -3,6 +3,9 @@ namespace Szurubooru\Services\ThumbnailGenerators; class VideoThumbnailGenerator implements IThumbnailGenerator { + const PROGRAM_NAME_FFMPEG = 'ffmpeg'; + const PROGRAM_NAME_FFMPEGTHUMBNAILER = 'ffmpegthumbnailer'; + private $imageThumbnailGenerator; public function __construct(ImageThumbnailGenerator $imageThumbnailGenerator) @@ -10,39 +13,43 @@ class VideoThumbnailGenerator implements IThumbnailGenerator $this->imageThumbnailGenerator = $imageThumbnailGenerator; } - public function generate($srcPath, $dstPath, $width, $height) + public function generate($source, $width, $height, $cropStyle) { - if (!file_exists($srcPath)) - throw new \InvalidArgumentException($srcPath . ' does not exist'); + $tmpSourcePath = tempnam(sys_get_temp_dir(), 'thumb') . '.dat'; + $tmpTargetPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; + file_put_contents($tmpSourcePath, $source); - $tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg'; - - $cmd = sprintf( - 'ffmpegthumbnailer -i"%s" -o"%s" -s0 -t"12%%"', - $srcPath, - $tmpPath); - exec($cmd); - - if (file_exists($tmpPath)) + if (\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_FFMPEGTHUMBNAILER)) { - $this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); - unlink($tmpPath); - return; + \Szurubooru\Helpers\ProgramExecutor::run( + self::PROGRAM_NAME_FFMPEGTHUMBNAILER, + [ + '-i' . $tmpSourcePath, + '-o' . $tmpTargetPath, + '-s0', + '-t12%%' + ]); } - $cmd = sprintf( - 'ffmpeg -i "%s" -vframes 1 "%s"', - $srcPath, - $tmpPath); - exec($cmd); - - if (file_exists($tmpPath)) + if (!file_exists($tmpTargetPath) and \Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_FFMPEG)) { - $this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); - unlink($tmpPath); - return; + \Szurubooru\Helpers\ProgramExecutor::run(self::PROGRAM_NAME_FFMEPG, + [ + '-i', $tmpSourcePath, + '-vframes', '1', + $tmpTargetPath + ]); } - throw new \RuntimeException('Failed to generate thumbnail'); + if (!file_exists($tmpTargetPath)) + { + unlink($tmpSourcePath); + return null; + } + + $ret = $this->imageThumbnailGenerator->generate(file_get_contents($tmpTargetPath), $width, $height, $cropStyle); + unlink($tmpSourcePath); + unlink($tmpTargetPath); + return $ret; } } diff --git a/src/Services/ThumbnailService.php b/src/Services/ThumbnailService.php index 09d0b234..8fb0a68e 100644 --- a/src/Services/ThumbnailService.php +++ b/src/Services/ThumbnailService.php @@ -3,48 +3,69 @@ namespace Szurubooru\Services; class ThumbnailService { + private $config; private $fileService; private $thumbnailGenerator; public function __construct( - FileService $fileService, - ThumbnailGenerators\SmartThumbnailGenerator $thumbnailGenerator) + \Szurubooru\Config $config, + \Szurubooru\Services\FileService $fileService, + \Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator $thumbnailGenerator) { + $this->config = $config; $this->fileService = $fileService; $this->thumbnailGenerator = $thumbnailGenerator; } - public function getOrGenerate($source, $width, $height) - { - $target = $this->getPath($source, $width, $height); - - if (!$this->fileService->exists($target)) - $this->generate($source, $width, $height); - - return $target; - } - - public function deleteUsedThumbnails($source) + public function deleteUsedThumbnails($sourceName) { foreach ($this->getUsedThumbnailSizes() as $size) { list ($width, $height) = $size; - $target = $this->getPath($source, $width, $height); - if ($this->fileService->exists($target)) - $this->fileService->delete($target); + $target = $this->getThumbnailName($sourceName, $width, $height); + $this->fileService->delete($target); } } - public function generate($source, $width, $height) + public function generateIfNeeded($sourceName, $width, $height) { - $target = $this->getPath($source, $width, $height); + $thumbnailName = $this->getThumbnailName($sourceName, $width, $height); - $fullSource = $this->fileService->getFullPath($source); - $fullTarget = $this->fileService->getFullPath($target); - $this->fileService->createFolders($target); - $this->thumbnailGenerator->generate($fullSource, $fullTarget, $width, $height); + if (!$this->fileService->exists($thumbnailName)) + $this->generate($sourceName, $width, $height); + } - return $target; + public function generate($sourceName, $width, $height) + { + switch ($this->config->misc->thumbnailCropStyle) + { + case 'outside': + $cropStyle = \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_OUTSIDE; + break; + + case 'inside': + $cropStyle = \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_INSIDE; + break; + + default: + throw new \InvalidArgumentException('Invalid thumbnail crop style'); + } + + $thumbnailName = $this->getThumbnailName($sourceName, $width, $height); + + $source = $this->fileService->load($sourceName); + $result = null; + + if (!$source) + $source = $this->fileService->load($this->getBlankThumbnailName()); + + if ($source) + $result = $this->thumbnailGenerator->generate($source, $width, $height, $cropStyle); + + if (!$result) + $result = $this->fileService->load($this->getBlankThumbnailName()); + + $this->fileService->save($thumbnailName, $result); } public function getUsedThumbnailSizes() @@ -64,8 +85,13 @@ class ThumbnailService } } - private function getPath($source, $width, $height) + public function getThumbnailName($source, $width, $height) { return 'thumbnails' . DIRECTORY_SEPARATOR . $width . 'x' . $height . DIRECTORY_SEPARATOR . $source; } + + public function getBlankThumbnailName() + { + return 'thumbnails' . DIRECTORY_SEPARATOR . 'blank.png'; + } } diff --git a/tests/Helpers/ProgramExecutorTest.php b/tests/Helpers/ProgramExecutorTest.php new file mode 100644 index 00000000..bc94a868 --- /dev/null +++ b/tests/Helpers/ProgramExecutorTest.php @@ -0,0 +1,10 @@ +assertFalse(\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable('there_is_no_way_my_os_can_have_this_program')); + } +} diff --git a/tests/Services/ImageManipulatorTest.php b/tests/Services/ImageManipulatorTest.php new file mode 100644 index 00000000..e31e75e7 --- /dev/null +++ b/tests/Services/ImageManipulatorTest.php @@ -0,0 +1,114 @@ +imageManipulators = [ + $imagickImageManipulator, + $gdImageManipulator, + $autoImageManipulator, + ]; + } + + public function testImageSize() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $image = $imageManipulator->loadFromBuffer($this->getTestFile('image.jpg')); + $this->assertEquals(640, $imageManipulator->getImageWidth($image)); + $this->assertEquals(480, $imageManipulator->getImageHeight($image)); + } + } + + public function testNonImage() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $this->assertNull($imageManipulator->loadFromBuffer($this->getTestFile('flash.swf'))); + } + } + + public function testImageResizing() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $image = $imageManipulator->loadFromBuffer($this->getTestFile('image.jpg')); + $imageManipulator->resizeImage($image, 400, 500); + $this->assertEquals(400, $imageManipulator->getImageWidth($image)); + $this->assertEquals(500, $imageManipulator->getImageHeight($image)); + } + } + + public function testImageCroppingBleedWidth() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $image = $imageManipulator->loadFromBuffer($this->getTestFile('image.jpg')); + $imageManipulator->cropImage($image, 640, 480, 200, 200); + $this->assertEquals(440, $imageManipulator->getImageWidth($image)); + $this->assertEquals(280, $imageManipulator->getImageHeight($image)); + } + } + + public function testImageCroppingBleedPosition() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $image = $imageManipulator->loadFromBuffer($this->getTestFile('image.jpg')); + $imageManipulator->cropImage($image, 640, 480, -200, -200); + $this->assertEquals(440, $imageManipulator->getImageWidth($image)); + $this->assertEquals(280, $imageManipulator->getImageHeight($image)); + } + } + + public function testImageCroppingBleedBoth() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $image = $imageManipulator->loadFromBuffer($this->getTestFile('image.jpg')); + $imageManipulator->cropImage($image, 642, 481, -1, -1); + $this->assertEquals(640, $imageManipulator->getImageWidth($image)); + $this->assertEquals(480, $imageManipulator->getImageHeight($image)); + } + } + + public function testImageCroppingMaxBleeding() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $image = $imageManipulator->loadFromBuffer($this->getTestFile('image.jpg')); + $imageManipulator->cropImage($image, 100, 100, 1000, 1000); + $this->assertEquals(1, $imageManipulator->getImageWidth($image)); + $this->assertEquals(1, $imageManipulator->getImageHeight($image)); + } + } + + public function testSaving() + { + foreach ($this->getImageManipulators() as $imageManipulator) + { + $image = $imageManipulator->loadFromBuffer($this->getTestFile('image.jpg')); + $jpegBuffer = $imageManipulator->saveToBuffer($image, \Szurubooru\Services\ImageManipulation\IImageManipulator::FORMAT_JPEG); + $pngBuffer = $imageManipulator->saveToBuffer($image, \Szurubooru\Services\ImageManipulation\IImageManipulator::FORMAT_PNG); + $this->assertEquals('image/jpeg', \Szurubooru\Helpers\MimeHelper::getMimeTypeFromBuffer($jpegBuffer)); + $this->assertEquals('image/png', \Szurubooru\Helpers\MimeHelper::getMimeTypeFromBuffer($pngBuffer)); + } + } + + private function getImageManipulators() + { + return $this->imageManipulators; + } +} diff --git a/tests/Services/PostServiceTest.php b/tests/Services/PostServiceTest.php index f69747ef..54a7c53c 100644 --- a/tests/Services/PostServiceTest.php +++ b/tests/Services/PostServiceTest.php @@ -11,6 +11,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase private $authServiceMock; private $timeServiceMock; private $fileServiceMock; + private $imageManipulatorMock; public function setUp() { @@ -23,9 +24,9 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); $this->configMock->set('database/maxPostSize', 1000000); + $this->imageManipulatorMock = $this->mock(\Szurubooru\Services\ImageManipulation\ImageManipulator::class); } - public function testCreatingYoutubePost() { $formData = new \Szurubooru\FormData\UploadFormData; @@ -60,15 +61,17 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase $formData->contentFileName = 'blah'; $this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); + $this->imageManipulatorMock->expects($this->once())->method('getImageWidth')->willReturn(640); + $this->imageManipulatorMock->expects($this->once())->method('getImageHeight')->willReturn(480); $this->postService = $this->getPostService(); $savedPost = $this->postService->createPost($formData); $this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_IMAGE, $savedPost->getContentType()); $this->assertEquals('24216edd12328de3a3c55e2f98220ee7613e3be1', $savedPost->getContentChecksum()); - $this->assertEquals(640, $savedPost->getImageWidth()); - $this->assertEquals(480, $savedPost->getImageHeight()); $this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName()); $this->assertEquals(687645, $savedPost->getOriginalFileSize()); + $this->assertEquals(640, $savedPost->getImageWidth()); + $this->assertEquals(480, $savedPost->getImageHeight()); } public function testCreatingVideos() @@ -85,8 +88,6 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase $savedPost = $this->postService->createPost($formData); $this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_VIDEO, $savedPost->getContentType()); $this->assertEquals('16dafaa07cda194d03d590529c06c6ec1a5b80b0', $savedPost->getContentChecksum()); - $this->assertNull($savedPost->getImageWidth()); - $this->assertNull($savedPost->getImageHeight()); $this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName()); $this->assertEquals(14667, $savedPost->getOriginalFileSize()); } @@ -105,8 +106,6 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase $savedPost = $this->postService->createPost($formData); $this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_FLASH, $savedPost->getContentType()); $this->assertEquals('d897e044b801d892291b440534c3be3739034f68', $savedPost->getContentChecksum()); - $this->assertEquals(320, $savedPost->getImageWidth()); - $this->assertEquals(240, $savedPost->getImageHeight()); $this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName()); $this->assertEquals(226172, $savedPost->getOriginalFileSize()); } @@ -180,6 +179,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase $this->postSearchServiceMock, $this->authServiceMock, $this->timeServiceMock, - $this->fileServiceMock); + $this->fileServiceMock, + $this->imageManipulatorMock); } } diff --git a/tests/Services/ThumbnailGeneratorTest.php b/tests/Services/ThumbnailGeneratorTest.php new file mode 100644 index 00000000..aa6eed65 --- /dev/null +++ b/tests/Services/ThumbnailGeneratorTest.php @@ -0,0 +1,99 @@ +markTestSkipped('External software necessary to run this test is missing.'); + } + + $thumbnailGenerator = $this->getThumbnailGenerator(); + $imageManipulator = $this->getImageManipulator(); + + $result = $thumbnailGenerator->generate( + $this->getTestFile('flash.swf'), + 150, + 150, + \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_OUTSIDE); + + $image = $imageManipulator->loadFromBuffer($result); + $this->assertEquals(150, $imageManipulator->getImageWidth($image)); + $this->assertEquals(150, $imageManipulator->getImageHeight($image)); + } + + public function testVideoThumbnails() + { + if (!\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(\Szurubooru\Services\ThumbnailGenerators\VideoThumbnailGenerator::PROGRAM_NAME_FFMPEG) + and !\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(\Szurubooru\Services\ThumbnailGenerators\VideoThumbnailGenerator::PROGRAM_NAME_FFMPEGTHUMBNAILER)) + { + $this->markTestSkipped('External software necessary to run this test is missing.'); + } + + $thumbnailGenerator = $this->getThumbnailGenerator(); + $imageManipulator = $this->getImageManipulator(); + + $result = $thumbnailGenerator->generate( + $this->getTestFile('video.mp4'), + 150, + 150, + \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_OUTSIDE); + + $image = $imageManipulator->loadFromBuffer($result); + $this->assertEquals(150, $imageManipulator->getImageWidth($image)); + $this->assertEquals(150, $imageManipulator->getImageHeight($image)); + } + + public function testImageThumbnails() + { + $thumbnailGenerator = $this->getThumbnailGenerator(); + $imageManipulator = $this->getImageManipulator(); + + $result = $thumbnailGenerator->generate( + $this->getTestFile('image.jpg'), + 150, + 150, + \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_OUTSIDE); + + $image = $imageManipulator->loadFromBuffer($result); + $this->assertEquals(150, $imageManipulator->getImageWidth($image)); + $this->assertEquals(150, $imageManipulator->getImageHeight($image)); + + $result = $thumbnailGenerator->generate( + $this->getTestFile('image.jpg'), + 150, + 150, + \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_INSIDE); + + $image = $imageManipulator->loadFromBuffer($result); + $this->assertEquals(150, $imageManipulator->getImageWidth($image)); + $this->assertEquals(112, $imageManipulator->getImageHeight($image)); + } + + public function testBadThumbnails() + { + $thumbnailGenerator = $this->getThumbnailGenerator(); + $imageManipulator = $this->getImageManipulator(); + + $result = $thumbnailGenerator->generate( + $this->getTestFile('text.txt'), + 150, + 150, + \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_OUTSIDE); + + $this->assertNull($result); + } + + public function getImageManipulator() + { + return \Szurubooru\Injector::get(\Szurubooru\Services\ImageManipulation\ImageManipulator::class); + } + + public function getThumbnailGenerator() + { + return \Szurubooru\Injector::get(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class); + } +} diff --git a/tests/Services/ThumbnailServiceTest.php b/tests/Services/ThumbnailServiceTest.php index 1b1753c0..75ee6199 100644 --- a/tests/Services/ThumbnailServiceTest.php +++ b/tests/Services/ThumbnailServiceTest.php @@ -3,29 +3,18 @@ namespace Szurubooru\Tests\Services; class ThumbnailServiceTest extends \Szurubooru\Tests\AbstractTestCase { - public function testDeleteUsedThumbnails() + private $configMock; + private $fileServiceMock; + private $thumbnailGeneratorMock; + + public function setUp() { - define('DS', DIRECTORY_SEPARATOR); + parent::setUp(); - $tempDirectory = $this->createTestDirectory(); - mkdir($tempDirectory . DS . 'thumbnails'); - mkdir($tempDirectory . DS . 'thumbnails' . DS . '5x5'); - mkdir($tempDirectory . DS . 'thumbnails' . DS . '10x10'); - touch($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'remove'); - touch($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'keep'); - touch($tempDirectory . DS . 'thumbnails' . DS . '10x10' . DS . 'remove'); - - $configMock = $this->mockConfig(null, $tempDirectory); - $httpHelperMock = $this->mock(\Szurubooru\Helpers\HttpHelper::class); - $fileService = new \Szurubooru\Services\FileService($configMock, $httpHelperMock); - $thumbnailGeneratorMock = $this->mock(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class); - - $thumbnailService = new \Szurubooru\Services\ThumbnailService($fileService, $thumbnailGeneratorMock); - $thumbnailService->deleteUsedThumbnails('remove'); - - $this->assertFalse(file_exists($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'remove')); - $this->assertTrue(file_exists($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'keep')); - $this->assertFalse(file_exists($tempDirectory . DS . 'thumbnails' . DS . '10x10' . DS . 'remove')); + $this->configMock = $this->mockConfig(); + $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); + $this->thumbnailServiceMock = $this->mock(\Szurubooru\Services\ThumbnailService::class); + $this->thumbnailGeneratorMock = $this->mock(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class); } public function testGetUsedThumbnailSizes() @@ -36,11 +25,8 @@ class ThumbnailServiceTest extends \Szurubooru\Tests\AbstractTestCase mkdir($tempDirectory . DIRECTORY_SEPARATOR . 'something unexpected'); touch($tempDirectory . DIRECTORY_SEPARATOR . '15x15'); - $fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); - $fileServiceMock->expects($this->once())->method('getFullPath')->willReturn($tempDirectory); - $thumbnailGeneratorMock = $this->mock(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class); - - $thumbnailService = new \Szurubooru\Services\ThumbnailService($fileServiceMock, $thumbnailGeneratorMock); + $this->fileServiceMock->expects($this->once())->method('getFullPath')->with('thumbnails')->willReturn($tempDirectory); + $thumbnailService = $this->getThumbnailService(); $expected = [[5, 5], [10, 10]]; $actual = iterator_to_array($thumbnailService->getUsedThumbnailSizes()); @@ -49,4 +35,101 @@ class ThumbnailServiceTest extends \Szurubooru\Tests\AbstractTestCase foreach ($expected as $v) $this->assertContains($v, $actual); } + + public function testDeleteUsedThumbnails() + { + $tempDirectory = $this->createTestDirectory(); + mkdir($tempDirectory . DIRECTORY_SEPARATOR . '5x5'); + mkdir($tempDirectory . DIRECTORY_SEPARATOR . '10x10'); + touch($tempDirectory . DIRECTORY_SEPARATOR . '5x5' . DIRECTORY_SEPARATOR . 'remove'); + touch($tempDirectory . DIRECTORY_SEPARATOR . '5x5' . DIRECTORY_SEPARATOR . 'keep'); + touch($tempDirectory . DIRECTORY_SEPARATOR . '10x10' . DIRECTORY_SEPARATOR . 'remove'); + + $this->fileServiceMock->expects($this->once())->method('getFullPath')->with('thumbnails')->willReturn($tempDirectory); + $this->fileServiceMock->expects($this->exactly(2))->method('delete')->withConsecutive( + ['thumbnails' . DIRECTORY_SEPARATOR . '10x10' . DIRECTORY_SEPARATOR . 'remove'], + ['thumbnails' . DIRECTORY_SEPARATOR . '5x5' . DIRECTORY_SEPARATOR . 'remove']); + $thumbnailService = $this->getThumbnailService(); + + $thumbnailService->deleteUsedThumbnails('remove'); + } + + public function testGeneratingFromNonExistingSource() + { + $this->configMock->set('misc/thumbnailCropStyle', 'outside'); + + $this->fileServiceMock + ->expects($this->exactly(2)) + ->method('load') + ->withConsecutive( + ['nope'], + ['thumbnails/blank.png']) + ->will( + $this->onConsecutiveCalls( + null, + 'content of blank thumbnail')); + + $this->thumbnailGeneratorMock + ->expects($this->once()) + ->method('generate') + ->with( + 'content of blank thumbnail', + 100, + 100, + \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_OUTSIDE) + ->willReturn('generated thumbnail'); + + $this->fileServiceMock + ->expects($this->once()) + ->method('save') + ->with('thumbnails/100x100/nope', 'generated thumbnail'); + + $thumbnailService = $this->getThumbnailService(); + $thumbnailService->generate('nope', 100, 100); + } + + public function testThumbnailGeneratingFail() + { + $this->configMock->set('misc/thumbnailCropStyle', 'outside'); + + $this->fileServiceMock + ->expects($this->exactly(3)) + ->method('load') + ->withConsecutive( + ['nope'], + ['thumbnails/blank.png'], + ['thumbnails/blank.png']) + ->will( + $this->onConsecutiveCalls( + null, + 'content of blank thumbnail', + 'content of blank thumbnail (2)')); + + $this->thumbnailGeneratorMock + ->expects($this->once()) + ->method('generate') + ->with( + 'content of blank thumbnail', + 100, + 100, + \Szurubooru\Services\ThumbnailGenerators\IThumbnailGenerator::CROP_OUTSIDE) + ->willReturn(null); + + $this->fileServiceMock + ->expects($this->once()) + ->method('save') + ->with('thumbnails/100x100/nope', 'content of blank thumbnail (2)'); + + $thumbnailService = $this->getThumbnailService(); + $thumbnailService->generate('nope', 100, 100); + } + + + private function getThumbnailService() + { + return new \Szurubooru\Services\ThumbnailService( + $this->configMock, + $this->fileServiceMock, + $this->thumbnailGeneratorMock); + } } diff --git a/tests/test_files/text.txt b/tests/test_files/text.txt new file mode 100644 index 00000000..01a59b01 --- /dev/null +++ b/tests/test_files/text.txt @@ -0,0 +1 @@ +lorem ipsum