Skip to content

Commit

Permalink
Add option to automically add webp image formats (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-schranz authored Jul 7, 2020
1 parent d4df8d3 commit 4cda686
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The [web intl extension](docs/intl.md) gives you a simple and efficient way to g

[More](docs/intl.md)

### 4. count
### 4. Count

The [web count extension](docs/count.md) gives you a simple and efficient way to have a global counter in your twig template.

Expand Down
44 changes: 44 additions & 0 deletions docs/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,47 @@ services:
$defaultAttributes:
loading: 'lazy'
```

##### 7. Webp Support

If your server supports converting images to webp you can automatically enable webp
output for the image twig functions per default the following way:

```yaml
services:
Sulu\Twig\Extensions\ImageExtension:
arguments:
$defaultAdditionalTypes:
webp: 'image/webp'
```

This will render a picture tag which look like the following:

```twig
<picture>
<source media="(max-width: 1024px)"
srcset="/uploads/media/sulu-170x170/01/image.webp?v=1-0"
type="image/webp">
<source media="(max-width: 1024px)"
srcset="/uploads/media/sulu-170x170/01/image.jpg?v=1-0">
<source media="(max-width: 650px)"
srcset="/uploads/media/sulu-100x100/01/image.webp?v=1-0"
type="image/webp">
<source media="(max-width: 650px)"
srcset="/uploads/media/sulu-100x100/01/image.jpg?v=1-0">
<source srcset="/uploads/media/sulu-100x100/01/image.webp?v=1-0"
type="image/webp">
<img alt="Title"
title="Description"
src="/uploads/media/sulu-400x400/01/image.jpg?v=1-0">
</picture>
```

You can also only activate it for a specific call:

```twig
{{ get_image(headerImage, 'sulu-400x400', {
'(max-width: 1024px)': 'sulu-170x170',
'(max-width: 650px)': 'sulu-100x100',
}, { webp: 'image/webp' }) }}
```
165 changes: 152 additions & 13 deletions src/ImageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Sulu\Twig\Extensions;

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

Expand All @@ -22,6 +23,11 @@
*/
class ImageExtension extends AbstractExtension
{
/**
* @var PropertyAccessor
*/
private $propertyAccessor;

/**
* @var string|null
*/
Expand All @@ -42,16 +48,38 @@ class ImageExtension extends AbstractExtension
*/
private $defaultAttributes = null;

/**
* @var string[]
*/
private $defaultAdditionalTypes = [];

/**
* @var string[]
*/
private $ignoreTypesMimeTypes = [
// the official svg mimetype when the <?xml header exist
'image/svg+xml',
// if the <?xml header does not exist this is the mimetype returned by php:
// https://bugs.php.net/bug.php?id=79045
// https://3v4l.org/0h5jI
'image/svg',
];

/**
* @param string[] $defaultAttributes
* @param string[] $defaultAdditionalTypes
*/
public function __construct(?string $placeholderPath = null, array $defaultAttributes = [])
{
public function __construct(
?string $placeholderPath = null,
array $defaultAttributes = [],
array $defaultAdditionalTypes = []
) {
if (null !== $placeholderPath) {
$this->placeholderPath = rtrim($placeholderPath, '/') . '/';
}

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

/**
Expand All @@ -72,10 +100,11 @@ public function getFunctions()
* @param mixed $media
* @param array<string, string|null>|string $attributes
* @param mixed[] $sources
* @param mixed[] $additionalTypes
*
* @return string
*/
public function getLazyImage($media, $attributes = [], array $sources = []): string
public function getLazyImage($media, $attributes = [], array $sources = [], array $additionalTypes = []): string
{
if (null === $this->placeholderPath) {
throw new \InvalidArgumentException(
Expand All @@ -87,11 +116,10 @@ public function getLazyImage($media, $attributes = [], array $sources = []): str
$media = (object) $media;
}

$propertyAccessor = PropertyAccess::createPropertyAccessor();
$thumbnails = $propertyAccessor->getValue($media, 'thumbnails');
$thumbnails = $this->getPropertyAccessor()->getValue($media, 'thumbnails');
$this->hasLazyImage = true;

return $this->createImage($media, $attributes, $sources, $this->getLazyThumbnails($thumbnails));
return $this->createImage($media, $attributes, $sources, $this->getLazyThumbnails($thumbnails), $additionalTypes);
}

/**
Expand All @@ -110,12 +138,13 @@ public function hasLazyImage(): bool
* @param mixed $media
* @param array<string, string|null>|string $attributes
* @param mixed[] $sources
* @param mixed[] $additionalTypes
*
* @return string
*/
public function getImage($media, $attributes = [], array $sources = []): string
public function getImage($media, $attributes = [], array $sources = [], array $additionalTypes = []): string
{
return $this->createImage($media, $attributes, $sources);
return $this->createImage($media, $attributes, $sources, null, $additionalTypes);
}

/**
Expand All @@ -125,14 +154,16 @@ public function getImage($media, $attributes = [], array $sources = []): string
* @param array<string, string|null>|string $attributes
* @param mixed[] $sources
* @param string[]|null $lazyThumbnails
* @param string[] $additionalTypes
*
* @return string
*/
private function createImage(
$media,
$attributes = [],
array $sources = [],
?array $lazyThumbnails = null
?array $lazyThumbnails = null,
array $additionalTypes = []
): string {
// Return an empty string if no one of the needed parameters is set.
if (empty($media) || empty($attributes)) {
Expand All @@ -143,7 +174,7 @@ private function createImage(
$media = (object) $media;
}

$propertyAccessor = PropertyAccess::createPropertyAccessor();
$propertyAccessor = $this->getPropertyAccessor();

// Thumbnails exists all times - it only can be empty.
$thumbnails = $propertyAccessor->getValue($media, 'thumbnails');
Expand All @@ -155,11 +186,45 @@ private function createImage(
];
}

// The default attributes and attributes are merged together and not replaced
/** @var array<string, string|null> $attributes */
$attributes = array_merge(
$this->defaultAttributes,
$attributes
);

// The default additional types and additional types are merged together and not replaced
/** @var string[] $additionalTypes */
$additionalTypes = array_merge(
$this->defaultAdditionalTypes,
$additionalTypes
);

// Get MimeType from Media
$mimeType = $this->getMimeType($media);

// Add src and srcset configuration as additional types at end of source tags
if (!\in_array($mimeType, $this->ignoreTypesMimeTypes, true)) {
foreach ($additionalTypes as $extension => $type) {
$srcset = null;

if (isset($attributes['src'])) {
$srcset = $this->addExtension($attributes['src'], $extension);
}

if (isset($attributes['srcset'])) {
$srcset .= ', ' . $this->addExtension($attributes['srcset'], $extension);
}

if ($srcset) {
$sources[] = [
'srcset' => $srcset,
'type' => $type,
];
}
}
}

if ($lazyThumbnails) {
$attributes['class'] = trim((isset($attributes['class']) ? $attributes['class'] : '') . ' lazyload');
}
Expand Down Expand Up @@ -192,10 +257,33 @@ private function createImage(
'srcset' => $sourceAttributes,
];
}

if (!isset($sourceAttributes['media']) && \is_string($media)) {
$sourceAttributes = array_merge(['media' => $media], $sourceAttributes);
}

if (!\in_array($mimeType, $this->ignoreTypesMimeTypes, true)) {
// Type specific source tag should be rendered before untyped
foreach ($additionalTypes as $extension => $type) {
// avoid duplicated output of the same type
if (!isset($sourceAttributes['type']) || $sourceAttributes['type'] !== $type) {
$sourceTags .= $this->createTag(
'source',
array_merge($sourceAttributes, [
'srcset' => $this->addExtension($sourceAttributes['srcset'], $extension),
'type' => $type,
]),
$thumbnails,
$lazyThumbnails
);
}
}
}

// Get the source tag with all given attributes.
$sourceTags .= $this->createTag(
'source',
array_merge(['media' => $media], $sourceAttributes),
$sourceAttributes,
$thumbnails,
$lazyThumbnails
);
Expand Down Expand Up @@ -289,7 +377,7 @@ private function srcsetThumbnailReplace(string $value, array $thumbnails): strin
/**
* Get lazy thumbnails.
*
* @param string[] $thumbnails
* @param array<string, string> $thumbnails
*
* @return string[]|null
*/
Expand All @@ -302,12 +390,63 @@ private function getLazyThumbnails(array $thumbnails): ?array
if (null === $this->placeholders) {
$placeholders = [];
foreach (array_keys($thumbnails) as $key) {
$placeholders[$key] = $this->placeholderPath . $key . '.svg';
$placeholders[$key] = $this->placeholderPath . $this->removeExtensionFromFormat($key) . '.svg';
}

$this->placeholders = $placeholders;
}

return $this->placeholders;
}

/**
* Return a given src with with a new extension.
*/
private function addExtension(string $srcsets, string $extension): string
{
$newSrcsets = [];
foreach (explode(',', $srcsets) as $srcset) {
$srcset = trim($srcset);
// when a specific format is given e.g.: "50x50.inverted.jpg" we trim away it here:
$srcset = $this->removeExtensionFromFormat($srcset);
$srcParts = explode(' ', $srcset, 2);
$srcParts[0] .= '.' . $extension;

$newSrcsets[] = implode(' ', $srcParts);
}

return implode(', ', $newSrcsets);
}

/**
* Will return the sulu image format without the extension if given.
*/
private function removeExtensionFromFormat(string $format): string
{
return str_replace(['.svg', '.jpg', '.gif', '.png', '.webp'], '', $format);
}

/**
* @param mixed $media
*/
private function getMimeType($media): string
{
$propertyAccessor = $this->getPropertyAccessor();

if ($propertyAccessor->isReadable($media, 'mimeType')) {
return $propertyAccessor->getValue($media, 'mimeType');
}

return 'image/jpeg';
}

private function getPropertyAccessor(): PropertyAccessor
{
// lazy initializing of the property accessor
if (null === $this->propertyAccessor) {
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}

return $this->propertyAccessor;
}
}
Loading

0 comments on commit 4cda686

Please sign in to comment.