Skip to content

Commit

Permalink
Add favicon generation functionalities (#189)
Browse files Browse the repository at this point in the history
Introduce favicon generation capabilities using the GDImage library and workbox integration for service worker builds. This includes browser config for Windows 8+ tile architecture, supporting different image sizes and output formats. New validations in the configuration test ensure that these additions function as expected. Modifications in the service worker compiler class aim at better usage of interfaces and performance improvements.
  • Loading branch information
Spomky authored Apr 29, 2024
1 parent 826b264 commit 83644b7
Show file tree
Hide file tree
Showing 18 changed files with 761 additions and 82 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"ekino/phpstan-banned-code": "^1.0",
"ergebnis/phpunit-slow-test-detector": "^2.14",
"infection/infection": "^0.28",
"matthiasnoback/symfony-config-test": "^5.1",
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpstan/extension-installer": "^1.1",
"phpstan/phpdoc-parser": "^1.28",
Expand Down
34 changes: 22 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ parameters:
count: 1
path: src/Dto/BackgroundSync.php

-
message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\Favicons has an uninitialized property \\$src\\. Give it default value or assign it in the constructor\\.$#"
count: 1
path: src/Dto/Favicons.php

-
message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\File has an uninitialized property \\$accept\\. Give it default value or assign it in the constructor\\.$#"
count: 1
Expand Down Expand Up @@ -440,14 +445,24 @@ parameters:
count: 1
path: src/ImageProcessor/GDImageProcessor.php

-
message: "#^Parameter \\#1 \\$dst_image of function imagecopyresampled expects GdImage, GdImage\\|false given\\.$#"
count: 1
path: src/ImageProcessor/GDImageProcessor.php

-
message: "#^Parameter \\#1 \\$image of function imagealphablending expects GdImage, GdImage\\|false given\\.$#"
count: 1
path: src/ImageProcessor/GDImageProcessor.php

-
message: "#^Parameter \\#1 \\$image of function imagepng expects GdImage, GdImage\\|false given\\.$#"
count: 1
path: src/ImageProcessor/GDImageProcessor.php

-
message: "#^Parameter \\#1 \\$image of function imagesavealpha expects GdImage, GdImage\\|false given\\.$#"
count: 1
count: 2
path: src/ImageProcessor/GDImageProcessor.php

-
Expand Down Expand Up @@ -485,6 +500,11 @@ parameters:
count: 1
path: src/Normalizer/ServiceWorkerNormalizer.php

-
message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#"
count: 1
path: src/Resources/config/definition/favicons.php

-
message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#"
count: 1
Expand Down Expand Up @@ -623,14 +643,4 @@ parameters:
-
message: "#^Method SpomkyLabs\\\\PwaBundle\\\\SpomkyLabsPwaBundle\\:\\:loadExtension\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
count: 1
path: src/SpomkyLabsPwaBundle.php

-
message: "#^Property SpomkyLabs\\\\PwaBundle\\\\Subscriber\\\\ManifestCompileEventListener\\:\\:\\$jsonOptions type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Subscriber/ManifestCompileEventListener.php

-
message: "#^Property SpomkyLabs\\\\PwaBundle\\\\Subscriber\\\\PwaDevServerSubscriber\\:\\:\\$jsonOptions type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Subscriber/PwaDevServerSubscriber.php
path: src/SpomkyLabsPwaBundle.php
41 changes: 41 additions & 0 deletions src/Dto/Favicons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Dto;

use Symfony\Component\Serializer\Attribute\SerializedName;

final class Favicons
{
public bool $enabled = false;

public Asset $src;

#[SerializedName('background_color')]
public null|string $backgroundColor = null;

#[SerializedName('safari_pinned_tab_color')]
public null|string $safariPinnedTabColor = null;

#[SerializedName('tile_color')]
public null|string $tileColor = null;

/**
* @var int<1, 50>|null
*/
#[SerializedName('border_radius')]
public null|int $borderRadius = null;

/**
* @var int<1, 100>|null
*/
#[SerializedName('image_scale')]
public null|int $imageScale = null;

#[SerializedName('only_high_resolution')]
public null|bool $onlyHighResolution = null;

#[SerializedName('only_tile_silhouette')]
public null|bool $onlyTileSilhouette = null;
}
10 changes: 9 additions & 1 deletion src/ImageProcessor/GDImageProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ public function process(string $image, ?int $width, ?int $height, ?string $forma
assert($image !== false);
imagealphablending($image, true);
if ($width !== null && $height !== null) {
$image = imagescale($image, $width, $height);
if ($width === $height) {
$image = imagescale($image, $width, $height);
} else {
$newImage = imagecreatetruecolor($width, $height);
imagealphablending($newImage, false);
imagesavealpha($newImage, true);
imagecopyresampled($newImage, $image, 0, 0, 0, 0, $width, $height, imagesx($image), imagesy($image));
$image = $newImage;
}
}
ob_start();
imagesavealpha($image, true);
Expand Down
18 changes: 11 additions & 7 deletions src/ImageProcessor/ImagickImageProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@

final readonly class ImagickImageProcessor implements ImageProcessorInterface
{
public function __construct(
private int $filters = Imagick::FILTER_LANCZOS2,
private float $blur = 1,
) {
}

public function process(string $image, ?int $width, ?int $height, ?string $format): string
{
if ($width === null && $height === null) {
Expand All @@ -23,7 +17,17 @@ public function process(string $image, ?int $width, ?int $height, ?string $forma
$imagick = new Imagick();
$imagick->readImageBlob($image);
if ($width !== null && $height !== null) {
$imagick->resizeImage($width, $height, $this->filters, $this->blur, true);
if ($width === $height) {
$imagick->scaleImage($width, $height);
} else {
$imagick->scaleImage(min($width, $height), min($width, $height));
$imagick->extentImage(
$width,
$height,
-($width - min($width, $height)) / 2,
-($height - min($width, $height)) / 2
);
}
}
$imagick->setImageBackgroundColor(new ImagickPixel('transparent'));
if ($format !== null) {
Expand Down
71 changes: 71 additions & 0 deletions src/Resources/config/definition/favicons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;

return static function (DefinitionConfigurator $definition): void {
$definition->rootNode()
->beforeNormalization()
->ifTrue(
static fn (null|array $v): bool => $v !== null && isset($v['manifest']) && $v['manifest']['enabled'] === true && isset($v['favicons']) && $v['favicons']['enabled'] === true && isset($v['manifest']['theme_color'])
)
->then(static function (array $v): array {
$v['favicons']['background_color'] = $v['manifest']['theme_color'];
return $v;
})
->end()
->children()
->arrayNode('favicons')
->canBeEnabled()
->children()
->scalarNode('src')
->isRequired()
->info('The source of the favicon. Shall be a SVG or large PNG.')
->end()
->scalarNode('background_color')
->defaultNull()
->info(
'The background color of the application. If this value is not defined and that of the Manifest section is, the value of the latter will be used.'
)
->example(['red', '#f5ef06'])
->end()
->scalarNode('safari_pinned_tab_color')
->defaultNull()
->info('The color of the Safari pinned tab.')
->example(['red', '#f5ef06'])
->end()
->scalarNode('tile_color')
->defaultNull()
->info('The color of the tile for Windows 8+.')
->example(['red', '#f5ef06'])
->end()
->integerNode('border_radius')
->defaultNull()
->min(1)
->max(50)
->info('The border radius of the icon.')
->end()
->integerNode('image_scale')
->defaultNull()
->min(1)
->max(100)
->info('The scale of the icon.')
->end()
->booleanNode('generate_precomposed')
->defaultFalse()
->info('Generate precomposed icons. Useful for old iOS devices.')
->end()
->booleanNode('only_high_resolution')
->defaultTrue()
->info('Only high resolution icons.')
->end()
->booleanNode('only_tile_silhouette')
->defaultTrue()
->info('Only tile silhouette for Windows 8+.')
->end()
->end()
->end()
->end()
->end();
};
14 changes: 14 additions & 0 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
use SpomkyLabs\PwaBundle\Command\CreateScreenshotCommand;
use SpomkyLabs\PwaBundle\Command\ListCacheStrategiesCommand;
use SpomkyLabs\PwaBundle\DataCollector\PwaCollector;
use SpomkyLabs\PwaBundle\Dto\Favicons;
use SpomkyLabs\PwaBundle\Dto\Manifest;
use SpomkyLabs\PwaBundle\Dto\ServiceWorker;
use SpomkyLabs\PwaBundle\EventSubscriber\ScreenshotSubscriber;
use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor;
use SpomkyLabs\PwaBundle\ImageProcessor\ImagickImageProcessor;
use SpomkyLabs\PwaBundle\MatchCallbackHandler\MatchCallbackHandlerInterface;
use SpomkyLabs\PwaBundle\Service\FaviconsBuilder;
use SpomkyLabs\PwaBundle\Service\FaviconsCompiler;
use SpomkyLabs\PwaBundle\Service\FileCompilerInterface;
use SpomkyLabs\PwaBundle\Service\ManifestBuilder;
use SpomkyLabs\PwaBundle\Service\ManifestCompiler;
Expand Down Expand Up @@ -56,6 +59,17 @@
;
$container->set(ManifestCompiler::class);

/*** Favicons ***/
$container->set(FaviconsBuilder::class)
->args([
'$config' => param('spomky_labs_pwa.favicons.config'),
])
;
$container->set(Favicons::class)
->factory([service(FaviconsBuilder::class), 'create'])
;
$container->set(FaviconsCompiler::class);

/*** Service Worker ***/
$container->set(ServiceWorkerBuilder::class)
->args([
Expand Down
6 changes: 3 additions & 3 deletions src/Service/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
final readonly class Data
{
/**
* @param string[] $headers
* @param array<string, string|bool> $headers
*/
public function __construct(
public string $url,
public string $data,
public array $headers
){
) {
}

/**
* @param array<string, string> $headers
* @param array<string, string|bool> $headers
*/
public static function create(string $url, string $data, array $headers = []): self
{
Expand Down
34 changes: 34 additions & 0 deletions src/Service/FaviconsBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Service;

use SpomkyLabs\PwaBundle\Dto\Favicons;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use function assert;

final class FaviconsBuilder
{
private null|Favicons $favicons = null;

/**
* @param array<string, mixed> $config
*/
public function __construct(
private readonly DenormalizerInterface $denormalizer,
private readonly array $config,
) {
}

public function create(): Favicons
{
if ($this->favicons === null) {
$result = $this->denormalizer->denormalize($this->config, Favicons::class);
assert($result instanceof Favicons);
$this->favicons = $result;
}

return $this->favicons;
}
}
Loading

0 comments on commit 83644b7

Please sign in to comment.