Loading image into ImageSource - incorrect width and height - wpf

My problem is that the image loading seems to be uncorrectly from application resources. This is code:
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri(#"pack://application:,,,/WpfApplication3;component/Resources/Images/16x16_incorrect.png", UriKind.Absolute);
bi.EndInit();
ImageSource s = bi;
Image file 16x16_incorrect.png is 16x16 32bpp PNG file, but after executing above code, s.Width = s.Height = 21,59729.... I also have another file - 16x16_correct.png, when it is loaded, both the ImageSource's Width and Height are equal to 16,002.
Other images each of them are loading incorrectly & it looks blurred (or smoothly), because system stretches it from 16x16 to 21x21.
correct image :
incorrect image :
What is causing this? If the problem in source image files, how can I change ImageSource.Width to desired size in order to use this files?

If you don't want to change DPI externally you can do it with this:
public static BitmapSource ConvertBitmapTo96DPI(BitmapImage bitmapImage)
{
double dpi = 96;
int width = bitmapImage.PixelWidth;
int height = bitmapImage.PixelHeight;
int stride = width * bitmapImage.Format.BitsPerPixel;
byte[] pixelData = new byte[stride * height];
bitmapImage.CopyPixels(pixelData, stride, 0);
return BitmapSource.Create(width, height, dpi, dpi, bitmapImage.Format, null, pixelData, stride);
}
If you just need correct values in Image.Source.Width/Height you can do something like I did:
this.myImage.Tag = new double[] { bitmapImage.DpiX, bitmapImage.DpiY };
this.myImage.Source = bitmapImage;
and resize it like so:
public static void ResizeImage(Image img, double maxWidth, double maxHeight)
{
if (img == null || img.Source == null)
return;
double srcWidth = img.Source.Width;
double srcHeight = img.Source.Height;
// Set your image tag to the sources DPI value for smart resizing if DPI != 96
if (img.Tag != null && img.Tag.GetType() == typeof(double[]))
{
double[] DPI = (double[])img.Tag;
srcWidth = srcWidth / (96 / DPI[0]);
srcHeight = srcHeight / (96 / DPI[1]);
}
double resizedWidth = srcWidth;
double resizedHeight = srcHeight;
double aspect = srcWidth / srcHeight;
if (resizedWidth > maxWidth)
{
resizedWidth = maxWidth;
resizedHeight = resizedWidth / aspect;
}
if (resizedHeight > maxHeight)
{
aspect = resizedWidth / resizedHeight;
resizedHeight = maxHeight;
resizedWidth = resizedHeight * aspect;
}
img.Width = resizedWidth;
img.Height = resizedHeight;
}

You need to set the image resolution to 96 DPI (currently it's 71.12 for the incorrect png).
You can do it using the free paint.net program ( http://getpaint.net ) from the Image menu select Canvas size and set the "resolution" field to 96

This is because of the DPI of the images. WPF renders default with 96 dpi. If you look at the dpi of the incorrect png image. You will see that it is set to 72. This causes WPF to scale the image to 96 DPI and keep the original size.
There are two solutions.
You can:
Modify the DPI using e.g XnView. Set it to 96.
Set the Width and Height properties to 16, and the Stretch property to Uniform
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Image x:Name="MyIncorrectImageFixed" Source="http://i.piccy.info/i5/24/41/504124/16x16_incorrect.png" Width="16" Height="16" Stretch="Uniform" />
<Image x:Name="MyIncorrectImage" Source="http://i.piccy.info/i5/24/41/504124/16x16_incorrect.png" Stretch="None" Grid.Row="1" />
<Image x:Name="MyCorrectImage" Source="http://i.piccy.info/i5/22/41/504122/16x16_correct.png" Stretch="None" Grid.Row="2" />

Related

WPF - Black Background surrounding saved canvas as jpeg

I'm having difficulty with JpegBitmapEncoder since it is creating an image that is placed in black rectangle. and I don't have the solution for fix.
private void SaveImage(Canvas canvas, string fileName)
{
SaveFileDialog s = new SaveFileDialog();
s.FileName = "Pic";
s.DefaultExt = ".jpg";
s.Filter = "JPG files (.jpg)|*.jpg";
Nullable<bool> result = s.ShowDialog();
if (result == true)
{
RenderTargetBitmap renderBitmap = new RenderTargetBitmap(6646, 3940, 2000d, 2000d, PixelFormats.Pbgra32);
canvas.Measure(new Size((int)canvas.Width, (int)canvas.Height));
canvas.Arrange(new Rect(new Size((int)canvas.Width, (int)canvas.Height)));
renderBitmap.Render(canvas);
string filename = s.FileName;
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
using (FileStream file = File.Create(filename))
{
encoder.Save(file);
}
}
}
with this code i get:
but when i use PngBitmap Encoder this doesn't happen. Can anyone shine a light?
How do I remove the black rectangle or perhaps fill it by increasing the dimensions of the picture?
.png supports transparency, whereas .jpg doesn't. I suspect that the background of your canvas is transparent, and as it doesn't know what to do with it it just leaves the pixels default (0,0,0) i.e. black. Png defaults to (0,0,0,0), which is transparent black.
Or, your canvas isn't the same dimensions as the image (your create your image with hardcoded width & height, rather than use the Width and Height of your canvas). As it's larger, it only renders the parts where the canvas actually covers.
If it's the first case, try setting your canvas Background="White" to your canvas so it renders all of it with no transparency. If it's the second, try creating a Rectangle the dimensions of your image with the appropriate Fill="White" and rendering that first before your canvas. Something like this:
Rectangle fillBackground = new Rectangle {
Width = 6646,
Height = 3940,
Fill=Brushes.White
}
renderBitmap.Render(fillBackground);
renderBitmap.Render(canvas);
Some advice, you shouldn't really use Width and Height, these can be NAN for a control that's layout is determined by it's parent. You should actually use Canvas.ActualWidth and Canvas.ActualHeight or Canvas.RenderSize.Width/Height. This will actually reflect the on-screen size all the time.
Also, to calculate the size of your output image adjusting for your DPI choice you can use the following:
RenderTargetBitmap renderBitmap = new RenderTargetBitmap(Width * DPI/96,
Height * DPI/96,
DPI, DPI, PixelFormats.Pbgra32);

Unable to present a BitmapSource properly

I have a program which generates a System.Drawing.Bitmap and then uses it to create a BitmapSource. I then assign the CurrentImage property of a WPF Image to the BitmapSource I create.
And here is the problem: no matter what I set the DPI values of the BitmapSource to, I end up with a grainy picture on my 4K display.
The piece of XAML I'm using is:
<Image Source="{Binding Path=CurrentImage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SnapsToDevicePixels="True"/>
and the code is as follows:
private static void SetBitmap(FreeformEditor fe, Bitmap bmp)
{
if (fe.lastBitmap != IntPtr.Zero)
DeleteObject(fe.lastBitmap);
try
{
var data = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadOnly, bmp.PixelFormat);
var dpi = baseDPI * (1 + fe.RenderForRetina.ToInt());
fe.CurrentImage = BitmapSource.Create(
data.Width, data.Height, dpi, dpi, PixelFormats.Bgr32, null,
data.Scan0, data.Stride * data.Height, data.Stride);
bmp.UnlockBits(data);
fe.lastBitmap = bmp.GetHbitmap();
fe.imageSizeRun.Content = string.Format("{0}x{1} #{2}dpi", fe.CurrentImage.Width,
fe.CurrentImage.Height,
fe.CurrentImage.DpiX);
}
catch
{
// this happens due to framework bugs
}
}

MatrixTransform in combination with Width/Height of a Canvas

When zooming in on a drawing on a Canvas I have the requirement to show scrollbars.
The Canvas is in a ScrollViewer and I increase the Width/Height of the Canvas so that that the scollbars appear (otherwise they don't).
To zoom in with a factor of 1.1 I use this code:
Matrix m = this.LayoutTransform.Value;
if (e.Delta > 0) f = 1.1;
else f = 1.0 / 1.1;
m.Scale(f, f);
this.LayoutTransform = new MatrixTransform(m);
this.Height = this.ActualHeight * f;
this.Width = this.ActualWidth * f;
It turns out that the Canvas becomes much too large. The drawing zooms in 10% but the width seems to become 20% more like the square of 1.1. So I use Math.Sqrt(f); instead of f.
Can anybody explain why it behaves this way?
You should only change the LayoutTransform of the Canvas, like in this simplified example:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Canvas Width="1000" Height="1000" Background="Transparent"
MouseWheel="Canvas_MouseWheel">
<Canvas.LayoutTransform>
<MatrixTransform/>
</Canvas.LayoutTransform>
</Canvas>
</ScrollViewer>
The MouseWheel event handler:
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
var element = (FrameworkElement)sender;
var transform = (MatrixTransform)element.LayoutTransform;
var matrix = transform.Matrix;
var scale = e.Delta >= 0d ? 1.1 : (1d / 1.1);
matrix.Scale(scale, scale);
transform.Matrix = matrix;
e.Handled = true;
}

pixel selection for PanZoomImage

I am new to WPF and am trying to write a clickable zoom-pan image control. I already have a zoom-pan image which seems to work:
<Border Name="border" ClipToBounds="True">
<Canvas>
<Image Name ="image">
Source="{Binding Path=Source}"
MouseLeftButtonDown="image_MouseLeftButtonDown"
MouseLeftButtonUp="image_MouseLeftButtonUp"
MouseMove="image_MouseMove"
MouseWheel="image_MouseWheel">
</Image>
</Canvas>
</Border>
For the mouse and wheel events I used this post: http://www.codeproject.com/Articles/168176/Zooming-and-panning-in-WPF-with-fixed-focus
I am writing the clickable control by inheriting from ZoomPanImage and adding an event for LeftMouseUp.
public class ClickableImage : PanZoomImage
{
public event Action<Point> Click;
//...
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// ... all sorts of checks to distinguish click from mouse move
if (Click != null)
{
Click(ControlToImage(mouseUpCoordinates));
}
}
protected Point ControlToImage(Point controlPixel)
{
//this is where i am stuck...
}
}
My problem is that I can't seem to calculate the correct image coordinates given the control coordinates. I need to take into account that the image can be zoomed and panned and that the window itself can be resized.
I tried using the rendering transform. When I zoom and pan the image I update the transform. And when I try to convert control coordinates to image coordinates I use the inverse transform:
Point imagePixel = image.RenderTransform.Inverse.Transform(controlPixel);
But this didn't work. One of the problems is that the Transform starts as Identity while in fact the image is stretched uniformly to the control's size.
Thanks,
Dina
Here's how I solved it. As Clemens suggested, I set the image stretch mode to none.
<Image Name="image" RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="None"
Source="{Binding Path=Source}"
MouseLeftButtonDown="image_MouseLeftButtonDown"
MouseLeftButtonUp="image_MouseLeftButtonUp"
MouseMove="image_MouseMove"
MouseWheel="image_MouseWheel"
Loaded="image_Loaded">
<Image.ContextMenu>
<ContextMenu>
<MenuItem Header="Fit to window" Click="FitToWindow_MenuItem_Click"></MenuItem>
</ContextMenu>
</Image.ContextMenu>
</Image>
This means that when the image is loaded into the window, you can only see part of it - depending on the window size. This is bad, but what's important is that the transform is identity and you can now manually set it such that the image is fully shown in the window.
private void FitViewToWindow()
{
if (Source == null)
throw new InvalidOperationException("Source not set");
BitmapSource bitmapSource = Source as BitmapSource;
if (bitmapSource == null)
throw new InvalidOperationException("Unsupported Image Source Type");
if (border.ActualWidth <= 0 || border.ActualHeight <= 0)
return;
double scaleX = border.ActualWidth / bitmapSource.PixelWidth;
double scaleY = border.ActualHeight / bitmapSource.PixelHeight;
double scale = Math.Min(scaleX, scaleY);
Matrix m = Matrix.Identity;
m.ScaleAtPrepend(scale, scale, 0, 0);
double centerX = (border.ActualWidth - bitmapSource.PixelWidth * scale) / 2;
double centerY = (border.ActualHeight - bitmapSource.PixelHeight * scale) / 2;
m.Translate(centerX, centerY);
image.RenderTransform = new MatrixTransform(m);
}
This function should be called upon loading the image and upon changing the source of the image. As for resizing the window - as long as you keep track of the transform, you will be able to convert coordinate systems correctly. For example, here's what I do for window resize:
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
//center the image in the new size
if (sizeInfo.PreviousSize.Width <= 0 || sizeInfo.PreviousSize.Height <= 0)
return;
Matrix m = image.RenderTransform.Value;
double offsetX = (sizeInfo.NewSize.Width - sizeInfo.PreviousSize.Width) / 2;
double offsetY = (sizeInfo.NewSize.Height - sizeInfo.PreviousSize.Height) / 2;
m.Translate(offsetX, offsetY);
image.RenderTransform = new MatrixTransform(m);
}

WPF: Detect Image click only on non-transparent portion

I have an Image control in WPF which contains an image with lots of transparent pixels. Right now, the MouseDown event on Image fires whenever I click within the full rectangular region of the Image control. I would like some way to detect if the mouse click occurred on a nontransparent portion of the image.
What would be the best way of doing this?
Using the technique in this answer you can derive from Image to create an OpaqueClickableImage that only responds to hit-testing in sufficiently non-transparent areas of the image:
public class OpaqueClickableImage : Image
{
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
var source = (BitmapSource)Source;
// Get the pixel of the source that was hit
var x = (int)(hitTestParameters.HitPoint.X / ActualWidth * source.PixelWidth);
var y = (int)(hitTestParameters.HitPoint.Y / ActualHeight * source.PixelHeight);
// Copy the single pixel into a new byte array representing RGBA
var pixel = new byte[4];
source.CopyPixels(new Int32Rect(x, y, 1, 1), pixel, 4, 0);
// Check the alpha (transparency) of the pixel
// - threshold can be adjusted from 0 to 255
if (pixel[3] < 10)
return null;
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
}
after adding this class, just use it like a regular image:
<utils:OpaqueClickableImage Name="image" Source="http://entropymine.com/jason/testbed/pngtrans/rgb8_t_bk.png" Stretch="None"/>
public class OpaqueClickableImage : Image
{
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
var source = (BitmapSource)Source;
var x = (int)(hitTestParameters.HitPoint.X / ActualWidth * source.PixelWidth);
var y = (int)(hitTestParameters.HitPoint.Y / ActualHeight * source.PixelHeight);
if (x == source.PixelWidth)
x--;
if (y == source.PixelHeight)
y--;
var pixels = new byte[4];
source.CopyPixels(new Int32Rect(x, y, 1, 1), pixels, 4, 0);
return (pixels[3] < 1) ? null : new PointHitTestResult(this, hitTestParameters.HitPoint);
}
}

Resources