Skip to content

Commit

Permalink
Update favicon processing to include hashing (#205)
Browse files Browse the repository at this point in the history
The modifications to the FaviconsCompiler service now includes hashing in the favicon processing. This includes changes to the structure of the URL and the generation of a unique hash based on the image content and configuration. This update also has some error handling improvements, including better handling of file reading failures. This provides more robust processing and clearer URLs which include the hash of the specific favicon configuration.
  • Loading branch information
Spomky authored May 25, 2024
1 parent 0c874f3 commit ac880cb
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 54 deletions.
12 changes: 1 addition & 11 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -565,23 +565,13 @@ parameters:
count: 1
path: src/Resources/config/definition/web_client.php

-
message: "#^Call to function assert\\(\\) with true and 'The asset does not…' will always evaluate to true\\.$#"
count: 1
path: src/Service/FaviconsCompiler.php

-
message: "#^Cannot call method process\\(\\) on SpomkyLabs\\\\PwaBundle\\\\ImageProcessor\\\\ImageProcessorInterface\\|null\\.$#"
count: 1
path: src/Service/FaviconsCompiler.php

-
message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Service\\\\FaviconsCompiler\\:\\:getFavicon\\(\\) should return string but returns string\\|false\\.$#"
count: 2
path: src/Service/FaviconsCompiler.php

-
message: "#^Strict comparison using \\!\\=\\= between string and null will always evaluate to true\\.$#"
message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Service\\\\FaviconsCompiler\\:\\:getFavicon\\(\\) should return array\\{string, string\\} but returns array\\{string\\|false, string\\}\\.$#"
count: 1
path: src/Service/FaviconsCompiler.php

Expand Down
16 changes: 15 additions & 1 deletion src/ImageProcessor/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
namespace SpomkyLabs\PwaBundle\ImageProcessor;

use InvalidArgumentException;
use Stringable;

final readonly class Configuration
final readonly class Configuration implements Stringable
{
public function __construct(
public int $width,
Expand All @@ -21,6 +22,19 @@ public function __construct(
}
}

public function __toString(): string
{
return sprintf(
'%d%d%s%s%s%s',
$this->width,
$this->height,
$this->format,
$this->backgroundColor ?? '',
$this->borderRadius ?? '',
$this->imageScale ?? ''
);
}

public static function create(
int $width,
int $height,
Expand Down
110 changes: 68 additions & 42 deletions src/Service/FaviconsCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function getFiles(): array
if ($this->imageProcessor === null || $this->favicons->enabled === false) {
return [];
}
$asset = $this->getFavicon();
[$asset, $hash] = $this->getFavicon();
assert($asset !== null, 'The asset does not exist.');
$this->files = [];
$sizes = [
Expand All @@ -56,15 +56,15 @@ public function getFiles(): array
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 16,
'height' => 16,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 32,
'height' => 32,
'format' => 'png',
Expand All @@ -73,7 +73,7 @@ public function getFiles(): array
],
//High resolution iOS
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 180,
'height' => 180,
'format' => 'png',
Expand All @@ -82,15 +82,15 @@ public function getFiles(): array
],
//High resolution chrome
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 192,
'height' => 192,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 512,
'height' => 512,
'format' => 'png',
Expand All @@ -103,23 +103,23 @@ public function getFiles(): array
...$sizes,
//Prior iOS 6
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 57,
'height' => 57,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'apple-touch-icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 72,
'height' => 72,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'apple-touch-icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 114,
'height' => 114,
'format' => 'png',
Expand All @@ -129,31 +129,31 @@ public function getFiles(): array

//Prior iOS 7
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 60,
'height' => 60,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'apple-touch-icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 76,
'height' => 76,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'apple-touch-icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 120,
'height' => 120,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'apple-touch-icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 152,
'height' => 152,
'format' => 'png',
Expand All @@ -163,55 +163,55 @@ public function getFiles(): array

//Other resolution
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 36,
'height' => 36,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 48,
'height' => 48,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 72,
'height' => 72,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 96,
'height' => 96,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 144,
'height' => 144,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 256,
'height' => 256,
'format' => 'png',
'mimetype' => 'image/png',
'rel' => 'icon',
],
[
'url' => '/favicons/icon-%sx%s.png',
'url' => '/favicons/icon-%dx%d-%s.png',
'width' => 384,
'height' => 384,
'format' => 'png',
Expand All @@ -230,19 +230,22 @@ public function getFiles(): array
$this->favicons->borderRadius,
$this->favicons->imageScale,
);
$this->files[sprintf($size['url'], $size['width'], $size['height'])] = $this->processIcon(
$completeHash = hash('xxh128', $hash . $configuration);
$filename = sprintf($size['url'], $size['width'], $size['height'], $completeHash);
$this->files[$filename] = $this->processIcon(
$asset,
sprintf($size['url'], $size['width'], $size['height']),
$filename,
$configuration,
$size['mimetype'],
$size['rel'],
);
}
if ($this->favicons->tileColor !== null) {
$this->files = [...$this->files, ...$this->processBrowserConfig($asset)];
$this->files = [...$this->files, ...$this->processBrowserConfig($asset, $hash)];
}
if ($this->favicons->safariPinnedTabColor !== null && $this->favicons->useSilhouette === true) {
$this->files['/safari-pinned-tab.svg'] = $this->generateSafariPinnedTab($asset);
$safariPinnedTab = $this->generateSafariPinnedTab($asset);
$this->files[$safariPinnedTab->url] = $safariPinnedTab;
}

return $this->files;
Expand Down Expand Up @@ -299,45 +302,59 @@ private function processIcon(
/**
* @return array<Data>
*/
private function processBrowserConfig(string $asset): array
private function processBrowserConfig(string $asset, string $hash): array
{
if ($this->favicons->useSilhouette === true) {
$asset = $this->generateSilhouette($asset);
}
$configuration = Configuration::create(70, 70, 'png', null, null, $this->favicons->imageScale);
$hash = hash('xxh128', $hash . $configuration);
$icon70x70 = $this->processIcon(
$asset,
'/favicons/icon-70x70.png',
Configuration::create(70, 70, 'png', null, null, $this->favicons->imageScale),
sprintf('/favicons/icon-%dx%d-%s.png', 70, 70, $hash),
$configuration,
'image/png',
null
);

$configuration = Configuration::create(150, 150, 'png', null, null, $this->favicons->imageScale);
$hash = hash('xxh128', $hash . $configuration);
$icon150x150 = $this->processIcon(
$asset,
'/favicons/icon-150x150.png',
Configuration::create(150, 150, 'png', null, null, $this->favicons->imageScale),
sprintf('/favicons/icon-%dx%d-%s.png', 150, 150, $hash),
$configuration,
'image/png',
null
);

$configuration = Configuration::create(310, 310, 'png', null, null, $this->favicons->imageScale);
$hash = hash('xxh128', $hash . $configuration);
$icon310x310 = $this->processIcon(
$asset,
'/favicons/icon-310x310.png',
Configuration::create(310, 310, 'png', null, null, $this->favicons->imageScale),
sprintf('/favicons/icon-%dx%d-%s.png', 310, 310, $hash),
$configuration,
'image/png',
null
);

$configuration = Configuration::create(310, 150, 'png', null, null, $this->favicons->imageScale);
$hash = hash('xxh128', $hash . $configuration);
$icon310x150 = $this->processIcon(
$asset,
'/favicons/icon-310x150.png',
Configuration::create(310, 150, 'png', null, null, $this->favicons->imageScale),
sprintf('/favicons/icon-%dx%d-%s.png', 310, 150, $hash),
$configuration,
'image/png',
null
);

$configuration = Configuration::create(144, 144, 'png', null, null, $this->favicons->imageScale);
$hash = hash('xxh128', $hash . $configuration);
$icon144x144 = $this->processIcon(
$asset,
'/favicons/icon-144x144.png',
Configuration::create(144, 144, 'png', null, null, $this->favicons->imageScale),
sprintf('/favicons/icon-%dx%d-%s.png', 144, 144, $hash),
$configuration,
'image/png',
null//'<meta name="msapplication-TileImage" content="/favicons/icon-144x144.png">'
null
);

if ($this->favicons->tileColor === null) {
Expand All @@ -359,7 +376,8 @@ private function processBrowserConfig(string $asset): array
</msapplication>
</browserconfig>
XML;
$url = '/favicons/browserconfig.xml';
$browserConfigHash = hash('xxh128', $content);
$url = sprintf('/favicons/browserconfig-%s.xml', $browserConfigHash);
$browserConfig = Data::create(
$url,
$content,
Expand Down Expand Up @@ -387,22 +405,30 @@ private function processBrowserConfig(string $asset): array
];
}

private function getFavicon(): string
/**
* @return array{0: string, 1: string}
*/
private function getFavicon(): array
{
$source = $this->favicons->src;
if (! str_starts_with($source->src, '/')) {
$asset = $this->assetMapper->getAsset($source->src);
assert($asset !== null, 'Unable to find the favicon source asset');
return $asset->content ?? file_get_contents($asset->sourcePath);
return [$asset->content ?? file_get_contents($asset->sourcePath), $asset->digest];
}
assert(file_exists($source->src), 'Unable to find the favicon source file');
return file_get_contents($source->src);
$data = file_get_contents($source->src);
assert($data !== false, 'Unable to read the favicon source file');
$hash = hash('xxh128', $data);

return [$data, $hash];
}

private function generateSafariPinnedTab(string $content): Data
{
$silhouette = $this->generateSilhouette($content);
$url = '/safari-pinned-tab.svg';
$hash = hash('xxh128', $silhouette);
$url = sprintf('/safari-pinned-tab-%s.svg', $hash);

return Data::create(
$url,
Expand All @@ -411,7 +437,7 @@ private function generateSafariPinnedTab(string $content): Data
'Cache-Control' => 'public, max-age=604800, immutable',
'Content-Type' => 'image/svg+xml',
'X-Favicons-Dev' => true,
'Etag' => hash('xxh128', $silhouette),
'Etag' => $hash,
],
sprintf('<link rel="mask-icon" href="%s" color="%s">', $url, $this->favicons->safariPinnedTabColor)
);
Expand Down

0 comments on commit ac880cb

Please sign in to comment.