Thursday, March 15, 2007

Writing a Simple Image Editor in CAB - Part 4

In today’s installment we’ll continue where we left off and add the functionality to apply image filters.

In order to do this, let’s take a step back and think about how we want to accomplish this.  I’m imagining two use cases:

  • Load filters from dynamic assemblies
  • Apply a filter to the image

To load the filters, I’ll have a separate assembly implement a specific interface, then they will be loaded from a directory and placed in the menu.

First, our core interface:

using System;

using System.Drawing;

 

namespace ImageFiltering

{

    public interface IImageFilter

    {

        string Name { get; }

        Bitmap Filter(Bitmap source);

    }

}

The name property determines what will be displayed on the menu.  The Filter method is, as they say on MTV Cribs, “where the magic happens.”

I’ll implement some basic image filters in a bit.  Since both of these use cases are related, I can package them up in the same module.  I create a module called ImageFiltering.

The first workitem is going to be the LoadFiltersWorkItem.  It will run at startup and read up the assemblies and obtain a list of IImageFilter implementations:

private UIExtensionSite GetMainMenu()

{

    //return the well known ui extension site for the main menu

    return RootWorkItem.UIExtensionSites["MainMenuStrip"];

}

 

protected override void OnRunStarted()

{

    base.OnRunStarted();

 

    //get the menu strip

    UIExtensionSite site = GetMainMenu();

 

    //create the new menu item

    ToolStripMenuItem filterMenu = new ToolStripMenuItem();

    filterMenu.Text = "Filters";

 

    //load the filters

    LoadFiltersFromDirectory(filterMenu);

 

    //add the filters menu to the site

    site.Add<ToolStripMenuItem>(filterMenu);

}

The LoadFiltesrFromDirectory(filterMenu) method basically uses reflection to pull out all of the assemblies in the /filters directory of our application.  The details of this aren’t important here, but if you want to see it you can download the project at the end of today’s article.

Since this needs to run when the application starts up, we code that up in the Module Init class:

public class ImageFilteringModuleInit : ModuleInit

{

    private WorkItem _rootWorkItem;

    [ServiceDependency]

    public WorkItem RootWorkItem

    {

        get { return _rootWorkItem; }

        set { _rootWorkItem = value; }

    }

 

    private LoadFiltersWorkItem _loadFiltersWorkItem;

 

    public override void Load()

    {

        base.Load();

 

        //load up our work items

        _loadFiltersWorkItem = this.RootWorkItem.WorkItems.AddNew<LoadFiltersWorkItem>("LoadFiltersWorkItem");

        _loadFiltersWorkItem.Run();

    } 

}

This is pretty basic Module Init stuff.  Create the work item and run it.  Assuming we’ve already created some filters and placed the dll inside of the filters directory, we should be able to run the application and see the filters loaded in the menu.

Our filters have been loaded

Notice how I initially set the menu items to be disabled.  This is because no image has been loaded yet!  We already have an event being raise when the image is loaded, so I handle that in the LoadFiltersWorkItem and enable the menu items:

[EventSubscription("topic://ImageEditor/ImageDisplayModule/ImageLoaded", ThreadOption.UserInterface)]

public void OnImageSelected(object sender, DataEventArgs<Bitmap> e)

{

    _image = e.Data;

    foreach (ToolStripMenuItem filterItem in _filterItems)

    {

        filterItem.Enabled = true;               

    }

}

Attached to the Tag property of each of these menu items is the concrete filter that we dynamically loaded.  The next step is to raise an event when the filter is clicked, so that the other work item (that we’re about to create) can apply the filter.

As I create each of the menu items, I wire up the click event to call this handler:

void filterItem_Click(object sender, EventArgs e)

{

    ToolStripMenuItem menuItem = sender as ToolStripMenuItem;

    IImageFilter filter = menuItem.Tag as IImageFilter;

 

    if (FilterSelected != null)

    {

        DataEventArgs<IImageFilter> args = new DataEventArgs<IImageFilter>(filter);

        FilterSelected(this, args);

    }

}

The event is raised with the concrete filter passed along as data event args.

The next work item doesn’t get created at start up.  It begins when the user clicks the menu item.  So when/where do we kick off the work item?  We handle the FilterSelected event (that is being published as a CAB event) in our module, and create the workitem there:

 [EventSubscription(ModuleConstants.Events.FilterSelected, ThreadOption.UserInterface)]

 public void OnFilterSelected(object sender, DataEventArgs<IImageFilter> e)

 {

    //get the current image

    Bitmap image = _loadFiltersWorkItem.Image;

 

    //create a new workitem to apply the filter           

    WorkItem wi = new FilterImageWorkItem(e.Data, image);

    this.RootWorkItem.WorkItems.Add(wi);

 

    wi.Run();

 }

We have to get a handle on the current image as well as the filter, and this proved to be a bit difficult.  I’d like to have some sort of global state (and maybe I’m missing something) but the State property is WorkItem specific.  I was already handling the ImageLoaded event in the LoadFiltersWorkItem, so I snag the image from that event and hang on to it.  The module reads this and passes it to the FilterImageWorkItem.  Until I can figure out a cleaner way to do this, it will have to stay for now.

Here’s the FilterImageWorkItem:

public class FilterImageWorkItem : WorkItem

{

    private IImageFilter _filter;

    private Bitmap _image;

 

    [EventPublication(ModuleConstants.Events.ImageLoaded, PublicationScope.Global)]

    public event EventHandler<DataEventArgs<Bitmap>> ImageLoaded;

 

    public FilterImageWorkItem(IImageFilter filter, Bitmap image)

    {

        _filter = filter;

        _image = image;

    }   

 

    protected override void OnRunStarted()

    {

        base.OnRunStarted();

 

        Bitmap newImage = _filter.Filter(_image);

        if (ImageLoaded != null)

        {

            ImageLoaded(this, new DataEventArgs<Bitmap>(newImage));

        }

    }

}

Pretty plain and simple.  It calls the filter on the image when it runs.  We need to notify the canvas to redraw the new image, so we raise the already implemented event ImageLoaded.  Once this fires, the DisplayImageWorkItem picks it up and displays the filtered image.

Let’s check out the app in action.

The original image
The original image

The Black & White filter applied
The Black & White filter applied

The inversion filter applied
The inversion filter applied

The image filters I originally wrote were painfully slow, so I borrowed some excellent filters from the code project article here by Christian Graus.  I’m glad I could leverage somebody else’s hard work, because it’s been a few years since I did complex image processing .

I hope you enjoyed this short series of articles on building an application using CAB.  I only touched on some of the topics.  For example, ObjectBuilder was barely used in this application.  I also didn’t use WorkItem State or the StatePersistenceService.  This would be a quick and easy way to save the users work, or you could provide a standard FileSaveDialog box.

You can download the source here:
File Attachment: CAB-ImageEditor.zip (86 KB)
(you’ll need to have CAB already installed and you need to point the reference to the CAB dlls)

I’d appreciate any comments or criticisms you may have for me.

Debt - Credit Card Consolidation - Phoenix Pools - Arizona Landscaping