How to save a non-blurry, high quality image of a control in WPF?

338 views Asked by At

I am using a DrawingContext to draw images. I then render the result to a RenderTargetBitmap. The image is crisp & sharp on the screen, but becomes blurred when saved. Even on 100% scaling the issue persists. Interesting part is that if the image width is less than 2000px it exports fine. But as the width increases, exported image keeps getting more and more blur.

Here is my code ( Sample project )

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            BitmapImage bitmap = new BitmapImage();
            bitmap.BeginInit();
            bitmap.UriSource = new Uri(@"image-full-hd.png");
            bitmap.EndInit();
            img1.Source = bitmap;

            img1.Width = bitmap.Width;
            img1.Height= bitmap.Height;
        }

        private void btnExport_Click(object sender, RoutedEventArgs e)
        {
            var result = CaptureSnapshot(img1);
            Clipboard.SetImage(result);
        }

        public BitmapSource CaptureSnapshot(UIElement source)
        {
            var dpi = VisualTreeHelper.GetDpi(source);
            double dpiX = dpi.DpiScaleX, dpiY = dpi.DpiScaleY;

            double newImageWidth = source.RenderSize.Width * dpiX;
            double newImageHeight = source.RenderSize.Height * dpiY;

            RenderTargetBitmap renderTarget = new RenderTargetBitmap((int)newImageWidth, (int)newImageHeight, 96, 96, PixelFormats.Pbgra32);

            DrawingVisual visual = new DrawingVisual();

            using (DrawingContext context = visual.RenderOpen())
            {
                VisualBrush sourceBrush1 = new VisualBrush(source)
                {
                    Stretch = Stretch.None
                };

                context.DrawRectangle(sourceBrush1, null, new Rect(source.RenderSize));
            }

            System.Windows.Size s1size = new System.Windows.Size(newImageWidth, newImageHeight);
            source.Measure(s1size); 
            source.Arrange(new Rect(s1size));

            RenderOptions.SetEdgeMode(renderTarget, EdgeMode.Unspecified);
            RenderOptions.SetBitmapScalingMode(renderTarget, BitmapScalingMode.HighQuality);
            renderTarget.Render(visual);

            return renderTarget;
        }

Application Window enter image description here

Clip from an Exported Image which was 3400px wide enter image description here

2

There are 2 answers

9
Clemens On BEST ANSWER

If the source UIElement has already been rendered - and you are using its RenderSize for calculating the size of the bitmap - there is no need for another layout pass, so the Measure and Arrange calls seem unnecessary.

In order to create an unblurred DrawingVisual output of the scaled original image, you could directly draw the ImageSource of the Image element into the Visual when the source argument is an Image. Use a VisualBrush only when source is not an Image.

public static BitmapSource CaptureSnapshot(UIElement source)
{
    var dpi = VisualTreeHelper.GetDpi(source);
    var width = source.RenderSize.Width * dpi.DpiScaleX;
    var height = source.RenderSize.Height * dpi.DpiScaleY;
    var rect = new Rect(0, 0, width, height);
    var bitmap = new RenderTargetBitmap((int)width, (int)height, 96, 96, PixelFormats.Pbgra32);
    var visual = new DrawingVisual();

    using (var context = visual.RenderOpen())
    {
        if (source is Image image)
        {
            context.DrawImage(image.Source, rect);
        }
        else
        {
            context.DrawRectangle(new VisualBrush(source), null, rect);
        }
    }

    bitmap.Render(visual);
    return bitmap;
}
3
Legkov Ivan On

Is there a need for using DrawingVisual, DrawingContext and VisualBrush? Because if not, you can just pass the UIElement directly to RenderTargetBitmap.Render(). I cloned your sample project and changed CaptureSnapshot to this:

public BitmapSource CaptureSnapshot(UIElement source)
{
    var dpi = VisualTreeHelper.GetDpi(source);
    var width = source.RenderSize.Width * dpi.DpiScaleX;
    var height = source.RenderSize.Height * dpi.DpiScaleY;

    var bitmap = new RenderTargetBitmap((int)width, (int)height,
        dpi.PixelsPerInchX, dpi.PixelsPerInchY, PixelFormats.Pbgra32);

    Size s1size = new Size(width, height);
    source.Measure(s1size);
    source.Arrange(new Rect(s1size));

    bitmap.Render(source);

    return bitmap;
}

And the result does not appear blurry for both sample images in the project.

EDIT

Since using Visual is required I did some more search and discovered this thread. You can use BitmapCacheBrush with SnapsToDevicePixels set to true and it should work without the blur:

using (var context = visual.RenderOpen())
{
    var vb = new BitmapCacheBrush(source)
    {
        BitmapCache = new BitmapCache { SnapsToDevicePixels = true}
    };
    context.DrawRectangle(vb, null, new Rect(0, 0, width, height));
}