]> Raphaël G. Git Repositories - packbundle/blob - Controller/ImageController.php
Move package and route configurations in Resources/config/ to config/
[packbundle] / Controller / ImageController.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys PackBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\PackBundle\Controller;
13
14 use Rapsys\PackBundle\Util\ImageUtil;
15 use Rapsys\PackBundle\Util\SluggerUtil;
16
17 use Psr\Container\ContainerInterface;
18
19 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
20 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
21 use Symfony\Component\Filesystem\Filesystem;
22 use Symfony\Component\HttpFoundation\BinaryFileResponse;
23 use Symfony\Component\HttpFoundation\HeaderUtils;
24 use Symfony\Component\HttpFoundation\Request;
25 use Symfony\Component\HttpFoundation\Response;
26 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
27 use Symfony\Component\Routing\RequestContext;
28 use Symfony\Contracts\Service\ServiceSubscriberInterface;
29
30 /**
31 * {@inheritdoc}
32 */
33 class ImageController extends AbstractController implements ServiceSubscriberInterface {
34 /**
35 * Creates a new image controller
36 *
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
43 */
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') {
45 }
46
47 /**
48 * Return captcha image
49 *
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
57 */
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])) {
61 //Throw new exception
62 throw new NotFoundHttpException(sprintf('Unable to match captcha hash: %s', $hash));
63 }
64
65 //Set hashed tree
66 $hashed = array_reverse(str_split(strval($updated)));
67
68 //Set captcha
69 $captcha = $this->path.'/'.$this->prefix.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$equation.'/'.$width.'x'.$height.'.jpeg';
70
71 //Unshort equation
72 $equation = $this->slugger->unshort($equation);
73
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();
80
81 try {
82 //Create path
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) {
87 //Throw error
88 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
89 }
90 }
91
92 //Create image instance
93 $image = new \Imagick();
94
95 //Add imagick draw instance
96 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
97 $draw = new \ImagickDraw();
98
99 //Set text antialias
100 $draw->setTextAntialias(true);
101
102 //Set stroke antialias
103 $draw->setStrokeAntialias(true);
104
105 //Set text alignment
106 $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
107
108 //Set gravity
109 $draw->setGravity(\Imagick::GRAVITY_CENTER);
110
111 //Set fill color
112 $draw->setFillColor($this->image->getFill());
113
114 //Set stroke color
115 $draw->setStrokeColor($this->image->getStroke());
116
117 //Set font size
118 $draw->setFontSize($this->image->getFontSize() / 1.5);
119
120 //Set stroke width
121 $draw->setStrokeWidth($this->image->getStrokeWidth() / 3);
122
123 //Set rotation
124 $draw->rotate($rotate = (rand(25, 75)*(rand(0,1)?-.1:.1)));
125
126 //Get font metrics
127 $metrics2 = $image->queryFontMetrics($draw, strval('stop spam'));
128
129 //Add annotation
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'));
131
132 //Set rotation
133 $draw->rotate(-$rotate);
134
135 //Set font size
136 $draw->setFontSize($this->image->getFontSize());
137
138 //Set stroke width
139 $draw->setStrokeWidth($this->image->getStrokeWidth());
140
141 //Set rotation
142 $draw->rotate($rotate = (rand(25, 50)*(rand(0,1)?-.1:.1)));
143
144 //Get font metrics
145 $metrics = $image->queryFontMetrics($draw, strval($equation));
146
147 //Add annotation
148 $draw->annotation($width / 2, ceil($metrics['textHeight'] + $metrics['descender'] + $metrics['ascender']) / 2 - $this->image->getStrokeWidth(), strval($equation));
149
150 //Set rotation
151 $draw->rotate(-$rotate);
152
153 //Add new image
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');
156
157 //Draw on image
158 $image->drawImage($draw);
159
160 //Strip image exif data and properties
161 $image->stripImage();
162
163 //Set compression quality
164 $image->setImageCompressionQuality(70);
165
166 //Save captcha
167 if (!$image->writeImage($captcha)) {
168 //Throw error
169 throw new \Exception(sprintf('Unable to write image "%s"', $captcha));
170 }
171
172 //Set mtime
173 $mtime = stat($captcha)['mtime'];
174 }
175
176 //Read captcha from cache
177 $response = new BinaryFileResponse($captcha);
178
179 //Set file name
180 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'captcha-stop-spam-'.str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation).'-'.$width.'x'.$height.'.jpeg');
181
182 //Set etag
183 $response->setEtag(md5($hash));
184
185 //Set last modified
186 $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
187
188 //Set as public
189 $response->setPublic();
190
191 //Return 304 response if not modified
192 $response->isNotModified($request);
193
194 //Return response
195 return $response;
196 }
197
198 /**
199 * Return thumb image
200 *
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
208 */
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));
214 }
215
216 //Set hashed tree
217 $hashed = array_reverse(str_split(strval($updated)));
218
219 //Set thumb
220 $thumb = $this->path.'/'.$this->prefix.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$path.'/'.$width.'x'.$height.'.jpeg';
221
222 //Unshort path
223 $path = $this->slugger->unshort($path);
224
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();
231
232 try {
233 //Create path
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) {
238 //Throw error
239 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
240 }
241 }
242
243 //Create image instance
244 $image = new \Imagick();
245
246 //Read image
247 $image->readImage(realpath($path));
248
249 //Crop using aspect ratio
250 //XXX: for better result upload image directly in aspect ratio :)
251 $image->cropThumbnailImage($width, $height);
252
253 //Strip image exif data and properties
254 $image->stripImage();
255
256 //Set compression quality
257 //TODO: ajust that
258 $image->setImageCompressionQuality(70);
259
260 //Save thumb
261 if (!$image->writeImage($thumb)) {
262 //Throw error
263 throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
264 }
265
266 //Set mtime
267 $mtime = stat($thumb)['mtime'];
268 }
269
270 //Read thumb from cache
271 $response = new BinaryFileResponse($thumb);
272
273 //Set file name
274 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.str_replace('/', '_', $path).'-'.$width.'x'.$height.'.jpeg');
275
276 //Set etag
277 $response->setEtag(md5($hash));
278
279 //Set last modified
280 $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
281
282 //Set as public
283 $response->setPublic();
284
285 //Return 304 response if not modified
286 $response->isNotModified($request);
287
288 //Return response
289 return $response;
290 }
291 }