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\Util
;
14 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
15 use Symfony\Component\Filesystem\Filesystem
;
16 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
;
17 use Symfony\Component\Routing\RouterInterface
;
20 * Helps manage facebook images
26 const fonts
= [ 'default' => 'ttf/default.ttf' ];
31 const font
= 'default';
34 * The default font size
51 const stroke
= '#00c3f9';
56 const align
= 'center';
59 * Creates a new facebook util
61 * @param RouterInterface $router The RouterInterface instance
62 * @param string $cache The cache directory
63 * @param string $path The public path
64 * @param string $prefix The prefix
65 * @param ?string $source The source
66 * @param array $fonts The fonts
67 * @param string $font The font
68 * @param int $size The size
69 * @param int $width The width
70 * @param string $fill The fill
71 * @param string $stroke The stroke
72 * @param string $align The align
74 function __construct(protected RouterInterface
$router, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'facebook', protected ?string $source = null, protected array $fonts = self
::fonts
, protected string $font = self
::font
, protected int $size = self
::size
, protected int $width = self
::width
, protected string $fill = self
::fill
, protected string $stroke = self
::stroke
, protected string $align = self
::align
) {
78 * Return the facebook image
80 * Generate simple image in jpeg format or load it from cache
82 * @param string $pathInfo The request path info
83 * @param array $texts The image texts
84 * @param int $updated The updated timestamp
85 * @param ?string $source The image source
86 * @param int $width The width
87 * @param int $height The height
88 * @return array The image array
90 public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array {
92 if ($source === null && $this->source
=== null) {
93 //Return empty image data
95 //Without local source
96 } elseif ($source === null) {
98 $source = $this->source
;
102 $path = $this->path
.'/'.$this->prefix
.$pathInfo.'.jpeg';
104 //Without existing path
105 if (!is_dir($dir = dirname($path))) {
106 //Create filesystem object
107 $filesystem = new Filesystem();
111 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
112 $filesystem->mkdir($dir, 0775);
113 } catch (IOExceptionInterface
$e) {
115 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
120 if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) {
121 #XXX: we used to drop texts with $data['canonical'] === true !!!
125 'og:image' => $this->router
->generate('rapsys_pack_facebook', ['mtime' => $mtime, 'path' => $pathInfo], UrlGeneratorInterface
::ABSOLUTE_URL
),
126 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
127 'og:image:height' => $height,
128 'og:image:width' => $width
133 $cache = $this->cache
.'/'.$this->prefix
.$pathInfo.'.png';
136 if (!is_dir($dir = dirname($cache))) {
137 //Create filesystem object
138 $filesystem = new Filesystem();
142 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
143 $filesystem->mkdir($dir, 0775);
144 } catch (IOExceptionInterface
$e) {
146 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
150 //Create image object
151 $image = new \
Imagick();
153 //Without cache image
154 if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
155 //Check target directory
156 if (!is_dir($dir = dirname($cache))) {
157 //Create filesystem object
158 $filesystem = new Filesystem();
162 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
163 $filesystem->mkdir($dir, 0775);
164 } catch (IOExceptionInterface
$e) {
166 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
171 if (!is_file($source)) {
173 throw new \
Exception(sprintf('Source file "%s" do not exists', $this->source
));
176 //Convert to absolute path
177 $source = realpath($source);
180 //XXX: Imagick::readImage only supports absolute path
181 $image->readImage($source);
183 //Crop using aspect ratio
184 //XXX: for better result upload image directly in aspect ratio :)
185 $image->cropThumbnailImage($width, $height);
187 //Strip image exif data and properties
188 $image->stripImage();
191 if (!$image->writeImage($cache)) {
193 throw new \
Exception(sprintf('Unable to write image "%s"', $cache));
198 $image->readImage($cache);
202 $draw = new \
ImagickDraw();
204 //Set stroke antialias
205 $draw->setStrokeAntialias(true);
208 $draw->setTextAntialias(true);
212 'left' => \Imagick
::ALIGN_LEFT
,
213 'center' => \Imagick
::ALIGN_CENTER
,
214 'right' => \Imagick
::ALIGN_RIGHT
221 $count = count($texts);
223 //Draw each text stroke
224 foreach($texts as $text => $data) {
226 $draw->setFont($this->fonts
[$data['font']??$this->font
]);
229 $draw->setFontSize($data['size']??$this->size
);
232 $draw->setStrokeWidth($data['width']??$this->width
);
235 $draw->setTextAlignment($align = ($aligns[$data['align']??$this->align
]));
238 $metrics = $image->queryFontMetrics($draw, $text);
241 if (empty($data['y'])) {
242 //Position verticaly each text evenly
243 $texts[$text]['y'] = $data['y'] = (($height +
100) / (count($texts) +
1) * $i) - 50;
247 if (empty($data['x'])) {
248 if ($align == \Imagick
::ALIGN_CENTER
) {
249 $texts[$text]['x'] = $data['x'] = $width/2;
250 } elseif ($align == \Imagick
::ALIGN_LEFT
) {
251 $texts[$text]['x'] = $data['x'] = 50;
252 } elseif ($align == \Imagick
::ALIGN_RIGHT
) {
253 $texts[$text]['x'] = $data['x'] = $width - 50;
258 //XXX: add ascender part then center it back by half of textHeight
259 //TODO: maybe add a boundingbox ???
260 $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2;
263 $draw->setStrokeColor(new \
ImagickPixel($data['stroke']??$this->stroke
));
266 $draw->setFillColor(new \
ImagickPixel($data['stroke']??$this->stroke
));
269 $draw->annotation($data['x'], $data['y'], $text);
275 //Create stroke object
276 $stroke = new \
Imagick();
279 $stroke->newImage($width, $height, new \
ImagickPixel('transparent'));
282 $stroke->drawImage($draw);
285 //XXX: blur the stroke canvas only
286 $stroke->blurImage(5,3);
289 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
290 $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
);
293 $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0);
305 $draw->setTextAntialias(true);
308 foreach($texts as $text => $data) {
310 $draw->setFont($this->fonts
[$data['font']??$this->font
]);
313 $draw->setFontSize($data['size']??$this->size
);
316 $draw->setTextAlignment($aligns[$data['align']??$this->align
]);
319 $draw->setFillColor(new \
ImagickPixel($data['fill']??$this->fill
));
322 $draw->annotation($data['x'], $data['y'], $text);
324 //With canonical text
325 if (!empty($data['canonical'])) {
326 //Prevent canonical to finish in alt
327 unset($texts[$text]);
332 $image->drawImage($draw);
334 //Strip image exif data and properties
335 $image->stripImage();
338 $image->setImageFormat('jpeg');
340 //Set progressive jpeg
341 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
344 if (!$image->writeImage($path)) {
346 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
351 'og:image' => $this->router
->generate('rapsys_pack_facebook', ['mtime' => stat($path)['mtime'], 'path' => $pathInfo], UrlGeneratorInterface
::ABSOLUTE_URL
),
352 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
353 'og:image:height' => $height,
354 'og:image:width' => $width