Missing images in FlowDocument saved as XPS document
Asked Answered
B

4

7

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:

  1. Create an image using the Image control of WPF. I set the image source bracketed by calls to BeginInit/EndInit.
  2. Add the image to the FlowDocument wrapping it in a BlockUIContainer.
  3. 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:

  1. 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).
  2. 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?
Berty answered 11/3, 2010 at 13:19 Comment(3)
Did you find a way to show the FlowDocument in a viewer before printing? I'm considering a similar "hack" to get my document to render correctly.Protestantism
@DennisRoche: No, unfortunately I never found a better solution than showing the document briefly on screen before saving it to a file. Please let me know if you find a better solution.Berty
I may have one possible solution that uses the ContextualLayoutManager to walk to logical tree. I will let you know if it works I will let you know. Otherwise I will resort to loading the document in a viewer as you have done, however will set the window location to X:10,000 Y:10,000 so that user doesn't see it.Protestantism
P
3

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,

Protestantism answered 27/2, 2012 at 11:37 Comment(2)
Thank you for your answer. I have marked your answer as the accepted answer even though I would have liked to see a different solution ;-).Berty
I would have liked a better solution however I believe that exhausted all other possibilities. It is a rather clean workaround as the user will never see the window appear as it way off-screen.Protestantism
L
1

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(() =>{}));
Ladybird answered 18/1, 2011 at 19:18 Comment(6)
Using that Dispatcher trick does indeed work for small images or ones that are already cached, but not for large images that download from the internet in the background. Any idea how to delay the attempt to save to XPS until the 1-2 second download is complete? Or better, wait until all images that the FlowDocument might contain are fully downloaded and rendered?Tret
@Tret you could try DispatcherPriority.Idle?Ladybird
yes, I used DispatcherPriority.SystemIdle. I suppose the dispatcher trick isn't working if the image is downloading in the background?Tret
@Tret sounds like it. You'll have to track how many images are downloading and then continue after they're completed.Ladybird
What I would like to do is somehow 1) await the downloading of each image first (if it's not already in the WPF image cache) using await/async, and 2) put the downloaded image in the cache, then 3) load my FlowDocument. In theory, if all images are already in the cache then it will work no problem. Do you have any idea how to accomplish #1 and 2?Tret
@Tret Nope. Sounds like a good question to ask. Are you wondering how to know when ImageSource has completed downloading? You can cast it to a BitmapImage and watch the DownloadCompleted event. It might require lots of nasty hacking to get each. Perhaps add a custom IValueConverter that converts a URL into a BitmapImage, keeps track of all of them, and exposes some static event to indicate all downloads are complete? Smell that fresh hack scent. Smells like manure. Mmmm.Ladybird
S
0

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();
            }
        }
Sickert answered 12/3, 2012 at 13:38 Comment(4)
I tried calling XpsSerializationManager.Commit but it did not have the desired effect.Berty
Sorry about not responding sooner. Here is a real dirty version that should work. Let me know how it turns outSickert
I have not had a chance to try it out. I'll get back to you :-)Berty
Have you had a chance to try this out yet?Sickert
I
0

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)));
Impenetrable answered 16/1, 2020 at 1:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.