2

Overview
I'm using WinForms. In my code I have this method. I'm attempting to proportionally re-size tif images using this code . My goal is to get the image from the stream (source) and re-size it. The images I want to re-size is tif documents that has multiple pages. How can i re-size the image proportionally?

Code functionality & Plan
The code below is caching tif images in another thread. The problem is that I work with really big tif images. When I open these really big tif images with multiple pages my application crashes and lags. My goal or thought is before the images load i should re-size it so the application wont crash and the performance becomes faster.

Example Tif Document:

I created a large tif document for testing you can download it from this link: http://www.filedropper.com/tiftestingdoc

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);
    }
  }
}
taji01
  • 2,527
  • 8
  • 36
  • 80
  • What doesn't work with what you tried? – Jacobr365 Mar 02 '16 at 19:14
  • One of the methods i tried is this, but the image looses quality, and application still is a bit slow. The only way to not break it is if i re-size it. Well that's the solution i came up with like, (source, 800, 1100) when i do this it works fine. If i don't do this the application stops and gives me an error OutOfMemoryException was unhandled my process memory was 864. @Jacobr365 – taji01 Mar 02 '16 at 23:42

2 Answers2

1

One attempt would be to limit the size of the cached bitmaps.

In order to do that, let introduce a new member

private Size pageSize;

and use it as follows

private Image LoadPage(int index)
{
    source.SelectActiveFrame(FrameDimension.Page, index);
    return new Bitmap(source, pageSize);
}

The last line does actual resizing according to Bitmap Constructor (Image, Size) documentation:

Initializes a new instance of the Bitmap class from the specified existing image, scaled to the specified size.

The we need to introduce additional parameter:

public static PageBuffer Open(string path, Size maxSize, int cacheSize = DefaultCacheSize)
{
    return new PageBuffer(File.OpenRead(path), maxSize, cacheSize);
}

private PageBuffer(Stream stream, Size maxSize, int cacheSize)
{
    // ...
} 

so you can pass the maximum desired size. The actual page size under that limit, keeping the original image size proportion is calculated like this:

var sourceSize = source.Size;
float scale = Math.Min((float)maxSize.Width / sourceSize.Width, (float)maxSize.Height / sourceSize.Height);
var targetSize = new Size((int)(sourceSize.Width * scale), (int)(sourceSize.Height * scale));

Here is the full code:

class PageBuffer : IDisposable
{
    public const int DefaultCacheSize = 5;

    public static PageBuffer Open(string path, Size maxSize, int cacheSize = DefaultCacheSize)
    {
        return new PageBuffer(File.OpenRead(path), maxSize, cacheSize);
    }

    private PageBuffer(Stream stream, Size maxSize, 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))];
        pageSize = source.Size;
        if (!maxSize.IsEmpty)
        {
            float scale = Math.Min((float)maxSize.Width / pageSize.Width, (float)maxSize.Height / pageSize.Height);
            pageSize = new Size((int)(pageSize.Width * scale), (int)(pageSize.Height * scale));
        }
        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, pageSize);
    }

    private Stream stream;
    private Image source;
    private int pageCount;
    private Image[] pageCache;
    private int pageCacheStart, pageCacheSize;
    private object syncLock = new object();
    private bool disposed;
    private Size pageSize;

    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);
        }
    }
}

Sample usage:

var data = PageBuffer.Open(path, new Size(850, 1100));
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • In the private void Open(string path) `{ var data = PageBuffer.Open(path,maxSize:1100,cashesize:5);` method. I'm not correctly writing it. maxSize has an error on it. – taji01 Mar 03 '16 at 19:00
  • 1
    Should be `maxSize:new Size(850,1100)` – Ivan Stoev Mar 03 '16 at 19:04
  • Hey thanks for helping me out for a couple for a couple days now. I really appreciate all the help. This is an excellent piece of code, it uses less memory when you re-size the image, but it distorts it slightly. Just to let you know i try to code/test/solve it myself before I ask you, but anyways you probably already know when you re-size or scale the image down to fit in a regular (8.5 x 11) piece of printing paper the image document gets slightly distorted. Is there a way or a strategy to scale down the image so it will use less memory but print high quality tif images? @Ivan Stoev – taji01 Mar 05 '16 at 00:15
  • 1
    One thing you can try is to use a better resize method instead of the `Bitmap` constructor. For instance the `ResizeImage` function from [High Quality Image Scaling Library](http://stackoverflow.com/questions/249587/high-quality-image-scaling-library). Another option is to use the original image when printing instead of the scaled down from the picture box. – Ivan Stoev Mar 05 '16 at 14:30
  • Thank you @Ivan Stoev you have been a ton of help. Everything works perfectly :) – taji01 Mar 07 '16 at 02:33
0
        Bitmap bmp = new Bitmap(newWidth, newHeight);
        Graphics g = Graphics.FromImage(bmp);
    for (int idx = 0; idx < count; idx++)
        {
            bmp.SelectActiveFrame(FrameDimension.Page, idx);
        g.DrawImage(source, new Rectangle(0,0,source.Width,source.Height), 
         new Rectangle(0,0,bmp.Width,bmp.Height));
            ImageCodecInfo myImageCodecInfo;
            System.Drawing.Imaging.Encoder myEncoder;
            EncoderParameter myEncoderParameter;
            EncoderParameters myEncoderParameters;
            myImageCodecInfo = GetEncoderInfo("image/tiff");
            myEncoder = System.Drawing.Imaging.Encoder.Compression;
            myEncoderParameters = new EncoderParameters(1);
            myEncoderParameter = new EncoderParameter(
                myEncoder,
                (long)EncoderValue.CompressionLZW);
            myEncoderParameters.Param[0] = myEncoderParameter;    
            bmp.SaveAdd(byteStream, ImageFormat.Tiff);
        }


 private static ImageCodecInfo GetEncoderInfo(String mimeType)
        {
            int j;
            ImageCodecInfo[] encoders;
            encoders = ImageCodecInfo.GetImageEncoders();
            for (j = 0; j < encoders.Length; ++j)
            {
                if (encoders[j].MimeType == mimeType)
                    return encoders[j];
            }
            return null;
        }
Ashkan Mobayen Khiabani
  • 33,575
  • 33
  • 102
  • 171