Create Freezable image from non-Freezable image - wpf

I have a DrawingImage that uses DynamicResources for its colors. I want to then use that image in an animation, however you can't use objects that use DynamicResources in animations because they are not Freezable. I know I can't have its colors dynamically change during the animation, I don't care about that. I just want to create a version of this image which I can use in the animation. So basically have the image evaluate all its resources, then spit out a version of it that I can call Freeze on to use in the animation. Surely this must be possible? I must be able to, for instance, write this image out to disk? That must create a Freezable version of it, no? I've googled and searched StackOverflow but can find nothing useful... Any help would be much appreciated.

This was surprisingly difficult to dig up a solution to, but I seem to have found one. I made this handy extension, which will take my DrawingImage and encode it as a PNG via the PngBitmapEncoder, then write that to a memory stream, then load that into a BitmapImage which is Freezable and can be used in my animation. Whole lotta work for something so simple!
public static class DrawingExtensions
{
public static BitmapImage GetAsFreezableBitmapImage(this DrawingImage drawingImage)
{
return drawingImage?.Drawing?.GetAsFreezableBitmapImage();
}
public static BitmapImage GetAsFreezableBitmapImage(this Drawing drawing)
{
if (drawing == null)
return null;
var drawingVisual = new DrawingVisual();
using (var drawingContext = drawingVisual.RenderOpen())
{
drawingContext.PushTransform(new TranslateTransform(-drawing.Bounds.X, -drawing.Bounds.Y));
drawingContext.DrawDrawing(drawing);
}
var bitmap = new RenderTargetBitmap((int)drawing.Bounds.Width, (int)drawing.Bounds.Height, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(drawingVisual);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
var bitmapImage = new BitmapImage();
using (var stream = new MemoryStream())
{
encoder.Save(stream);
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = stream;
bitmapImage.DecodePixelHeight = bitmap.PixelHeight;
bitmapImage.DecodePixelWidth = bitmap.PixelWidth;
bitmapImage.EndInit();
}
return bitmapImage;
}
}

Related

How can I draw a Polyline onto an Image (WPF)

I've tried a few different approaches to this, but can't seem to get a combination that works.
Creating WPF app in C#, Visual Studio.
System.Windows.Shapes.Polyline works really nicely to draw into a Canvas in real-time, but I want to be able to draw in higher resolution onto a non-visual component that I can then render onto an Image.
If I create a Polyline on a Canvas that's visible in the UI, this works fine:
// Make rendertarget size of full page
RenderTargetBitmap rtb = new RenderTargetBitmap((int)wPage, (int)hPage, 96, 96, PixelFormats.Default);
// Render the polyline
rtb.Render(lineVirt);
// Apply to background image
imgBG.Source = rtb;
But if I create a Polyline on a Canvas that's not visible in the UI, then nothing renders to the image. This is probably fair enough. My guess is that the component recognises that it's not visible and therefore doesn't bother to render.
I've considered putting the Canvas somewhere in the UI buried under other controls, but that seems like a horrible kind of hack.
Essentially, all I need is a clean and fast way to draw a multi-point line of a specified width and color onto an Image. I thought that Polyline would work well, but only seems to work in a visible container.
What are my options?
You do not need a rendered Canvas or any other visible Panel at all.
Just use basic drawing primitives available at the Visual layer.
The DrawGeometry method below draws a Geometry onto a BitmapSource, using the bitmap's rendered size, i.e. the size that takes its DPI into account, and returns the resulting BitmapSource.
public static BitmapSource DrawGeometry(
BitmapSource source, Pen pen, Geometry geometry)
{
var visual = new DrawingVisual();
var rect = new Rect(0, 0, source.Width, source.Height);
using (var dc = visual.RenderOpen())
{
dc.DrawImage(source, rect);
dc.DrawGeometry(null, pen, geometry);
}
var target = new RenderTargetBitmap(
(int)rect.Width, (int)rect.Height, 96, 96, PixelFormats.Default);
target.Render(visual);
return target;
}
In order to draw in the bitmap's pixel units and hence ignore its DPI, modify the method like this:
var rect = new Rect(0, 0, source.PixelWidth, source.PixelHeight);
using (var dc = visual.RenderOpen())
{
dc.DrawRectangle(new ImageBrush(source), null, rect);
dc.DrawGeometry(null, pen, geometry);
}
The following method uses the above to draw a polyline as an IEnumerable<Point>.
public static BitmapSource DrawPolyline(
BitmapSource source, Pen pen, IEnumerable<Point> points)
{
var geometry = new PathGeometry();
if (points.Count() >= 2)
{
var figure = new PathFigure { StartPoint = points.First() };
figure.Segments.Add(new PolyLineSegment(points.Skip(1), true));
geometry.Figures.Add(figure);
}
return DrawGeometry(source, pen, geometry);
}
It would be used like
var source = new BitmapImage(new Uri(...));
var pen = new Pen
{
Brush = Brushes.Blue,
Thickness = 2,
};
var points = new List<Point>
{
new Point(100, 100),
new Point(1000, 100),
new Point(1000, 1000),
new Point(100, 1000),
new Point(100, 100),
};
image.Source = DrawPolyline(source, pen, points);
Your canvas needs a size, so someone or something has to Arrange it. That might already be enough to get it to render, but the only reliable way of rendering arbitrary visuals to a bitmap is to actually place them in the visual tree of a window that's displayed and thus laid out by WPF. You can then render to the bitmap in a deferred task at ContextIdle priority to ensure that layout is complete.

how to get BitmapImage in codebehind from the image tag in xaml in wpf/silverlight

i dont have a problem with binding a bitmapimage to image tag in codebehind for eg.
BitmapImage image = new BitmapImage();
imagetaginxaml.Source = image; // this will remove whatever image is currently on the image tag in xaml and attach the empty bitmapimage above
but i'm not able to get the image by doing the reverse, for example, i want to process the image that is currently on the image tag. i am not able to do this
BitmapImage image = imagetaginxaml.Source;
what should i do
Well, Image.Source is of type ImageSource, there is no quarantee that it will be a BitmapImage, it may be though. If the source is created by the XAML parser it will be a BitmapFrameDecode (which is an internal class). Anyway, the only save assignment is:
ImageSource source = img.Source;
otherwise you need to cast:
BitmapImage source = (BitmapImage)img.Source;
which will throw an exception if the Source is not of this type. So you can either save-cast or try-catch:
//(Possibly check for img.Source != null first)
BitmapImage source = img.Source as BitmapImage;
if (source != null)
{
//If img.Source is not null the cast worked.
}
try
{
BitmapImage source = (BitmapImage)img.Source;
//If this line is reached it worked.
}
catch (Exception)
{
//Cast failed
}
You could also check the type beforehand using img.SourceisBitmapImage.
How about using WriteableBitmap to make a copy of the image, and then using a MemoryStream to copy the original image into a copy?
// Create a WriteableBitmap from the Image control
WriteableBitmap bmp = new WriteableBitmap(imagetaginxaml, null);
// Load the contents of a MemoryStream from the WritableBitmap
MemoryStream m = new MemoryStream();
bmp.SaveJpeg(m, bmp.PixelWidth, bmp.PixelHeight, 0, 100);
// Read from the stream into a new BitmapImage object
m.Position = 0;
BitmapImage image = new BitmapImage();
image.SetSource(m);
// do something with the new BitmapImage object
// (for example, load another image control)
anotherimagetaginxaml.Source = image;

WPF: System.Windows.Interop.InteropBitmap to System.Drawing.Bitmap

Is there a way to convert System.Windows.Interop.InteropBitmap to System.Drawing.Bitmap?
Thank you
I was able to solve my problem using the code below. msg.ThumbnailSource contains System.Windows.Interop.InteropBitmap type of object
BitmapSource bmpSource = msg.ThumbnailSource as BitmapSource;
MemoryStream ms = new MemoryStream();
BitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmpSource));
encoder.Save(ms);
ms.Seek(0, SeekOrigin.Begin);
System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(ms);
You could take the pixels using CopyPixels and use the code from the Bitmap.Lock/Unlock documentation to pass the pixels to the Bitmap.

Binding Bitmapimge to Image in Wpf?

This is a simple Question (lets see)
I want to bind bitmap image to Image. For doing this in cs code u must write this line.
this.leftImage.Source = new BitmapImage(new Uri(#"C:\a.bmp"));
But I want make Binding from resources. Because In release time resources became part of project.exe file and if you make binding from file(Mean set Image.source with Image file address), you must always put image file in the same address(disaster programing) :)
One option is to get it from a resx file. You can do something similar to this. Assuming Images.resx contains a Left image bitmap.
leftImage.Source = ConvertBitmapToBitmapImage(Images.Left);
...
private BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap)
{
MemoryStream memoryStream = new MemoryStream();
bitmap.Save(memoryStream, ImageFormat.Png);
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = new MemoryStream(memoryStream.ToArray());
bitmapImage.EndInit();
return bitmapImage;
}
With some more work, you can do this from XAML too.

Missing images in FlowDocument saved as XPS document

I am having some difficulties getting images contained in a FlowDocument to show when the FlowDocument is saved as an XPS document.
Here is what I do:
Create an image using the Image control of WPF. I set the image source bracketed by calls to BeginInit/EndInit.
Add the image to the FlowDocument wrapping it in a BlockUIContainer.
Save the FlowDocument object to an XPS file using a modified version of this code.
If I then view the saved file in the XPS viewer, the image is not shown. The problem is that the images are not loaded until actually shown on the screen by WPF so they are not saved to the XPS file. Hence, there is a workaround: If I first show the document on screen using the FlowDocumentPageViewer and then save the XPS file afterwards, the image is loaded and shows up in the XPS file. This works even if the FlowDocumentPageViewer is hidden. But that gives me another challenge. Here is what I wish to do (in pseudocode):
void SaveDocument()
{
AddFlowDocumentToFlowDocumentPageViewer();
SaveFlowDocumentToXpsFile();
}
This of course does not work since the FlowDocumentPageViewer never gets a chance to show its contents before the document is saved to the XPS file. I tried wrapping SaveFlowDocumentToXpsFile in a call to Dispatcher.BeginInvoke but it did not help.
My questions are:
Can I somehow force the images to load before saving the XPS file without actually showing the document on screen? (I tried fiddling with BitmapImage.CreateOptions with no luck).
If there is no solution to question #1, is there a way to tell when FlowDocumentPageViewer has finished loading its contents so that I know when it is save to create the XPS file?
The eventual solution was the same as you came to, which is to put the document in a viewer and briefly show it on screen. Below is the helper method that I wrote to do this for me.
private static string ForceRenderFlowDocumentXaml =
#"<Window xmlns=""http://schemas.microsoft.com/netfx/2007/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
<FlowDocumentScrollViewer Name=""viewer""/>
</Window>";
public static void ForceRenderFlowDocument(FlowDocument document)
{
using (var reader = new XmlTextReader(new StringReader(ForceRenderFlowDocumentXaml)))
{
Window window = XamlReader.Load(reader) as Window;
FlowDocumentScrollViewer viewer = LogicalTreeHelper.FindLogicalNode(window, "viewer") as FlowDocumentScrollViewer;
viewer.Document = document;
// Show the window way off-screen
window.WindowStartupLocation = WindowStartupLocation.Manual;
window.Top = Int32.MaxValue;
window.Left = Int32.MaxValue;
window.ShowInTaskbar = false;
window.Show();
// Ensure that dispatcher has done the layout and render passes
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => {}));
viewer.Document = null;
window.Close();
}
}
Edit: I just added window.ShowInTaskbar = false to the method as if you were quick you could see the window appear in the taskbar.
The user will never "see" the window as it is positioned way off-screen at Int32.MaxValue - a trick that was common back in the day with early multimedia authoring (e.g. Macromedia/Adobe Director).
For people searching and finding this question, I can tell you that there is no other way to force the document to render.
HTH,
Couple things...
You sure the image is sized before its written? Usually you have to call Measure on the control so that it may size itself accordingly (infinity lets the control expand to its Width and Height)
image.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Also, sometimes you have to bump the UI thread so that everything gets updated in the control
Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>{}));
You do not have to display the document in order to have images saved into the xps. Are you calling commit on the XpsSerializationManager?
FlowDocument fd = new FlowDocument();
fd.Blocks.Add(new Paragraph(new Run("This is a test")));
string image = #"STRING_PATH";
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri(image, UriKind.RelativeOrAbsolute);
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.EndInit();
MemoryStream ms = new MemoryStream();
Package pkg = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
Uri pkgUri = bi.UriSource;
PackageStore.AddPackage(pkgUri, pkg);
Image img = new Image();
img.Source = bi;
BlockUIContainer blkContainer = new BlockUIContainer(img);
fd.Blocks.Add(blkContainer);
DocumentPaginator paginator = ((IDocumentPaginatorSource)fd).DocumentPaginator;
using (XpsDocument xps = new XpsDocument(#"STRING PATH WHERE TO SAVE FILE", FileAccess.ReadWrite, CompressionOption.Maximum))
{
using (XpsSerializationManager serializer = new XpsSerializationManager(new XpsPackagingPolicy(xps), false))
{
serializer.SaveAsXaml(paginator);
serializer.Commit();
}
}
I was able to address this by throwing the flowdocument into a viewer, and then do a measure/arrange.
FlowDocumentScrollViewer flowDocumentScrollViewer = new FlowDocumentScrollViewer();
flowDocumentScrollViewer.Document = flowDocument;
flowDocumentScrollViewer.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
flowDocumentScrollViewer.Arrange(new Rect(new Point(0, 0), new Point(Double.MaxValue, Double.MaxValue)));

Resources