Login
 
Atalasoft DotImage
Released: 03/09/2009

Using the Flexibility of the TIFF Document Format and DotImage to Solve Problems

Resources for this article:

Every problem has a story attached to it.  In product design and development, these are typically called Use Cases: stories that describe a user who is trying to accomplish a particular goal and the roadblocks in her or his path.  This particular story for me centers on a feature within the TIFF specification and how it has affected me and my users.  In black and white images, pixels are typically represented by a single value.  Usually zero, the lowest value, represents black and the highest value represents white.  For monitors, this is a consistent model in that black represents a pixel with no energy and white represents a pixel emitting maximum energy.  In 1-bit images, a pixel is represented by a single bit.  In the black is zero model, a 0 bit represents black and a 1 bit represents white.

It is entirely reasonable to consider a different model, however.  In white is zero , a pixel is considered to be white at its least value and black at its greatest value.  This corresponds to a paper model: zero is blank (white) paper and the maximum pixel value is the most possible ink on the paper at that pixel.

These two views are called the photometric interpretation of the image.  For 1-bit images, white is zero is by far the most common image type.  For 8 bit images and above, black is zero is the most common image type.

In the TIFF specification, it is possible to have images with either photometric interpretation.  In other words, I can have TIFF images that are either white is zero or black is zero .  Furthermore, since TIFF is a multipage format, I can have images that contain either or both formats in one file.

Because of the flexibility of TIFF, it can be a challenge to write programs that honor all the possible conditions that are present in a TIFF file.  The Windows Picture and Fax Viewer attempts to do this, but it only understands white is zero as a photometric interpretation.  The result of opening a black is zero file is shown in Figure 1.  This result is clearly not correct.  Peeking inside the file using a TIFF utility reveals that the image should be black is zero the text should be black, not white.  The overall problem is slightly worse the Windows Picture and Fax Viewer will also incorrectly display all other pages if the first is black is zero .

Figure 1 Output from Windows Picture and Fax Viewer with a black is zero image.

I hear about this issue most often from customers who are generating TIFF images and are puzzled when they open one in the Windows Picture and Fax Viewer and it comes out inverted.  For that, I ve created a small chunk of code that can normalize 1-bit images to white is zero :

public static void NormalizeToMinIsWhite(AtalaImage image) {     Normalize(image, true); } static void Normalize(AtalaImage image, bool minIsWhite) {     if (image == null)         throw new ArgumentNullException("image");     if (image.PixelFormat!= PixelFormat.Pixel1bppIndexed)         return;     bool isWhiteZero = IsWhiteIsZero(image);     if ((minIsWhite && isWhiteZero) || (!minIsWhite && !isWhiteZero))         return;     InvertCommand invert = new InvertCommand();     if (!invert.InPlaceProcessing)         throw new Exception("This should never happen; we're really depending on this being in place processing.");     invert.Apply(image);     Color col0 = image.Palette.GetEntry(0);     image.Palette.SetEntry(0, image.Palette.GetEntry(1));     image.Palette.SetEntry(1, col0); } static bool IsWhiteIsZero(AtalaImage image) {     // sleazy - get the luminance of the palette 0 and 1 and compare, brighter is more white than black     int luma0 = GetLuminance(image, 0);     int luma1 = GetLuminance(image, 1);         return luma0 > luma1; } static int GetLuminance(AtalaImage image, int paletteIndex) {     Color c = image.Palette.GetEntry(paletteIndex);     return (int)(c.R * 0.3 + c.G * 0.59 + c.B * 0.11); }

The main method, NormalizeToMinIsWhite will take any type of image and if it is 1-bit zero is black will change it in place to zero is white .  This is all well and good, but it doesn t solve the image viewing problem until all 1-bit TIFF images are stored as zero is white.   Since this bug has been present as long as I ve seen the Windows Picture and Fax Viewer, I don t anticipate a bug fix any time soon.

I decided that the best approach to this was to create my own application that serves in the same role on the user s machine.  I had a small break during my own product release cycle so I decided to put our own tools to the test and see if I could write an equivalent program.

Looking at Microsoft s application, I decided that I would create an application that would include a subset of its features, including next and previous document, best fit and actual size,  a slide show, zoom buttons, rotate buttons, page selection, print, and save current image.  For the display of the image, I used the Atalasoft DotImage ImageViewer control [you can download a free trial], the rest of the app was built on that.

I started with a utility class to represent the current image file which would manage loading pages as well as knowing the best encoder to use for saving a page.

This class is called ImageFile and includes the following properties and methods:

public AtalaImage Image { get; set;  } // represents the current image
public ImageEncoder Encoder { get; } // represent the best encoder to use
public int PageCount { get; } // the number of pages in this image
public int CurrentPageIndex { get; } // the index of the current image
public string Path { get; } // the path to the original file
public void LoadNextImage(); // moves to the next image in the file
public void LoadPreviousImage(); // moves to the previous image in the file
public void SetImageIndex(int i); // moves to a particular image in the file
 

The public methods all deal with changing to a different page in the file.  Next and Previous do nothing when CurrentPageIndex is at its limits.  SetImageIndex will throw if the index is out of range.

Given this class, I have a representation of the current file as well as the page navigation.   From there I wrote a method to find all the image files in a directory:

private List<string> GetImageFiles(string directory)
{
    // if there is an image decoder for a file, consider it to be loadable
    // it still may be a damaged file, but we'll check that later.
 
    string[] files = Directory.GetFiles(directory);
    List<string> final = new List<string>();
    foreach (string file in files)
    {
        try
        {
            using (Stream stm = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                if (RegisteredDecoders.GetDecoder(stm) != null)
                    final.Add(file);
            }
        }
        catch { }
        }
    return final;
}

This method loops over every file in a directory and asks if there exists a decoder for that file type.  If so, it adds the file to an accumulated list.

I chose to deviate from the way the best fit/actual size buttons work in the Microsoft viewer by making them always active.  The Microsoft viewer makes some somewhat confusing decisions as to how it will make the buttons active or inactive and how the viewer will behave when the window is grown or shrunk.  I tried to make the behavior simpler: clicking on best fit makes the image best fit into the viewer; clicking on actual size makes 1 pixel in the image 1 pixel in the viewer.  The buttons are always active.

The rest of the UI code is very straight-forward exactly as it should be.  For example, the code to rotate a page 90 degrees to the right is this:

private void rotateRightButton_Click(object sender, EventArgs e)
{
    if (_loadableFiles.Count == 0)
        return;
    RotateImage(90);
}
 
private void RotateImage(int degrees)
{
    RotateCommand rotator = new RotateCommand((double)degrees);
    AtalaImage image = _imageFile.Image;
    AtalaImage final = rotator.Apply(image).Image;
    if (image != final)
    {
       image.Dispose();
       imageViewer1.Image = _imageFile.Image = final;
    }
}
 

In addition to the main form, I include a form that runs a slide show.  The SlideShow form is built to be completely isolated from the main form, so the code itself could be extracted completely from the project and used elsewhere.  Like the main form, it also depends on ImageFile.

After building and running the project, the example black is zero file now loads and views correctly, since I m using Atalasoft s image decoders (see Figure 2).

As I had hoped with this project, the bulk of my time was spent fiddling with UI controls and not with images.  This is exactly the way it should be: I can concentrate on the user experience and leave the worries about image reading, viewing and saving to DotImage.

Figure 2 - a black is zero image in the Atalasoft viewer.

 

Download 30-day Trial
preload preload preload