0

I have a set of methods in my utility class.

They should be converting between WPF and Windows image types but when I use them my image gets downscaled by a lot(from reviewing the code it should only resize it by 1 pixel or so on both axes due to double to int conversions but I'm obviously missing something)

I apologize for the gigantic post, I just can't seem to find the problem.

ImageSource (WPF) to Bitmap (Windows) methods :

public Bitmap ImageSourceToBitmap(Image source)
{
    var targetBitmap = new RenderTargetBitmap(
        (int) source.Source.Width,
        (int) source.Source.Height,
        96d, 96d,
        PixelFormats.Pbgra32);

    targetBitmap.Render(source);

    var memoryStream = new MemoryStream();
    var bitmapEncoder = new BmpBitmapEncoder();
    bitmapEncoder.Frames.Add(BitmapFrame.Create(targetBitmap));
    bitmapEncoder.Save(memoryStream);

    memoryStream.Position = 0;
    var bitmapImage = new BitmapImage();
    bitmapImage.BeginInit();
    bitmapImage.StreamSource = memoryStream;
    bitmapImage.EndInit();

    var resultBitmap = BitmapSourceToBitmap(bitmapImage);

    return resultBitmap;
}

public Bitmap BitmapSourceToBitmap(BitmapSource source)
{
    var width = source.PixelWidth;
    var height = source.PixelHeight;
    var stride = width * ((source.Format.BitsPerPixel + 7) / 8);
    var ptr = IntPtr.Zero;
    try
    {
        ptr = Marshal.AllocHGlobal(height * stride);
        source.CopyPixels(new Int32Rect(0, 0, width, height), ptr, height * stride, stride);
        using (var bitmap = new Bitmap(width, height, stride, 
        PixelFormat.Format32bppPArgb, ptr))
        {
            return new Bitmap(bitmap);
        }
    }
    finally
    {
        if (ptr != IntPtr.Zero)
            Marshal.FreeHGlobal(ptr);
    }
}

Bitmap(Windows) to BitmapSource(WPF) Method :

public BitmapSource BitmapToBitmapSource(Bitmap source)
{
    var bitmapData = source.LockBits( 
        new Rectangle( 0, 0,source.Width, source.Height ),
        ImageLockMode.ReadOnly,
        source.PixelFormat );

    //Names might be a bit confusing but I didn't have time to refactor
    var bitmapSource = BitmapSource.Create( 
        bitmapData.Width,
        bitmapData.Height,
        source.HorizontalResolution,
        source.VerticalResolution,
        PixelFormats.Pbgra32,
        null,
        bitmapData.Scan0,
        bitmapData.Stride * bitmapData.Height,
        bitmapData.Stride );

    source.UnlockBits(bitmapData);
    return bitmapSource;
}

XAML of my Image Control :

<Border Name="CurrentImageGridBorder" Grid.Column="0" Grid.Row="2" Margin="10" BorderThickness="1" Width="Auto" Height="Auto">
    <Border.BorderBrush>
        <SolidColorBrush Color="{DynamicResource BorderColor}"/>
    </Border.BorderBrush>
    <Grid x:Name="CurrentImageGrid" Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Image x:Name="CurrentImage" 
               RenderOptions.BitmapScalingMode="HighQuality"
               UseLayoutRounding="True"
               SnapsToDevicePixels="True"/>
    </Grid>
</Border>

WPF Control that triggers my GetImageSource method

<Button x:Name="OpenButton"
        Content="Open" 
        Style="{DynamicResource {x:Type Button}}"
        HorizontalAlignment="Left" Margin="20" VerticalAlignment="Top" Width="75" Height="20" 
        Click="GetImageSource"/>

GetImageSource Method :

private void GetImageSource(object sender, RoutedEventArgs e)
{ 
    var openFileDialog = new OpenFileDialog
    {
        Title = "Select an Image",
        Filter = "Image Files (*.jpg;*.jpeg;*.png;*.bmp)|*.jpg;*.jpeg;*.png;*.bmp|" +
                 "JPEG (*.jpg;*.jpeg)|*.jpg;*.jpeg|" +
                 "Portable Network Graphic|*.png",
        ValidateNames = true,
        Multiselect = false
    };

    if (openFileDialog.ShowDialog() != true) return;
    CurrentImage.Source = new BitmapImage(new Uri(openFileDialog.FileName));
    CurrentImage.Stretch = Stretch.None;
    if (!(CurrentImage.Source.Width > CurrentImageGridBorder.ActualWidth) &&
        !(CurrentImage.Source.Height > CurrentImageGridBorder.ActualHeight)) return;
    CurrentImage.StretchDirection = StretchDirection.Both;
    CurrentImage.Stretch = Stretch.Uniform;

}
Xeyth
  • 150
  • 10

1 Answers1

0

EDIT: Since you are only dealing with bitmaps (that are loaded from files or other streams, or created from raw pixel arrays) you don't need the RenderTargetBitmap at all. The Source property of your Image elements always contains an ImageSource that is already a BitmapSource.

Hence you can safely always cast ImageSource to BitmapSource:

public System.Drawing.Bitmap ImageToBitmap(Image image)
{
    return BitmapSourceToBitmap((BitmapSource) image.Source));
}

Using a RenderTargetBitmap would only by necessary when the Image's Source is not already a BitmapSource (e.g. when it is a DrawingImage). Then it is still neither necessary to render the Image element, nor to encode and decode the RenderTargetBitmap to/from a MemoryStream.

Just use a DrawingVisual and directly use the RenderTargetBitmap as BitmapSource:

public BitmapSource ImageSourceToBitmapSource(ImageSource imageSource)
{
    var bitmapSource = imageSource as BitmapSource;

    if (bitmapSource == null)
    {
        // This part is only necessary if an ImageSource is not a BitmapSource,
        // which may be the case when it is a DrawingImage or a D3DImage.
        // ImageSource instances loaded from files or streams are always BitmapSources.
        //
        var rect = new Rect(0, 0, imageSource.Width, imageSource.Height);

        var renderTargetBitmap = new RenderTargetBitmap(
            (int)Math.Ceiling(rect.Width),
            (int)Math.Ceiling(rect.Height),
            96d, 96d, PixelFormats.Pbgra32);

        var drawingVisual = new DrawingVisual();

        using (var drawingContext = drawingVisual.RenderOpen())
        {
            drawingContext.DrawImage(imageSource, rect);
        }

        renderTargetBitmap.Render(drawingVisual);
        bitmapSource = renderTargetBitmap;
    }

    return bitmapSource;
}

public System.Drawing.Bitmap ImageSourceToBitmap(ImageSource imageSource)
{
    return BitmapSourceToBitmap(ImageSourceToBitmapSource(imageSource));
}

public System.Drawing.Bitmap ImageToBitmap(Image image)
{
    return ImageSourceToBitmap(image.Source));
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • I think my issue lives in the dpiX and dpiY values that I have set to 96d. Is there a way to get the horizontalResolution and verticalResolution in wpf as you would do for windows images? – Xeyth Jul 12 '18 at 09:40
  • Sure, see [e.g. this](https://stackoverflow.com/q/1918877/1136211). However, I still doubt that you actually need a RenderTargetBitmap at all. Do you really convert DrawingImages or D3DImages? Everything else is already a BitmapSource. – Clemens Jul 12 '18 at 09:43
  • I'm not sure what a d3dimage is, but I'll explain shortly what I'm trying to do. My application is WPF so my sources are WPF Images. I need to convert these to Windows.Drawing.Bitmap in order to use them with a technology that will only accept these bitmaps. I'm not sure what the best approach would be. Also thanks for your time by the way, much appreciated EDIT : The technology converts these bitmaps to Leptonica Pix(probably irrelevant ) – Xeyth Jul 12 '18 at 10:06
  • I've understood that. Unless you are using DrawingImages, the Source property of your Image elements *always* contains a BitmapSource, which can directly be converted to Bitmap, without RenderTargetBitmap and all the other stuff. Please take a closer look at the code in my answer, especially `var bitmapSource = imageSource as BitmapSource` – Clemens Jul 12 '18 at 10:08
  • I'm not using DrawingImages, I load the images from a local folder – Xeyth Jul 12 '18 at 10:09
  • Then you **do not need** RenderTargetBitmap. The BitmapImage that you assign to Image.Source *is already* a BitmapSource, because that's its base class. You should try my example and step through the code with the debugger. You'll see that it will skip the `if (bitmapSource == null)` block – Clemens Jul 12 '18 at 10:09
  • See the edited answer for a one-line implementation of your ImageToBitmap method. That is all you need. As a note, your utility class should not deal with the Image class (since it's a UIElement), but only convert ImageSource/BitmapSource to/from Bitmap – Clemens Jul 12 '18 at 10:31
  • I have adjusted my code to your answer and everything works nicely and it's faster for bigger images(thanks). The resize bug is still present so I'll investigate my 'BitmapSourcetoBitmap' and my 'BitmaptoBitmapSource' methods – Xeyth Jul 12 '18 at 10:38
  • Be aware that the size of a bitmap (in pixels) is something entirely different than the size of an Image element, which is measured in device independent units, and which may also stretch its Source. Your utility class should only deal with bitmaps (i.e. Bitmap and BitmapSource). Then there can't be any "resize issue", since it would only deal with pixel widths and heights. – Clemens Jul 12 '18 at 10:48
  • After some testing I've come to the conclusion that my BitmapToBitmapSource method is causing the resize bug, everything else works perfectly – Xeyth Jul 12 '18 at 11:47
  • You may simply verify that by testing some of the other methods mentioned e.g. here: https://stackoverflow.com/q/30727343/1136211 or here: https://stackoverflow.com/q/94456/1136211 – Clemens Jul 12 '18 at 11:49