The ImageViewer class supports unlimited number of items through the user of the view layouts (Refer to Image Viewer Layouts). With this support, the viewer can be used to load a document with large amount of pages and large physical image sizes. Naturally, there's a hardware limit on how many of these images can be kept in the physical memory at any time and the Image Viewer provides the Virtualizer mechanism to support loading/unloading image data on demand.
For an example, the image viewer is using a vertical layout with continuous scroll. And the user is trying to load a PDF file with 1000 pages in it with each page is a typical document size at 8.5 by 11 inches. Assuming the pages are at 24 bits per pixel, that's a pixel size of 2550 by 3300 and a physical size of 24MB for each page of uncompressed data and a total of 24MB * 1000 = 24GB of physical memory required to hold all these pages in memory.
In the vertical layout above, the pages (each contained in an ImageViewerItem object) will not be all visible on the screen at the same time. In fact, most of them will stay invisible till they come into view when the user scrolls the viewer or zooms it out. The viewer needs the information on the number of items and the physical size of each for layout and scrollbar calculation. But the image data itself is not needed unless the item is visible. A better approach would be to not load the image data till the item is about to become visible into the view. And to discard the image data when the item goes out of view and is invisible. The ImageViewerVirtualizer performs exactly that. It is an abstract class that allows you to easily load and unload the items data on demand with full control for rendering place-holders and controlling the number of items to cache in memory.
The first thing we need is a document with large number of pages. Here is code that uses LEADTOOLS to create such a document:
private static void CreateTestDocument()
{
// Create a 400 pages PDF file. Each page is 8.5 by 11 inches
var width = 8.5;
var height = 11.0;
var resolution = 300;
var pixelWidth = (int)(width * resolution + 0.5);
var pixelHeight = (int)(height * resolution + 0.5);
var pageCount = 400;
var fileName = string.Format(@"C:\Users\Public\Documents\LEADTOOLS Images\{0}Pages.pdf", pageCount);
if (File.Exists(fileName))
File.Delete(fileName);
using (var codecs = new RasterCodecs())
{
var pageMode = CodecsSavePageMode.Overwrite;
for (var pageNumber = 1; pageNumber <= pageCount; pageNumber++)
{
using (var rasterImage = CreatePageImage(pixelWidth, pixelHeight, resolution, pageNumber))
{
codecs.Save(rasterImage, fileName, RasterImageFormat.RasPdfLzw, 24, 1, 1, -1, pageMode);
pageMode = CodecsSavePageMode.Append;
}
}
}
}
private static RasterImage CreatePageImage(int pixelWidth, int pixelHeight, int resolution, int pageNumber)
{
Pen[] pens =
{
Pens.Red,
Pens.Green,
Pens.Blue,
};
Brush[] brushes =
{
Brushes.Red,
Brushes.Green,
Brushes.Blue
};
Pen pen = pens[pageNumber % 3];
Brush brush = brushes[pageNumber % 3];
var rasterImage = RasterImage.Create(pixelWidth, pixelHeight, 24, resolution, RasterColor.FromKnownColor(RasterKnownColor.White));
var hdc = RasterImagePainter.CreateLeadDC(rasterImage);
using (var graphics = Graphics.FromHdc(hdc))
{
var rc = new Rectangle(0, 0, pixelWidth, pixelHeight);
graphics.DrawRectangle(pen, rc.X, rc.Y, rc.Width - 2, rc.Height - 2);
graphics.DrawRectangle(pen, rc.X + 1, rc.Y + 1, rc.Width - 3, rc.Height - 3);
var text = string.Format("Page {0}", pageNumber);
using (var font = new Font("Arial", 72, FontStyle.Regular))
using (var sf = new StringFormat())
{
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
var textSize = graphics.MeasureString(text, font);
float y = 10;
float x = 10;
while (y < pixelHeight)
{
while (x < pixelWidth)
{
graphics.DrawString(text, font, brush, x, y);
x += textSize.Width + 80;
}
y += textSize.Height + 80;
x = 10;
}
}
}
RasterImagePainter.DeleteLeadDC(hdc);
return rasterImage;
}
Next, create an image viewer with a vertical view layout:
// Create a new image viewer instance with a vertical layout
ImageViewerViewLayout viewLayout = new ImageViewerVerticalViewLayout();
ImageViewer imageViewer = new ImageViewer(viewLayout);
// Set some properties
imageViewer.Dock = DockStyle.Fill;
imageViewer.BackColor = Color.Bisque;
// Add a border (need some padding as well)
imageViewer.ImageBorderThickness = 1;
imageViewer.ItemPadding = new Padding(imageViewer.ImageBorderThickness);
// Take into consideration the resolution image when viewing, so an 8.5 by 11 inch image size
// will take 8.5 by 11 inches on screen when zoom is 1:1
imageViewer.UseDpi = true;
// Add it to the form
this.Controls.Add(imageViewer);
imageViewer.BringToFront();
// Add pan/zoom interactive mode. Click and drag to pan the image and ctrl-click
// and drag to zoom in/out
imageViewer.InteractiveModes.Add(new ImageViewerPanZoomInteractiveMode());
Let us try to load this document in the viewer as is. This code will add all the pages into the document as items:
var fileName = @"C:\Users\Public\Documents\LEADTOOLS Images\400Pages.pdf";
using (var codecs = new RasterCodecs())
{
codecs.Options.RasterizeDocument.Load.Resolution = 300;
// Do not update till we have all the pages
imageViewer.BeginUpdate();
// Get the number of pages
int pageCount = codecs.GetTotalPages(fileName);
for (var pageNumber = 1; pageNumber <= pageCount; pageNumber++)
{
var item = new ImageViewerItem();
item.Image = codecs.Load(fileName, pageNumber);
imageViewer.Items.Add(item);
}
imageViewer.EndUpdate();
}
Try to run this code. It will work on 64-bit systems after taking a large amount of time to load all the pages and most probably fail on 32-bit systems with an Out Of Memory exception. The system cannot load all this image data in memory at once. Clearly a better approach is needed.
As described in Image Viewer Items, the image viewer supports creating items without image data. All the viewer needs is the number of items as well their sizes (pixel and resolution). So let us modify the code to add the items without the image data:
var fileName = @"C:\Users\Public\Documents\LEADTOOLS Images\400Pages.pdf";
using (var codecs = new RasterCodecs())
{
codecs.Options.RasterizeDocument.Load.Resolution = 300;
// Do not update till we have all the pages
imageViewer.BeginUpdate();
// Get the number of pages and the size of each page
int pageCount;
LeadSize imageSize;
LeadSizeD resolution;
using (var info = codecs.GetInformation(fileName, true))
{
// Get number of pages
pageCount = info.TotalPages;
// Get size in pixels
imageSize = LeadSize.Create(info.Width, info.Height);
// Get resolution
resolution = LeadSizeD.Create(info.XResolution, info.YResolution);
}
for (var pageNumber = 1; pageNumber <= pageCount; pageNumber++)
{
var item = new ImageViewerItem();
// Set the members needed for the viewer to create the correct layout
item.ImageSize = imageSize;
item.Resolution = resolution;
// Add it
imageViewer.Items.Add(item);
}
imageViewer.EndUpdate();
}
Run this code and notice that the viewer is up and running almost instantly. Blank pages with the correct size are added and you can zoom and pan around the image freely.
The final step is to populate the image data on demand. This is easily accomplished by creating an image virtualizer derived class that handles loading and freeing the image data on demand.
Create a new class and add the following code:
// Class that derives from the abstract ImageViewerVirtualizer
public class MyVirtualizer : ImageViewerVirtualizer
{
public MyVirtualizer() { }
// The file containing the large image
private string _fileName;
public MyVirtualizer(string fileName) :
base()
{
// Save the file
_fileName = fileName;
// Number of items to keep cached in memory, the default is 16. Changing to 4
this.MaximumItems = 4;
}
protected override object LoadItem(ImageViewerItem item)
{
// This method is called when an item comes into view
// and is not cached in memory
// For this example, all we need is to load the image
// from the original file. But we can also load other
// state and data from a database or using deserialization.
// In this example, the item index is the page index
// However, we can use the item .Tag property or derive our
// own class to hold the data needed to load the page
// Index is 0-based, so add 1 to get the page number
var pageNumber = this.ImageViewer.Items.IndexOf(item) + 1;
// Load the page and return it
using (var codecs = new RasterCodecs())
{
codecs.Options.RasterizeDocument.Load.Resolution = 300;
return codecs.Load(_fileName, 0, CodecsLoadByteOrder.BgrOrGray, pageNumber, pageNumber);
}
}
protected override void SaveItem(ImageViewerItem item, object data)
{
// This method is called when an item is about to be deleted
// from the cache. In this example, we do not have anything to do
// but you can modify the code if your application needs to serialize
// data to disk or a database for example
}
protected override void DeleteItem(ImageViewerItem item, object data)
{
// This method is called when the item is no longer used
// In this example, we simply dispose the RasterImage we loaded
var image = data as RasterImage;
if (image != null)
image.Dispose();
}
protected override void RenderItemPlaceholder(ImageViewerRenderEventArgs e)
{
// This method is called while an item is being loaded and give us a chance
// to offer a hint to the user
// Lets render a Loading ... message on the item
var transform = this.ImageViewer.GetItemImageTransform(e.Item);
var graphics = e.PaintEventArgs.Graphics;
var pt = LeadPointD.Create(0, 0);
pt = transform.Transform(pt);
graphics.DrawString("Loading...", this.ImageViewer.Font, Brushes.Black, (float)pt.X, (float)pt.Y);
}
}
Finally, set our virtualizer in the viewer
// Add our virtualizer
imageViewer.Virtualizer = new MyVirtualizer(fileName);
Run this demo and notice how:
The viewer is up and running instantly
When scrolling and zooming, the virtualizer gets called on demand to load and discard the pages
The ImageViewerVirtualizer methods will be called from a separate thread than the main UI thread. This is important to ensure that the user can pan and zoom smoothly without interruptions. DeleteItem will be called when items are removed or the viewer is disposed (if AutoDisposeImages is set to true) to ensure the resources are freed.
The RenderItemPlaceholder method is always called in the same thread that created the viewer.