Skip to content

Commit

Permalink
Add option to guess and calculate width and height for an image format (
Browse files Browse the repository at this point in the history
#36)

* Guess width and height by given image format

* Add option to guess and calculate width and height for an image format

* Fix phsptan

* Implement aspect ration with image configuration

* Apply suggestions from code review

Co-authored-by: nnatter <niklas.natter@gmail.com>

* Fix phpstan

* Update docs/image.md

Co-authored-by: nnatter <niklas.natter@gmail.com>

Co-authored-by: nnatter <niklas.natter@gmail.com>
  • Loading branch information
alexander-schranz and niklasnatter committed Sep 17, 2021
1 parent 789a019 commit d1da80d
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 26 deletions.
47 changes: 47 additions & 0 deletions docs/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ services:

If autoconfigure is not active you need to tag it with [twig.extension](https://symfony.com/doc/current/service_container.html#the-autoconfigure-option).

**Recommended Configuration**

```yaml
Sulu\Twig\Extensions\ImageExtension:
arguments:
$defaultAttributes:
loading: 'lazy'
$defaultAdditionalTypes:
webp: 'image/webp'
$aspectRatio: true
$imageFormatConfiguration: '%sulu_media.image.formats%'
```

## Usage

#### Get an image
Expand Down Expand Up @@ -227,3 +240,37 @@ You can also only activate it for a specific call:
'(max-width: 650px)': 'sulu-100x100',
}, { webp: 'image/webp' }) }}
```

##### 8. Set width and height attribute automatically

Since Sulu 2.3 the original image width and height are saved as properties.
This allows to guess the width and height of a image format and set the respective HTML attributes.

Setting the width and height attribute allows modern browsers to avoid layer shifts
and therefore the page will not jump when images are loaded.

This feature can be activated the following way:

```yaml
services:
Sulu\Twig\Extensions\ImageExtension:
arguments:
$aspectRatio: true
$imageFormatConfiguration: '%sulu_media.image.formats%' # optional but recommended
```

For example, if the original image has a resolution of 1920x1080 and the image format is called 100x:

```twig
{{ get_image(headerImage, '100x') }}
```

The feature will automatically add a width and height attribute to the rendered image tag:

```twig
<img alt="Title" title="Description" src="/uploads/media/100x/01/image.jpg?v=1-0" width="'100" height="56">
```

The `$imageFormatConfiguration` parameter is optional. If it is not set, the extension
tries to guess the dimensions by the given format key. This will only work for format keys
in the format of 100x, x100, 100x@2x and 100x100-inset.
115 changes: 114 additions & 1 deletion src/ImageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,50 @@ class ImageExtension extends AbstractExtension
'image/svg',
];

/**
* @var bool
*/
private $aspectRatio = false;

/**
* @var array<string, array{
* scale: array{
* x: int|null,
* y: int|null,
* mode: int,
* retina: bool,
* },
* }>|null
*/
private $imageFormatConfiguration = null;

/**
* @param string[] $defaultAttributes
* @param string[] $defaultAdditionalTypes
* @param array<string, array{
* scale: array{
* x: int|null,
* y: int|null,
* mode: int,
* retina: bool,
* },
* }>|null $imageFormatConfiguration
*/
public function __construct(
?string $placeholderPath = null,
array $defaultAttributes = [],
array $defaultAdditionalTypes = []
array $defaultAdditionalTypes = [],
bool $aspectRatio = false,
array $imageFormatConfiguration = null,
) {
if (null !== $placeholderPath) {
$this->placeholderPath = rtrim($placeholderPath, '/') . '/';
}

$this->defaultAttributes = $defaultAttributes;
$this->defaultAdditionalTypes = $defaultAdditionalTypes;
$this->aspectRatio = $aspectRatio;
$this->imageFormatConfiguration = $imageFormatConfiguration;
}

/**
Expand Down Expand Up @@ -193,6 +222,13 @@ private function createImage(
$attributes
);

if ($this->aspectRatio && isset($attributes['src']) && !isset($attributes['width']) && !isset($attributes['height'])) {
list($width, $height) = $this->guessAspectRatio($media, $attributes);

$attributes['width'] = ((string) $width) ?: null;
$attributes['height'] = ((string) $height) ?: null;
}

// The default additional types and additional types are merged together and not replaced
/** @var string[] $additionalTypes */
$additionalTypes = array_merge(
Expand Down Expand Up @@ -407,6 +443,83 @@ private function getLazyThumbnails(array $thumbnails): ?array
return $this->placeholders;
}

/**
* @param mixed $media
* @param array<string, string|null> $attributes
*
* @return array{
* 0: int|null,
* 1: int|null,
* }
*/
private function guessAspectRatio($media, array $attributes): array
{
$src = $attributes['src'] ?? '';

if ($this->imageFormatConfiguration) {
$scale = $this->imageFormatConfiguration[$src]['scale']['retina'] ? 2 : 1;
$isInset = \in_array($this->imageFormatConfiguration[$src]['scale']['mode'], [
1,
'inset',
], true);
$x = $this->imageFormatConfiguration[$src]['scale']['x'];
$y = $this->imageFormatConfiguration[$src]['scale']['y'];
$width = $x ? (int) round($x * $scale) : null;
$height = $y ? (int) round($y * $scale) : null;
} else {
/*
* This will extract the format dimension in all common formats.
* @see ImageExtensionTest::testGuessAspectRatio
*/
preg_match('/(\d+)?x(\d+)?(-inset)?(@)?(\d)?(x)?/', $src, $matches);

$scale = !empty($matches[5]) ? (float) $matches[5] : 1;
$width = !empty($matches[1]) ? (int) round($matches[1] * $scale) : null;
$height = !empty($matches[2]) ? (int) round($matches[2] * $scale) : null;
$isInset = !empty($matches[3]);
}

// fixed formats can directly be returned
if ($width && $height && !$isInset) {
return [$width, $height];
}

$properties = $this->getPropertyAccessor()->getValue($media, 'properties');
$originalWidth = $properties['width'] ?? null;
$originalHeight = $properties['height'] ?? null;

if (!$originalWidth || !$originalHeight) {
return [null, null];
}

if ($isInset && $width && $height) {
// calculate inset width and height e.g. 200x50-inset, 100x100-inset
$insetWidth = $width;
$insetHeight = $height;
if ($originalWidth > $width) {
$insetHeight = $originalHeight / $originalWidth * $width;
}

if (round($insetHeight) > $height) {
$insetHeight = $height;
$insetWidth = $originalWidth / $originalHeight * $height;
}

return [(int) round($insetWidth), (int) round($insetHeight)];
}

// calculate the not given dimension parameter
if (!$width) {
$width = $originalWidth / $originalHeight * $height;
}

if (!$height) {
$height = $originalHeight / $originalWidth * $width;
}

return [(int) round($width), (int) round($height)];
}

/**
* Return a given src with with a new extension.
*/
Expand Down
Loading

0 comments on commit d1da80d

Please sign in to comment.