Why It Helps to Understand Pixel Formats
A pixel format in dotImage is a description of the depth, color space and layout of the memory used to represent a pixel. Mathematically, there are an infinite number of possible pixel formats as color space is as much a space as Cartesian space. You can create any number of basis sets that map onto any particular color space. However, only a small subset are actually useful. In dotImage we represent 12 formats, 3 paletted, 3 gray, and the rest color.
It helps to understand the meaning of pixel formats in terms of quantity of information rather than quality of information. Having this understanding will help you in avoiding possible problems when processing images or converting from one format to another.
All ImageCommand classes in dotImage advertise a list of all PixelFormats that they operate in. The property SupportedPixelFormats gives you this list. You can also call IsPixelFormatSupported() to ask if a specific format is supported.
I would like to say that IsPixelFormatSupported returns true if and only if an ImageCommand natively supports a given PixelFormat. In most cases this is true, but some older commands (before I got my hands on them) were written to operate on some formats as a convenience, but without letting the caller know. They still behave exactly as before for compatibility. ScribbleCommand is an example of this – it will strip out the alpha channel into a temporary image, before performing the operation.
What is true, is that every implementation of SupportedPixelFormats will return the “best” pixel format for the command as the first entry in the list. This is by convention, but we enforce it strictly. There’s a reason for this: all ImageCommands have the ability to operate on any pixel format. This is done by setting the property ApplyToAnyPixelFormat to true. Then, when the command is executed, if the source image is not in SupportedPixelFormats, the command will create a new version of the image in a better PixelFormat for the command. This is done internally by the command by calling SelectBestAlternatePixelFormat. The default implementation is to return the 0th entry in SupportedPixelFormats.
In short, if ApplyToAnyPixelFormat is true, the command will create a new image in the pixel format SupportedPixelFormats[0]. The default value for ApplyToAnyPixelFormat is false.
Wait a minute – this sounds like a tremendously useful feature! Why would we have it off by default?
There are two reasons: (1) backwards compatibility – where possible we try to assure that all commands in new versions of dotImage operate as they did in the past. (2) “Do No Harm” – changing the PixelFormat of an image may be a lossy operation. If the amount of information in the source image’s pixel format is greater than the amount of information in the target pixel format, then information has to be discarded in the translation. How that information is discarded makes all the difference in the world, and the choice that is made should be dependent on the source image domain. That is, if the image is a photograph, you should use one technique (dithering or another type of halftoning) and if it is a document, you should use another (dynamic or adaptive threshold).
When ApplyToAnyPixelFormat is turned on, ImageCommands will use AtalaImage.GetChangedPixelFormat() to create the new image. By default (and again, to match legacy behavior), when dotImage reduces a image to 1-bit per pixel, it uses either dithering or color quantization to generate the target image. This is fine for photographic images, but lousy for documents. Fortunately, you can change how this change will happen.
How can I know if changing a pixel format would be lossy?
You can do that by comparing the number of bits per pixel:
public bool IsPixelFormatChangeLossy(PixelFormat source, PixelFormat target)
{
return PixelFormatUtilities.BitsPerPixel(source) >
PixelFormatUtilities.BitsPerPixel(target);
}
and while the accuracy of this predicate is arguable, it will be good enough for most cases. For the nit pickers, conversion from color to 8 or 16 bpp gray, while lossy is well-defined enough to let it go – although you should be aware that the color conversion is done through a perceptual model, rather than a linear model. Also, conversions of 24 bit color to 8 bit paletted may be lossless in some cases, but that is more computationally and memory intensive than I’d like to put in such a simple predicate.
As a side node, PixelFormatUtilities is my favorite grab bag of useful functions and predicates based around PixelFormats. They are akin to the functions in the C routines defined in ctype.h in that they are almost always table driven and operate in constant time.