DelegatedImageSource
One common task in working with images is, given a file or a set of files, read in each page, perform some operation on it and write it as a page in a target file.
This is tricky to do out of the box in dotImage in that a lot of the onus of memory management falls onto client code. The client needs to know how to append images onto a possibly existing file in a way that won’t use up a ton of memory.
Let me give you an example:
TiffDocument doc = new TiffDocument();
for (int i = 0; i < totalPages; i++) {
AtalaImage page = GetSomePage(i);
Watermark(page);
doc.Pages.Add(new TiffPage(page, TiffCompression.Default));
page.Dispose();
}
doc.Save(someOutputFile);
This is a chunk of code that is meant to loop through some set of pages, watermark each one, add it to a TiffDocument and then save it to a TIFF. We’re being good in that we’re calling Dispose() on the AtalaImage to let it go. The problem is that TiffPage allocates memory for the image (in most cases compressed) so if you’re working with a dozen pages or less, you’ll be fine, but if you work with hundreds, you will bring your machine to a crawl.
In thinking about this, I decided that what we needed was a flavor of ImageSource and RandomAccessImageSource that could adapt an existing ImageSource and allow the user to inject code into the process.
To do this, we’ll create a subclass of ImageSource that takes another ImageSource as an argument. It will let the provided ImageSource do all the heavy lifting, allowing code injection at the appropriate time:
using System;
using Atalasoft.Imaging;
namespace DelegatedImageSource
{
public class DelegatedImageSource : ImageSource
{
Func<AtalaImage, AtalaImage> _imageProc;
ImageSource _source;
public DelegatedImageSource(ImageSource source, Func<AtalaImage, AtalaImage> imageProc)
{
if (source == null)
throw new ArgumentNullException("source");
if (imageProc == null)
throw new ArgumentNullException("imageProc");
_imageProc = imageProc;
_source = source;
}
protected override ImageSourceNode LowLevelAcquireNextImage()
{
AtalaImage image = _source.AcquireNext();
AtalaImage processed = _imageProc(image);
processed = processed ?? image;
if (processed == image)
{
processed = (AtalaImage)image.Clone();
}
_source.Release(image);
return new ImageSourceNode(processed, null);
}
protected override void LowLevelDispose()
{
_source.Dispose();
}
protected override bool LowLevelFlushOnReset()
{
return true;
}
protected override bool LowLevelHasMoreImages()
{
return _source.HasMoreImages();
}
protected override void LowLevelReset()
{
_source.Reset();
}
protected override void LowLevelSkipNextImage()
{
_source.Release(_source.AcquireNext());
}
protected override int LowLevelTotalImages()
{
return _source.TotalImages;
}
protected override bool LowLevelTotalImagesKnown()
{
return _source.TotalImagesKnown;
}
}
}
The main meat of the code is in LowLevelAcquireNextImage, which calls AcquireNext on the provided source, then hands the resulting AtalaImage off to the delegate. If the delegate returns null or the original image, we return a clone, otherwise we return the image created by user code.
Here is an example snippet which combines a set of images into a single TIFF:
FileSystemImageSource source = new FileSystemImageSource(@"\Images\Documents", "*", true);
DelegatedImageSource dis = new DelegatedImageSource(source,
(image) => { return null; });
TiffEncoder encoder = new TiffEncoder();
using (FileStream fs = new FileStream("output.tif", FileMode.Create))
{
encoder.Save(fs, dis, null);
}
which is a sweet little chunk of code to combine image files into one TIFF. Further, there won’t be the same memory issue as with our naive example because ImageSource handles resource management for you via the Acquire/Release model. You might ask, “so why do need the TiffDocument class when I can do all the work like this?” The answer is that when an AtalaImage is created from a TIFF document, the metadata associated with the page (EXIF, etc) is shed. TiffDocument maintains that information.
Now let’s change our task to be “mark each page with a red X and save as a PDF”. The code doesn’t change much:
DelegatedImageSource dis = new DelegatedImageSource(source, (image) =>
{
AtalaImage rgb = null;
if (image.PixelFormat == PixelFormat.Pixel24bppBgr || image.PixelFormat == PixelFormat.Pixel32bppBgr ||
image.PixelFormat == PixelFormat.Pixel32bppBgra)
{
rgb = image;
}
else
{
rgb = image.GetChangedPixelFormat(PixelFormat.Pixel24bppBgr);
}
using (Graphics g = rgb.GetGraphics())
{
using (Pen p = new Pen(Color.Red, 25))
{
g.DrawLine(p, 0, 0, rgb.Width, rgb.Height);
g.DrawLine(p, 0, rgb.Height, rgb.Width, 0);
}
}
return rgb;
});
PdfEncoder encoder = new PdfEncoder();
using (FileStream fs = new FileStream("output.pdf", FileMode.Create))
{
encoder.Save(fs, dis, null);
}
Note that the save code is identical. This is because both PdfEncoder and TiffEncoder are MultiFramedEncoder objects which can take an ImageSource in one flavor of Save().
Let’s say that you want instead to overlay a watermark onto each image – the code, again, doesn’t change much:
AtalaImage watermark = new AtalaImage(@"C:\Users\shawley\Pictures\bunny.png");
OverlayCommand overlay = new OverlayCommand(watermark, new Point(20, 20));
overlay.Opacity = 0.50;
overlay.ApplyToAnyPixelFormat = true;
DelegatedImageSource dis = new DelegatedImageSource(source, (image) =>
{
return overlay.Apply(image).Image;
});
TiffEncoder encoder = new TiffEncoder();
using (FileStream fs = new FileStream("output.tif", FileMode.Create))
{
encoder.Save(fs, dis, null);
}
In this case, I’m using the captured variable, overlay, to write onto the image, turning the actual watermarking code into a one-liner.
And here, finally, is the same code modified to work specifically for RandomAccessImageSource:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Atalasoft.Imaging;
namespace DelegatedImageSource
{
public class DelegatedRandomAccessImageSource : RandomAccessImageSource
{
Func<AtalaImage, int, AtalaImage> _imageProc;
RandomAccessImageSource _source;
public DelegatedRandomAccessImageSource(RandomAccessImageSource source, Func<AtalaImage, int, AtalaImage> imageProc)
{
if (source == null)
throw new ArgumentNullException("source");
if (imageProc == null)
throw new ArgumentNullException("imageProc");
_imageProc = imageProc;
_source = source;
}
protected override ImageSourceNode LowLevelAcquire(int index)
{
AtalaImage image = _source[index];
AtalaImage processed = _imageProc(image, index);
processed = processed ?? image;
if (processed == image)
{
processed = (AtalaImage)image.Clone();
}
_source.Release(image);
return new ImageSourceNode(image, null);
}
protected override ImageSourceNode LowLevelAcquireNextImage()
{
AtalaImage image = _source.AcquireNext();
AtalaImage processed = _imageProc(image, -1);
processed = processed ?? image;
if (processed == image)
{
processed = (AtalaImage)image.Clone();
}
_source.Release(image);
return new ImageSourceNode(processed, null);
}
protected override void LowLevelDispose()
{
_source.Dispose();
}
protected override bool LowLevelFlushOnReset()
{
return true;
}
protected override bool LowLevelHasMoreImages()
{
return _source.HasMoreImages();
}
protected override void LowLevelReset()
{
_source.Reset();
}
protected override void LowLevelSkipNextImage()
{
_source.Release(_source.AcquireNext());
}
protected override int LowLevelTotalImages()
{
return _source.TotalImages;
}
}
}
So we see that by using delegates in tandem with ImageSource objects, we can make repetitive tasks much easier and remove a lot of issues in resource management at the same time.