Refactored thumbnail system

This commit is contained in:
Marcin Kurczewski 2014-09-20 18:30:48 +02:00
parent fbdb4e5128
commit 42001d3edf
26 changed files with 817 additions and 334 deletions

5
TODO
View file

@ -6,11 +6,6 @@ everything related to posts:
- fav count - fav count
- score - score
- comment count - 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 - single post view
- post content - post content

View file

@ -1,2 +1 @@
posts posts
thumbnails

View file

@ -0,0 +1,3 @@
*
!.gitignore
!blank.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -29,18 +29,19 @@ final class PostContentController extends AbstractController
public function getPostContent($postName) public function getPostContent($postName)
{ {
$post = $this->postService->getByName($postName); $post = $this->postService->getByName($postName);
$source = $post->getContentPath(); $this->fileService->serve($post->getContentPath());
$this->fileService->serve($source);
} }
public function getPostThumbnail($postName, $size) public function getPostThumbnail($postName, $size)
{ {
$post = $this->postService->getByName($postName); $post = $this->postService->getByName($postName);
$source = $post->getThumbnailSourceContentPath();
if (!$this->fileService->exists($source))
$source = $post->getContentPath();
$sizedSource = $this->thumbnailService->getOrGenerate($source, $size, $size); $sourceName = $post->getThumbnailSourceContentPath();
$this->fileService->serve($sizedSource); 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);
} }
} }

View file

@ -38,7 +38,7 @@ final class UserAvatarController extends AbstractController
break; break;
case \Szurubooru\Entities\User::AVATAR_STYLE_BLANK: case \Szurubooru\Entities\User::AVATAR_STYLE_BLANK:
$this->serveFromFile($this->getBlankAvatarSourcePath(), $size); $this->serveFromFile($this->getBlankAvatarSourceContentPath(), $size);
break; break;
case \Szurubooru\Entities\User::AVATAR_STYLE_MANUAL: case \Szurubooru\Entities\User::AVATAR_STYLE_MANUAL:
@ -46,7 +46,7 @@ final class UserAvatarController extends AbstractController
break; break;
default: default:
$this->serveFromFile($this->getBlankAvatarSourcePath(), $size); $this->serveFromFile($this->getBlankAvatarSourceContentPath(), $size);
break; break;
} }
} }
@ -56,17 +56,15 @@ final class UserAvatarController extends AbstractController
$this->httpHelper->redirect($url); $this->httpHelper->redirect($url);
} }
private function serveFromFile($file, $size) private function serveFromFile($sourceName, $size)
{ {
if (!$this->fileService->exists($file)) $this->thumbnailService->generateIfNeeded($sourceName, $size, $size);
$file = $this->getBlankAvatarSourcePath(); $thumbnailName = $this->thumbnailService->getThumbnailName($sourceName, $size, $size);
$this->fileService->serve($thumbnailName);
$sizedFile = $this->thumbnailService->getOrGenerate($file, $size, $size);
$this->fileService->serve($sizedFile);
} }
private function getBlankAvatarSourcePath() private function getBlankAvatarSourceContentPath()
{ {
return 'avatars' . DIRECTORY_SEPARATOR . 'blank.png'; return 'avatars/blank.png';
} }
} }

View file

@ -0,0 +1,26 @@
<?php
namespace Szurubooru\Helpers;
class ProgramExecutor
{
public static function run($programName, $arguments)
{
$quotedArguments = array_map('escapeshellarg', $arguments);
$cmd = sprintf('%s %s 2>&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;
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace Szurubooru\Services\ImageManipulation;
class GdImageManipulator implements IImageManipulator
{
public function loadFromBuffer($source)
{
try
{
return imagecreatefromstring($source);
}
catch (\Exception $e)
{
return null;
}
}
public function getImageWidth($imageResource)
{
return imagesx($imageResource);
}
public function getImageHeight($imageResource)
{
return imagesy($imageResource);
}
public function resizeImage(&$imageResource, $width, $height)
{
$targetImageResource = imagecreatetruecolor($width, $height);
imagecopyresampled(
$targetImageResource,
$imageResource,
0, 0,
0, 0,
$width,
$height,
imagesx($imageResource),
imagesy($imageResource));
$imageResource = $targetImageResource;
}
public function cropImage(&$imageResource, $width, $height, $originX, $originY)
{
if ($originX + $width > 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;
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Szurubooru\Services\ImageManipulation;
interface IImageManipulator
{
const FORMAT_JPEG = 0;
const FORMAT_PNG = 1;
public function loadFromBuffer($source);
public function getImageWidth($imageResource);
public function getImageHeight($imageResource);
public function resizeImage(&$imageResource, $width, $height);
public function cropImage(&$imageResource, $width, $height, $originX, $originY);
public function saveToBuffer($imageResource, $format);
}

View file

@ -0,0 +1,55 @@
<?php
namespace Szurubooru\Services\ImageManipulation;
class ImageManipulator implements IImageManipulator
{
private $strategy;
public function __construct(
\Szurubooru\Services\ImageManipulation\ImagickImageManipulator $imagickImageManipulator,
\Szurubooru\Services\ImageManipulation\GdImageManipulator $gdImageManipulator)
{
if (extension_loaded('imagick'))
{
$this->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);
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Szurubooru\Services\ImageManipulation;
class ImagickImageManipulator implements IImageManipulator
{
public function loadFromBuffer($source)
{
try
{
$image = new \Imagick();
$image->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();
}
}

View file

@ -11,6 +11,7 @@ class PostService
private $timeService; private $timeService;
private $authService; private $authService;
private $fileService; private $fileService;
private $imageManipulator;
public function __construct( public function __construct(
\Szurubooru\Config $config, \Szurubooru\Config $config,
@ -20,7 +21,8 @@ class PostService
\Szurubooru\Dao\Services\PostSearchService $postSearchService, \Szurubooru\Dao\Services\PostSearchService $postSearchService,
\Szurubooru\Services\AuthService $authService, \Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\TimeService $timeService, \Szurubooru\Services\TimeService $timeService,
\Szurubooru\Services\FileService $fileService) \Szurubooru\Services\FileService $fileService,
\Szurubooru\Services\ImageManipulation\ImageManipulator $imageManipulator)
{ {
$this->config = $config; $this->config = $config;
$this->validator = $validator; $this->validator = $validator;
@ -30,6 +32,7 @@ class PostService
$this->timeService = $timeService; $this->timeService = $timeService;
$this->authService = $authService; $this->authService = $authService;
$this->fileService = $fileService; $this->fileService = $fileService;
$this->imageManipulator = $imageManipulator;
} }
public function getByNameOrId($postNameOrId) public function getByNameOrId($postNameOrId)
@ -137,9 +140,9 @@ class PostService
$post->setContent($content); $post->setContent($content);
list ($imageWidth, $imageHeight) = getimagesizefromstring($content); $image = $this->imageManipulator->loadFromBuffer($content);
$post->setImageWidth($imageWidth); $post->setImageWidth($this->imageManipulator->getImageWidth($image));
$post->setImageHeight($imageHeight); $post->setImageHeight($this->imageManipulator->getImageHeight($image));
$post->setOriginalFileSize(strlen($content)); $post->setOriginalFileSize(strlen($content));
} }

View file

@ -3,6 +3,9 @@ namespace Szurubooru\Services\ThumbnailGenerators;
class FlashThumbnailGenerator implements IThumbnailGenerator class FlashThumbnailGenerator implements IThumbnailGenerator
{ {
const PROGRAM_NAME_DUMP_GNASH = 'dump-gnash';
const PROGRAM_NAME_SWFRENDER = 'swfrender';
private $imageThumbnailGenerator; private $imageThumbnailGenerator;
public function __construct(ImageThumbnailGenerator $imageThumbnailGenerator) public function __construct(ImageThumbnailGenerator $imageThumbnailGenerator)
@ -10,35 +13,47 @@ class FlashThumbnailGenerator implements IThumbnailGenerator
$this->imageThumbnailGenerator = $imageThumbnailGenerator; $this->imageThumbnailGenerator = $imageThumbnailGenerator;
} }
public function generate($srcPath, $dstPath, $width, $height) public function generate($source, $width, $height, $cropStyle)
{ {
if (!file_exists($srcPath)) $tmpSourcePath = tempnam(sys_get_temp_dir(), 'thumb') . '.dat';
throw new \InvalidArgumentException($srcPath . ' does not exist'); $tmpTargetPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
file_put_contents($tmpSourcePath, $source);
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png'; if (\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_DUMP_GNASH))
$cmd = sprintf(
'dump-gnash --screenshot last --screenshot-file "%s" -1 -r1 --max-advances 15 "%s"',
$tmpPath,
$srcPath);
exec($cmd);
if (file_exists($tmpPath))
{ {
$this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); \Szurubooru\Helpers\ProgramExecutor::run(
unlink($tmpPath); self::PROGRAM_NAME_DUMP_GNASH,
return; [
'--screenshot', 'last',
'--screenshot-file', $tmpTargetPath,
'-1',
'-r1',
'--max-advances', '15',
$tmpSourcePath,
]);
} }
exec('swfrender ' . $srcPath . ' -o ' . $tmpPath); if (!file_exists($tmpTargetPath) and \Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_SWFRENDER))
if (file_exists($tmpPath))
{ {
$this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); \Szurubooru\Helpers\ProgramExecutor::run(
unlink($tmpPath); self::PROGRAM_NAME_SWFRENDER,
return; [
'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;
} }
} }

View file

@ -3,5 +3,8 @@ namespace Szurubooru\Services\ThumbnailGenerators;
interface IThumbnailGenerator interface IThumbnailGenerator
{ {
public function generate($srcPath, $dstPath, $width, $height); const CROP_OUTSIDE = 0;
const CROP_INSIDE = 1;
public function generate($sourceString, $width, $height, $cropStyle);
} }

View file

@ -1,98 +0,0 @@
<?php
namespace Szurubooru\Services\ThumbnailGenerators;
class ImageGdThumbnailGenerator implements IThumbnailGenerator
{
private $config;
public function __construct(\Szurubooru\Config $config)
{
$this->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;
}
}

View file

@ -1,78 +0,0 @@
<?php
namespace Szurubooru\Services\ThumbnailGenerators;
class ImageImagickThumbnailGenerator implements IThumbnailGenerator
{
private $config;
public function __construct(\Szurubooru\Config $config)
{
$this->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);
}
}

View file

@ -3,26 +3,77 @@ namespace Szurubooru\Services\ThumbnailGenerators;
class ImageThumbnailGenerator implements IThumbnailGenerator class ImageThumbnailGenerator implements IThumbnailGenerator
{ {
private $imageImagickThumbnailGenerator; private $imageManipulator;
private $imageGdThumbnailGenerator;
public function __construct( public function __construct(\Szurubooru\Services\ImageManipulation\ImageManipulator $imageManipulator)
ImageImagickThumbnailGenerator $imageImagickThumbnailGenerator,
ImageGdThumbnailGenerator $imageGdThumbnailGenerator)
{ {
$this->imageImagickThumbnailGenerator = $imageImagickThumbnailGenerator; $this->imageManipulator = $imageManipulator;
$this->imageGdThumbnailGenerator = $imageGdThumbnailGenerator;
} }
public function generate($srcPath, $dstPath, $width, $height) public function generate($source, $width, $height, $cropStyle)
{ {
if (extension_loaded('imagick')) try
$strategy = $this->imageImagickThumbnailGenerator; {
elseif (extension_loaded('gd')) $image = $this->imageManipulator->loadFromBuffer($source);
$strategy = $this->imageGdThumbnailGenerator; $srcWidth = $this->imageManipulator->getImageWidth($image);
$srcHeight = $this->imageManipulator->getImageHeight($image);
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 else
throw new \Exception('Both imagick and gd extensions are disabled'); {
$cropWidth = $dstWidth;
$cropHeight = $dstWidth * $srcHeight / $srcWidth;
}
$cropX = ($cropWidth - $dstWidth) >> 1;
$cropY = ($cropHeight - $dstHeight) >> 1;
return $strategy->generate($srcPath, $dstPath, $width, $height); $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);
} }
} }

View file

@ -17,22 +17,19 @@ class SmartThumbnailGenerator implements IThumbnailGenerator
$this->imageThumbnailGenerator = $imageThumbnailGenerator; $this->imageThumbnailGenerator = $imageThumbnailGenerator;
} }
public function generate($srcPath, $dstPath, $width, $height) public function generate($source, $width, $height, $cropStyle)
{ {
if (!file_exists($srcPath)) $mime = \Szurubooru\Helpers\MimeHelper::getMimeTypeFromBuffer($source);
throw new \InvalidArgumentException($srcPath . ' does not exist');
$mime = \Szurubooru\Helpers\MimeHelper::getMimeTypeFromFile($srcPath);
if (\Szurubooru\Helpers\MimeHelper::isFlash($mime)) 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)) 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)) 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;
} }
} }

View file

@ -3,6 +3,9 @@ namespace Szurubooru\Services\ThumbnailGenerators;
class VideoThumbnailGenerator implements IThumbnailGenerator class VideoThumbnailGenerator implements IThumbnailGenerator
{ {
const PROGRAM_NAME_FFMPEG = 'ffmpeg';
const PROGRAM_NAME_FFMPEGTHUMBNAILER = 'ffmpegthumbnailer';
private $imageThumbnailGenerator; private $imageThumbnailGenerator;
public function __construct(ImageThumbnailGenerator $imageThumbnailGenerator) public function __construct(ImageThumbnailGenerator $imageThumbnailGenerator)
@ -10,39 +13,43 @@ class VideoThumbnailGenerator implements IThumbnailGenerator
$this->imageThumbnailGenerator = $imageThumbnailGenerator; $this->imageThumbnailGenerator = $imageThumbnailGenerator;
} }
public function generate($srcPath, $dstPath, $width, $height) public function generate($source, $width, $height, $cropStyle)
{ {
if (!file_exists($srcPath)) $tmpSourcePath = tempnam(sys_get_temp_dir(), 'thumb') . '.dat';
throw new \InvalidArgumentException($srcPath . ' does not exist'); $tmpTargetPath = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
file_put_contents($tmpSourcePath, $source);
$tmpPath = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg'; if (\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_FFMPEGTHUMBNAILER))
$cmd = sprintf(
'ffmpegthumbnailer -i"%s" -o"%s" -s0 -t"12%%"',
$srcPath,
$tmpPath);
exec($cmd);
if (file_exists($tmpPath))
{ {
$this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); \Szurubooru\Helpers\ProgramExecutor::run(
unlink($tmpPath); self::PROGRAM_NAME_FFMPEGTHUMBNAILER,
return; [
'-i' . $tmpSourcePath,
'-o' . $tmpTargetPath,
'-s0',
'-t12%%'
]);
} }
$cmd = sprintf( if (!file_exists($tmpTargetPath) and \Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(self::PROGRAM_NAME_FFMPEG))
'ffmpeg -i "%s" -vframes 1 "%s"',
$srcPath,
$tmpPath);
exec($cmd);
if (file_exists($tmpPath))
{ {
$this->imageThumbnailGenerator->generate($tmpPath, $dstPath, $width, $height); \Szurubooru\Helpers\ProgramExecutor::run(self::PROGRAM_NAME_FFMEPG,
unlink($tmpPath); [
return; '-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;
} }
} }

View file

@ -3,48 +3,69 @@ namespace Szurubooru\Services;
class ThumbnailService class ThumbnailService
{ {
private $config;
private $fileService; private $fileService;
private $thumbnailGenerator; private $thumbnailGenerator;
public function __construct( public function __construct(
FileService $fileService, \Szurubooru\Config $config,
ThumbnailGenerators\SmartThumbnailGenerator $thumbnailGenerator) \Szurubooru\Services\FileService $fileService,
\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator $thumbnailGenerator)
{ {
$this->config = $config;
$this->fileService = $fileService; $this->fileService = $fileService;
$this->thumbnailGenerator = $thumbnailGenerator; $this->thumbnailGenerator = $thumbnailGenerator;
} }
public function getOrGenerate($source, $width, $height) public function deleteUsedThumbnails($sourceName)
{
$target = $this->getPath($source, $width, $height);
if (!$this->fileService->exists($target))
$this->generate($source, $width, $height);
return $target;
}
public function deleteUsedThumbnails($source)
{ {
foreach ($this->getUsedThumbnailSizes() as $size) foreach ($this->getUsedThumbnailSizes() as $size)
{ {
list ($width, $height) = $size; list ($width, $height) = $size;
$target = $this->getPath($source, $width, $height); $target = $this->getThumbnailName($sourceName, $width, $height);
if ($this->fileService->exists($target))
$this->fileService->delete($target); $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); if (!$this->fileService->exists($thumbnailName))
$fullTarget = $this->fileService->getFullPath($target); $this->generate($sourceName, $width, $height);
$this->fileService->createFolders($target); }
$this->thumbnailGenerator->generate($fullSource, $fullTarget, $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() 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; return 'thumbnails' . DIRECTORY_SEPARATOR . $width . 'x' . $height . DIRECTORY_SEPARATOR . $source;
} }
public function getBlankThumbnailName()
{
return 'thumbnails' . DIRECTORY_SEPARATOR . 'blank.png';
}
} }

View file

@ -0,0 +1,10 @@
<?php
namespace Szurubooru\Tests;
class ProgramExecutorTest extends \Szurubooru\Tests\AbstractTestCase
{
public function testIsProgramAvailable()
{
$this->assertFalse(\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable('there_is_no_way_my_os_can_have_this_program'));
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace Szurubooru\Tests\Services;
class ImageManipulatorTest extends \Szurubooru\Tests\AbstractTestCase
{
private $imageManipulators;
public function setUp()
{
parent::setUp();
$imagickImageManipulator = new \Szurubooru\Services\ImageManipulation\ImagickImageManipulator();
$gdImageManipulator = new \Szurubooru\Services\ImageManipulation\GdImageManipulator();
$autoImageManipulator = new \Szurubooru\Services\ImageManipulation\ImageManipulator(
$imagickImageManipulator,
$gdImageManipulator);
$this->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;
}
}

View file

@ -11,6 +11,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
private $authServiceMock; private $authServiceMock;
private $timeServiceMock; private $timeServiceMock;
private $fileServiceMock; private $fileServiceMock;
private $imageManipulatorMock;
public function setUp() public function setUp()
{ {
@ -23,9 +24,9 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
$this->configMock->set('database/maxPostSize', 1000000); $this->configMock->set('database/maxPostSize', 1000000);
$this->imageManipulatorMock = $this->mock(\Szurubooru\Services\ImageManipulation\ImageManipulator::class);
} }
public function testCreatingYoutubePost() public function testCreatingYoutubePost()
{ {
$formData = new \Szurubooru\FormData\UploadFormData; $formData = new \Szurubooru\FormData\UploadFormData;
@ -60,15 +61,17 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$formData->contentFileName = 'blah'; $formData->contentFileName = 'blah';
$this->postDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $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(); $this->postService = $this->getPostService();
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
$this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_IMAGE, $savedPost->getContentType()); $this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_IMAGE, $savedPost->getContentType());
$this->assertEquals('24216edd12328de3a3c55e2f98220ee7613e3be1', $savedPost->getContentChecksum()); $this->assertEquals('24216edd12328de3a3c55e2f98220ee7613e3be1', $savedPost->getContentChecksum());
$this->assertEquals(640, $savedPost->getImageWidth());
$this->assertEquals(480, $savedPost->getImageHeight());
$this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName()); $this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName());
$this->assertEquals(687645, $savedPost->getOriginalFileSize()); $this->assertEquals(687645, $savedPost->getOriginalFileSize());
$this->assertEquals(640, $savedPost->getImageWidth());
$this->assertEquals(480, $savedPost->getImageHeight());
} }
public function testCreatingVideos() public function testCreatingVideos()
@ -85,8 +88,6 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
$this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_VIDEO, $savedPost->getContentType()); $this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_VIDEO, $savedPost->getContentType());
$this->assertEquals('16dafaa07cda194d03d590529c06c6ec1a5b80b0', $savedPost->getContentChecksum()); $this->assertEquals('16dafaa07cda194d03d590529c06c6ec1a5b80b0', $savedPost->getContentChecksum());
$this->assertNull($savedPost->getImageWidth());
$this->assertNull($savedPost->getImageHeight());
$this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName()); $this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName());
$this->assertEquals(14667, $savedPost->getOriginalFileSize()); $this->assertEquals(14667, $savedPost->getOriginalFileSize());
} }
@ -105,8 +106,6 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$savedPost = $this->postService->createPost($formData); $savedPost = $this->postService->createPost($formData);
$this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_FLASH, $savedPost->getContentType()); $this->assertEquals(\Szurubooru\Entities\Post::POST_TYPE_FLASH, $savedPost->getContentType());
$this->assertEquals('d897e044b801d892291b440534c3be3739034f68', $savedPost->getContentChecksum()); $this->assertEquals('d897e044b801d892291b440534c3be3739034f68', $savedPost->getContentChecksum());
$this->assertEquals(320, $savedPost->getImageWidth());
$this->assertEquals(240, $savedPost->getImageHeight());
$this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName()); $this->assertEquals($formData->contentFileName, $savedPost->getOriginalFileName());
$this->assertEquals(226172, $savedPost->getOriginalFileSize()); $this->assertEquals(226172, $savedPost->getOriginalFileSize());
} }
@ -180,6 +179,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->postSearchServiceMock, $this->postSearchServiceMock,
$this->authServiceMock, $this->authServiceMock,
$this->timeServiceMock, $this->timeServiceMock,
$this->fileServiceMock); $this->fileServiceMock,
$this->imageManipulatorMock);
} }
} }

View file

@ -0,0 +1,99 @@
<?php
namespace Szurubooru\Tests\Services;
class ThumbnailGeneratorTest extends \Szurubooru\Tests\AbstractTestCase
{
public function testFlashThumbnails()
{
if (!\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(\Szurubooru\Services\ThumbnailGenerators\FlashThumbnailGenerator::PROGRAM_NAME_DUMP_GNASH)
and !\Szurubooru\Helpers\ProgramExecutor::isProgramAvailable(\Szurubooru\Services\ThumbnailGenerators\FlashThumbnailGenerator::PROGRAM_NAME_SWFRENDER))
{
$this->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);
}
}

View file

@ -3,29 +3,18 @@ namespace Szurubooru\Tests\Services;
class ThumbnailServiceTest extends \Szurubooru\Tests\AbstractTestCase 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(); $this->configMock = $this->mockConfig();
mkdir($tempDirectory . DS . 'thumbnails'); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
mkdir($tempDirectory . DS . 'thumbnails' . DS . '5x5'); $this->thumbnailServiceMock = $this->mock(\Szurubooru\Services\ThumbnailService::class);
mkdir($tempDirectory . DS . 'thumbnails' . DS . '10x10'); $this->thumbnailGeneratorMock = $this->mock(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class);
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'));
} }
public function testGetUsedThumbnailSizes() public function testGetUsedThumbnailSizes()
@ -36,11 +25,8 @@ class ThumbnailServiceTest extends \Szurubooru\Tests\AbstractTestCase
mkdir($tempDirectory . DIRECTORY_SEPARATOR . 'something unexpected'); mkdir($tempDirectory . DIRECTORY_SEPARATOR . 'something unexpected');
touch($tempDirectory . DIRECTORY_SEPARATOR . '15x15'); touch($tempDirectory . DIRECTORY_SEPARATOR . '15x15');
$fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); $this->fileServiceMock->expects($this->once())->method('getFullPath')->with('thumbnails')->willReturn($tempDirectory);
$fileServiceMock->expects($this->once())->method('getFullPath')->willReturn($tempDirectory); $thumbnailService = $this->getThumbnailService();
$thumbnailGeneratorMock = $this->mock(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class);
$thumbnailService = new \Szurubooru\Services\ThumbnailService($fileServiceMock, $thumbnailGeneratorMock);
$expected = [[5, 5], [10, 10]]; $expected = [[5, 5], [10, 10]];
$actual = iterator_to_array($thumbnailService->getUsedThumbnailSizes()); $actual = iterator_to_array($thumbnailService->getUsedThumbnailSizes());
@ -49,4 +35,101 @@ class ThumbnailServiceTest extends \Szurubooru\Tests\AbstractTestCase
foreach ($expected as $v) foreach ($expected as $v)
$this->assertContains($v, $actual); $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);
}
} }

View file

@ -0,0 +1 @@
lorem ipsum