Unlocking ImageCommand: Custom Commands

May 12, 2015 Alex Huck

In this series we are going to work with Atalasoft's ImageCommand class. I have a unique perspective at Atalasoft because my educational background is in game engine programming. The GPU is something that modern computers have that could augment the speed and power that ImageCommands could use to interact with your documents. So to begin, I am going to demonstrate how to create a simple ImageCommand, and later demonstrate how the ImageCommand can be implemented to benefit from a GPU.  

When implementing the ImageCommand class two methods and one property that must be implemented:

  • PerformActualCommand
    This is where all the magic happens. An AtalaImage is brought in and its pixels are adjusted, according to the desired algorithm.
  • VerifyProperties 
    This is used to check that all of the properties pass a sanity check. It occurs before PerformActualCommand and should throw ArgumentExceptions if something is off.
  • SupportedPixelFormats
    This property returns an array of PixelFormat enumerations. Each pixel format that the command supports needs to be included in this array. 

To demonstrate an implementation, I am going to create a AddBorderCommand, that will add a border to an AtalaImage object. To start we are going to add a constructor and a few properties to define how the border should appear.

        public AddBorderCommand(int thickness, Color color)
        {
            this.Thickness = thickness;
            this.Color = color;
        }

        public int Thickness { get; set; }
        public Color Color { get; set; }

For SupportedPixelFormats, specifically and simplistically for demonstration purposes we will only support Pixel24bppBgr (8 bit color using Red Blue and Green channels).

        public override PixelFormat[] SupportedPixelFormats
        {
            get { return new []{PixelFormat.Pixel24bppBgr}; }
        }

For verify properties we need to establish that the border thickness is non-negitive and not greater than half the size of the image, the System.Drawing.Color class provides a 32-bit RGBa color, so we will simply ignore the alpha channel.

        protected override void VerifyProperties(AtalaImage image)
        {
            if(this.Thickness < 0)
            {
                throw new ArgumentException("Thickness must be positive.", "Thickness");
            }

            if(this.Thickness > Math.Min(image.Width, image.Height) / 2)
            {
                throw new ArgumentException("Thickness must less than or equal to half the image size (The smaller of either the Width or Height).", "Thickness");
            }
        }
    }

Finally for PerformActualCommand, we will simply check to see if our X or Y position is within the thickness to the edge of the image and apply the color to the location if it is. Since this command can be optimized to save memory and time by using in place processing we can configure the command to be done in place by overriding the InPlaceProcessing property

        public override bool InPlaceProcessing
        {
            get
            {
                return true;
            }
        }

And this then allows us to write directly to the source image in our PerformActualCommand:

        protected override AtalaImage PerformActualCommand(AtalaImage source, AtalaImage dest, Rectangle imageArea, ref ImageResults results)
        {
            using(PixelAccessor pixelAccessor = source.PixelMemory.AcquirePixelAccessor())
            {
                // Prepare a scanline containing the bytes of the color of the border.

                // BGR formatted bytes of the color:
                byte[] colorBytes =
                {
                    this.Color.B,
                    this.Color.G,
                    this.Color.R
                };

                // Make the scanline and fill it with colorBytes repeated.
                byte[] borderScanline = new byte[colorBytes.Length * source.Width];
                for(int i = 0; i < borderScanline.Length; ++i)
                {
                    borderScanline[i] = colorBytes[i % colorBytes.Length];
                }

                // Write scanlines

                int borderByteCount = colorBytes.Length * this.Thickness;

                for(int y = 0; y < pixelAccessor.Height; ++y)
                {
                    if(y < this.Thickness || y > pixelAccessor.Height - this.Thickness - 1)
                    {
                        // For y coordinates of the image we simply copy the prepared border scanline across whole.
                        pixelAccessor.WriteToScanline(y, 0, borderScanline, 0, borderScanline.Length);
                    }
                    else
                    {
                        // For any other scanline we copy just a region of the prepared border scanline to the left portion and the right portion of the image.

                        // Left border segment:
                        pixelAccessor.WriteToScanline(y, 0, borderScanline, 0, borderByteCount);

                        // Right border segment:
                        pixelAccessor.WriteToScanline(y, borderScanline.Length - borderByteCount, borderScanline, 0, borderByteCount);
                    }
                }

                return source;
            }
        }

So this leaves us ready to move forward with any other image manipulation that is desired. The next series will cover making a GPU based ImageCommand and will cover some of the available GPU APIs. 

About the Author

Alex Huck

Alex Huck joined Atalasoft as a Software Engineer in 2013.

More Content by Alex Huck
Previous Article
Spring Fling: Atalasoft 10.6 Document SDKs and MobileImage Now Available
Spring Fling: Atalasoft 10.6 Document SDKs and MobileImage Now Available

The bitter New England winter is now behind us here in sunny Easthampton,...

Next Article
Functional Programming: It Is/Is Not a Silver Bullet
Functional Programming: It Is/Is Not a Silver Bullet

Functional Programming Is a Silver Bullet Having written a decent amount...

Try any of our Imaging SDKs free for 30 days with Full Support

Download Now