views:

2977

answers:

3

Hi everyone,

I've got a richtextbox, that I plan on saving to a database, which can be loaded back into the same richtextbox. I've got it working so that I can save the flowdocument as DataFormats.XamlPackage, which saves the images, but the issue is that the text isn't searchable. With DataFormats.Xaml, I've got the text of course, but no images. The images will be pasted in by the end user, not images included with the application.

I tried using XamlWriter to get the text into XML, and then grab the images from the document separately and insert them as binary into the XML, but I can't seem to find a way to get the images to binary...

Does anyone have ideas on how to get the images into binary, separate from the text?

Thanks in advance!

GetImageByteArray() is where the issue is.

Code:

private void SaveXML()
{
            TextRange documentTextRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
            FlowDocument flowDocument = richTextBox.Document;
using (StringWriter stringwriter = new StringWriter())
                {
                    using (System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(stringwriter))
                    {
                        XamlWriter.Save(flowDocument, writer );
                    }

                    testRTF t = new testRTF();
                    t.RtfText = new byte[0];
                    t.RtfXML = GetImagesXML(flowDocument);
                    t.RtfFullText = stringwriter.ToString();
                    //save t to database
                }
                richTextBox.Document.Blocks.Clear();
}


private string GetImagesXML(FlowDocument flowDocument)
        {

            using (StringWriter stringwriter = new StringWriter())
            {
                using (System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(stringwriter))
                {

                    Type inlineType;
                    InlineUIContainer uic;
                    System.Windows.Controls.Image replacementImage;
                    byte[] bytes;
                    System.Text.ASCIIEncoding enc;

                    //loop through replacing images in the flowdoc with the byte versions
                    foreach (Block b in flowDocument.Blocks)
                    {
                        foreach (Inline i in ((Paragraph)b).Inlines)
                        {
                            inlineType = i.GetType();

                            if (inlineType == typeof(Run))
                            {
                                //The inline is TEXT!!!
                            }
                            else if (inlineType == typeof(InlineUIContainer))
                            {
                                //The inline has an object, likely an IMAGE!!!
                                uic = ((InlineUIContainer)i);

                                //if it is an image
                                if (uic.Child.GetType() == typeof(System.Windows.Controls.Image))
                                {
                                    //grab the image
                                    replacementImage = (System.Windows.Controls.Image)uic.Child;

                                    //get its byte array
                                    bytes = GetImageByteArray((BitmapImage)replacementImage.Source);
                                    //write the element
                                    writer.WriteStartElement("Image");
                                    //put the bytes into the tag
                                    enc = new System.Text.ASCIIEncoding();
                                    writer.WriteString(enc.GetString(bytes));
                                    //close the element
                                    writer.WriteEndElement();
                                }
                            }
                        }
                    }
                }

                return stringwriter.ToString();
            }
        }


//This function is where the problem is, i need a way to get the byte array
        private byte[] GetImageByteArray(BitmapImage bi)
        {
            byte[] result = new byte[0];
                    using (MemoryStream ms = new MemoryStream())
                    {
                        XamlWriter.Save(bi, ms);
                        //result = new byte[ms.Length];
                        result = ms.ToArray();
                    }
            return result;
}

UPDATE

I think I may have finally found a solution, which I will post below. It uses BmpBitmapEncoder and BmpBitmapDecoder. This allows me to get binary from the bitmap image, store it to the database, and load it back up and display it right back into the FlowDocument. Initial tests have proven successful. For testing purposes I'm bypassing my database step and basically duplicating the image by creating binary, then taking the binary and turning it into a new image and adding it to the FlowDocument. The only issue is that when I try and take the modified FlowDocument and use the XamlWriter.Save function, it errors on the newly created Image with "Cannot serialize a non-public type 'System.Windows.Media.Imaging.BitmapFrameDecode". This will take some further investigation. I'll have to leave it alone for now though.

private void SaveXML()
        {
            TextRange documentTextRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
            FlowDocument flowDocument = richTextBox.Document;

            string s = GetImagesXML(flowDocument);//temp
            LoadImagesIntoXML(s);

                using (StringWriter stringwriter = new StringWriter())
                {
                    using (System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(stringwriter))
                    {
                        XamlWriter.Save(flowDocument, writer );//Throws error here
                    }

                }
}

private string GetImagesXML(FlowDocument flowDocument)
        {
            string s= "";

            using (StringWriter stringwriter = new StringWriter())
            {


                    Type inlineType;
                    InlineUIContainer uic;
                    System.Windows.Controls.Image replacementImage;
                    byte[] bytes;
                    BitmapImage bi;

                    //loop through replacing images in the flowdoc with the byte versions
                    foreach (Block b in flowDocument.Blocks)
                    {
                        foreach (Inline i in ((Paragraph)b).Inlines)
                        {
                            inlineType = i.GetType();

                            if (inlineType == typeof(Run))
                            {
                                //The inline is TEXT!!!
                            }
                            else if (inlineType == typeof(InlineUIContainer))
                            {
                                //The inline has an object, likely an IMAGE!!!
                                uic = ((InlineUIContainer)i);

                                //if it is an image
                                if (uic.Child.GetType() == typeof(System.Windows.Controls.Image))
                                {
                                    //grab the image
                                    replacementImage = (System.Windows.Controls.Image)uic.Child;
                                    bi = (BitmapImage)replacementImage.Source;

                                    //get its byte array
                                    bytes = GetImageByteArray(bi);

                                    s = Convert.ToBase64String(bytes);//temp
                                }
                            }
                        }
                    }

                return s;
            }
        }

private byte[] GetImageByteArray(BitmapImage src)
        {
                MemoryStream stream = new MemoryStream();
                BmpBitmapEncoder encoder = new BmpBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create((BitmapSource)src));
                encoder.Save(stream);
                stream.Flush();
            return stream.ToArray();
        }


private void LoadImagesIntoXML(string xml)
        {


            byte[] imageArr = Convert.FromBase64String(xml);
System.Windows.Controls.Image img = new System.Windows.Controls.Image()

MemoryStream stream = new MemoryStream(imageArr);
            BmpBitmapDecoder decoder = new BmpBitmapDecoder(stream, BitmapCreateOptions.None, BitmapCacheOption.Default);
            img.Source = decoder.Frames[0];
            img.Stretch = Stretch.None;

Paragraph p = new Paragraph();
            p.Inlines.Add(img);
            richTextBox.Document.Blocks.Add(p);
        }
A: 

Save your image to a MemoryStream and write that stream to your XML file.

The memory stream will convert it to an Byte[].

Tony
Thanks for the reply Tony. The problem is that the image is a System.Windows.Controls.Image object. I kept moving down the hierarchy but couldn't find in what child object the bytes are stored. I tried saving the BitmapImage object to the memory stream, but all I get back is the xaml tag. using (MemoryStream ms = new MemoryStream()) { XamlWriter.Save(bi, ms); result = ms.ToArray(); }
sub-jp
Result:<BitmapImage BaseUri="pack://payload:,,wpf2,/Xaml/Document.xaml" UriSource="./Image1.bmp" CacheOption="OnLoad" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
sub-jp
can you post the complete code?
Tony
So you just want a binary encoding of your image file in your XML or XAML file?
Tony
I need the text in xaml format (saving it to xml) and the images converted to binary and inserted back into the xml. Something like this:Original Flowdocument:<FlowDocument PagePadding="5,0,5,0" AllowDrop="True"><Paragraph FontFamily="Microsoft Sans Serif" FontSize="12">asdf</Paragraph><BlockUIContainer TextAlignment="Justify"><Image Width="214" Height="172"><Image.Source><BitmapImage BaseUri="pack://payload:,,wpf2,/Xaml/Document.xaml" UriSource="./Image1.bmp" CacheOption="OnLoad" /></Image.Source></Image></BlockUIContainer></FlowDocument>
sub-jp
I need to then take that original flowdocument and save it as xml in the database, replacing the image tag and its children with my own image tag, which would contain the actual binary data for the image (which this does not)
sub-jp
to get the bytes of the actual image you will have to grab the UriSource from the BitmapImage you have there and then load that into a FileStream, then give that stream to your MemoryStream, although i am not sure what you want to do with those bytes once you have them...
David Rogers
Thanks David, I tried doing that as well. In the BitmapImage object there is BaseUri and UriSource. I tried using both of them. BaseUri is "Xaml\Document.xaml" and UriSource is "./Image1.bmp" (which is probably the one I want, but it's trying to look for the image in the bin\debug\ folder of my project. Since this is an image pasted by an end user, I'm not sure how to open it in a filestream. using (FileStream fs = new FileStream(bi.UriSource.ToString(), FileMode.Open)) { }
sub-jp
why don't you use a hyperlink to represent your images, instead of their 'byte' versions? Unless you need to send them over the network or something. I don't see why you'd want to put them inside the XML file though... what's the reason for that?
Tony
I want to be able to put the bytes into the XML doc that I created from the Xaml text, then save that XML to the database in one column. The text should be searchable and the images saved in binary.
sub-jp
Tony, this is for an app that runs in disconnected mode, and saves data locally, but when connected, syncs data up with the cloud. The images a user pastes into a richtextbox needs to be saved locally, then synced up to the cloud. They also can be synced down from other users. There will be thousands of users creating many documents from the richtextbox. The images will need to retain their place in the document where the user pasted them. Placing the binary into the xml would achieve that while keeping image storage low and text searchable.
sub-jp
well my other suggestion then would be to try the BitmapImage.CopyPixels method which would copy all the bitmap info into an array, which you could then create a MemoryStream from...
David Rogers
also i just noticed that you need to combine the baseUri with the UriSource to find the actual image file you want, check out the Path type in System.IO
David Rogers
I'm not sure I follow, combine baseUri and UriSource? BaseUri is "{pack://payload:,,wpf2,/Xaml/Document.xaml}". UriSource is "{./Image1.bmp}". If I look at the BitmapImage, putting a watch on it gives me "{pack://payload:,,wpf2,/Xaml/Image1.bmp}" which is a value that drills down all the way to a dispatcher object. I couldn't find anything on that "payload" in the URI. msdn has no mention of "payload"http://msdn.microsoft.com/en-us/library/aa970069.aspx
sub-jp
A: 

Here is the sample code for both of my suggestions that i have made already, ill have to look into the payload issue if my examples dont work...

     // get raw bytes from BitmapImage using BaseUri and SourceUri
 private byte[] GetImageByteArray(BitmapImage bi)
 {
  byte[] result = new byte[0];
  string strImagePath = Path.Combine(Path.GetDirectoryName(bi.BaseUri.OriginalString), bi.UriSource.OriginalString);
  byte[] fileBuffer;
  using (FileStream fileStream = new FileStream(strImagePath, FileMode.Open))
  {
   fileBuffer = new byte[fileStream.Length];
   fileStream.Write(fileBuffer, 0, (int)fileStream.Length);
  }
  using (MemoryStream ms = new MemoryStream(fileBuffer))
  {
   XamlWriter.Save(bi, ms);
   //result = new byte[ms.Length];
   result = ms.ToArray();
  }
  return result;
 }
 // get raw bytes from BitmapImage using BitmapImage.CopyPixels
 private byte[] GetImageByteArray(BitmapSource bi)
 {
  int rawStride = (bi.PixelWidth * bi.Format.BitsPerPixel + 7) / 8;
  byte[] result = new byte[rawStride * bi.PixelHeight];
  bi.CopyPixels(result, rawStride, 0);
  return result;
 }
 private BitmapSource GetImageFromByteArray(byte[] pixelInfo, int height, int width)
 {
  PixelFormat pf = PixelFormats.Bgr32;
  int stride = (width * pf.BitsPerPixel + 7) / 8;
  BitmapSource image = BitmapSource.Create(width, height, 96, 96, pf, null, pixelInfo, stride);
  return image;
 }
David Rogers
The first one doesn't seem to work. The BaseUri is "pack://payload:,,wpf2,/Xaml/Document.xaml" and the UriSource is "./Image1.bmp". Combining them results in "pack:\payload:,,wpf2,\Xaml\./Image1.bmp". I tried a mixture of removing the dot in the Image1.bmp path. Not even sure where that dot is supposed to be pointing to.The second solution looks like it starts to work, but then when the byte array comes back, when i try to add it to the XML, I try ascii encoding it and i get this error:'.', hexadecimal value 0x00, is an invalid character.(see my GetImagesXML() in the question)
sub-jp
I'm not too familiar with the stride property. I know its the width of the image in bytes, but can you briefly explain the math going on in that line, where does "+ 7" come from? Thanks
sub-jp
the error that is popping up is because you are using ASCII encoding to convert the byte array into its ASCII form, try using Convert.ToBase64String(/*byte[]*/) to convert the byte array to a base64 string
David Rogers
did this help solve your problem?
David Rogers
I wasn't able to look at it for very long yesterday. Going to and from the byte array seems to work, but when I try plugging the byte array into a memory stream and either assigning that stream to a new bitmapImage or using BitmapFrame.Create(stream, ...) to create a BitmapFrame for teh Image.Source, I get this error, which I'll be looking into today. Thanks for your help."No imaging component suitable to complete this operation was found."
sub-jp
it happens here in bi.EndInit():byte[] imageArr = Convert.FromBase64String(xml); BitmapImage bi = new BitmapImage(); bi.BeginInit(); bi.CreateOptions = BitmapCreateOptions.None; bi.CacheOption = BitmapCacheOption.Default; bi.StreamSource = new MemoryStream(imageArr,0,imageArr.Length); bi.EndInit();
sub-jp
and the same one inside the using:System.Windows.Controls.Image img = new System.Windows.Controls.Image(); //Image control of wpf using (MemoryStream stream = new MemoryStream(imageArr)) { img.Source = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); }
sub-jp
you need to use the Create method on ImageSource to get your image, see http://msdn.microsoft.com/en-us/library/system.windows.media.imaging.bitmapsource(VS.85).aspx for an example, i also updated my code with a new method that should do what you need, you might have to store the original image dimensions in your xml, im thinking you might just add a height/width attribute on the element that store the image array...
David Rogers
A: 

Good news. I had to work on something else for a while, but this allowed me to come back with a fresh pair of eyes. I quickly realized that I could just combine what I knew was working. I doubt this solution will win any awards, but it works. I know that I can wrap a FlowDocument up as text using the XamlReader, keeping the image elements but losing image data. I also knew that I can turn a FlowDocument into binary using XamlFormat. So I had the idea of taking the FlowDocument, and using a function I already wrote to iterate through it to find the images, I take each image, basically clone it and put the clone into a new FlowDocument. I take that new FlowDocument that now contains the single image, turn it into binary, and then take the resulting binary, turn it into base64 string and stick it into the tag property of the image in the original FlowDocument. This keeps image data in the original FlowDocument as text. This way I can pass the FlowDocument with image data (which I call SUBString Format) into the XamlReader to get searchable text. When it comes out of the database, I pull the FlowDocument out of the Xaml as normal, but then iterate through each image, extracting the data from the tag property using XamlFormat, and then creating another clone image to provide the Source property for my actual image. I have provided the steps to get to SUBString format below.

/// <summary>
    /// Returns a FlowDocument in SearchableText UI Binary (SUB)String format.
    /// </summary>
    /// <param name="flowDocument">The FlowDocument containing images/UI formats to be converted</param>
    /// <returns>Returns a string representation of the FlowDocument with images in base64 string in image tag property</returns>
    private string ConvertFlowDocumentToSUBStringFormat(FlowDocument flowDocument)
    {
        //take the flow document and change all of its images into a base64 string
        FlowDocument fd = TransformImagesTo64(flowDocument);

        //apply the XamlWriter to the newly transformed flowdocument
        using (StringWriter stringwriter = new StringWriter())
        {
            using (System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(stringwriter))
            {
                XamlWriter.Save(flowDocument, writer);
            }
            return stringwriter.ToString();
        }
    }

    /// <summary>
    /// Returns a FlowDocument with images in base64 stored in their own tag property
    /// </summary>
    /// <param name="flowDocument">The FlowDocument containing images/UI formats to be converted</param>
    /// <returns>Returns a FlowDocument with images in base 64 string in image tag property</returns>
    private FlowDocument TransformImagesTo64(FlowDocument flowDocument)
    {
        FlowDocument img_flowDocument;
        Paragraph img_paragraph;
        InlineUIContainer img_inline;
        System.Windows.Controls.Image newImage;
        Type inlineType;
        InlineUIContainer uic;
        System.Windows.Controls.Image replacementImage;

        //loop through replacing images in the flowdoc with the base64 versions
        foreach (Block b in flowDocument.Blocks)
        {
            //loop through inlines looking for images
            foreach (Inline i in ((Paragraph)b).Inlines)
            {
                inlineType = i.GetType();

                /*if (inlineType == typeof(Run))
                {
                    //The inline is TEXT!!! $$$$$ Kept in case needed $$$$$
                }
                else */if (inlineType == typeof(InlineUIContainer))
                {
                    //The inline has an object, likely an IMAGE!!!
                    uic = ((InlineUIContainer)i);

                    //if it is an image
                    if (uic.Child.GetType() == typeof(System.Windows.Controls.Image))
                    {
                        //grab the image
                        replacementImage = (System.Windows.Controls.Image)uic.Child;

                        //create a new image to be used to get base64
                        newImage = new System.Windows.Controls.Image();
                        //clone the image from the image in the flowdocument
                        newImage.Source = replacementImage.Source;

                        //create necessary objects to obtain a flowdocument in XamlFormat to get base 64 from
                        img_inline = new InlineUIContainer(newImage);
                        img_paragraph = new Paragraph(img_inline);
                        img_flowDocument = new FlowDocument(img_paragraph);

                        //Get the base 64 version of the XamlFormat binary
                        replacementImage.Tag = TransformImageTo64String(img_flowDocument);
                    }
                }
            }
        }
        return flowDocument;
    }

    /// <summary>
    /// Takes a FlowDocument containing a SINGLE Image, and converts to base 64 using XamlFormat
    /// </summary>
    /// <param name="flowDocument">The FlowDocument containing a SINGLE Image</param>
    /// <returns>Returns base 64 representation of image</returns>
    private string TransformImageTo64String(FlowDocument flowDocument)
    {
        TextRange documentTextRange = new TextRange(flowDocument.ContentStart, flowDocument.ContentEnd);
        using (MemoryStream ms = new MemoryStream())
        {
            documentTextRange.Save(ms, DataFormats.XamlPackage);
            ms.Position = 0;
            return Convert.ToBase64String(ms.ToArray());
        }
    }
sub-jp