使用 .tif 图像前进到下一页时提高性能
Boost the performance when advancing to the next page using .tif images
我正在使用 WinForms。在我的表单中,我有一个打开按钮和一个下一步按钮。我的应用程序将 .tif
图像打开到图片框中。我使用的所有 .tif
图像都有多个页面。下一个按钮用于转到 tif
图像中的下一页。这些 .tif
我使用的图像非常大。
示例: 尺寸:2600 x 3300(.tif
张图片)
问题:如何优化应用程序的性能?我 read/researched 我可能必须直接从计算机内存和其他一些方法加载图像。我将如何解决这个问题或者是否有更好的编码方法?
这是我目前的代码,但是当我转到下一页时我的应用程序有点滞后。
下面是一张 link 包含多个页面的大型 TIFF 图像用于测试。
Link
http://www.filedropper.com/tiftestingdoc
FileStream _stream;
Image _myImg; // setting the selected tiff
string _fileName;
private Image _Source = null;
private int _TotalPages = 0;
private int intCurrPage = 0;
private void Clone_File()
{ // Reads file, then copys the file and loads it in the picture box as a temporary image doc. That way files are not locked in users directory when in use by this application.
try
{
if (_myImg == null)
{
try
{
_fileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
File.Copy(@"C:\Picture_Doc\The_Image.tif", _fileName);
_stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read);
this._Source = Image.FromStream(_stream);
}
catch (Exception ex)
{
}
}
_TotalPages = _Source.GetFrameCount(System.Drawing.Imaging.FrameDimension.Page);
intCurrPage = 1;
Display_Page(intCurrPage);
}catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void Show_Processing_Image_Label()
{
Application.DoEvents();
}
private void Display_Page(int PageNumber, RotateFlipType Change)
{
if (pictureBox1.Image != null && pictureBox1.Image != _Source)
{
//Release memory for old rotated image
pictureBox1.Image.Dispose();
}
// set the variable to null for easy Garbage Collection cleanup
pictureBox1.Image = null;
_Source.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, PageNumber - 1);
pictureBox1.Image = new Bitmap(_Source);
pictureBox1.Image.RotateFlip(Change);
pictureBox1.Refresh();
//Refresh() Calls Invalidate and then Update to refresh synchronously.
}
private void Display_Page(int PageNumber)
{
Show_Processing_Image_Label();
//You could adjust the PictureBox size here for each frame OR adjust the image to fit the picturebox nicely
if (pictureBox1.Image != _Source)
{
if (pictureBox1.Image != null)
{
//Release memory for old copy and set the variable to null for easy GC cleanup
pictureBox1.Image.Dispose();
pictureBox1.Image = null;
}
pictureBox1.Image = _Source;
}
pictureBox1.Image.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, PageNumber - 1);
pictureBox1.Refresh();
}
private void Next_btn_Click(object sender, EventArgs e)
{
intCurrPage++;
Display_Page(intCurrPage);
}
private void Open_btn_Click(object sender, EventArgs e)
{
if (_stream != null)
{
_myImg = null; //dispose the copy image
}
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
Clone_File();
}
pictureBox1.Size = new Size(850, 1100);
}
您可以尝试对表单进行双缓冲;我知道这帮助我解决了一个类似的问题,即当涉及高分辨率图像或大量基于图像的控件时,我的表单绘制控件的速度非常慢。
public partial class form1 : Form
{
public form1()
{
InitializeComponent();
}
//Paste this in your form:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000;
return cp;
}
}
//... your code here
}
原来慢的部分是Image.SelectActiveFrame调用。
像往常一样,解决方案是缓存。但是,为了不增加初始加载时间,应该在后台延迟执行。
这个想法很简单。启动工作线程并将所有图像帧作为单独的 Bitmap
加载到数组中。然后使用数组中的缓存图像而不是 SelectActiveFrame
。
由于所有这些都需要一些线程同步,所以我将其封装在一个帮助程序中 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;
}
}
}
一个完整的工作演示:
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;
}
}
}
}
更新: 正如评论中提到的,上面的实现使用了非常简单的贪婪缓存策略,它使用大量内存并且不适用于大文件。
但好处是,一旦逻辑封装在 class 中,我们就可以更改策略,而无需触及我们的应用程序代码。例如,我们可以完全删除缓存(return 到初始状态),或者通过维护一小组缓存图像 "window" 来优化 "prev/next" 导航,就像这样
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);
}
}
}
或实施其他 "smart" 缓存策略。我们甚至可以通过实施 Strategy pattern 使策略可选择。
但那将是另一个故事。第二个 PageBuffer
实现对于 OP 用例应该足够了。
我正在使用 WinForms。在我的表单中,我有一个打开按钮和一个下一步按钮。我的应用程序将 .tif
图像打开到图片框中。我使用的所有 .tif
图像都有多个页面。下一个按钮用于转到 tif
图像中的下一页。这些 .tif
我使用的图像非常大。
示例: 尺寸:2600 x 3300(.tif
张图片)
问题:如何优化应用程序的性能?我 read/researched 我可能必须直接从计算机内存和其他一些方法加载图像。我将如何解决这个问题或者是否有更好的编码方法?
这是我目前的代码,但是当我转到下一页时我的应用程序有点滞后。
下面是一张 link 包含多个页面的大型 TIFF 图像用于测试。
Link
http://www.filedropper.com/tiftestingdoc
FileStream _stream;
Image _myImg; // setting the selected tiff
string _fileName;
private Image _Source = null;
private int _TotalPages = 0;
private int intCurrPage = 0;
private void Clone_File()
{ // Reads file, then copys the file and loads it in the picture box as a temporary image doc. That way files are not locked in users directory when in use by this application.
try
{
if (_myImg == null)
{
try
{
_fileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
File.Copy(@"C:\Picture_Doc\The_Image.tif", _fileName);
_stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read);
this._Source = Image.FromStream(_stream);
}
catch (Exception ex)
{
}
}
_TotalPages = _Source.GetFrameCount(System.Drawing.Imaging.FrameDimension.Page);
intCurrPage = 1;
Display_Page(intCurrPage);
}catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void Show_Processing_Image_Label()
{
Application.DoEvents();
}
private void Display_Page(int PageNumber, RotateFlipType Change)
{
if (pictureBox1.Image != null && pictureBox1.Image != _Source)
{
//Release memory for old rotated image
pictureBox1.Image.Dispose();
}
// set the variable to null for easy Garbage Collection cleanup
pictureBox1.Image = null;
_Source.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, PageNumber - 1);
pictureBox1.Image = new Bitmap(_Source);
pictureBox1.Image.RotateFlip(Change);
pictureBox1.Refresh();
//Refresh() Calls Invalidate and then Update to refresh synchronously.
}
private void Display_Page(int PageNumber)
{
Show_Processing_Image_Label();
//You could adjust the PictureBox size here for each frame OR adjust the image to fit the picturebox nicely
if (pictureBox1.Image != _Source)
{
if (pictureBox1.Image != null)
{
//Release memory for old copy and set the variable to null for easy GC cleanup
pictureBox1.Image.Dispose();
pictureBox1.Image = null;
}
pictureBox1.Image = _Source;
}
pictureBox1.Image.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, PageNumber - 1);
pictureBox1.Refresh();
}
private void Next_btn_Click(object sender, EventArgs e)
{
intCurrPage++;
Display_Page(intCurrPage);
}
private void Open_btn_Click(object sender, EventArgs e)
{
if (_stream != null)
{
_myImg = null; //dispose the copy image
}
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
Clone_File();
}
pictureBox1.Size = new Size(850, 1100);
}
您可以尝试对表单进行双缓冲;我知道这帮助我解决了一个类似的问题,即当涉及高分辨率图像或大量基于图像的控件时,我的表单绘制控件的速度非常慢。
public partial class form1 : Form
{
public form1()
{
InitializeComponent();
}
//Paste this in your form:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000;
return cp;
}
}
//... your code here
}
原来慢的部分是Image.SelectActiveFrame调用。
像往常一样,解决方案是缓存。但是,为了不增加初始加载时间,应该在后台延迟执行。
这个想法很简单。启动工作线程并将所有图像帧作为单独的 Bitmap
加载到数组中。然后使用数组中的缓存图像而不是 SelectActiveFrame
。
由于所有这些都需要一些线程同步,所以我将其封装在一个帮助程序中 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;
}
}
}
一个完整的工作演示:
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;
}
}
}
}
更新: 正如评论中提到的,上面的实现使用了非常简单的贪婪缓存策略,它使用大量内存并且不适用于大文件。
但好处是,一旦逻辑封装在 class 中,我们就可以更改策略,而无需触及我们的应用程序代码。例如,我们可以完全删除缓存(return 到初始状态),或者通过维护一小组缓存图像 "window" 来优化 "prev/next" 导航,就像这样
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);
}
}
}
或实施其他 "smart" 缓存策略。我们甚至可以通过实施 Strategy pattern 使策略可选择。
但那将是另一个故事。第二个 PageBuffer
实现对于 OP 用例应该足够了。