In my last post I put together a simple C# application that loads a comic book archive and lists its content to the console. Let’s load some pages now.

First we need to get the first image in the archive. Add this method to the code for the main window. As always, the complete code for this project is in GitHub.

[code language=”csharp”]
private IArchiveEntry getFirstEntry()
{
foreach (IArchiveEntry entry in archive.Entries)
{
if (!entry.IsDirectory)
{
return entry;
}
}
return null;
}
[/code]

This seems like a trivial amount of code for a method, and it’s not even the easiest way to get the first entry in the archive. If we take a look at the IArchive defintion, we see that all of the entries are available as an IEnumerable collection. We could actually do this:

[code language=”csharp”]
var entry = archive.Entries.First();
[/code]

But there’s a catch: what if the first entry is a directory? We need to skip it, since trying to load it as an image won’t work. Also, we’re going to be replacing this method soon enough and it can act as a place holder until then.

Next we need to load the image and display it. First add this method:
[code language=”csharp”]
private void displayImage(Bitmap bitmap)
{
MemoryStream memoryStream = new MemoryStream();
bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);

BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memoryStream;
bitmapImage.EndInit();
ImageViewer1.Source = bitmapImage;
}
[/code]

And then update MenuItem_Click_1:
[code language=”csharp”]
private void MenuItem_Click_1(object sender, RoutedEventArgs e)
{

Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();

// Set filter for comic book archives
dlg.Filter = "CBR files (*.cbr)|*.cbr|CBZ files (*.cbz)|*.cbz|All Files (*.*)|*.*";

// Display OpenFileDialog by calling ShowDialog method
Nullable<bool> result = dlg.ShowDialog();

// Get the selected file name and log to console (for now)
if (result == true)
{
// Open document
openArchive(dlg.FileName);
fileEntry = getFirstEntry();

Bitmap bitmap = (Bitmap)Bitmap.FromStream(fileEntry.OpenEntryStream());
displayImage(bitmap);
}
}
[/code]

Build and run and open a document. You’ll see the first image, or at least most of it.

We take the fileEntry returned by getFirstEntry() and use it’s OpenEntryStream() method to create a Bitmap.

Then we pass that as an Image to displayImage(), which converts it to a BitmapImage and passes it to our ImageViewer for display.

This code is a bit convoluted. Why load one format and the convert to another?

Comic book archives tend to have very large images in them. Unless you have a remarkably high-resolution screen, when the image loaded with the code above you saw something less than half of the page. We need to resize our images to make them easier to view. For this we need a Bitmap.

What size makes sense? As tall and as wide as fits works for our purposes. So the first thing we need to the system’s screen dimension. Add this to the class definition for MainWindow:

[code language=”csharp”]
public partial class MainWindow : Window
{

IArchive archive;
IArchiveEntry fileEntry;
int maxImageHeight = (int)System.Windows.SystemParameters.PrimaryScreenHeight – 75;
[/code]

System.Windows.SystemParameters tells us the size of the primary screen.

Now two methods for figuring out our image size and then resizing the image to it:
[code language=”csharp”]
private System.Drawing.Size getNewImageSize(System.Drawing.Size oldSize)
{
Console.WriteLine("Old dimensions: " + oldSize.Width + "x" + oldSize.Height);
float ratio;
if (oldSize.Height > oldSize.Width)
{
ratio = (float)oldSize.Width / oldSize.Height;
}
else
{
ratio = (float)oldSize.Height / oldSize.Width;
}

int newWidth = (int)(maxImageHeight * ratio);
System.Drawing.Size newSize = new System.Drawing.Size(newWidth, maxImageHeight);
Console.WriteLine("New dimensions: " + newSize.Width + "x" + newSize.Height);
return newSize;
}

public static Bitmap ResizeImage(Image image, System.Drawing.Size newSize)
{

// Make a rectangle that is the new size
Rectangle destRect = new Rectangle(0, 0, newSize.Width, newSize.Height);

// Make a bitmap that is the new size
Bitmap destImage = new Bitmap(newSize.Width, newSize.Height);

// Set new image to the resolution of the original
destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);

// Create a GDI holder and use it
using (Graphics graphics = Graphics.FromImage(destImage))
{

// Set our quality options
graphics.CompositingMode = CompositingMode.SourceCopy;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

// Resize original image into new one
using (var wrapMode = new ImageAttributes())
{
wrapMode.SetWrapMode(WrapMode.TileFlipXY);
graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
}
}
return destImage;
}
[/code]
In getnewImageSize() we calculate the ratio of height to width (or width to height if it is a landscape page) and then use that to figure out the dimensions that best fit the screen.

Then, we resize the image. I found an old MSDN article with a resize method that required some updating.

Plug this in to our loading process. Update the event handler for MenuItem_Click_1 to the following.
[code language=”csharp”]
private void MenuItem_Click_1(object sender, RoutedEventArgs e)
{

Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();

// Set filter for comic book archives
dlg.Filter = "CBR files (*.cbr)|*.cbr|CBZ files (*.cbz)|*.cbz|All Files (*.*)|*.*";

// Display OpenFileDialog by calling ShowDialog method
Nullable<bool> result = dlg.ShowDialog();

// Get the selected file name and log to console (for now)
if (result == true)
{
// Open document
openArchive(dlg.FileName);

fileEntry = getFirstEntry();

Bitmap bitmap = loadAndResizeBitmap();
displayImage(bitmap);
}
}
[/code]

And last, tell the ImageViewer to resize itself to the size of its contents:

[code language=”csharp”]
private void displayImage(Bitmap bitmap)
{
MemoryStream memoryStream = new MemoryStream();
bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);

BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memoryStream;
bitmapImage.EndInit();
ImageViewer1.Source = bitmapImage;
ImageViewer1.Height = bitmap.Height;
ImageViewer1.Width = bitmap.Width;
}
[/code]

Run the project and open a file. We see the first page, scaled to the screen!

So now let’s add the ability to see each image.

Rather than add more controls to our window, let’s use clicking on the image to “turn the page.” We can use clicking on the left side to page back and clicking on the right to page forward.

Before we set up the actual event handler, we need some machinery for opening different images in the archive.

First, refactor getFirstEntry() to getPreviousEntry() and getNextEntry():
[code language=”csharp”]
private IArchiveEntry getPreviousEntry()
{

// Don’t try to go back before the head of the collection
if (currentIndex == 0)
{
return currentEntry;
}
else
{
IArchiveEntry entry = archive.Entries.ElementAt(–currentIndex);
while (entry.IsDirectory)
{
getPreviousEntry();
}
return entry;
}
}

private IArchiveEntry getNextEntry()
{
if (currentIndex == (archive.Entries.Count() – 1))
{
return currentEntry;
}
else
{
IArchiveEntry entry = archive.Entries.ElementAt(++currentIndex);
while (entry.IsDirectory)
{
getNextEntry();
}
return entry;
}
}
[/code]

If it is isn’t obvious yet, I am sticking to the Single Responsibility Principle as much as possible here.

getPreviousEntry() and getnextEntry() are responsible for getting us the next (or previous) valid entry and also for keeping track of where we are. If we try to run pastthe beginning or end of the file, they return the right one instead. This will mean we’ll reload the current page. A more efficient algorithm would avoid this.

The way we open the load pages needs to change. Let’s move the code to open an image to its own function and generalize it a bit.
[code language=”csharp”]
private void loadPage(Direction direction)
{

if (direction == Direction.Forward)
{
currentEntry = getNextEntry();
}
else
{
currentEntry = getPreviousEntry();
}

Bitmap bitmap = loadAndResizeBitmap(currentEntry);
displayImage(bitmap);
currentWidth = bitmap.Width;
}
[/code]

So MenuItem_Click_1 is short and sweet now:

[code language=”csharp”]
private void MenuItem_Click_1(object sender, RoutedEventArgs e)
{

Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();

// Set filter for comic book archives
dlg.Filter = "CBR files (*.cbr)|*.cbr|CBZ files (*.cbz)|*.cbz|All Files (*.*)|*.*";

// Display OpenFileDialog by calling ShowDialog method
Nullable<bool> result = dlg.ShowDialog();

// Get the selected file name and log to console (for now)
if (result == true)
{
// Open document, reset page counter
currentIndex = -1;
openArchive(dlg.FileName);
loadPage(Direction.Forward);
}
}
[/code]

Again, there are more efficient (and prettier) algorithms for this. I’m aiming for simplicity.

Now we need to add a bit of state to the application too. You probably noticed two new variables in loadPage() We need to know what page we are on so we can move the the next or the previous. We also need to know how wide the current page is. Here are our declarations.

[code language=”csharp”]
IArchive archive;
IArchiveEntry currentEntry;
int currentIndex = -1;
int currentWidth = 0;

int maxImageHeight = (int)System.Windows.SystemParameters.PrimaryScreenHeight – 75;

enum Direction {
Forward,
Back
}
[/code]
So all we need is an event handler. Add it to the XAML:

[code language=”xml”]
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="ComicViewer.MainWindow"
Title="ComicViewer" Width="Auto" Height="Auto" SizeToContent="WidthAndHeight">
<Grid>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_Open" Click="MenuItem_Click_1"/>
<MenuItem Header="_Close" Click="MenuItem_Click_2"/>
<MenuItem Header="_Save" Click="MenuItem_Click_3"/>
</MenuItem>
</Menu>
<StackPanel Orientation="Vertical">
<StackPanel >
<Image x:Name="ImageViewer1" MouseDown="Image_Click"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
[/code]

And then define it:
[code language=”csharp”]
private void Image_Click(object sender, MouseButtonEventArgs e)
{
double azimuth = e.GetPosition(ImageViewer1).X;
if (currentWidth != 0)
{
if (azimuth > (currentWidth / 2))
{
loadPage(Direction.Forward);
}
else
{
loadPage(Direction.Back);
}
}
}
[/code]

Run the code, load a file, and click on either side of the page to page forward or back.

Image_Click examines the X axis of the click event it is passed. If is it less than half of the current image width, if loads a page with Direction.Back. If it is greater, Direction.Forward. All of our bounds checking is done in getPreviousEntry() and getnextEntry(), which makes the event handler very readable.

Load the app and give it a try. You may notice one that not all archives have the images in the correct order. Better readers parse the file names and attempt to display them in the correct order. That’s beyond the scope of this project.

We need two more event handlers and we’re done.

Let’s do the easy one first:
[code language=”csharp”]
private void MenuItem_Click_2(object sender, RoutedEventArgs e)
{
archive = null;
currentEntry = null;
ImageViewer1.Source = null;
ImageViewer1.Height = 0;
ImageViewer1.Width = 0;
currentIndex = 0;
currentWidth = 0;
}
[/code]

Run the code, load a file, close it, load another.

Not a very useful menu item, but it illustrates how to reset the app to its starting point.

And finally, let’s save a file.
[code language=”csharp”]
private void MenuItem_Click_3(object sender, RoutedEventArgs e)
{
Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
dlg.FileName = Path.GetFileName(currentEntry.Key);
Nullable<bool> result = dlg.ShowDialog();
if (result == true)
{
currentEntry.WriteToFile(dlg.FileName);
}
}
[/code]
Since SaveFileDialog acts a lot like OpenFileDialog, this looks familiar.

Rather than saving the resized image, we simple use the currentEntry member that holds the current archive entry to initialize the filename in the save dialog (after stripping off any embedded directory names with Path), and also to write itself to file.

Run it and save an image.

We’re done! As mentioned above, the complete code for this project is in GitHub. Please check it out and let me know how it goes.