Creating zoomable images using Graphics Mill and Leaflet

The usual way to display large images on the Web is to split the original image into multiple tiles of different resolution and load them on demand.

This approach is used in online mapping solutions (like Google Maps or Bing Maps) as well as in applications offering high-res image display (Zoomify, Deep Zoom).

It is pretty easy to prepare and display images this way. You just need to cut the image using any imaging library. To display the image you can use Leaflet library. This brilliant method was initially designed for online map solutions, however you can also use it for plain image displaying. Leaflet has awesome performance as well as great mobile support.

You can create a pretty simple program to slice image into tiles using Graphics Mill or any other imaging library. You shouldn’t expect a problem with 20 or 30 MP images, however it is hard to handle really large image like 100 MP (10 000x10 000 pixels) and larger due high memory consumption

Graphics Mill since version 6 supports Pipeline API which allows you to process an image chunk-by-chunk without loading the image to memory. Actually you need just to build a graph and configure flowing data from ImageReader to Resize, Crop, and ImageWriter elements:

Split an image into tiles using the pipeline approach

Then you need to start the first element ImageReader processing pass as the argument to Pipeline.Run method:

using System;     
using System.Collections.Generic;     
using System.IO;     
using Aurigma.GraphicsMill;     
using Aurigma.GraphicsMill.Codecs;     
using Aurigma.GraphicsMill.Transforms;     
     
     
class SplitImageIntoTilesExample     
{     
 private struct ZoomLevel     
 {     
  public int ImageWidth;     
  public int ImageHeight;     
  public int GridWidth;     
  public int GridHeight;     
 }     
     
     
 static void Main(string[] args)     
 {     
  SplitImageIntoTiles(256);     
 }     
     
     
 static void SplitImageIntoTiles(int tileSize)     
 {     
  var outputPath = "../../../../Output/SplitImagesintoTiles/";     
     
  if (!Directory.Exists(outputPath))     
  {     
   Directory.CreateDirectory(outputPath);     
  }     
     
  //Store reference to all pipeline elements for further correct object disposing     
  var pipelineElements = new List<PipelineElement>();     
     
  try     
  {     
   var reader = ImageReader.Create("../../../../Input/Venice.jpg");     
   pipelineElements.Add(reader);     
     
   var d = 1f;     
     
   var zoomLevels = new List<ZoomLevel>();     
     
   ZoomLevel zoomLevel;     
     
   do     
   {     
    zoomLevel.ImageWidth = (int)((float)reader.Width / d);     
    zoomLevel.ImageHeight = (int)((float)reader.Height / d);     
    zoomLevel.GridWidth = (zoomLevel.ImageWidth + tileSize - 1) / tileSize;     
    zoomLevel.GridHeight = (zoomLevel.ImageHeight + tileSize - 1) / tileSize;     
     
    zoomLevels.Add(zoomLevel);     
     
    d *= 2;     
     
   } while (zoomLevel.ImageWidth > tileSize || zoomLevel.ImageHeight > tileSize);     
     
   zoomLevels.Reverse();     
     
     
   for (int zoom = 0; zoom < zoomLevels.Count; zoom++)     
   {     
    PipelineElement resize;     
     
    if (zoom == zoomLevels.Count)     
    {     
     resize = reader;     
    }     
    else     
    {     
     resize = new Resize(zoomLevels[zoom].ImageWidth, zoomLevels[zoom].ImageHeight, ResizeInterpolationMode.Lanczos3);     
     pipelineElements.Add(resize);     
     reader.Receivers.Add(resize);     
    }     
     
     
    for (int tileX = 0; tileX < zoomLevels[zoom].GridWidth; tileX++)     
    {     
     for (int tileY = 0; tileY < zoomLevels[zoom].GridHeight; tileY++)     
     {     
      int x = tileX * tileSize;     
      int y = tileY * tileSize;     
      int width = Math.Min((tileX + 1) * tileSize, zoomLevels[zoom].ImageWidth) - x;     
      int height = Math.Min((tileY + 1) * tileSize, zoomLevels[zoom].ImageHeight) - y;     
     
      var crop = new Crop(x, y, width, height);     
      pipelineElements.Add(crop);     
      resize.Receivers.Add(crop);     
     
      var outputFilePath = String.Format(outputPath + "{0}-{1}-{2}.jpg", zoom, tileX, tileY);     
     
      var p = Path.GetDirectoryName(outputFilePath);     
     
      var writer = new JpegWriter(outputFilePath);     
     
      pipelineElements.Add(writer);     
      crop.Receivers.Add(writer);     
     }     
    }     
   }     
     
   Pipeline.Run(reader);     
  }     
  finally     
  {     
   for (var i = 0; i < pipelineElements.Count; i++)     
   {     
    pipelineElements[i].Dispose();     
   }     
  }     
 }     
}     

Using this approach you can process a really large image. During testing I processed 2500 MP images (~40000 x 60000 pixels) successfully.

The real code is a bit more complicated as during pipeline processing Graphics Mill reads and writes multiple files at the same time. The library doesn’t release file handlers before the completion of Pipeline.Run. The image of 10000x10000  pixels consists of ~2500 tiles of 256x256 size so we need to open ~2500 files for writing. Windows has a limit in maximum file handlers per application so you can get an exception. You just need to keep this limitation in mind and modify the code accordingly.

I have prepared the sample and posted it on GitHub. I also have created the GUI and Command Line front end for the tile image generator:

Zoomable Image Windows application
Usage: ZoomableImage.Cmd [INPUT FILE] [OUTPUT DIR] [OPTIONS]

Available options:
  -v       Create HTML viewer
  -b       Base name (used only with HTML viewer)
  -w       HTML viewer width
  -h       HTML viewer height
  -t       Tile size
  -f       Tile image format (JPEG, PNG)
  -q       Tile image JPEG Quality (0, 100)
  -s       File structure (ex. {z}-{x}-{y}.{ext} or {z}/{x}-{y}.{ext})

D:\ >ZoomableImage.Cmd.exe "D:\ny.jpg" "D:\ny_viewer" -v -b ny

The application also generates an example HTML viewer powered by Leaflet library.