Creating Control Sets and Filters for the Image Editor

Note: this requires Concrete CMS 7.5 or greater

Concrete version 7 shipped with an exciting, full-featured image editor directly accessible from within the File Manager.

The Image Editor provides simple cropping, rotation and resizing controls, and some simple, fun image filters. Extending this functionality is something that developers can do.

Extension Points

Image editor extensions give you the ability to add html into the image editor sidebar and control the editing image using javascript. There are two types of extensions in Concrete, "control" and "filter". Controls manage things like resizing and rotating an image while filters just change the imagedata to do things like blur or brighten the active image.

Adding a control with configuration

Image editor configuration lives under its own group imageeditor, to modify this group create and edit application/config/imageeditor.php. Extensions live underneath the extensions item so a proper image editor config file looks something like:

<?php
return array(
    'extensions' => array(

        // Name the item in the extensions array so that we can easily override it
        'custom_extension' => array(

            // Define the type as ImageEditorExtensionControl
            'type' => Concrete\Core\ImageEditor\ImageEditor::ImageEditorExtensionControl,

            // Give it a cool display name
            'name' => 'My Custom Extension',

            // Give it a neat handle
            'handle' => 'custom_extension',

            // Provide the name of a registered javascript asset
            'src' => 'my_javascript_asset',

            // Provide the path to a view
            'view' => 'editor/controls/custom_extension',

            // List the additional assets you want added to the page, make the key the asset handle and the value an array of types
            'assets' => array(
                'my_javascript_asset' => array('css')
            )
        )
    )
);

Adding a control with code

You can retrieve the editor object by using the `editor/image/core' application binding:

<?php
$editor = \Core::make('editor/image/core');

Using the core extension class

Concrete provides an EditorExtensionInterface implementation that is easy set up without having to extend any classes

<?php
// Create a new extension instance
$extension = new \Concrete\Core\ImageEditor\Extension();

// Set name and handle
$extension->setName('My Custom Extension');
$extension->setHandle('custom_extension');

// Set the view
$view = new View('path/to/view');
$extension->setView($view);

// Create a new JavaScript asset
$asset = new \Concrete\Core\Asset\JavascriptAsset();
$asset->mapAssetLocation('imageeditor/controls/custom.js');
$extension->setExtensionAsset($asset);

// Add additional assets
$asset = new \Concrete\Core\Asset\CssAsset();
$asset->mapAssetLocation('imageeditor/controls/custom.css');
$extension->addAsset($asset);

// Add the extension to the editor as a control
$editor = \Core::make('editor/image/core');
$editor->addControl($extension);

// If this extension is a filter, you can run `$editor->addFilter` instead of `$editor->addControl`

Providing your own implementation

The core image editor expects only implementations of EditorExtensionInterface to be added, because it's an interface we can provide a completely custom implementation

<?php

class CustomExtension implements \Concrete\Core\ImageEditor\EditorExtensionInterface
{

    protected $view;
    protected $assets;
    protected $extensionAsset;

    public function getHandle()
    {
        return "custom_asset";
    }

    /**
     * @return string
     */
    public function getName()
    {
        return "My Custom Extension";
    }

    /**
     * @return \Concrete\Core\Asset\AssetInterface
     */
    public function getExtensionAsset()
    {
        return $this->extensionAsset;
    }

    /**
     * @return \Concrete\Core\Asset\AssetInterface[]
     */
    public function getAssets()
    {
        return $this->assets;
    }

    /**
     * @return AbstractView
     */
    public function getView()
    {
        if (!$this->view) {
            $this->view = new View('some/view/path);
        }

        return $this->view;
    }

}

And then you can register it by running:

<?php

$extension = new CustomExtension();
\Core::make('editor/image')->addControl($extension);

Implementing a Filter

Once you have your filter extension loading, we can worry about the JavaScript that actually does the image processing.

First we need a javascript function that will modify an ImageData object. For this example, we will make a filter that shifts an image to red.

// Shift each pixel 50% to red
var red_filter = function(imageData) {
    var data = imageData.data, amount = 0.5, length, i;

    for (i = 0, len = data.length; i < len; i += 4) {
        // Modify only the red channel to be 50% closer to 255
        data[i] += (255 - data[i]) * amount;
    }
};

Example image

The next step is to set up our example thumbnail, to do that we listen for an event filterApplyExample and fire an event filterBuiltExample when we finish applying the filter.

var filter = this;
filter.im.bind('filterApplyExample', function (e, data) {
    if (data.namespace === filter.im.namespace) {
        // Set the filter on the example image
        data.image.setFilter(red_filter);
        filter.im.fire('filterBuiltExample', filter, data.elem);
    }
});

Applying a filter

Now to apply this filter when the user selects it, we listen for an event filterChange.

var filter = this;
filter.im.bind('filterChange', function (e, data) {

    // Ensure that this is our filter being selected
    if (data.im.namespace == filter.im.namespace) {

        // Set the filter on the active element
        filter.im.activeElement.setFilter(red_filter);

        // Let everyone know
        filter.im.fire('filterApplied', filter);
    }
});

Controls

Some filter like the gaussian blur filter have extra controls that let you alter the filter while it is applied. The best way to set up your custom controls is to hook into the FilterFullyLoaded event

filter.im.bind('filterFullyLoaded', function (e, data) {
    if (data.im.namespace === filter.im.namespace) {
        data.parent.find('.some-control').change(function() {
            ...
        });
    }
});

Full example

So all together, a simple filter's JS will like this:

/**
 * Red shift filter
 */
$(function(filter) {
    var active = false, input;

    // Shift each pixel 50% to red
    var red_filter = function(imageData) {
        var data = imageData.data, amount = 0.5, length, i;

        for (i = 0, len = data.length; i < len; i += 4) {
            // Modify only the red channel to be 50% closer to 255
            data[i] += (255 - data[i]) * amount;
        }
    };

    filter.im.bind('filterChange', function (e, data) {
        // Ensure that this is our filter being selected
        if (data.im.namespace == filter.im.namespace) {

            // Set the filter on the active element
            filter.im.activeElement.setFilter(red_filter);

            // Let everyone know
            filter.im.fire('filterApplied', filter);
        }
    });

    filter.im.bind('filterApplyExample', function (e, data) {
        if (data.namespace === filter.im.namespace) {
            // Set the filter on the example image
            data.image.setFilter(red_filter);
            filter.im.fire('filterBuiltExample', filter, data.elem);
        }
    });

}(this));