1 <?php
declare(strict_types
=1);
4 * This file is part of the Rapsys PackBundle package.
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Rapsys\PackBundle\Controller
;
14 use Psr\Container\ContainerInterface
;
16 use Symfony\Component\HttpFoundation\HeaderUtils
;
17 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController
;
18 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
19 use Symfony\Component\Filesystem\Filesystem
;
20 use Symfony\Component\HttpFoundation\BinaryFileResponse
;
21 use Symfony\Component\HttpFoundation\Request
;
22 use Symfony\Component\HttpFoundation\Response
;
23 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException
;
24 use Symfony\Component\Routing\RequestContext
;
25 use Symfony\Contracts\Service\ServiceSubscriberInterface
;
27 use Rapsys\PackBundle\Util\ImageUtil
;
28 use Rapsys\PackBundle\Util\SluggerUtil
;
33 class ImageController
extends AbstractController
implements ServiceSubscriberInterface
{
35 * Creates a new image controller
37 * @param ContainerInterface $container The ContainerInterface instance
38 * @param ImageUtil $image The MapUtil instance
39 * @param SluggerUtil $slugger The SluggerUtil instance
40 * @param string $cache The cache path
41 * @param string $path The public path
42 * @param string $prefix The prefix
44 function __construct(protected ContainerInterface
$container, protected ImageUtil
$image, protected SluggerUtil
$slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'image') {
48 * Return captcha image
50 * @param Request $request The Request instance
51 * @param string $hash The hash
52 * @param int $updated The updated timestamp
53 * @param string $equation The shorted equation
54 * @param int $width The width
55 * @param int $height The height
56 * @return Response The rendered image
58 public function captcha(Request
$request, string $hash, int $updated, string $equation, int $width, int $height): Response
{
59 //Without matching hash
60 if ($hash !== $this->slugger
->serialize([$updated, $equation, $width, $height])) {
62 throw new NotFoundHttpException(sprintf('Unable to match captcha hash: %s', $hash));
66 $hashed = array_reverse(str_split(strval($updated)));
69 $captcha = $this->path
.'/'.$this->prefix
.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$equation.'/'.$width.'x'.$height.'.jpeg';
72 $equation = $this->slugger
->unshort($equation);
74 //Without captcha up to date file
75 if (!is_file($captcha) || !($mtime = stat($captcha)['mtime']) || $mtime < $updated) {
76 //Without existing captcha path
77 if (!is_dir($dir = dirname($captcha))) {
78 //Create filesystem object
79 $filesystem = new Filesystem();
83 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
84 //XXX: on CoW filesystems execute a chattr +C before filling
85 $filesystem->mkdir($dir, 0775);
86 } catch (IOExceptionInterface
$e) {
88 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
92 //Create image instance
93 $image = new \
Imagick();
95 //Add imagick draw instance
96 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
97 $draw = new \
ImagickDraw();
100 $draw->setTextAntialias(true);
102 //Set stroke antialias
103 $draw->setStrokeAntialias(true);
106 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
109 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
112 $draw->setFillColor($this->image
->getFill());
115 $draw->setStrokeColor($this->image
->getStroke());
118 $draw->setFontSize($this->image
->getFontSize() / 1.5);
121 $draw->setStrokeWidth($this->image
->getStrokeWidth() / 3);
124 $draw->rotate($rotate = (rand(25, 75)*(rand(0,1)?-.1:.1)));
127 $metrics2 = $image->queryFontMetrics($draw, strval('stop spam'));
130 $draw->annotation($width / 2 - ceil(rand(intval(-$metrics2['textWidth']), intval($metrics2['textWidth'])) / 2) - abs($rotate), ceil($metrics2['textHeight'] +
$metrics2['descender'] +
$metrics2['ascender']) / 2 - $this->image
->getStrokeWidth() - $rotate, strval('stop spam'));
133 $draw->rotate(-$rotate);
136 $draw->setFontSize($this->image
->getFontSize());
139 $draw->setStrokeWidth($this->image
->getStrokeWidth());
142 $draw->rotate($rotate = (rand(25, 50)*(rand(0,1)?-.1:.1)));
145 $metrics = $image->queryFontMetrics($draw, strval($equation));
148 $draw->annotation($width / 2, ceil($metrics['textHeight'] +
$metrics['descender'] +
$metrics['ascender']) / 2 - $this->image
->getStrokeWidth(), strval($equation));
151 $draw->rotate(-$rotate);
154 #$image->newImage(intval(ceil($metrics['textWidth'])), intval(ceil($metrics['textHeight'] + $metrics['descender'])), new \ImagickPixel($this->image->getBackground()), 'jpeg');
155 $image->newImage($width, $height, new \
ImagickPixel($this->image
->getBackground()), 'jpeg');
158 $image->drawImage($draw);
160 //Strip image exif data and properties
161 $image->stripImage();
163 //Set compression quality
164 $image->setImageCompressionQuality(70);
167 if (!$image->writeImage($captcha)) {
169 throw new \
Exception(sprintf('Unable to write image "%s"', $captcha));
173 $mtime = stat($captcha)['mtime'];
176 //Read captcha from cache
177 $response = new BinaryFileResponse($captcha);
180 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'captcha-stop-spam-'.str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation).'-'.$width.'x'.$height.'.jpeg');
183 $response->setEtag(md5($hash));
186 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
189 $response->setPublic();
191 //Return 304 response if not modified
192 $response->isNotModified($request);
201 * @param Request $request The Request instance
202 * @param string $hash The hash
203 * @param int $updated The updated timestamp
204 * @param string $path The image path
205 * @param int $width The width
206 * @param int $height The height
207 * @return Response The rendered image
209 public function thumb(Request
$request, string $hash, int $updated, string $path, int $width, int $height): Response
{
210 //Without matching hash
211 if ($hash !== $this->slugger
->serialize([$updated, $path, $width, $height])) {
212 //Throw new exception
213 throw new NotFoundHttpException(sprintf('Unable to match thumb hash: %s', $hash));
217 $hashed = array_reverse(str_split(strval($updated)));
220 $thumb = $this->path
.'/'.$this->prefix
.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$path.'/'.$width.'x'.$height.'.jpeg';
223 $path = $this->slugger
->unshort($path);
225 //Without thumb up to date file
226 if (!is_file($thumb) || !($mtime = stat($thumb)['mtime']) || $mtime < $updated) {
227 //Without existing thumb path
228 if (!is_dir($dir = dirname($thumb))) {
229 //Create filesystem object
230 $filesystem = new Filesystem();
234 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
235 //XXX: on CoW filesystems execute a chattr +C before filling
236 $filesystem->mkdir($dir, 0775);
237 } catch (IOExceptionInterface
$e) {
239 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
243 //Create image instance
244 $image = new \
Imagick();
247 $image->readImage(realpath($path));
249 //Crop using aspect ratio
250 //XXX: for better result upload image directly in aspect ratio :)
251 $image->cropThumbnailImage($width, $height);
253 //Strip image exif data and properties
254 $image->stripImage();
256 //Set compression quality
258 $image->setImageCompressionQuality(70);
261 if (!$image->writeImage($thumb)) {
263 throw new \
Exception(sprintf('Unable to write image "%s"', $thumb));
267 $mtime = stat($thumb)['mtime'];
270 //Read thumb from cache
271 $response = new BinaryFileResponse($thumb);
274 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'thumb-'.str_replace('/', '_', $path).'-'.$width.'x'.$height.'.jpeg');
277 $response->setEtag(md5($hash));
280 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
283 $response->setPublic();
285 //Return 304 response if not modified
286 $response->isNotModified($request);