Skip to content

Commit

Permalink
Implement missing effects (#10)
Browse files Browse the repository at this point in the history
Some notes about why certain things are implemented this way:

- Direct CSS filters like `filter: blur(1)` are not supported in SVG context in some browsers, so we have to use the `<filter>` element.
- Working around that by using `url()` with a data URI doesn’t work everywhere either.
- To avoid duplicate IDs we generate them with a hash of the SVG itself.
- Multiple values for the `filter` attribute are not supported in some browsers, so we are using multiple elements in `<filter>`.
- `filter` attribute on the main `<svg>` element is not supported in IE/Edge, this is why we are using a `<g>` wrapper.
  • Loading branch information
ausi committed Mar 1, 2018
1 parent f126fca commit ba853e2
Show file tree
Hide file tree
Showing 5 changed files with 444 additions and 31 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ $imagine
->resize(new Box(40, 40))
->save('/path/to/thumbnail.svg')
;

$image = $imagine->open('/path/to/image.svg');
$image->effects()
->gamma(1.5)
->negative()
->grayscale()
->colorize($color)
->sharpen()
->blur(2)
;
$image->save('/path/to/image.svg');
```

Because of the nature of SVG images, the `getSize()` method differs a little bit
Expand Down
241 changes: 214 additions & 27 deletions src/Effects.php
Original file line number Diff line number Diff line change
@@ -1,65 +1,252 @@
<?php

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\ImagineSvg;


use Imagine\Effects\EffectsInterface;
use Imagine\Exception\RuntimeException;
use Imagine\Exception\InvalidArgumentException;
use Imagine\Exception\NotSupportedException;
use Imagine\Image\Palette\Color\ColorInterface;
use Imagine\Image\Palette\Color\RGB;

class Effects implements EffectsInterface
{

/** @var Image */
private $image;

public function __construct(Image $image)
/**
* @var string
*/
const SVG_FILTER_ID_PREFIX = 'svgImagineFilterV1_';

/**
* @var \DOMDocument
*/
private $document;

/**
* @param \DOMDocument $document
*/
public function __construct(\DOMDocument $document)
{
$this->image = $image;
$this->document = $document;
}

/**
* {@inheritdoc}
*/
public function gamma($correction)
{
throw new RuntimeException('This method is not implemented');

// TODO: Implement gamma() method.
$gamma = (float) $correction;

if ($gamma <= 0) {
throw new InvalidArgumentException(sprintf(
'Invalid gamma correction value %s, must be a positive float or integer',
var_export($correction, true)
));
}

$exponent = 1 / $gamma;

$this->addFilterElement('feComponentTransfer', [
['feFuncR', [
'type' => 'gamma',
'exponent' => $exponent,
]],
['feFuncG', [
'type' => 'gamma',
'exponent' => $exponent,
]],
['feFuncB', [
'type' => 'gamma',
'exponent' => $exponent,
]],
]);

return $this;
}

/**
* {@inheritdoc}
*/
public function negative()
{
throw new RuntimeException('This method is not implemented');
// TODO: Implement negative() method.
$this->addFilterElement('feColorMatrix', [
'type' => 'matrix',
'values' => implode(' ', [
'-1 0 0 0 1',
'0 -1 0 0 1',
'0 0 -1 0 1',
'0 0 0 1 0',
]),
]);

return $this;
}

/**
* {@inheritdoc}
*/
public function grayscale()
{
throw new RuntimeException('This method is not implemented');
// TODO: Implement grayscale() method.
$this->addFilterElement('feColorMatrix', [
'type' => 'saturate',
'values' => '0',
]);

return $this;
}

/**
* {@inheritdoc}
*/
public function colorize(ColorInterface $color)
{
throw new RuntimeException('This method is not implemented');
// TODO: Implement colorize() method.
if (!$color instanceof RGB) {
throw new NotSupportedException('Colorize with non-rgb color is not supported');
}

$this->addFilterElement('feColorMatrix', [
'type' => 'matrix',
'values' => implode(' ', [
'1 0 0 0 '.json_encode($color->getRed() / 255),
'0 1 0 0 '.json_encode($color->getGreen() / 255),
'0 0 1 0 '.json_encode($color->getBlue() / 255),
'0 0 0 1 0',
]),
]);

return $this;
}

/**
* {@inheritdoc}
*/
public function sharpen()
{
throw new RuntimeException('This method is not implemented');
// TODO: Implement sharpen() method.
$this->addFilterElement('feConvolveMatrix', [
'kernelMatrix' => implode(' ', [
'-0.02 -0.12 -0.02',
'-0.12 1.56 -0.12',
'-0.02 -0.12 -0.02',
]),
]);

return $this;
}

/**
* {@inheritdoc}
*/
public function blur($sigma = 1)
{
$deviation = (float) $sigma;

if ($deviation <= 0) {
throw new InvalidArgumentException(sprintf(
'Invalid sigma %s, must be a positive float or integer',
var_export($sigma, true)
));
}

$this->addFilterElement('feGaussianBlur', [
'stdDeviation' => json_encode($deviation),
]);

return $this;
}

public function blur($sigma)
/**
* Create and add a new filter element.
*
* @param string $name
* @param array $attributes
*/
private function addFilterElement($name, array $attributes)
{
$attributes['color-interpolation-filters'] = 'sRGB';

$dom = $this->image->getDomDocument();
$dom->documentElement->setAttribute("filter", "url(#rokkaBlur)");
$filter = $dom->createDocumentFragment();
$filter->appendXML('<filter id="rokkaBlur">
<feGaussianBlur in="SourceGraphic" stdDeviation="'.$sigma.'" />
</filter>');
$dom->documentElement->insertBefore($filter, $dom->documentElement->firstChild);
$this->getSvgFilter()->appendChild($this->createElement($name, $attributes));
}

/**
* Get the main filter element or create it if none is present.
*
* @return \DOMElement
*/
private function getSvgFilter()
{
$svg = $this->document->documentElement;
$filter = null;

if (
1 === $svg->childNodes->length
&& 'g' === $svg->firstChild->nodeName
&& preg_match(
'/^url\(#('.self::SVG_FILTER_ID_PREFIX.'[0-9a-f]{16})\)$/',
(string) $svg->firstChild->getAttribute('filter'),
$matches
)
) {
$id = $matches[1];
} else {
$this->wrapSvg();
$id = self::SVG_FILTER_ID_PREFIX.bin2hex(substr(hash('sha256', $this->document->saveXML()), 0, 8));
$svg->firstChild->setAttribute('filter', 'url(#'.$id.')');
}

foreach ($this->document->getElementsByTagName('filter') as $element) {
if ($element->getAttribute('id') === $id) {
return $element;
}
}

$filter = $this->document->createElement('filter');
$filter->setAttribute('id', $id);
$svg->firstChild->insertBefore($filter, $svg->firstChild->firstChild);

return $filter;
}

/**
* Add a group element that wraps all contents.
*/
private function wrapSvg()
{
$svg = $this->document->documentElement;
$group = $this->document->createElement('g');

while ($svg->firstChild) {
$group->appendChild($svg->firstChild);
}

$svg->appendChild($group);
}

/**
* Create element with the specified attributes.
*
* @param string $name
* @param array $attributes
*
* @return \DOMElement
*/
private function createElement($name, array $attributes)
{
$filter = $this->document->createElement($name);

foreach ($attributes as $key => $value) {
if (is_string($key)) {
$filter->setAttribute($key, $value);
} else {
$filter->appendChild($this->createElement($value[0], $value[1]));
}
}

return $filter;
}
}
2 changes: 1 addition & 1 deletion src/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ public function draw()
*/
public function effects()
{
return new Effects($this);
return new Effects($this->document);
}

/**
Expand Down
Loading

0 comments on commit ba853e2

Please sign in to comment.