Refactored thumbnail system
This commit is contained in:
parent
fbdb4e5128
commit
42001d3edf
26 changed files with 817 additions and 334 deletions
5
TODO
5
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
|
||||
|
|
1
public_html/data/.gitignore
vendored
1
public_html/data/.gitignore
vendored
|
@ -1,2 +1 @@
|
|||
posts
|
||||
thumbnails
|
||||
|
|
3
public_html/data/thumbnails/.gitignore
vendored
Normal file
3
public_html/data/thumbnails/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!.gitignore
|
||||
!blank.png
|
BIN
public_html/data/thumbnails/blank.png
Normal file
BIN
public_html/data/thumbnails/blank.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
26
src/Helpers/ProgramExecutor.php
Normal file
26
src/Helpers/ProgramExecutor.php
Normal 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;
|
||||
}
|
||||
}
|
93
src/Services/ImageManipulation/GdImageManipulator.php
Normal file
93
src/Services/ImageManipulation/GdImageManipulator.php
Normal 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;
|
||||
}
|
||||
}
|
20
src/Services/ImageManipulation/IImageManipulator.php
Normal file
20
src/Services/ImageManipulation/IImageManipulator.php
Normal 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);
|
||||
}
|
55
src/Services/ImageManipulation/ImageManipulator.php
Normal file
55
src/Services/ImageManipulation/ImageManipulator.php
Normal 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);
|
||||
}
|
||||
}
|
60
src/Services/ImageManipulation/ImagickImageManipulator.php
Normal file
60
src/Services/ImageManipulation/ImagickImageManipulator.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
try
|
||||
{
|
||||
$image = $this->imageManipulator->loadFromBuffer($source);
|
||||
$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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
$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';
|
||||
}
|
||||
}
|
||||
|
|
10
tests/Helpers/ProgramExecutorTest.php
Normal file
10
tests/Helpers/ProgramExecutorTest.php
Normal 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'));
|
||||
}
|
||||
}
|
114
tests/Services/ImageManipulatorTest.php
Normal file
114
tests/Services/ImageManipulatorTest.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
99
tests/Services/ThumbnailGeneratorTest.php
Normal file
99
tests/Services/ThumbnailGeneratorTest.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
1
tests/test_files/text.txt
Normal file
1
tests/test_files/text.txt
Normal file
|
@ -0,0 +1 @@
|
|||
lorem ipsum
|
Loading…
Reference in a new issue