Image Viewer Virtualizer

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.

Example

Test Document

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

Image Viewer

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

Adding Pages without Virtualizer

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.

Adding Pages with Size Information Only

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 Virtualizer

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:

Virtualizer and Multi-Threading Notes

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.

Help Version 20.0.2018.1.19
Products | Support | Contact Us | Copyright Notices
© 1991-2018 LEAD Technologies, Inc. All Rights Reserved.
LEADTOOLS Imaging, Medical, and Document