Skip to content
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

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

Merged
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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessing the %sulu_media.image.formats% parameter would allow us to not rely on the format key here. but i understand that it makes the method dependent on sulu.

another possibility would be trying to access the %sulu_media.image.formats% parameter and fallback to this implementation if it is not available.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont have a lot of experience with this package and i am not sure if anything sulu related is accessed somewhere else. if yes, i think it would be nice to use %sulu_media.image.formats%. if not, we should probably not start doing it 🙂

Copy link
Member Author

@alexander-schranz alexander-schranz Sep 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the web twig extension has no sulu dependencies and so are sulu independent. As sulu_media.image.formats is no service just an array configuration which we could reuse without a dependency to sulu.

I'm fine to add imageFormatsConfiguration as parameter and use it when is configured and else fallback to it. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it would be nice to do that, because the feature would not depend on the format of the keys then 🙂

Copy link
Member Author

@alexander-schranz alexander-schranz Sep 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did update the extension!

{
$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