I’m working on a Windows 8 app which has a drawing element to it. The original WP7 app in Silverlight was fine because a simple drawing rendered in XAML was captured using WritableBitmap.Render and written to a file stream. This feature is missing from the Windows 8 API, so it’s impossible to write a pure C#/XAML app which can export a user created image. To get round this it’s possible to use DirectX/XAML/C++ to create a XAML UI which can draw DirectX images.
DirectX/XAML/C++
offers a number of XAML containers which interface with DirectX and have
different behaviours:
I wanted to use a SurfaceImageSource, so I could have my DirectX image in line with
other XAML controls and containers.
The big problem
is that I don’t want to write a whole app in C++, it would take me a lot longer
than C# and it seemed like a sledge hammer to crack a nut using full
C++/DirectX for a simple application.
So after much research
and stress about what to do, I found SharpDX and thought all my prayers had
been answered! SharpDX is a full DirectX to C# interop library which allows
DirectX to be brought into C#/XAML apps in the same way as C++/XAML apps.
The major
problem was how to save the image you’ve drawn out of the DirectX image? There
are a small number of examples of how to do it in C++, so after a lot of cross referencing
the SharpDX source and tips from Alexandre Mutel on interfacing with a
WICStream, I managed to get it working.
This save
method was tested inside the EffectRenderer in the SharpDX lighting effect
sample. This is the whole code so you don’t need to paste it back together in
sections:
public async void
Save()
{
// Launch file
picker
Windows.Storage.Pickers.FileSavePicker
picker = new
Windows.Storage.Pickers.FileSavePicker();
picker.FileTypeChoices.Add("PNG", new
List<string>() { ".png"
});
var file =
await picker.PickSaveFileAsync();
if (file ==
null)
return;
// Get stream
var fStream
= await file.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
var factory
= new ImagingFactory();
MemoryStream ms = new
MemoryStream(100000);
ms.Position = 0;
// Create a WIC
outputstream
WICStream stream = new
WICStream(factory, ms);
var size = this._deviceManager.ContextDirect2D.PixelSize;
var format
= new SharpDX.Direct2D1.PixelFormat(SharpDX.DXGI.Format.B8G8R8A8_UNorm,
AlphaMode.Premultiplied);
// Initialize a Png
encoder with this stream
var
wicBitmapEncoder = new
SharpDX.WIC.PngBitmapEncoder(factory, BitmapEncoderGuids.Png);
wicBitmapEncoder.Initialize(stream);
// Create a Frame
encoder
var
wicFrameEncoder = new
BitmapFrameEncode(wicBitmapEncoder);
wicFrameEncoder.Initialize();
// Create image
encoder
ImageEncoder wicImageEncoder;
SharpDX.WIC.ImagingFactory2 factory2 = new ImagingFactory2();
factory2.CreateImageEncoder(this._deviceManager.DeviceDirect2D, out wicImageEncoder);
var
imgParams = new ImageParameters();
imgParams.PixelFormat = format;
imgParams.PixelHeight = (int)size.Height;
imgParams.PixelWidth = (int)size.Width;
wicImageEncoder.WriteFrame(_effectGraph.Output, wicFrameEncoder, ref imgParams);
// Commit changes
wicFrameEncoder.Commit();
wicBitmapEncoder.Commit();
byte[]
buffer = new byte[ms.Length];
ms.Position = 0;
await ms.ReadAsync(buffer, 0, (int)ms.Length);
var
opStream = fStream.AsStreamForWrite();
await opStream.WriteAsync(buffer, 0,
buffer.Length);
await opStream.FlushAsync();
// Dispose
wicFrameEncoder.Dispose();
wicBitmapEncoder.Dispose();
stream.Dispose();
ms.Dispose();
opStream.Dispose();
factory.Dispose();
}
The first section gets a file stream from the user to write
the data to once it’s been extracted from the image, then creates an
intermediary MemoryStream for the WICStream to interface with (Alexandre pointed out that
the Win8 RandomAccessStream will not interface with a WICStream since it needs
to perform seek operations) and finally the WICStream itself which WIC will use
to write the image to.
The
second section creates a Bitmap Encoder, Frame Encoder and Image Encoder and
writes the image frame into the stream.
The
final section and the bit which caused me a lot of problems, is getting the
data from the stream. The encoders all need committing in order, but in many
C++ examples the WICStream is also committed which should flush the stream to
it’s destination, however it was throwing an exception:
An exception of type 'SharpDX.SharpDXException'
occurred in SharpDX.DLL but was not handled in user code
Additional information: HRESULT: [0x88982F50], Module: [Unknown], ApiCode: [Unknown/Unknown], Message: The component cannot be found.
If there is a handler for this exception, the program may be safely continued.
Additional information: HRESULT: [0x88982F50], Module: [Unknown], ApiCode: [Unknown/Unknown], Message: The component cannot be found.
If there is a handler for this exception, the program may be safely continued.
This isn’t particularly helpful, but I was advised that the WIC
stream doesn’t need flushing as the underlying stream should take care of this.
At this point I could see the memory stream had a size, but when I wrote it to
a buffer it was coming out all zeros. This was a simple mistake – the memory stream
just needs setting back to the start and it all started magically working!
All I need to do now is work out how to use DirectX to draw
my picture! I hope this helps some other people as it’s been causing me a
massive headache for a number of days!
Hi Geoff,
ReplyDeleteThis is really great. I was looking for this solution for a long time.
Can you maybe help me with using this technique to save an effectRenderer object. It is used in the D2DEffectsHelloWorld Win8 sample.
Thanks,
Fons
Hi Fons,
ReplyDeleteThanks, I'll try and take a look tonight. I'm still getting my head around this subject!
Geoff
Hi Geoff,
ReplyDeleteThanks. My EffectRender has two bitmaps (Direct2D1.Effects) in which the second one (which is partly transparent) is rendered (drawn) on top of the first one. I want to save the result of this Render() must be saved to a file.
If you want I can send you my project.
Thanks,
Fons
Do you have an effect graph in the renderer? I used the _effectGraph.Output as the image. I can take a quick look, but I'm finishing a project for Friday and have another one to do (the DirectX one).
ReplyDeleteI too face similar issues in my project. For the last two months i was trying to create an image of my canvas. But i failed. Now it seems you found a promising logic to achieve the same but i didn't understand completely the code you provided. Where i can pass my UI control for taking the snapshot. what is this "_deviceManager". and for what it is used for. Can you provide a working sample. Then it would be helpful for me to implement my secondary tile. functionality with a snapshot of my control.
ReplyDeleteIt's not that straight forward, you need to create your drawing in directX not xaml. Then you can export.
ReplyDeleteLook at the SharpDX samples for how to do this.
Hey... I tried out your code and the png file that gets created has a security attriubute set (right click on the image -> properties) which states "this file came from another computer and might be blocked..." and you have to unblock it to be able to write to it again.
ReplyDeleteSounds strange. Is it saved to local disc?
ReplyDeleteYes, it's saved to local disk, just has that annoying security attribute set. Do you see the same behavior when you run it?
ReplyDeleteYes, I'm seeing the same thing, don't know why though
ReplyDeleteGreat. Just great
ReplyDeletePlease, can you explain why can't I find "factory2.CreateImageEncoder" in my sharpdx sources?
ReplyDeleteand what is _effectGraph?
I'm hardly new to sharpdx, sorry
I just want to save to local storage my SurfaceImageSource contents.. but I can't :(
Check out part 2 (link at top) you need to use WIC which is a different API to DirectX but is still included in SharpDX
ReplyDeleteI'm developing an App that has a Canvas element on XAML, and I drop some images into it. How can I use your code to get the children from the Canvas and save to image?
ReplyDeleteThanks
The short answer use you can't. You need to use a direct2d SurfaceImageSource instead of canvas to do your rendering, then use the same rendering routine to draw to an image as described in the article.
ReplyDeleteHad the same problem, could not find factory2.CreateImageEncoder method, I am using SharpDX.WIC in my assembly references but still doesn't work.
ReplyDeleteLooks like this method has been deprecated and ImageEncoder has a constructor with factory parameter. Check out change notes: http://code.google.com/p/sharpdx/source/browse/Source/SharpDX.Direct2D1/WIC/ImageEncoder.cs?r=453bb00a589e20a3c141b5718838febbfc842e5e
ReplyDelete