My WPF application has a custom control I wrote specifically for displaying part of a bitmap. The bitmap is contained in an object transmitted to my program from my company's back end Windows service as a byte
array. Included in the data is a rectangle which specifies the part of the image of interest which needs to be displayed.
Further, when the user clicks on the control, it cycles between three different "magnifications" of that region. This setting is called the "zoom state". Essentially, at one setting, the rectangle that is displayed is larger in width by 60 pixels than the region specified by the rectangle transmitted with the data. At the second setting, the rectangle's width is increased by 25%, and at the third, it is increased by 50%. The height is always computed to keep the aspect ratio of the portion of the bitmap displayed the same as the control's.
To date, I've been generating new bitmaps from the data that are cropped to the region specified by the rectangle computations described above. However, given the amount of data that we receive, and the size of the bitmaps, this is using huge amounts of memory. I need to find a way to reduce the memory consumption.
I did a search on cropping images in WPF & found this MSDN article on clipping an image. This seemed ideal, as I'm already computing a rectangle and it seems this would use a lot less memory. So I modified my code this morning so that instead of creating a CroppedBitmap
from the original image & an Int32Rect
, it creates a RetangleGeometry
struct and sets the Image
control's Clip
property to that rectangle. The result, however, is I'm not seeing anything at all.
I commented out the code that creates the RectangleGeometry
and I do see the whole image in the control at that point, so I know that the problem is somewhere in the code that computes the rectangle. I know the computations in the code are right, but I must be missing something when I convert it into a RectangleGeometry
.
Here's the template that the custom control uses:
<Style TargetType="{x:Type local:ZoomableImage}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ZoomableImage}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ContextMenu="{TemplateBinding ContextMenu}"
Cursor="{TemplateBinding Cursor}"
Margin="{TemplateBinding Margin}"
Name="ImageBorder">
<Image BitmapEffect="{TemplateBinding BitmapEffect}"
BitmapEffectInput="{TemplateBinding BitmapEffectInput}"
CacheMode="{TemplateBinding CacheMode}"
Effect="{TemplateBinding Effect}"
HorizontalAlignment="Stretch"
Name="Image"
Source="{TemplateBinding ImageToBeZoomed}"
Stretch="{TemplateBinding Stretch}"
VerticalAlignment="Stretch" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And here's the code that computes the clip rectangle:
private void CreateClipRectangle() {
Rect srcRect = new Rect( PlateRectangle.X, PlateRectangle.Y, PlateRectangle.Width, PlateRectangle.Height );
// We want to show some pixels outside the plate's rectangle, so add 60 to the PlateRectangle's Width.
srcRect.Width += 60.0;
// Adjust the Width property for the ZoomState, which increases the height & width of the rectangle around the license plate
if ( ZoomState == ZoomStates.ZoomPlus25 ) {
srcRect.Width = srcRect.Width * 1.25;
} else if ( ZoomState == ZoomStates.ZoomPlus50 ) {
srcRect.Width = srcRect.Width * 1.50;
}
// Make sure that srcRect.Width is not bigger than the ImageToBeZoomed's PixelWidth!
if ( srcRect.Width > ImageToBeZoomed.PixelWidth ) srcRect.Width = ImageToBeZoomed.PixelWidth;
// We need to keep the aspect ratio of the source rectangle the same as the Image's.
// Compute srcRect.Height so the srcRect will have the correct aspect ratio, but don't let
// the rectangle's height get bigger than the original image's height!
srcRect.Height = Math.Min( ImageToBeZoomed.PixelHeight, Math.Round( srcRect.Width * ImageBorder.ActualHeight / ImageBorder.ActualWidth ) );
// Adjust srcRect.X & srcRect.Y to center the source image in the output image
srcRect.X = srcRect.X - ( srcRect.Width - PlateRectangle.Width ) / 2.0;
srcRect.Y = srcRect.Y - ( srcRect.Height - PlateRectangle.Height ) / 2.0;
// Adjust srcRect to keep the cropped region from going off the image's edges.
if ( srcRect.X < 0 ) srcRect.X = 0.0;
if ( srcRect.Y < 0 ) srcRect.Y = 0.0;
if ( ( srcRect.X + srcRect.Width ) > ImageToBeZoomed.PixelWidth ) srcRect.X = ImageToBeZoomed.PixelWidth - srcRect.Width;
if ( ( srcRect.Y + srcRect.Height ) > ImageToBeZoomed.PixelHeight ) srcRect.Y = ImageToBeZoomed.PixelHeight - srcRect.Height;
// Create a new RectangleGeometry object that we will use to clip the ImageToBeZoomed and put it into the Clip property.
ImageControl.Clip = new RectangleGeometry( srcRect, 0.0, 0.0 );
}
ImageToBeZoomed
is a DependencyProperty
that is of type BitmapSource
. The byte
array is converted into a BitmapImage
using this code:
public static BitmapImage BitmapFromBytes( byte[] imageBytes ) {
BitmapImage image = null;
if ( imageBytes != null ) {
image = new BitmapImage();
try {
using ( MemoryStream memoryStream = new MemoryStream( imageBytes ) ) {
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = memoryStream;
image.EndInit();
// Freeze the BitmapImage. This helps plug memory leaks & saves memory.
image.Freeze();
}
} catch ( Exception ex ) {
// . . .
}
}
return image;
}
The values in the PlateRectangle
property are ints
and they are in pixels. Could the problem have something to do with needing to convert from pixels to device independent units?
EDIT 1
In playing with this, I've found that I see no image if I set the Y coordinate of the RectangleGeometry
struct's Rect
property to anything other than 0 or negative values. This makes no sense to me. In most cases, the area of concern is in the middle of the image & no where near the top or bottom edge. Does anyone have any ideas why this is so?
Edit 2
Am I the only one who is having problems with the WPF Clip
functionality?
It looks like the Rect
you're using for RectangleGeometry
is not being scaled. There also seem to be a few other scaling issues in there.
I replaced your CreateClipRectangle
method with this (see inline comments for what was changed/added):
private void CreateClipRectangle()
{
Rect srcRect = new Rect(PlateRectangle.X, PlateRectangle.Y, PlateRectangle.Width, PlateRectangle.Height);
// We want to show some pixels outside the plate's rectangle, so add 60 to the PlateRectangle's Width.
srcRect.Width += 60.0;
// Adjust the Width property for the ZoomState, which increases the height & width of the rectangle around the license plate
if (ZoomState == ZoomStates.ZoomPlus25)
{
srcRect.Width = srcRect.Width * 1.25;
}
else if (ZoomState == ZoomStates.ZoomPlus50)
{
srcRect.Width = srcRect.Width * 1.50;
}
// Make sure that srcRect.Width is not bigger than the ImageToBeZoomed's PixelWidth!
if (srcRect.Width > ImageToBeZoomed.PixelWidth) srcRect.Width = ImageToBeZoomed.PixelWidth;
// We need to keep the aspect ratio of the source rectangle the same as the Image's.
// Compute srcRect.Height so the srcRect will have the correct aspect ratio, but don't let
// the rectangle's height get bigger than the original image's height!
double aspectRatio = ((double)ImageToBeZoomed.PixelHeight / ImageToBeZoomed.PixelWidth); // <-- ADDED
srcRect.Height = Math.Min(ImageToBeZoomed.PixelHeight, Math.Round(srcRect.Width * aspectRatio)); // <-- CHANGED
// Adjust srcRect.X & srcRect.Y to center the source image in the output image
srcRect.X = srcRect.X - srcRect.Width / 2.0; // <-- CHANGED
srcRect.Y = srcRect.Y - srcRect.Height / 2.0; // <-- CHANGED
// Adjust srcRect to keep the cropped region from going off the image's edges.
if (srcRect.X < 0) srcRect.X = 0.0;
if (srcRect.Y < 0) srcRect.Y = 0.0;
if ((srcRect.X + srcRect.Width) > ImageToBeZoomed.PixelWidth) srcRect.X = ImageToBeZoomed.PixelWidth - srcRect.Width;
if ((srcRect.Y + srcRect.Height) > ImageToBeZoomed.PixelHeight) srcRect.Y = ImageToBeZoomed.PixelHeight - srcRect.Height;
double scaleX = (ImageControl.ActualWidth / ImageToBeZoomed.PixelWidth); // <-- ADDED
double scaleY = (ImageControl.ActualHeight / ImageToBeZoomed.PixelHeight); // <-- ADDED
srcRect.X *= scaleX; // <-- ADDED
srcRect.Y *= scaleY; // <-- ADDED
srcRect.Width *= scaleX; // <-- ADDED
srcRect.Height *= scaleY; // <-- ADDED
// Create a new RectangleGeometry object that we will use to clip the ImageToBeZoomed and put it into the Clip property.
ImageControl.Clip = new RectangleGeometry(srcRect, 0.0, 0.0);
}
My ImageControl XAML was just
<Image Name="ImageControl" Stretch="Uniform" />
There may be further issues with the bounds checking, and I removed the border stuff that I wasn't sure about but hopefully this will get you started.