It turns out that the slow part is the Image.SelectActiveFrame call.
As usual, the solution is caching. However, in order to not increase the initial load time, it should be performed lazily on background.
The idea is simple. Start a worker thread and load all the image frames as separate Bitmap
s in an array. Then use the cached image from the array instead of the SelectActiveFrame
.
Since all that require some thread synchronization, I've encapsulated it in a helper class:
class PageBuffer : IDisposable
{
public static PageBuffer Open(string path)
{
return new PageBuffer(File.OpenRead(path));
}
private PageBuffer(Stream stream)
{
this.stream = stream;
Source = Image.FromStream(stream);
PageCount = Source.GetFrameCount(FrameDimension.Page);
if (PageCount < 2) return;
pages = new Image[PageCount];
var worker = new Thread(LoadPages) { IsBackground = true };
worker.Start();
}
private void LoadPages()
{
for (int index = 0; ; index++)
{
lock (syncLock)
{
if (disposed) return;
if (index >= pages.Length)
{
// If you don't need the source image,
// uncomment the following line to free some resources
//DisposeSource();
return;
}
if (pages[index] == null)
pages[index] = LoadPage(index);
}
}
}
private Image LoadPage(int index)
{
Source.SelectActiveFrame(FrameDimension.Page, index);
return new Bitmap(Source);
}
private Stream stream;
private Image[] pages;
private object syncLock = new object();
private bool disposed;
public Image Source { get; private set; }
public int PageCount { get; private set; }
public Image GetPage(int index)
{
if (disposed) throw new ObjectDisposedException(GetType().Name);
if (PageCount < 2) return Source;
var image = pages[index];
if (image == null)
{
lock (syncLock)
{
image = pages[index];
if (image == null)
image = pages[index] = LoadPage(index);
}
}
return image;
}
public void Dispose()
{
if (disposed) return;
lock (syncLock)
{
disposed = true;
if (pages != null)
{
foreach (var item in pages)
if (item != null) item.Dispose();
pages = null;
}
DisposeSource();
}
}
private void DisposeSource()
{
if (Source != null)
{
Source.Dispose();
Source = null;
}
if (stream != null)
{
stream.Dispose();
stream = null;
}
}
}
A full working demo:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Windows.Forms;
namespace Demo
{
class TestForm : Form
{
public TestForm()
{
var panel = new Panel { Dock = DockStyle.Top, BorderStyle = BorderStyle.FixedSingle };
openButton = new Button { Text = "Open", Top = 8, Left = 16 };
prevButton = new Button { Text = "Prev", Top = 8, Left = 16 + openButton.Right };
nextButton = new Button { Text = "Next", Top = 8, Left = 16 + prevButton.Right };
panel.Height = 16 + openButton.Height;
panel.Controls.AddRange(new Control[] { openButton, prevButton, nextButton });
pageViewer = new PictureBox { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom };
ClientSize = new Size(850, 1100 + panel.Height);
Controls.AddRange(new Control[] { panel, pageViewer });
openButton.Click += OnOpenButtonClick;
prevButton.Click += OnPrevButtonClick;
nextButton.Click += OnNextButtonClick;
Disposed += OnFormDisposed;
UpdatePageInfo();
}
private Button openButton;
private Button prevButton;
private Button nextButton;
private PictureBox pageViewer;
private PageBuffer pageData;
private int currentPage;
private void OnOpenButtonClick(object sender, EventArgs e)
{
using (var dialog = new OpenFileDialog())
{
if (dialog.ShowDialog(this) == DialogResult.OK)
Open(dialog.FileName);
}
}
private void OnPrevButtonClick(object sender, EventArgs e)
{
SelectPage(currentPage - 1);
}
private void OnNextButtonClick(object sender, EventArgs e)
{
SelectPage(currentPage + 1);
}
private void OnFormDisposed(object sender, EventArgs e)
{
if (pageData != null)
pageData.Dispose();
}
private void Open(string path)
{
var data = PageBuffer.Open(path);
pageViewer.Image = null;
if (pageData != null)
pageData.Dispose();
pageData = data;
SelectPage(0);
}
private void SelectPage(int index)
{
pageViewer.Image = pageData.GetPage(index);
currentPage = index;
UpdatePageInfo();
}
private void UpdatePageInfo()
{
prevButton.Enabled = pageData != null && currentPage > 0;
nextButton.Enabled = pageData != null && currentPage < pageData.PageCount - 1;
}
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TestForm());
}
}
class PageBuffer : IDisposable
{
public static PageBuffer Open(string path)
{
return new PageBuffer(File.OpenRead(path));
}
private PageBuffer(Stream stream)
{
this.stream = stream;
Source = Image.FromStream(stream);
PageCount = Source.GetFrameCount(FrameDimension.Page);
if (PageCount < 2) return;
pages = new Image[PageCount];
var worker = new Thread(LoadPages) { IsBackground = true };
worker.Start();
}
private void LoadPages()
{
for (int index = 0; ; index++)
{
lock (syncLock)
{
if (disposed) return;
if (index >= pages.Length)
{
// If you don't need the source image,
// uncomment the following line to free some resources
//DisposeSource();
return;
}
if (pages[index] == null)
pages[index] = LoadPage(index);
}
}
}
private Image LoadPage(int index)
{
Source.SelectActiveFrame(FrameDimension.Page, index);
return new Bitmap(Source);
}
private Stream stream;
private Image[] pages;
private object syncLock = new object();
private bool disposed;
public Image Source { get; private set; }
public int PageCount { get; private set; }
public Image GetPage(int index)
{
if (disposed) throw new ObjectDisposedException(GetType().Name);
if (PageCount < 2) return Source;
var image = pages[index];
if (image == null)
{
lock (syncLock)
{
image = pages[index];
if (image == null)
image = pages[index] = LoadPage(index);
}
}
return image;
}
public void Dispose()
{
if (disposed) return;
lock (syncLock)
{
disposed = true;
if (pages != null)
{
foreach (var item in pages)
if (item != null) item.Dispose();
pages = null;
}
DisposeSource();
}
}
private void DisposeSource()
{
if (Source != null)
{
Source.Dispose();
Source = null;
}
if (stream != null)
{
stream.Dispose();
stream = null;
}
}
}
}
UPDATE: As mentioned in the comments, the above implementation is using quite simple greedy caching strategy, which uses a lot of memory and does not work for big files.
The good thing though is that once the logic is encapsulated inside the class, we can change the strategy without touching our app code. For instance, we can remove the caching at all (return to the initial state), or optimize for "prev/next" navigation by maintaining a small set of cached image "window" like this
class PageBuffer : IDisposable
{
public const int DefaultCacheSize = 5;
public static PageBuffer Open(string path, int cacheSize = DefaultCacheSize)
{
return new PageBuffer(File.OpenRead(path), cacheSize);
}
private PageBuffer(Stream stream, int cacheSize)
{
this.stream = stream;
source = Image.FromStream(stream);
pageCount = source.GetFrameCount(FrameDimension.Page);
if (pageCount < 2) return;
pageCache = new Image[Math.Min(pageCount, Math.Max(cacheSize, 3))];
var worker = new Thread(LoadPages) { IsBackground = true };
worker.Start();
}
private void LoadPages()
{
while (true)
{
lock (syncLock)
{
if (disposed) return;
int index = Array.FindIndex(pageCache, 0, pageCacheSize, p => p == null);
if (index < 0)
Monitor.Wait(syncLock);
else
pageCache[index] = LoadPage(pageCacheStart + index);
}
}
}
private Image LoadPage(int index)
{
source.SelectActiveFrame(FrameDimension.Page, index);
return new Bitmap(source);
}
private Stream stream;
private Image source;
private int pageCount;
private Image[] pageCache;
private int pageCacheStart, pageCacheSize;
private object syncLock = new object();
private bool disposed;
public Image Source { get { return source; } }
public int PageCount { get { return pageCount; } }
public Image GetPage(int index)
{
if (disposed) throw new ObjectDisposedException(GetType().Name);
if (PageCount < 2) return Source;
lock (syncLock)
{
AdjustPageCache(index);
int cacheIndex = index - pageCacheStart;
var image = pageCache[cacheIndex];
if (image == null)
image = pageCache[cacheIndex] = LoadPage(index);
return image;
}
}
private void AdjustPageCache(int pageIndex)
{
int start, end;
if ((start = pageIndex - pageCache.Length / 2) <= 0)
end = (start = 0) + pageCache.Length;
else if ((end = start + pageCache.Length) >= PageCount)
start = (end = PageCount) - pageCache.Length;
if (start < pageCacheStart)
{
int shift = pageCacheStart - start;
if (shift >= pageCacheSize)
ClearPageCache(0, pageCacheSize);
else
{
ClearPageCache(pageCacheSize - shift, pageCacheSize);
for (int j = pageCacheSize - 1, i = j - shift; i >= 0; j--, i--)
Exchange(ref pageCache[i], ref pageCache[j]);
}
}
else if (start > pageCacheStart)
{
int shift = start - pageCacheStart;
if (shift >= pageCacheSize)
ClearPageCache(0, pageCacheSize);
else
{
ClearPageCache(0, shift);
for (int j = 0, i = shift; i < pageCacheSize; j++, i++)
Exchange(ref pageCache[i], ref pageCache[j]);
}
}
if (pageCacheStart != start || pageCacheStart + pageCacheSize != end)
{
pageCacheStart = start;
pageCacheSize = end - start;
Monitor.Pulse(syncLock);
}
}
void ClearPageCache(int start, int end)
{
for (int i = start; i < end; i++)
Dispose(ref pageCache[i]);
}
static void Dispose<T>(ref T target) where T : class, IDisposable
{
var value = target;
if (value != null) value.Dispose();
target = null;
}
static void Exchange<T>(ref T a, ref T b) { var c = a; a = b; b = c; }
public void Dispose()
{
if (disposed) return;
lock (syncLock)
{
disposed = true;
if (pageCache != null)
{
ClearPageCache(0, pageCacheSize);
pageCache = null;
}
Dispose(ref source);
Dispose(ref stream);
if (pageCount > 2)
Monitor.Pulse(syncLock);
}
}
}
or implement other "smart" caching strategy. We can even make the strategy selectable by implementing the Strategy pattern.
Bu that will be another story. The second PageBuffer
implementation should be sufficient for the OP use case.
Application.DoEvents()
however after you call the .Refresh method try puttingApplication.DoEvents()
call after that – AndorraApplication.DoEvents()
? @Andorra – ErythroblastosisApplication.DoEvents()
there are tons of post onSO
about this as well as others that will tell you to steer clear of it but I was just making a personal opinion / statement about why I hate using it.. it's not worth going into in that regard... – AndorrapictureBox
size is set tozoom
and scales the picture down to (850 x 1100) if that is what you meant. Other than that, don't think i'm scaling down the resolution to view these image document because my code above is basically what I have now. I don't know if scaling down will effect the tif viewer app that I'm trying to make. Others have said like @Rugging and @Ima that it will not effect me unless I'm modifying the image. I don't know if they meant modifying the image as in cropping image, highlighting words, or inverting the colors which i currently have. @vidstige. – Erythroblastosis