RFC: Media / Imagine and Images

I have been working on a refactoring for image handling since a little while but figured I would write my thoughts down at this point and gather some feedback on it.

State currently: Media package holds media (meta) data, logic for image manipulation, thumbnails, basically everything. (Our) Imagine package is just a factory for imagine used inside the media package.
This makes it impossible to use anything else than imagine, which can be desireable in certain situations (think image manipulation services, command line tools etc.).

My idea is the following: All image manipulation code and logic resides in a respective package for the service doing the manipulation (eg. for imagine in the imagine package). Including “the” ImageService.
This (new) ImageService gets a resource and a TBD data structure describing the desired resulting resource (which would include image quality settings, “adjustments”, desired image file format and more), lets call this “ImageConfiguration” for now.
To connect this with the media package we need interface which would reside in a separate package that just contains a set of interfaces for image manipulation and maybe some basic implementations/traits with reusable code (think an ImageSize object). As you can see fromt he example of the ImageSize object I would introduce more typing/objects.
The interface package could then be used to connect media with implementing packages. The media package would contain a factory for the “ImageConfiguration” (which uses the settings + adjustments entities) and for b/c reasons an ImageService that maintains the current API while internally using an injected “ImageManipulator” implementation.

That is the basic idea which

  1. Allows image manipulation without installing media (only the interfaces + an implementation)
  2. allows switching of the implementation
  3. Can allow further refinements and more possibilities for image handling

Some things I want to discuss:

How/What data structure to use for the “ImageConfiguration”. An object would be the preferred choice, the question is how to make it generic enough to work with many implementations and at the same time specific enough to have a benefit? I guess the two mentioned areas “adjustments” and “image options” could be two properties of this object. But then the next question comes up, how to describe adjustments in a generic way?
We probably need to define “types” as a specific string" in the interface package that do something to an image (eg. resize, crop, greyscale etc.) Those should be the same across implementations. So ideally the interfaces package would predefine those + the possible default options for each, but then the interfaces package would have to be extended if you want to add something else. Maybe hybrid works, eg. we have some things predefined and then you can have a GenericAdjustment that just has a “type” and options and it’s up to the implementation to figure out what to do with it…

Second what about deferral. I am thinking about remote services here, they might take a while or even want to callback when done. And I have no clue how we can deal with that, maybe someone has a great idea?

And lastly naming. I currently have a “Neos.Image.Interfaces” package in my WIP code. That could well be “Neos.ImageInterfaces” or somthing totally different. Comments, wishes?

Apart from that I can say it looks much nicer to remove the specific imagine code from the media package adjustments and classes. Might be a bit cumbersome in the first moment to take the adjustments from the DB and convert them to a generic data structure to be used by the respective implementation but it works just fine and the cost should be negligible.

If you have any other comments/requests/wishes, let me know. I have a working WIP state that is halfway into this refactoring. But I now need to flesh out the data structure for image manipulation and wanted to get some feedback on that.

3 Likes

@christianm Thanks for describing your ideas and the short tour around the possible new concepts.

While I like and agree with almost all described ideas and suggestions, I personally find the third point most interesting:

Can allow further refinements and more possibilities for image handling

and the connected question

how to describe adjustments in a generic way?

So here is a thought how this would work in my ideal world, without providing any real solutions for the problems or whatsoever, and this is in no way a “feature list” that I would expect anybody to fulfill anytime soon :slight_smile::
So I have all this single adjustments that define an image manipulation with several (manipulation) options (whatever makes sense for the manipulation, like saturation or a certain crop size etc.) and it’s also possible to chain this adjustments via configuration.

So I want to apply a certain bundle (chain might be the correct term after all, since the order matters) of manipulations, but be able to overrule other chains without having to “hack” the respective packages.

Or even listen to certain signals that are fired when for example cropping occurs to apply a certain watermark (which would could be a custom manipulation). Ideally there would be a signal before and after the manipulation.

Neos.Image.Interfaces

Why not having all the image related packages prefixed with Neos.Image?

Just stumbled upon them: What about the media view helpers? Would you move them away from the media package as well?

Indeed, with deprecation etc. but in general I would like to provide generic image helpers if possible (this has to be seen)…

I have mulled over this a bit more and in the end, I want to create a defined API/DSL for image manipulation that can be implemented by packages to allow using different technologies/services to do the manipulation.

It could either be something like FlowQuery operations chaining together or like a Fusion configuration or a tree of objects. I am not really sure right now. I just identified two/three outcomes of every single “operation” that can happen:

  1. an intermediate state that can be passed to the next operation (looking at imagine, it woudl be a (wrapped, again similar to FlowQuery) imagine object with the current operation applied (could be lazily handled by the implementation)

  2. a temporary file that can be handed to some operation that needs a file instead of the above intermediate state (this could be something like an optimiser that is rather independent from the other operations)

  3. a finished resource (or rather again a wrapped thing that contains the resource object as well as the image size)

Not sure if 2 and 3 could be combined somehow. I just fear the overhead of always doing 3 if in many cases 2 could suffice.

So IF this would be (I am not in favor of this, just to make some examples) FQ syntax it could be something like this:

JpegOptimizer(Manipulate(variableWithResource).resize(300, 300).greyscale()).get()

Similar examples could be created with something Fusion like and also with an object struture.

What do you thing about the direction?

1 Like

Hi,

I didn’t follow the discussion closely, but @christianm just summarized it for me and we thought about a simple first solution for a more flexible, less coupled image manipulation API (that also supports other blobs such as videos or documents in theory):

So the idea would be to introduce some very slim Interfaces like:

<?php
interface BlobInterface
{
    public function getStream(): resource
}

interface BlobManipulatorInterface
{
    public function apply(): BlobInterface
}

(naming tbd)

Implementations of the BlobManipulatorInterface would always receive the original BlobInterface in their constructor and return a new instance upon apply().

Packages could implement specific BlobManipulators, for example:

<?php
namespace Neos\Imagine\Manipulators;

class ResizeManipulator implements BlobManipulatorInterface
{
    public function __construct(BlobInterface $originalBlob, int $width, int $height)
    {
        // initialize fields
    }

    public function apply(): BlobInterface
    {
        $imagineImage = $this->imagine->read($this->originalBlob->getStream());
        // do imagine stuff with $imagineImage
        return new Blob($imagineImage->get('jpg'));
    }
}

As an optimization, the specific implementations could introduce a more specific BlobInterface implementation such that they don’t have to convert the Blob to the custom implementation for each step:

<?php
namespace Neos\Imagine;

interface ImagineImageBlob implements BlobInterface
{
    public function getImagineImage(): Imagine\Image\ImageInterface
    {
        return $this->imagineImage;
    }
    // ....
}

Finally we could have a slim DSL on top of that or maybe use FQ as Christian suggested:

${q(image).resize(300, 300).greyscale().optimizeJpg()}

and some Service that makes interaction with PersistentResources easier (btw: a PersistentResource would also implement the BlobInterface so it can be used directly with the manipulators).

To be defined:
Do we also need some kind of Context that is passed through the chain (i.e. with informations about the desired image format etc) but hopefully that won’t be necessary because we could always have specific manipulators for that, i.e.:

<?php
$blob = new Blob($somePersistentResource->getStream());
$resized = (new ResizeManipulator($blob, 300, 200))->apply();
$greyscaled = (new GreyscaleManipulator($resized))->apply();
$jpgStream = (new ImageFormatManipulator($greyscaled, 'jpg'))->apply()->getStream();
$pngStream = (new ImageFormatManipulator($greyscaled, 'png'))->apply()->getStream();

(and again, this is just the low level API which would be wrapped by some Service and/or DSL)

2 Likes

I really like the direction towards a generic manipulation stack. :slight_smile:

This is moving forward quite nicely. A bunch of details still have to be ironed out but I wanted to get a quick feeling for the code and pushed forward towards something working. The result is:

This replaces the process method of the media packages ImageService with itself using the Blob + Manipulation implementations based on imagine. Basically this split into three separate packages (imagine stuff -> imagine package) and some tweaks and this could become a first step. DSL etc. can come afterwards.

2 Likes

Wanted to try it out on of our sites (in dev env ofc), but after removing all previous thumbnails this happened

Exception: Argument 1 passed to     Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription_Original::__construct() must be of the type array, string given

89 Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription_Original::__construct("complexResize", array|6|)
88 call_user_func_array("parent::__construct", array|2|)
87 Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription::__construct("complexResize", array|6|)
86 Kitsunet\ImageManipulation\Media\ResizeAdjustmentConverter_Original::getDescription()
85 Kitsunet\ImageManipulation\Media\ManipulationDescriptionFactory_Original::convertAdjustment(Neos\Media\Domain\Model\Adjustment\ResizeImageAdjustment)
84 array_map(array|2|, array|1|)
83 Kitsunet\ImageManipulation\Media\ManipulationDescriptionFactory_Original::convertAdjustments(array|1|)
82 call_user_func_array(array|2|, array|1|)
81 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("convertAdjustments", array|1|)
80 Kitsunet\ImageManipulation\Integration\ImageServiceReplacementAspect_Original::replaceProcessImage(Neos\Flow\Aop\JoinPoint)
79 Neos\Flow\Aop\Advice\AroundAdvice::invoke(Neos\Flow\Aop\JoinPoint)
78 Neos\Flow\Aop\Advice\AdviceChain::proceed(Neos\Flow\Aop\JoinPoint)
77 Neos\Media\Domain\Service\ImageService::processImage(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Flow\ResourceManagement\PersistentResource, array|1|)
76 call_user_func_array(array|2|, array|2|)
75 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("processImage", array|2|)
74 Neos\Media\Domain\Model\ThumbnailGenerator\ImageThumbnailGenerator_Original::refresh(Neos\Media\Domain\Model\Thumbnail)
73 Neos\Media\Domain\Strategy\ThumbnailGeneratorStrategy_Original::refresh(Neos\Media\Domain\Model\Thumbnail)
72 call_user_func_array(array|2|, array|1|)
71 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("refresh", array|1|)
70 Neos\Media\Domain\Model\Thumbnail_Original::refresh()
69 Neos\Media\Domain\Model\Thumbnail_Original::initializeObject(1)
68 Neos\Media\Domain\Model\Thumbnail::__construct(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration)
67 call_user_func_array(array|2|, array|2|)
66 Neos\Media\Domain\Model\Thumbnail::Flow_Aop_Proxy_invokeJoinPoint(Neos\Flow\Aop\JoinPoint)
65 Neos\Media\Domain\Model\Thumbnail::__construct(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration, FALSE)
64 Neos\Media\Domain\Service\ThumbnailService_Original::getThumbnail(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration)
63 Neos\Media\Domain\Service\AssetService_Original::getThumbnailUriAndSizeForAsset(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration, Neos\Flow\Mvc\ActionRequest)
62 call_user_func_array(array|2|, array|3|)
61 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("getThumbnailUriAndSizeForAsset", array|3|)
60 Neos\Media\ViewHelpers\ImageViewHelper_Original::render(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, NULL, NULL, NULL, NULL, FALSE, FALSE, FALSE, "GesagtGetan.ChaletobergurglAt:HeaderImage")
59 call_user_func_array(array|2|, array|9|)
58 Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper::callRenderMethod()
57 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper::initializeArgumentsAndRender()
56 TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInvoker::invoke("Neos\Media\ViewHelpers\ImageViewHelper", array|23|, Neos\FluidAdaptor\Core\Rendering\RenderingContext, Closure)
55 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper::renderStatic(array|23|, Closure, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
54 Frontend_Node_action_show_a63962872cef7968d66807775e4786ffa502f302::{closure}()
53 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper::renderStatic(array|4|, Closure, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
52 Frontend_Node_action_show_a63962872cef7968d66807775e4786ffa502f302::{closure}()
51 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper::evaluateElseClosures(array|1|, array|0|, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
50 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper::renderStatic(array|5|, Closure, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
49 Frontend_Node_action_show_a63962872cef7968d66807775e4786ffa502f302::render(Neos\FluidAdaptor\Core\Rendering\RenderingContext)
48 TYPO3Fluid\Fluid\View\AbstractTemplateView::render()
47 Neos\Fusion\FusionObjects\TemplateImplementation_Original::evaluate()
46 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\TemplateImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…<GesagtGetan.ChaletobergurglAt:Header>/imageSlider", array|9|, array|5|)
45 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…<GesagtGetan.ChaletobergurglAt:Header>/imageSlider", "NULL", Neos\Fusion\FusionObjects\TemplateImplementation)
44 Neos\Fusion\Core\Runtime_Original::evaluate("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…<GesagtGetan.ChaletobergurglAt:Header>/imageSlider", Neos\Fusion\FusionObjects\TemplateImplementation)
43 Neos\Fusion\FusionObjects\Helpers\FusionPathProxy_Original::objectAccess()
42 Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer::getByPath("imageSlider", array|0|)
41 Frontend_Node_action_show_183923ea049ad582bcaad1e627d3c1dd29811d75::{closure}()
40 Frontend_Node_action_show_183923ea049ad582bcaad1e627d3c1dd29811d75::render(Neos\FluidAdaptor\Core\Rendering\RenderingContext)
39 TYPO3Fluid\Fluid\View\AbstractTemplateView::render()
38 Neos\Fusion\FusionObjects\TemplateImplementation_Original::evaluate()
37 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\TemplateImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…mepage.Document>/body<Neos.Fusion:Template>/header", array|10|, array|5|)
36 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…mepage.Document>/body<Neos.Fusion:Template>/header", "NULL", Neos\Fusion\FusionObjects\TemplateImplementation)
35 Neos\Fusion\Core\Runtime_Original::evaluate("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…mepage.Document>/body<Neos.Fusion:Template>/header", Neos\Fusion\FusionObjects\TemplateImplementation)
34 Neos\Fusion\FusionObjects\Helpers\FusionPathProxy_Original::objectAccess()
33 Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer::getByPath("header", array|0|)
32 Frontend_Node_action_show_34320653bbe6792d812e17d6b97ec1112de2888a::{closure}()
31 Frontend_Node_action_show_34320653bbe6792d812e17d6b97ec1112de2888a::render(Neos\FluidAdaptor\Core\Rendering\RenderingContext)
30 TYPO3Fluid\Fluid\View\AbstractTemplateView::render()
29 Neos\Fusion\FusionObjects\TemplateImplementation_Original::evaluate()
28 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\TemplateImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…agtGetan.ChaletobergurglAt:Homepage.Document>/body", array|12|, array|5|)
27 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…agtGetan.ChaletobergurglAt:Homepage.Document>/body", "NULL", Neos\Fusion\FusionObjects\ArrayImplementation)
26 Neos\Fusion\Core\Runtime_Original::evaluate("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…agtGetan.ChaletobergurglAt:Homepage.Document>/body", Neos\Fusion\FusionObjects\ArrayImplementation)
25 Neos\Fusion\FusionObjects\AbstractFusionObject::fusionValue("body")
24 Neos\Fusion\FusionObjects\ArrayImplementation_Original::evaluate()
23 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\ArrayImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…t<GesagtGetan.ChaletobergurglAt:Homepage.Document>", array|19|, array|6|)
22 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…t<GesagtGetan.ChaletobergurglAt:Homepage.Document>", "Exception")
21 Neos\Fusion\Core\Runtime_Original::render("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…t<GesagtGetan.ChaletobergurglAt:Homepage.Document>")
20 Neos\Fusion\FusionObjects\RendererImplementation_Original::evaluate()
19 Neos\Fusion\FusionObjects\MatcherImplementation_Original::evaluate()
18 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\MatcherImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher>", array|5|, array|5|)
17 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher>", "Exception")
16 Neos\Fusion\Core\Runtime_Original::render("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher>")
15 Neos\Fusion\FusionObjects\CaseImplementation_Original::renderMatcher("default")
14 Neos\Fusion\FusionObjects\CaseImplementation_Original::evaluate()
13 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\CaseImplementation, "root", array|10|, array|6|)
12 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root", "Exception")
11 Neos\Fusion\Core\Runtime_Original::render("root")
10 Neos\Neos\View\FusionView_Original::render()
9 Neos\Flow\Mvc\Controller\ActionController_Original::callActionMethod()
8 Neos\Flow\Mvc\Controller\ActionController_Original::processRequest(Neos\Flow\Mvc\ActionRequest, Neos\Flow\Http\Response)
7 Neos\Flow\Mvc\Dispatcher_Original::initiateDispatchLoop(Neos\Flow\Mvc\ActionRequest, Neos\Flow\Http\Response)
6 Neos\Flow\Mvc\Dispatcher_Original::dispatch(Neos\Flow\Mvc\ActionRequest, Neos\Flow\Http\Response)
5 Neos\Flow\Mvc\DispatchComponent_Original::handle(Neos\Flow\Http\Component\ComponentContext)
4 Neos\Flow\Http\Component\ComponentChain_Original::handle(Neos\Flow\Http\Component\ComponentContext)
3 Neos\Flow\Http\Component\ComponentChain_Original::handle(Neos\Flow\Http\Component\ComponentContext)
2 Neos\Flow\Http\RequestHandler::handleRequest()
1 Neos\Flow\Core\Bootstrap::run()

HTTP REQUEST:
GET / HTTP/1.1
Host: chaletobergurgl-image-test.getan.at
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4
Cache-Control: max-age=0



HTTP RESPONSE:
[response was empty]

PHP PROCESS:
Inode: 11313693
PID: 13455
UID: 33
GID: 33
User: www-data

Good idea with the temporary testing package btw. Was super comfy to test, because it’s a single repo for now.

Mmm, seems you have old proxy code there, because looking at
Kitsunet\ImageManipulation\Media\ResizeAdjustmentConverter_Original::getDescription
it should call the constructor in the right way and not like in your error stack.

Maybe clear all caches, pull in latest version and try again. It works fine for me locally.

Really strange… Cleared caches multiple time, the checkout must be up to date since I installed it for the very first time. Does your plugin rely on anything new that was added to master recently? Running Neos 3.1.3 on the installation I want to test it on.

UPDATE:
Just saw you updated 3 hours ago, pulled newest changes.

Now the exception looks like this:

Exception in line 176 of /var/www/chaletobergurgl-image-    test.at/Data/Temporary/Development/Cache/Code/Flow_Object_Classes/Kitsunet_ImageManipulation_ImageBlob_Manipulation_Description_ComplexResizeDescription.php: Call to undefined method Kitsunet\ImageManipulation\ImageBlob\Point::in()

    96 Kitsunet\ImageManipulation\ImageBlob\Box_Original::contains(Kitsunet\ImageManipulation\ImageBlob\Box)
    95 Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription_Original::calculateOutboundBox(Kitsunet\ImageManipulation\ImageBlob\Box, 1920, 825)
    94 Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription_Original::calculateWithFixedDimensions(Kitsunet\ImageManipulation\ImageBlob\Box, 1920, 825)
    93 Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription_Original::calculateDimensions(Kitsunet\ImageManipulation\ImageBlob\Box)
    92 Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription_Original::decompose(Kitsunet\ImageManipulation\Imagine\ImagineImageBlob)
    91 Kitsunet\ImageManipulation\ImageBlob\ConfigurationBasedDescriptionMapper_Original::convertDescriptionToManipulations(Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription, Kitsunet\ImageManipulation\Imagine\ImagineImageBlob)
    90 Kitsunet\ImageManipulation\ImageBlob\ConfigurationBasedDescriptionMapper_Original::Kitsunet\ImageManipulation\ImageBlob\{closure}(array|0|, Kitsunet\ImageManipulation\ImageBlob\Manipulation\Description\ComplexResizeDescription)
    89 array_reduce(array|1|, Closure, array|0|)
    88 Kitsunet\ImageManipulation\ImageBlob\ConfigurationBasedDescriptionMapper_Original::convertDescriptionsToManipulations(array|1|, Kitsunet\ImageManipulation\Imagine\ImagineImageBlob)
    87 Kitsunet\ImageManipulation\ImageBlob\ConfigurationBasedDescriptionMapper_Original::getManipulationStackFor(Kitsunet\ImageManipulation\Imagine\ImagineImageBlob)
    86 Kitsunet\ImageManipulation\ImageBlob\DescriptionMappingService_Original::mapDescriptionsToManipulations(array|1|, Kitsunet\ImageManipulation\Imagine\ImagineImageBlob)
    85 call_user_func_array(array|2|, array|2|)
    84 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("mapDescriptionsToManipulations", array|2|)
    83 Kitsunet\ImageManipulation\Imagine\ImageService_Original::processImage(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Flow\ResourceManagement\PersistentResource, array|1|)
    82 call_user_func_array(array|2|, array|2|)
    81 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("processImage", array|2|)
    80 Kitsunet\ImageManipulation\Integration\ImageServiceReplacementAspect_Original::replaceProcessImage(Neos\Flow\Aop\JoinPoint)
    79 Neos\Flow\Aop\Advice\AroundAdvice::invoke(Neos\Flow\Aop\JoinPoint)
    78 Neos\Flow\Aop\Advice\AdviceChain::proceed(Neos\Flow\Aop\JoinPoint)
    77 Neos\Media\Domain\Service\ImageService::processImage(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Flow\ResourceManagement\PersistentResource, array|1|)
    76 call_user_func_array(array|2|, array|2|)
    75 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("processImage", array|2|)
    74 Neos\Media\Domain\Model\ThumbnailGenerator\ImageThumbnailGenerator_Original::refresh(Neos\Media\Domain\Model\Thumbnail)
    73 Neos\Media\Domain\Strategy\ThumbnailGeneratorStrategy_Original::refresh(Neos\Media\Domain\Model\Thumbnail)
    72 call_user_func_array(array|2|, array|1|)
    71 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("refresh", array|1|)
    70 Neos\Media\Domain\Model\Thumbnail_Original::refresh()
    69 Neos\Media\Domain\Model\Thumbnail_Original::initializeObject(1)
    68 Neos\Media\Domain\Model\Thumbnail::__construct(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration)
    67 call_user_func_array(array|2|, array|2|)
    66 Neos\Media\Domain\Model\Thumbnail::Flow_Aop_Proxy_invokeJoinPoint(Neos\Flow\Aop\JoinPoint)
    65 Neos\Media\Domain\Model\Thumbnail::__construct(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration, FALSE)
    64 Neos\Media\Domain\Service\ThumbnailService_Original::getThumbnail(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration)
    63 Neos\Media\Domain\Service\AssetService_Original::getThumbnailUriAndSizeForAsset(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, Neos\Media\Domain\Model\ThumbnailConfiguration, Neos\Flow\Mvc\ActionRequest)
    62 call_user_func_array(array|2|, array|3|)
    61 Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy::__call("getThumbnailUriAndSizeForAsset", array|3|)
    60 Neos\Media\ViewHelpers\ImageViewHelper_Original::render(Neos\Flow\Persistence\Doctrine\Proxies\__CG__\Neos\Media\Domain\Model\ImageVariant, NULL, NULL, NULL, NULL, FALSE, FALSE, FALSE, "GesagtGetan.ChaletobergurglAt:HeaderImage")
    59 call_user_func_array(array|2|, array|9|)
    58 Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper::callRenderMethod()
    57 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper::initializeArgumentsAndRender()
    56 TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInvoker::invoke("Neos\Media\ViewHelpers\ImageViewHelper", array|23|, Neos\FluidAdaptor\Core\Rendering\RenderingContext, Closure)
    55 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper::renderStatic(array|23|, Closure, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
    54 Frontend_Node_action_show_a63962872cef7968d66807775e4786ffa502f302::{closure}()
    53 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper::renderStatic(array|4|, Closure, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
    52 Frontend_Node_action_show_a63962872cef7968d66807775e4786ffa502f302::{closure}()
    51 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper::evaluateElseClosures(array|1|, array|0|, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
    50 TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper::renderStatic(array|5|, Closure, Neos\FluidAdaptor\Core\Rendering\RenderingContext)
    49 Frontend_Node_action_show_a63962872cef7968d66807775e4786ffa502f302::render(Neos\FluidAdaptor\Core\Rendering\RenderingContext)
    48 TYPO3Fluid\Fluid\View\AbstractTemplateView::render()
    47 Neos\Fusion\FusionObjects\TemplateImplementation_Original::evaluate()
    46 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\TemplateImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…<GesagtGetan.ChaletobergurglAt:Header>/imageSlider", array|9|, array|5|)
    45 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…<GesagtGetan.ChaletobergurglAt:Header>/imageSlider", "NULL", Neos\Fusion\FusionObjects\TemplateImplementation)
    44 Neos\Fusion\Core\Runtime_Original::evaluate("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…<GesagtGetan.ChaletobergurglAt:Header>/imageSlider", Neos\Fusion\FusionObjects\TemplateImplementation)
    43 Neos\Fusion\FusionObjects\Helpers\FusionPathProxy_Original::objectAccess()
    42 Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer::getByPath("imageSlider", array|0|)
    41 Frontend_Node_action_show_183923ea049ad582bcaad1e627d3c1dd29811d75::{closure}()
    40 Frontend_Node_action_show_183923ea049ad582bcaad1e627d3c1dd29811d75::render(Neos\FluidAdaptor\Core\Rendering\RenderingContext)
    39 TYPO3Fluid\Fluid\View\AbstractTemplateView::render()
    38 Neos\Fusion\FusionObjects\TemplateImplementation_Original::evaluate()
    37 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\TemplateImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…mepage.Document>/body<Neos.Fusion:Template>/header", array|10|, array|5|)
    36 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…mepage.Document>/body<Neos.Fusion:Template>/header", "NULL", Neos\Fusion\FusionObjects\TemplateImplementation)
    35 Neos\Fusion\Core\Runtime_Original::evaluate("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…mepage.Document>/body<Neos.Fusion:Template>/header", Neos\Fusion\FusionObjects\TemplateImplementation)
    34 Neos\Fusion\FusionObjects\Helpers\FusionPathProxy_Original::objectAccess()
    33 Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer::getByPath("header", array|0|)
    32 Frontend_Node_action_show_34320653bbe6792d812e17d6b97ec1112de2888a::{closure}()
    31 Frontend_Node_action_show_34320653bbe6792d812e17d6b97ec1112de2888a::render(Neos\FluidAdaptor\Core\Rendering\RenderingContext)
    30 TYPO3Fluid\Fluid\View\AbstractTemplateView::render()
    29 Neos\Fusion\FusionObjects\TemplateImplementation_Original::evaluate()
    28 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\TemplateImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…agtGetan.ChaletobergurglAt:Homepage.Document>/body", array|12|, array|5|)
    27 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…agtGetan.ChaletobergurglAt:Homepage.Document>/body", "NULL", Neos\Fusion\FusionObjects\ArrayImplementation)
    26 Neos\Fusion\Core\Runtime_Original::evaluate("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…agtGetan.ChaletobergurglAt:Homepage.Document>/body", Neos\Fusion\FusionObjects\ArrayImplementation)
    25 Neos\Fusion\FusionObjects\AbstractFusionObject::fusionValue("body")
    24 Neos\Fusion\FusionObjects\ArrayImplementation_Original::evaluate()
    23 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\ArrayImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…t<GesagtGetan.ChaletobergurglAt:Homepage.Document>", array|19|, array|6|)
    22 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…t<GesagtGetan.ChaletobergurglAt:Homepage.Document>", "Exception")
    21 Neos\Fusion\Core\Runtime_Original::render("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher…t<GesagtGetan.ChaletobergurglAt:Homepage.Document>")
    20 Neos\Fusion\FusionObjects\RendererImplementation_Original::evaluate()
    19 Neos\Fusion\FusionObjects\MatcherImplementation_Original::evaluate()
    18 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\MatcherImplementation, "root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher>", array|5|, array|5|)
    17 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher>", "Exception")
    16 Neos\Fusion\Core\Runtime_Original::render("root<Neos.Fusion:Case>/default<Neos.Fusion:Matcher>")
    15 Neos\Fusion\FusionObjects\CaseImplementation_Original::renderMatcher("default")
    14 Neos\Fusion\FusionObjects\CaseImplementation_Original::evaluate()
    13 Neos\Fusion\Core\Runtime_Original::evaluateObjectOrRetrieveFromCache(Neos\Fusion\FusionObjects\CaseImplementation, "root", array|10|, array|6|)
    12 Neos\Fusion\Core\Runtime_Original::evaluateInternal("root", "Exception")
    11 Neos\Fusion\Core\Runtime_Original::render("root")
    10 Neos\Neos\View\FusionView_Original::render()
    9 Neos\Flow\Mvc\Controller\ActionController_Original::callActionMethod()
    8 Neos\Flow\Mvc\Controller\ActionController_Original::processRequest(Neos\Flow\Mvc\ActionRequest, Neos\Flow\Http\Response)
    7 Neos\Flow\Mvc\Dispatcher_Original::initiateDispatchLoop(Neos\Flow\Mvc\ActionRequest, Neos\Flow\Http\Response)
    6 Neos\Flow\Mvc\Dispatcher_Original::dispatch(Neos\Flow\Mvc\ActionRequest, Neos\Flow\Http\Response)
    5 Neos\Flow\Mvc\DispatchComponent_Original::handle(Neos\Flow\Http\Component\ComponentContext)
    4 Neos\Flow\Http\Component\ComponentChain_Original::handle(Neos\Flow\Http\Component\ComponentContext)
    3 Neos\Flow\Http\Component\ComponentChain_Original::handle(Neos\Flow\Http\Component\ComponentContext)
    2 Neos\Flow\Http\RequestHandler::handleRequest()
    1 Neos\Flow\Core\Bootstrap::run()

    HTTP REQUEST:
    GET / HTTP/1.1
    Host: chaletobergurgl-image-test.getan.at
    Connection: keep-alive
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
    Accept-Encoding: gzip, deflate, br
    Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4
    Cache-Control: max-age=0



    HTTP RESPONSE:
    [response was empty]

    PHP PROCESS:
    Inode: 11313693
    PID: 40453
    UID: 33
    GID: 33
    User: www-data

Ok, that’s good input, I guess I didn’t hit case in my manual testing so far.

And fixed it.

1 Like

Aaannndd we have lift-off

Looking forward to try out some crazy image manipulations (will do so in one of the upcoming evenings)

And next update is in. Now with configurable pre/post manipulations and some code cleanup.

Generally it’s now shaping up really nicely. I have a few dangling ends for now and I would like to tackle the topic of remote services and image URIs. IMHO it would be great if the thing offered me a way to just get a (manipulated) image URI. This could be interesting in case of remote image manipulation services when you don’t want to download the result and store it on the server but just serve the resulting URI.

Hi all,

we just talked about this again and had a look at Christians first implementation

It’s already very promising and advanced, have a look!
We decided to change a few things though:

  1. Instead of having the BlobMetadata be basically a non-typed object, implement the minimal required methods (for now we could only really think of getMediaType() and maybe getFilename()). (Note: It should stay immutable, Manipulators can create new copies if required, e.g. if they change the media type)
  2. Remove the whole “description” abstraction from the core API and rather create some “composite manipulator” that takes some special configuration (i.e. DSL) to create the actual manipulators on the fly
  3. Add some sub interface of BlobInterface called UrlEnabledBlobInterface (name tbd) so manipulators working with remote services can return the remote URL::

Example:

<?php
interface UrlEnabledBlobInterface {
    public function getUrl(): Uri // or string?
}

final class SomeSpecialImageBlob implements ImageBlobInterface, UrlEnabledBlobInterface
{

    public function getSize(): BoxInterface
    {
        // determine size using the remote service
    }

    public function getMetadata(): BlobMetadata
    {
        return $this->blobMetadata;
    }

    public function getStream(): resource
    {
         // simplified
         return fopen((string)$this->getUrl(), 'r');
    }

    public function getUrl(): Uri
    {
         // determine URL using the remote service
    }
}
1 Like

Just a quick follow-up:
We were thinking about how to make some “implementation specific options” available to the manipulators.
These options could be Imagine Image Quality, some Remote Service API Key, …
One solution would be to inject those settings into the manipulators (or use some service with the settings injected).

But we were looking for ways to avoid these “Global contexts” and there are some reasons for this:

  • Make the manipulators more self-contained
  • …and easier to test
  • Allow different “presets”, e.g. use Service A in one context and Service B in another

In order to keep the core API slim, we came up with following approach:
Add an untyped options array to the BlobMetadata that can contained any prefixed(!) options, for example:

<?php
$metadata = new BlobMetadata('image/png', 'someFile.png', ['Neos.Imagine' => ['jpeg_quality' => 80]]);

Some default factory could create the metadata object and inject some options configured via Settings, for example:

Kitsunet:
  ImageManipulation:
    defaultOptions:
      'Neos.Imagine':
        'jpeg_quality' => 80

Questions:

  1. Maybe use the composer package key as namespace instead?
  2. Is BlobMetadata the right name now that this is more of a “Manipulation context” (that can even be replaced by individual manipulators)?

Note: I’m just writing down what we discussed yesterday (or what I took away from it) for the record. All credit to @christianm, I haven’t written a line of code :wink: