4

I'm writing a C# application that handles TIFF images (mainly displaying files, reordering pages, deleting pages, splitting multipage images, merging single images into one multipage image etc).

Most of the images we deal with are smaller (both in file size and in page numbers), but there are the odd larger ones.

When displaying the image, we need to split any multipage TIFF files into a List so the thumbnails can be displayed in a ListView. The issue we face is that for larger files, it takes way too long to perform the split. E.g. I just tested a 304 page image (which is only 10mb) and to split all the pages into the list took 137,145ms (137.14 seconds, or 2m 17s) which is far too slow.

The code I'm using is:

private void GetAllPages(string file)
{
    System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
    watch.Start();
    List<Image> images = new List<Image>();
    Bitmap bitmap = (Bitmap)Image.FromFile(file);
    int count = bitmap.GetFrameCount(FrameDimension.Page);
    for (int idx = 0; idx < count; idx++)
    {
        // save each frame to a bytestream
        bitmap.SelectActiveFrame(FrameDimension.Page, idx);
        System.IO.MemoryStream byteStream = new System.IO.MemoryStream();
        bitmap.Save(byteStream, ImageFormat.Tiff);

        // and then create a new Image from it
        images.Add(Image.FromStream(byteStream));
    }
    watch.Stop();
    MessageBox.Show(images.Count.ToString() + "\nTime taken: " + watch.ElapsedMilliseconds.ToString());
}

Any hints or pointers on what I can do or what I should look at to speed up this process? I KNOW it can be done significantly faster - I just don't know how.

Thanks!

Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
Benny O'Neill
  • 208
  • 4
  • 10
  • 1
    Have you tried to parallelize the work inside the loop? – Peter Bons Jun 04 '17 at 11:17
  • 1
    When you debug through the code (or by adding more 'stopwatching'), which lines are the slow ones? – mjwills Jun 04 '17 at 11:18
  • Consider memory-mapped files. My guess is your main problem here is having to leave the stream open for the entire lifetime of the Image, which as far as I'm concerned is a defect of the `Image.FromStream` method. – Cody Gray - on strike Jun 04 '17 at 11:20
  • Thanks for the replies. @PeterBons, I don't know what that means...? – Benny O'Neill Jun 04 '17 at 11:27
  • @mjwills, I ran through it and it's kinda hard to tell but it looks as though bitmap.Save(bytestream, ImageFormat.Tiff); is a bit delayed before skipping to the next line. – Benny O'Neill Jun 04 '17 at 11:28
  • @CodyGray, I'll have to look into that. I've never heard of it before nor do I know how to implement. But I'll check it out. – Benny O'Neill Jun 04 '17 at 11:28
  • Have you tried simply [Bitmap Constructor (Image)](https://msdn.microsoft.com/en-us/library/ts25csc8(v=vs.110).aspx), e.g. `bitmap.SelectActiveFrame(FrameDimension.Page, idx); images.Add(new Bitmap(bitmap));` – Ivan Stoev Jun 04 '17 at 11:35
  • The frames are now processed one by one (sequential), parallelization means processing the frames in parallel instead of sequential. There are several techniques for that in .Net. See [the docs](https://msdn.microsoft.com/en-us/library/dd460693(v=vs.110).aspx) for a lot of reading material. An example implementation could be https://stackoverflow.com/questions/28884090/parallel-loading-images-in-linq – Peter Bons Jun 04 '17 at 11:35
  • The challenge with parallelisation here is the call to `bitmap.SelectActiveFrame(FrameDimension.Page, idx);` (plus Bitmap.Save is not documented as thread-safe). Thus, likely, the main part of the code that can be parallelised is `images.Add(Image.FromStream(byteStream));` – mjwills Jun 04 '17 at 11:37
  • https://bitmiracle.github.io/libtiff.net/ (or other libraries) may be worth a try to compare performance. – mjwills Jun 04 '17 at 11:47

1 Answers1

1

If I use the TiffBitmapDecoder class from PresentationCore.dll (WPF) and then use it to create Bitmap instances, I get much faster loading of the frames (10s vs. 70s for a 150 MB TIFF with 360 frames).

 List<Image> images = new List<Image>();
 Stream imageStreamSource = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
 TiffBitmapDecoder decoder = new TiffBitmapDecoder(imageStreamSource, BitmapCreateOptions.None, BitmapCacheOption.Default);
 foreach (BitmapSource bitmapSource in decoder.Frames)
 {
     Bitmap bmp = new Bitmap(bitmapSource.PixelWidth, bitmapSource.PixelHeight, PixelFormat.Format32bppPArgb);
     BitmapData data = bmp.LockBits(new Rectangle(System.Drawing.Point.Empty, bmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppPArgb);
     bitmapSource.CopyPixels(Int32Rect.Empty,  data.Scan0,  data.Height * data.Stride, data.Stride);
     bmp.UnlockBits(data);
     images.Add(bmp);
 }
adjan
  • 13,371
  • 2
  • 31
  • 48
  • Thanks adjan. I'll give it a go and will report back. At the moment, I'm struggling with the Int32Rect.Empty part. It says it doesn't exist in the current context. I've referenced the PresentationCore and have tried System.Windows.Int32Rect but no go. Thoughts? – Benny O'Neill Jun 05 '17 at 00:18
  • @BennyO'Neill I guess you have to add WindowsBase.dll as well. Strangely, I did not have to. – adjan Jun 05 '17 at 04:09
  • I got it working but I receive an exception during the foreach loop that says "A first chance exception of type 'System.ArgumentException' occurred in System.Drawing.dll" I tried it on a smaller file (only 33 pages) and it was fine. But on this 304 page file, I receive the error. I ran through line by line and it looks like it happens at AROUND the 40-50 page mark. – Benny O'Neill Jun 07 '17 at 11:10