Skip to content

[LazyImage] Cache BlurHash, close #2 #1755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/LazyImage/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"require-dev": {
"intervention/image": "^2.5",
"kornrunner/blurhash": "^1.1",
"symfony/cache-contracts": "^2.2",
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
"symfony/phpunit-bridge": "^5.2|^6.0|^7.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0",
Expand Down
17 changes: 17 additions & 0 deletions src/LazyImage/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,28 @@ The ``data_uri_thumbnail`` function receives 3 arguments:
- the width of the BlurHash to generate
- the height of the BlurHash to generate

Performance considerations
~~~~~~~~~~~~~~~~~~~~~~~~~~

You should try to generate small BlurHash images as generating the image
can be CPU-intensive. Instead, you can rely on the browser scaling
abilities by generating a small image and using the ``width`` and
``height`` HTML attributes to scale up the image.

You can also configure a cache pool to store the generated BlurHash,
this way you can avoid generating the same BlurHash multiple times:

.. code-block:: yaml

# config/packages/lazy_image.yaml
framework:
cache:
pools:
cache.lazy_image: cache.adapter.redis # or any other cache adapter depending on your needs

lazy_image:
cache: cache.lazy_image # the cache pool to use

Extend the default behavior
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
8 changes: 3 additions & 5 deletions src/LazyImage/src/BlurHash/BlurHash.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@
*/
class BlurHash implements BlurHashInterface
{
private $imageManager;

public function __construct(?ImageManager $imageManager = null)
{
$this->imageManager = $imageManager;
public function __construct(
private ?ImageManager $imageManager = null,
) {
}

public function createDataUriThumbnail(string $filename, int $width, int $height, int $encodingWidth = 75, int $encodingHeight = 75): string
Expand Down
43 changes: 43 additions & 0 deletions src/LazyImage/src/BlurHash/CachedBlurHash.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\LazyImage\BlurHash;

use Symfony\Contracts\Cache\CacheInterface;

/**
* Decorate a BlurHashInterface to cache the result of the encoding, for performance purposes.
*
* @author Hugo Alliaume <[email protected]>
*
* @final
*/
class CachedBlurHash implements BlurHashInterface
{
public function __construct(
private BlurHashInterface $blurHash,
private CacheInterface $cache,
) {
}

public function createDataUriThumbnail(string $filename, int $width, int $height, int $encodingWidth = 75, int $encodingHeight = 75): string
{
return $this->blurHash->createDataUriThumbnail($filename, $width, $height, $encodingWidth, $encodingHeight);
}

public function encode(string $filename, int $encodingWidth = 75, int $encodingHeight = 75): string
{
return $this->cache->get(
'blurhash.'.hash('xxh3', $filename.$encodingWidth.$encodingHeight),
fn () => $this->blurHash->encode($filename, $encodingWidth, $encodingHeight)
);
}
}
36 changes: 36 additions & 0 deletions src/LazyImage/src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\LazyImage\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
* @author Hugo Alliaume <[email protected]>
*
* @internal
*/
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('ux_lazy_image');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->children()
->scalarNode('cache')->end()
->end()
;

return $treeBuilder;
}
}
15 changes: 15 additions & 0 deletions src/LazyImage/src/DependencyInjection/LazyImageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\UX\LazyImage\BlurHash\BlurHash;
use Symfony\UX\LazyImage\BlurHash\BlurHashInterface;
use Symfony\UX\LazyImage\BlurHash\CachedBlurHash;
use Symfony\UX\LazyImage\Twig\BlurHashExtension;

/**
Expand All @@ -32,6 +33,9 @@ class LazyImageExtension extends Extension implements PrependExtensionInterface
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

if (class_exists(ImageManager::class)) {
$container
->setDefinition('lazy_image.image_manager', new Definition(ImageManager::class))
Expand All @@ -47,6 +51,17 @@ public function load(array $configs, ContainerBuilder $container)

$container->setAlias(BlurHashInterface::class, 'lazy_image.blur_hash')->setPublic(false);

if (isset($config['cache'])) {
$container
->setDefinition('lazy_image.cached_blur_hash', new Definition(CachedBlurHash::class))
->setDecoratedService('lazy_image.blur_hash')
->addArgument(new Reference('lazy_image.cached_blur_hash.inner'))
->addArgument(new Reference($config['cache']))
;

$container->setAlias(BlurHashInterface::class, 'lazy_image.blur_hash')->setPublic(false);
}

$container
->setDefinition('twig.extension.blur_hash', new Definition(BlurHashExtension::class))
->addArgument(new Reference('lazy_image.blur_hash'))
Expand Down
74 changes: 74 additions & 0 deletions src/LazyImage/tests/BlurHash/BlurHashTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
namespace Symfony\UX\LazyImage\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\UX\LazyImage\BlurHash\BlurHashInterface;
use Symfony\UX\LazyImage\BlurHash\CachedBlurHash;
use Symfony\UX\LazyImage\Tests\Kernel\TwigAppKernel;

/**
Expand All @@ -37,6 +41,76 @@ public function testEncode()
);
}

public function testEnsureCacheIsNotUsedWhenNotConfigured()
{
$kernel = new TwigAppKernel('test', true);
$kernel->boot();
$container = $kernel->getContainer()->get('test.service_container');

/** @var BlurHashInterface $blurHash */
$blurHash = $container->get('test.lazy_image.blur_hash');

static::assertNotInstanceOf(CachedBlurHash::class, $blurHash);
}

public function testEnsureCacheIsUsedWhenConfigured()
{
$kernel = new class('test', true) extends TwigAppKernel {
public function registerContainerConfiguration(LoaderInterface $loader)
{
parent::registerContainerConfiguration($loader);

$loader->load(static function (ContainerBuilder $container) {
$container->loadFromExtension('framework', [
'cache' => [
'pools' => [
'cache.lazy_image' => [
'adapter' => 'cache.adapter.array',
],
],
],
]);

$container->loadFromExtension('lazy_image', [
'cache' => 'cache.lazy_image',
]);

$container->setAlias('test.cache.lazy_image', 'cache.lazy_image')->setPublic(true);
});
}
};

$kernel->boot();
$container = $kernel->getContainer()->get('test.service_container');

/** @var BlurHashInterface $blurHash */
$blurHash = $container->get('test.lazy_image.blur_hash');

static::assertInstanceOf(CachedBlurHash::class, $blurHash);
}

public function testEncodeShouldBeCalledOnceWhenCached()
{
$blurHash = $this->createMock(BlurHashInterface::class);
$blurHash->expects($this->once())->method('encode')->with(__DIR__.'/../Fixtures/logo.png')->willReturn('L54ec*~q_3?bofoffQWB9F9FD%IU');
$cache = new ArrayAdapter();
$cachedBlurHash = new CachedBlurHash($blurHash, $cache);

$this->assertEmpty($cache->getValues());

$this->assertSame(
'L54ec*~q_3?bofoffQWB9F9FD%IU',
$cachedBlurHash->encode(__DIR__.'/../Fixtures/logo.png')
);

$this->assertNotEmpty($cache->getValues());

$this->assertSame(
'L54ec*~q_3?bofoffQWB9F9FD%IU',
$cachedBlurHash->encode(__DIR__.'/../Fixtures/logo.png')
);
}

public function testCreateDataUriThumbnail()
{
$kernel = new TwigAppKernel('test', true);
Expand Down
Loading