The Winforms System.Windows.Forms.Control class has an instance method "DrawToBitmap" which I think is very useful in a variety of circumstances. I'm wondering if there's an equivalent way of getting a System.Drawing.Bitmap from a WPF application?
I realize I could do some P/Invoke stuff to just get the application window, however I don't like this because it doesn't accomodate the 64bit transition very well, and doesn't let me render sub-controls only, as DrawToBitmap does.
Thanks,
Richard
Use RenderTargetBitmap as on MSDN
RenderTargetBitmap bitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(this.YourVisualControlNameGoesHere);
TFD is spot on.
You could also use the less elegant reference example from MSDN:
Dim width As Integer = 128
Dim height As Integer = width
Dim stride As Integer = CType(width / 8, Integer)
Dim pixels(height * stride) As Byte
' Try creating a new image with a custom palette.
Dim colors As New List(Of System.Windows.Media.Color)()
colors.Add(System.Windows.Media.Colors.Red)
colors.Add(System.Windows.Media.Colors.Blue)
colors.Add(System.Windows.Media.Colors.Green)
Dim myPalette As New BitmapPalette(Colors)
' Creates a new empty image with the pre-defined palette
Dim image As BitmapSource = System.Windows.Media.Imaging.BitmapSource.Create(width, height, 96, 96, PixelFormats.Indexed1, myPalette, pixels, stride)
Dim stream As New FileStream("new.bmp", FileMode.Create)
Dim encoder As New BmpBitmapEncoder()
Dim myTextBlock As New TextBlock()
myTextBlock.Text = "Codec Author is: " + encoder.CodecInfo.Author.ToString()
encoder.Frames.Add(BitmapFrame.Create(image))
encoder.Save(stream)
Related
I have some images that I'am trying to print out. Those images can come in varying format, from different DPI's to different formats (JPEG, PNG, etc.)
Now what I've done for now, is to load the image into my application and try and
convert the dpi to say 96. However in this process i get an OutOfMemoryException, and I'm not sure how to continue.
Private Sub PrintImage(Optional providedPrintDialog As PrintDialog = Nothing)
Dim objPrintDialog As PrintDialog
If providedPrintDialog IsNot Nothing Then
objPrintDialog = providedPrintDialog
Else
objPrintDialog = New PrintDialog()
End If
Dim myPanel As New StackPanel
myPanel.Margin = New Thickness(15)
Dim myImage As New Controls.Image
Dim tempBitmapImage = ConvertBitmapToXDPI(Me.SelectedFileViewModel.File.GetPath, 96)
Dim tempBitmapImageWidth As Integer = CInt(objPrintDialog.PrintableAreaWidth)
' A4 max width = 793
If tempBitmapImage.Width > tempBitmapImageWidth Then
myImage.Stretch = System.Windows.Media.Stretch.Uniform
Else
myImage.Stretch = System.Windows.Media.Stretch.None
End If
myImage.Source = tempBitmapImage
myPanel.Children.Add(myImage)
myPanel.Measure(New System.Windows.Size(objPrintDialog.PrintableAreaWidth, objPrintDialog.PrintableAreaHeight))
myPanel.Arrange(New Rect(New System.Windows.Point(0, 0), myPanel.DesiredSize))
objPrintDialog.PrintVisual(myPanel, "Billede") ' <- OutOfMemoryException thrown here
End Sub
Private Function ConvertBitmapToXDPI(path As String, newDpi As Integer) As BitmapSource
Using bitmap As Bitmap = DirectCast(System.Drawing.Image.FromFile(path), Bitmap)
Dim bitmapData = bitmap.LockBits(New System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.[ReadOnly], bitmap.PixelFormat)
Dim bmSource = BitmapSource.Create(
bitmapData.Width,
bitmapData.Height, 96, 96, PixelFormats.Bgr24, Nothing,
bitmapData.Scan0,
bitmapData.Stride * bitmapData.Height,
bitmapData.Stride)
bitmap.UnlockBits(bitmapData)
Return bmSource
End Using
End Function
There is no need to do any DPI conversion. Just create a DrawingVisual and draw a BitmapImage into it with an appropriate size:
Dim image As New BitmapImage()
image.BeginInit()
image.CacheOption = BitmapCacheOption.OnLoad
image.UriSource = New Uri(path)
image.EndInit()
image.Freeze()
Dim size As New Size()
If image.Width < printDialog.PrintableAreaWidth Then
size.Width = image.Width
size.Height = image.Height
Else
size.Width = printDialog.PrintableAreaWidth
size.Height = size.Width / image.Width * image.Height
End If
Dim visual As New DrawingVisual()
Using dc As DrawingContext = visual.RenderOpen()
dc.DrawImage(image, New Rect(size))
End Using
printDialog.PrintVisual(visual, "Billede")
I'm trying to resize and save the 3 images as defined in the Page_load event.
Within method ResizeAndSave I have 2 methods I'm trying: FastResize and SlowResize.
When I uncomment the FastResize codeline: IMAGE 1 and 2 are saved and resized correctly. IMAGE 3 however, is saved in dimensions 625x441px and so does not respect the 200x200 box I want it to resize to.
When I instead use the SlowResize codeline: IMAGE 1 and 2 are again saved and resized correctly. IMAGE 3 however, is not saved at all.
No errors are thrown in my code. I will import images from a variety of sources so it's critical my code works on a wide range of image formats. And apparently there's something special about IMAGE 3 and I don't know what it is or how to handle it.
Here's my full code, you should be able to just copy/paste it and test it for yourself:
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Xml
Imports System.Data.SqlClient
Imports System.Net
Imports System.Windows.Media.Imaging
Imports System.Windows.Media
Partial Class importfeeds
Inherits System.Web.UI.Page
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
'IMAGE 1
ResizeAndSave(200, 200, "https://upload.wikimedia.org/wikipedia/commons/8/82/Dell_Logo.png")
'IMAGE 2
ResizeAndSave(200, 200, "https://upload.wikimedia.org/wikipedia/commons/d/d8/Square-1_solved.jpg")
'IMAGE 3
ResizeAndSave(200, 200, "http://cdn2.emobassets.eu/media/catalog/product/1/1/1116220.jpg")
End Sub
Private Sub ResizeAndSave(ByVal maxWidth As Integer, ByVal maxHeight As Integer, ByVal imageURL As String)
Dim imgRequest As WebRequest = WebRequest.Create(imageURL)
Dim imgResponse As WebResponse = imgRequest.GetResponse()
Dim streamPhoto As Stream = imgResponse.GetResponseStream()
Dim memStream As New MemoryStream
streamPhoto.CopyTo(memStream)
memStream.Position = 0
Dim bfPhoto As BitmapFrame = ReadBitmapFrame(memStream)
Dim newWidth, newHeight As Integer
Dim scaleFactor As Double
If bfPhoto.Width > maxWidth Or bfPhoto.Height > maxHeight Then
If bfPhoto.Width > maxWidth Then
scaleFactor = maxWidth / bfPhoto.Width
newWidth = Math.Round(bfPhoto.Width * scaleFactor, 0)
newHeight = Math.Round(bfPhoto.Height * scaleFactor, 0)
End If
If newHeight > maxHeight Then
scaleFactor = maxHeight / newHeight
newWidth = Math.Round(newWidth * scaleFactor, 0)
newHeight = Math.Round(newHeight * scaleFactor, 0)
End If
End If
Dim bfResize As BitmapFrame = FastResize(bfPhoto, newWidth, newHeight)
'Dim bfResize As BitmapFrame = SlowResize(bfPhoto, newWidth, newHeight, BitmapScalingMode.Linear)
Dim baResize As Byte() = ToByteArray(bfResize)
Dim strThumbnail As String = Guid.NewGuid.ToString() + ".png"
Dim saveToPath As String = Server.MapPath(ConfigurationManager.AppSettings("products_photospath")) + "\49\" + strThumbnail
File.WriteAllBytes(saveToPath, baResize)
End Sub
Private Shared Function FastResize(bfPhoto As BitmapFrame, nWidth As Integer, nHeight As Integer) As BitmapFrame
Dim tbBitmap As New TransformedBitmap(bfPhoto, New ScaleTransform(nWidth / bfPhoto.PixelWidth, nHeight / bfPhoto.PixelHeight, 0, 0))
Return BitmapFrame.Create(tbBitmap)
End Function
'http://weblogs.asp.net/bleroy/resizing-images-from-the-server-using-wpf-wic-instead-of-gdi
Public Shared Function SlowResize(photo As BitmapFrame, width As Integer, height As Integer, scalingMode As BitmapScalingMode) As BitmapFrame
Dim group = New DrawingGroup()
RenderOptions.SetBitmapScalingMode(group, scalingMode)
group.Children.Add(New ImageDrawing(photo, New Windows.Rect(0, 0, width, height)))
Dim targetVisual = New DrawingVisual()
Dim targetContext = targetVisual.RenderOpen()
targetContext.DrawDrawing(group)
Dim target = New RenderTargetBitmap(width, height, 96, 96, PixelFormats.[Default])
targetContext.Close()
target.Render(targetVisual)
Dim targetFrame = BitmapFrame.Create(target)
Return targetFrame
End Function
Private Shared Function ToByteArray(bfResize As BitmapFrame) As Byte()
Using msStream As New MemoryStream()
Dim pbdDecoder As New PngBitmapEncoder()
pbdDecoder.Frames.Add(bfResize)
pbdDecoder.Save(msStream)
Return msStream.ToArray()
End Using
End Function
Private Shared Function ReadBitmapFrame(streamPhoto As Stream) As BitmapFrame
Dim bdDecoder As BitmapDecoder = BitmapDecoder.Create(streamPhoto, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None)
Return bdDecoder.Frames(0)
End Function
End Class
UPDATE 1
#Hans Passant: Both your suggestions on filenaming and pixelWidth usage are spot on and helped me run this code successfully on the 3 images in the Page_load event.
I updated my original code.
However, when I run this code as part of my actual application, where I import ~100 images from a feed. The new code fails with an out of memory exception when it tries to process IMAGE 3. This happens for both FastResize and SlowResize methods. Is there something in my code or in the image in question that would cause this increase in memory usage, maybe a leak somewhere or an inefficient resizing method I use?
I have a lot of memory available on my machine, so would be very surprised if that were the problem, although I do see a big increase in the System and compressed memory (to 1.1GB) task in my Windows task manager. And still, this much memory usage would have me believe that there's something wrong in my code.
What can it be?
IMAGE 3 however, is saved in dimensions 625x441px
That is because the image is slightly different from the other ones, its DPI (dots per inch) is 300 instead of 96. Its size in pixels is 3071 x 2172 but you are using the Width and Height properties, the size in inches with a unit of 1/96" which is 982.72 x 695.04 for this image. Fix this by using the PixelWidth and PixelHeight properties instead:
Dim tbBitmap As New TransformedBitmap(bfPhoto,
New ScaleTransform(nWidth / bfPhoto.PixelWidth, nHeight / bfPhoto.PixelHeight, 0, 0))
IMAGE 3 however, is not saved at all
That doesn't add up completely, but you do have a critical bug in this statement:
Dim strThumbnail As String = "success" + Date.Now.Second.ToString + ".png"
This name is not sufficiently unique to ensure that you don't overwrite an existing file. And if the code is "fast" then Date.Now.Second will have the same value and your code overwrites a previous written image file. Note how this bug won't repro when you debug, that makes the code artificially slower and the second will be different.
You'll need a better way to name the file, Guid.NewGuid.ToString() is a very good way for example, guaranteed to be unique. Or use a simple counter that you increment for each image. You do need to focus on cleanup.
When inside the tool I want to text included in the image between the place where I want to speak, as the following picture:
enter image description here
I tried the following code but the photo does not show the speech, but end of the sentence appears:
Dim para As New Paragraph()
Dim bitmap As New BitmapImage(New Uri("D:\Happy.png"))
Dim image As New Image()
image.Source = bitmap
image.Width = 20
para.Inlines.Add(image)
RTB.Document.Blocks.Add(para)
See this link for examples Inline Images or Other Elements.
'A RichTextBox with an image.
Private Sub ImageRTB()
'Create a new RichTextBox.
Dim MyRTB As New RichTextBox()
' Create a Run of plain text and image.
Dim myRun As New Run()
myRun.Text = "Displaying text with inline image"
Dim MyImage As New Image()
MyImage.Source = New BitmapImage(New Uri("flower.jpg", UriKind.RelativeOrAbsolute))
MyImage.Height = 50
MyImage.Width = 50
Dim MyUI As New InlineUIContainer()
MyUI.Child = MyImage
' Create a paragraph and add the paragraph to the RichTextBox.
Dim myParagraph As New Paragraph()
MyRTB.Blocks.Add(myParagraph)
' Add the Run and image to it.
myParagraph.Inlines.Add(myRun)
myParagraph.Inlines.Add(MyUI)
'Add the RichTextBox to the StackPanel.
MySP.Children.Add(MyRTB)
End Sub
The solution found and this is the code:
Dim tp As TextPointer = rtb.CaretPosition.GetInsertionPosition(LogicalDirection.Forward)
Dim bm As New BitmapImage()
bm.BeginInit()
bm.UriSource = New Uri("Happy.png", UriKind.Relative)
bm.CacheOption = BitmapCacheOption.OnLoad
bm.EndInit()
Dim img As New Image()
img.Source = bm
img.Width = 20
img.Height = 20
img.Stretch = Stretch.Fill
Dim container As New InlineUIContainer(img, tp)
thank you :)
I'd like to have a way to export the graphical contents of a FlowLayoutPanel to a file (don't mind what format, bmp is probably the easiest). I'd also like it to scroll the contents so that the exported file contains the entire contents of the Panel.
Is there any way to do this? I'm using C# WinForms and Framework 4.
Try looking into xml serialization
You could serialize the panel and save the xml. and load the xml and deserialize it back into the panel
Also check this out.
To save as an image you just do this:
Bitmap image = new Bitmap(flowLayoutPanel1.Width, flowLayoutPanel1.Height);
flowLayoutPanel1.DrawToBitmap(image, new Rectangle(0, 0, flowLayoutPanel1.Width, flowLayoutPanel1.Height));
image.Save("SAVE PATH");
The trick is to temporaly set the flowLayoutPanel as large as to fit all the controls within, even if it gets too large for the visible screen, then do the DrawToBitmap using the flowLayoutPanel.clientRectangle area, not the .Width and .Height.
In my example, Outside_Splitter is docked to the form, with two panels, and fraAction is a groupbox and is the last control on the panel, that is vertically scrolled.
Public Sub Print_Panel()
Dim newHeight As Integer
Dim pos As Point, oheight As Integer, owidth As Integer, xDock As DockStyle
With Outside_Splitter ' This contains the two panel ...
pos.X = .Left ' Store original position and size
pos.Y = .Top
oheight = .Height
owidth = .Width
xDock = .Dock ' get original dock set
newHeight = FraAction.Top + FraAction.Height + 30 ' calculate new height based on position and size of the last control
.Dock = DockStyle.None ' undock it
.Height = newHeight ' set new height
.Refresh()
.SetBounds(pos.X, pos.Y, owidth, newHeight) ' Set position and size, temporarily
.Refresh()
End With
'Create Bitmap based on panel.ClientRectangle
Dim myBmp As New Bitmap(Painel_Detalhe_NC.ClientRectangle.Width, Painel_Detalhe_NC.ClientRectangle.Height)
'Paint the bitmap
Painel_Detalhe_NC.DrawToBitmap(myBmp, Painel_Detalhe_NC.ClientRectangle)
'Create pdf
Dim _pdf As New C1.C1Pdf.C1PdfDocument
_pdf.Clear()
_pdf.Landscape = False
_pdf.PaperKind = PaperKind.A4
Dim rec As New RectangleF ' Set 5% margin around the page
rec = _pdf.PageRectangle
rec.X = 0.05 * rec.Width
rec.Y = 0.05 * rec.Height
rec.Width = 0.9 * _pdf.PageRectangle.Width
rec.Height = 0.9 * _pdf.PageRectangle.Height
_pdf.DrawImage(myBmp, rec) ' paint/resize bitmap to that size on the pdf
'Save it and show it
_pdf.Save(My.Computer.FileSystem.SpecialDirectories.Temp & "\temp.pdf")
Process.Start(My.Computer.FileSystem.SpecialDirectories.Temp & "\temp.pdf")
myBmp.Dispose() ' Clear it
With Outside_Splitter ' put it back to where it was
.Left = pos.X
.Top = pos.Y
.Dock = xDock ' Back to filling the form
.Refresh()
End With
End Sub
Is there a way to change the palette of a BitmapSource in WPF? (at runtime)
Every BitmapSource hierarchy that I try has read-only access to palette.
I have not found a way to do it in a nice way, but you can always recreate a bitmap from the original one:
If you have:
Private myBitmap As System.Windows.Media.Imaging.WriteableBitmap
Private myCurrentPalette As List(Of Color)
Then when you want to change your palette do this:
Dim iWidth As Integer = myBitmap.Width
Dim iHeight As Integer = myBitmap.Height
Dim iPixel(iWidth * iHeight - 1) As Byte
myBitmap.CopyPixels(iPixel, iWidth, 0)
myBitmap = New WriteableBitmap(iWidth, iHeight, 96, 96, PixelFormats.Indexed8, New BitmapPalette(myCurrentPalette))
myBitmap.WritePixels(New Int32Rect(0, 0, iWidth, iHeight), iPixel, iWidth, 0)
Then you need to apply:
Image1.Source = myBitmap