Monday, July 7, 2008

Deep Zoom - Rendering in 2 different layouts based on zoom factor

Berend, the author of Generate Silverlight 2 DeepZoom image collection from multi-page tiff, wanted to know if it was possible to render the contents of the deep zoom control in different layouts based on the current zoom factor. Initially, he wanted to show the images in a single row layout with each image taking the entire height of the control. As the user zooms out, after the image reaches a certain factor - say half the height, he wanted to switch to a thumbnails mode where all the images are displayed in a multi row and column format (much like how it is now).

It was pretty easy to do and here is the result of my experimentation:


Try zooming out so that the first image height is less than half the height of the control. After the motion is completed, it should switch to the thumbnails mode. I have currently based this off the height of the first image. The layout will switch depending on whether the height of the first image is greater than or less than half the height of the control.

The code:

enum DisplayMode {
Full = 1,
Thumbnails = 2
}

DisplayMode _displayMode = DisplayMode.Full;

msi.MotionFinished += delegate(object sender, RoutedEventArgs e)
{
// This is required to ensure that ArrangeImages is called only once
if (msi.UseSprings == false) {
ShowAllImages();
msi.UseSprings = true;
}
else {
DisplayMode _origDisplayMode = _displayMode;

Rect imageRectElement = msiLogicalToElementRect(GetSubImageRect(0));
_displayMode = (msi.ActualHeight / imageRectElement.Height < 2) ? DisplayMode.Full : DisplayMode.Thumbnails;

if (_displayMode != _origDisplayMode) {
Reset();
}
}
};

public void Reset()
{
_lastMousePos = new Point(0, 0);
msi.ViewportOrigin = new Point (0, 0);
msi.ViewportWidth = 1.0;

// Show all the images
_imagesToShow.Clear();
_imagesToHide.Clear();
for (int i=0; i<msi.SubImages.Count; i++) {
_imagesToShow.Add(msi.SubImages[i]);
}
ArrangeImages();
}

public void ArrangeImages()
{
InitStoryboard();

double containerAspectRatio = this.msi.ActualWidth / this.msi.ActualHeight;
double spaceBetweenImages = 0.005;

List<SubImage> subImages = new List<SubImage>();
_imagesToShow.ForEach(subImage => subImages.Add(new SubImage(subImage)));

// Capture the total width of all images
double totalImagesWidth = 0.0;
subImages.ForEach(subImage => totalImagesWidth += subImage.Width);

// Calculate the total number of rows required to display all the images
int numRows = (_displayMode == DisplayMode.Thumbnails) ? (int)Math.Sqrt((totalImagesWidth / containerAspectRatio)+1) : 1;

// Assign images to each row
List<Row> rows = new List<Row>(numRows);
for (int i=0; i<numRows; i++)
rows.Add(new Row(spaceBetweenImages));

double widthPerRow = totalImagesWidth / numRows;
double imagesWidth = 0;
// Separate the images into rows. The total width of all images in a row should not exceed widthPerRow
for (int i=0, j=0; i<numRows; i++, imagesWidth=0) {
while (imagesWidth < widthPerRow && j < subImages.Count) {
rows[i].AddImage(subImages[j]);
subImages[j].RowNum = i;
imagesWidth += subImages[j++].Width;
}
}

// At this point in time the subimage height is 1
// If we assume that the total height is also 1 we need to scale the subimages to fit within a total height of 1
// If the total height is 1, the total width is aspectRatio. Hence (aspectRatio)/(total width of all images in a row) is the scaling factor.
// Added later: take into account spacing between images
rows.ForEach(Row => Row.Scale(containerAspectRatio));

// Calculate the total height, with space between images, of the images across all rows
// Also adjust the colNum for each image
double totalImagesHeight = (numRows - 1) * spaceBetweenImages;
rows.ForEach(Row => totalImagesHeight += Row.Height);

// The totalImagesHeight should not exceed 1.
// if it does, we need to scale all images by a factor of (1 / totalImagesHeight)
if (((_displayMode == DisplayMode.Thumbnails && totalImagesHeight > 1)) || _displayMode == DisplayMode.Full) {
subImages.ForEach(subImage => subImage.Scale(1 / (totalImagesHeight+spaceBetweenImages)));
totalImagesHeight = (numRows - 1) * spaceBetweenImages;
rows.ForEach(Row => totalImagesHeight += Row.Height);
Debug.Assert(totalImagesHeight <= 1);
}

// Calculate the top and bottom margin
double margin = (1 - totalImagesHeight) / 2;

// First hide all the images that should not be displayed
_imagesToHide.ForEach(subImage => subImage.Opacity = 0.0);

// Then display the displayable images to scale
for (int i=0; i<_imagesToShow.Count; i++) {
double X = rows[subImages[i].RowNum].CalcX(subImages[i].ColNum);
double Y = margin;
for (int j=0; j<subImages[i].RowNum; j++)
Y += spaceBetweenImages + rows[j].Height;

_imagesToShow[i].ViewportWidth = containerAspectRatio / subImages[i].Width;
AnimateImage(_imagesToShow[i], new Point(-(X / subImages[i].Width), -(Y / subImages[i].Width))); // for animation, use this statement instead of the next one
_imagesToShow[i].Opacity = 1.0;
}
// Play Storyboard
_moveStoryboard.Begin();
}

Thursday, July 3, 2008

I'm honored!

I just noticed that my blog is referenced in the MSDN Silverlight Beta 2 documentation, specifically http://msdn.microsoft.com/en-us/library/system.windows.controls.multiscaleimage(VS.95).aspx (search for Filter Example). This is definitely an honor!

Does this mean that I can now ask Microsoft for a free copy of Visual Studio 2008? Just kidding :) I am currently working on a Silverlight project so I have access to VS2008. I still use Notepad++ on my laptop though, since it is underpowered for VS.

I finally got around to uploading the source code for the DeepZoom sample. You will find the link to the code in this post - DeepZoom sample ported to Silverlight 2 Beta 2

Tuesday, July 1, 2008

Deep Zoom Composer source code

Not really...but pretty close! Check out Berend's article on CodeProject -
Generate Silverlight 2 DeepZoom image collection from multi-page tiff

I haven't tried it myself so I don't know if it works, but it definitely opens up a lot of possibilities - Dynamic gallery generation; Adding images to collection without regenerating all the tiles...

If only Microsoft would open source the DeepZoom Composer code, or at least the image/tile generation part!

Great work, Berend Engelbrecht!