Skip to content

Commit

Permalink
[LazyImage] Cache BlurHash, close symfony#2
Browse files Browse the repository at this point in the history
  • Loading branch information
Kocal committed Apr 18, 2024
1 parent e6b827b commit 3b13145
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 25 deletions.
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
59 changes: 35 additions & 24 deletions src/LazyImage/src/BlurHash/BlurHash.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Intervention\Image\ImageManager;
use kornrunner\Blurhash\Blurhash as BlurhashEncoder;
use Symfony\Contracts\Cache\CacheInterface;

/**
* @author Titouan Galopin <[email protected]>
Expand All @@ -21,11 +22,10 @@
*/
class BlurHash implements BlurHashInterface
{
private $imageManager;

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

public function createDataUriThumbnail(string $filename, int $width, int $height, int $encodingWidth = 75, int $encodingHeight = 75): string
Expand Down Expand Up @@ -62,28 +62,39 @@ public function encode(string $filename, int $encodingWidth = 75, int $encodingH
throw new \LogicException('To use the Blurhash feature, install kornrunner/blurhash.');
}

// Resize image to increase encoding performance
$image = $this->imageManager->make(file_get_contents($filename));
$image->resize($encodingWidth, $encodingHeight, static function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});

// Encode using BlurHash
$width = $image->getWidth();
$height = $image->getHeight();

$pixels = [];
for ($y = 0; $y < $height; ++$y) {
$row = [];
for ($x = 0; $x < $width; ++$x) {
$color = $image->pickColor($x, $y);
$row[] = [$color[0], $color[1], $color[2]];
$doEncode = function (string $filename, int $encodingWidth, int $encodingHeight) {
// Resize image to increase encoding performance
$image = $this->imageManager->make(file_get_contents($filename));
$image->resize($encodingWidth, $encodingHeight, static function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});

// Encode using BlurHash
$width = $image->getWidth();
$height = $image->getHeight();

$pixels = [];
for ($y = 0; $y < $height; ++$y) {
$row = [];
for ($x = 0; $x < $width; ++$x) {
$color = $image->pickColor($x, $y);
$row[] = [$color[0], $color[1], $color[2]];
}

$pixels[] = $row;
}

$pixels[] = $row;
return BlurhashEncoder::encode($pixels, 4, 3);
};

if (null === $this->cache) {
return $doEncode($filename, $encodingWidth, $encodingHeight);
}

return BlurhashEncoder::encode($pixels, 4, 3);
return $this->cache->get(
'blurhash.'.hash('xxh3', $filename.$encodingWidth.$encodingHeight),
fn () => $doEncode($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;
}
}
9 changes: 8 additions & 1 deletion src/LazyImage/src/DependencyInjection/LazyImageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 @@ -41,10 +44,14 @@ public function load(array $configs, ContainerBuilder $container)

$container
->setDefinition('lazy_image.blur_hash', new Definition(BlurHash::class))
->addArgument(new Reference('lazy_image.image_manager', ContainerInterface::NULL_ON_INVALID_REFERENCE))
->setArgument(0, new Reference('lazy_image.image_manager', ContainerInterface::NULL_ON_INVALID_REFERENCE))
->setPublic(false)
;

if (isset($config['cache'])) {
$container->getDefinition('lazy_image.blur_hash')->setArgument(1, new Reference($config['cache']));
}

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

$container
Expand Down
48 changes: 48 additions & 0 deletions src/LazyImage/tests/BlurHash/BlurHashTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
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\Tests\Kernel\TwigAppKernel;

Expand All @@ -37,6 +40,51 @@ public function testEncode()
);
}

public function testEncodeWithCache()
{
$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');

$cache = $container->get('test.cache.lazy_image');
static::assertInstanceOf(ArrayAdapter::class, $cache);
static::assertEmpty($cache->getValues());

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

static::assertSame('s:28:"L54ec*~q_3?bofoffQWB9F9FD%IU";', $cache->getValues()['blurhash.70d3aaba5c301af7']);
}

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

0 comments on commit 3b13145

Please sign in to comment.