4

UPDATE 3

There's a bounty on this question now. The sample project and problem brief are at the bottom of the page under TL;DR; so save yourself some reading and skip to the end!

...

I've been trying to get this highly detailed, upvoted answer from JerryNixon working using a statically loaded page via NavigateToString. I don't see why this shouldn't work, but admittedly there are parts of the code that absolutely baffle me.

The original answer is here but I'll replicate the code here for completeness sake. Jerry provides the following sample / solution to those who want to print from a WebView

<Grid Background="Blue">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="995" />
        <ColumnDefinition Width="300" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <WebView Grid.Column="0" x:Name="MyWebView" Source="http://www.stackoverflow.com" HorizontalAlignment="Right" />
    <Rectangle Grid.Column="1" x:Name="MyWebViewRectangle" Fill="Red" />
    <ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <ItemsControl x:Name="MyPrintPages" VerticalAlignment="Top" HorizontalAlignment="Left">
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
            <Rectangle Height="150" Width="100" Fill="White" Margin="5" />
        </ItemsControl>
    </ScrollViewer>
</Grid>

public MainPage()
{
    this.InitializeComponent();
    MyWebView.LoadCompleted += MyWebView_LoadCompleted;
}

void MyWebView_LoadCompleted(object sender, NavigationEventArgs e)
{
    MyWebViewRectangle.Fill = GetWebViewBrush(MyWebView);
    MyPrintPages.ItemsSource = GetWebPages(MyWebView, new Windows.Foundation.Size(100d, 150d));
    MyWebView.Visibility = Windows.UI.Xaml.Visibility.Visible;
}

WebViewBrush GetWebViewBrush(WebView webView)
{
    // resize width to content
    var _OriginalWidth = webView.Width;
    var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // resize height to content
    var _OriginalHeight = webView.Height;
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // create brush
    var _OriginalVisibilty = webView.Visibility;
    webView.Visibility = Windows.UI.Xaml.Visibility.Visible;
    var _Brush = new WebViewBrush
    {
        SourceName = webView.Name,
        Stretch = Stretch.Uniform
    };
    _Brush.Redraw();

    // reset, return
    webView.Width = _OriginalWidth;
    webView.Height = _OriginalHeight;
    webView.Visibility = _OriginalVisibilty;
    return _Brush;
}

IEnumerable<FrameworkElement> GetWebPages(WebView webView, Windows.Foundation.Size page)
{
    // ask the content its width
    var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // ask the content its height
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // how many pages will there be?
    var _Scale = page.Width / _ContentWidth;
    var _ScaledHeight = (_ContentHeight * _Scale);
    var _PageCount = (double)_ScaledHeight / page.Height;
    _PageCount = _PageCount + ((_PageCount > (int)_PageCount) ? 1 : 0);

    // create the pages
    var _Pages = new List<Windows.UI.Xaml.Shapes.Rectangle>();
    for (int i = 0; i < (int)_PageCount; i++)
    {
        var _TranslateY = -page.Height * i;
        var _Page = new Windows.UI.Xaml.Shapes.Rectangle
        {
            Height = page.Height,
            Width = page.Width,
            Margin = new Thickness(5),
            Tag = new TranslateTransform { Y = _TranslateY },
        };
        _Page.Loaded += (s, e) =>
        {
            var _Rectangle = s as Windows.UI.Xaml.Shapes.Rectangle;
            var _Brush = GetWebViewBrush(webView);
            _Brush.Stretch = Stretch.UniformToFill;
            _Brush.AlignmentY = AlignmentY.Top;
            _Brush.Transform = _Rectangle.Tag as TranslateTransform;
            _Rectangle.Fill = _Brush;
        };
        _Pages.Add(_Page);
    }
    return _Pages;
}

My only change is to remove the Source attribute from the webview and add this line to the MainPage() method

var html = await Windows.Storage.PathIO.ReadTextAsync("ms-appx:///Assets/sherlock.html");
MyWebView.NavigateToString(html);

Sherlock.html is a excerpt from the Arthur Conan Doyle story, The Red-Headed League, the full code of this HTML page is here - note that a segment near the end repeats several times. This was done in testing, I'll explain below

This sample initially worked for me, on a small file. (Sherlock.html without the padded chapters)

However when I get above a certain file-size (usually around 4000 words+) the following behaviour occurs with the sample.

The WebView shrinks to a very narrow view

(see update notes on why this is struck out)

and then the screen goes gray. (I feel a little silly posting a screenshot of a gray screen, but I'm trying to be thorough here)

enter image description here

The app does NOT crash. No debugging messages appear, no exceptions occur. It is as if the Grid is simply removed. . It's not the splash screen (which I've set blue) and it's not the app background (darker gray). It's as if something else is loaded over the top of the page.

I've tried adjusting every part of the code, but I can't seem to isolate what causes the issue. Furthermore, parts of this code just confuse the hell out of me. For instance, in the Load_Completed Method

MyWebViewRectangle.Fill = GetWebViewBrush(MyWebView);
MyPrintPages.ItemsSource = GetWebPages(MyWebView, new Windows.Foundation.Size(100d, 150d));
MyWebView.Visibility = Windows.UI.Xaml.Visibility.Visible;

Line1: MyWebViewRectangle is filled with a shot of the WebView. Why? This control is never referenced again. I guess it could be for the user to get a clearer view of the page, but it seems to serve no programmatic purpose

Line3: MyWebView.Visibility is set to Visible This happens again later, in GetWebViewBrush.

var _OriginalVisibilty = webView.Visibility;
webView.Visibility = Windows.UI.Xaml.Visibility.Visible;
// and a few lines later... 
webView.Visibility = _OriginalVisibilty;

As far as I can tell, the webview is never Collapsed, yet it is set to Visible three times.

If anyone can explain any of this to me (maybe even JerryNixon himself?) I would really appreciate it. I've spent several months working on an app and this is the last piece of the puzzle. I can't launch without the ability to print!

UPDATE

Made some discoveries this morning. I had been setting the width of the WebView in XAML. Without this even the smallest files were failing. Based on this discovery I set the width in the html file itself to 800px, and it worked. Hooray, problem solved, right? Unfortunately, no. I tried with a longer file again (the full version of The Red-headed league, here) and the same issue is happening again. It's now gotten a lot weirder.

I inserted a breakpoint at webView.Height = _ContentHeight; in the GetWebPages method. This shows me that _ContentHeight is exactly "13241".

However, inserting a breakpoint makes the code work. Once I hit the break and continue, it all seems to load. Although oddly again, the WebView is invisible until I mouseover the scrollbar.

If I remove the breakpoint, I get the grey screen again.

If I manually set _ContentHeight to 13230, the page loads. This seems to be the magic number. Anything higher than this and the page fails. However, if I change the width of the html to less than 800px, it also fails. So logically, there's some kind of magic ratio of 800:13230 pixels (or 1:16.5375). If I go over this ratio, I set off the gray screen of death. I can disable the Red rectangle and restrict GetWebPages to 3 pages, and the gray screen is still there. That tells me that the issue is with the WebView itself

An issue that goes away if I set a breakpoint.

Also note the extra whitespace in MyWebViewRectangle and extra blank pages in MyPrintPages when it does work:

Working result, but with extra whitespace and blank pages appended

Note: I've also added NateDiamond's suggestions of the events LongRunningScriptDetected, UnsafeContentWarningDisplaying, and UnviewableContentIdentified

Here's the complete source of my sample project: https://onedrive.live.com/redir?resid=EE99EF9560A6740E!110509&authkey=!ACrZgm6uq5k5yck&ithint=file%2czip

UPDATE 2

Some success.

I put some headers into my sample file (found the "bottom" of each computed page by trial and error) and upped the page size so I could see the markers. Although there are 9 pages, only 5 and a small part of 6 are rendered. 9 Page objects are created by GetWebPages, but only 5ish have rendered HTML content. The rest are empty

nothing past page 5

(Note, I changed the background colors as the red on blue was giving me a migraine)

I've gone through the code and made some modifications, and now I no longer get the gray screen at all. However, about 2 out of every 3 times I run it, only the first page is rendered, and it is cropped. This seems to be totally random. When 9 pages are rendered not all have content, as stated.

Here are my changes:

added new method Resize_WebView()

public void ResizeWebView()
{
    OriginalHeight = MyWebView.Height;

    var _WidthString = MyWebView.InvokeScript("eval", new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    MyWebView.Width = _ContentWidth;

    // ask the content its height
    var _HeightString = MyWebView.InvokeScript("eval", new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    MyWebView.Height = _ContentHeight;
}

in MyWebView_LoadCompleted commented out the rectangle fill, added call to ResizeWebView():

void MyWebView_LoadCompleted(object sender, NavigationEventArgs e)
{
    ResizeWebView();

    //MyWebViewRectangle.Fill = GetWebViewBrush(MyWebView);
    MyPrintPages.ItemsSource = GetWebPages(MyWebView, new Windows.Foundation.Size(300d, 450d));
}

in GetMyWebPages changed where the brush is loaded:

IEnumerable<FrameworkElement> GetWebPages(WebView webView, Windows.Foundation.Size page) 
{
    // ask the content its width
    /*var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // ask the content its height
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // Manually set _ContentHeight: comment out the above block and uncomment this to test. 
    // int _ContentHeight = 13230;
    // webView.Height = _ContentHeight;*/

    // how many pages will there be?
    //var _Scale = page.Width / _ContentWidth;
    var _Scale = page.Width / webView.Width; // now sourced directly from webview
    //var _ScaledHeight = (_ContentHeight * _Scale); 
    var _ScaledHeight = (webView.Height * _Scale); // now sourced directly from webview
    var _PageCount = (double)_ScaledHeight / page.Height;
    _PageCount = _PageCount + ((_PageCount > (int)_PageCount) ? 1 : 0);

    // create the pages
    var _Pages = new List<Windows.UI.Xaml.Shapes.Rectangle>();
    for (int i = 0; i < (int)_PageCount; i++)
    {
        var _TranslateY = -page.Height * i;
        var _Page = new Windows.UI.Xaml.Shapes.Rectangle
        {
            Height = page.Height,
            Width = page.Width,
            Margin = new Thickness(5),
            Tag = new TranslateTransform { Y = _TranslateY },
        };
        var _Brush = GetWebViewBrush(webView);
        _Brush.Stretch = Stretch.UniformToFill;
        _Brush.AlignmentY = AlignmentY.Top;
        _Brush.Transform = _Page.Tag as TranslateTransform;
        _Page.Fill = _Brush;
        /*_Page.Loaded += async (s, e) =>
        {
            var _Rectangle = s as Windows.UI.Xaml.Shapes.Rectangle;
            var _Brush = await GetMyWebViewBrush(webView);
            _Brush.Stretch = Stretch.UniformToFill;
            _Brush.AlignmentY = AlignmentY.Top;
            _Brush.Transform = _Rectangle.Tag as TranslateTransform;
            _Rectangle.Fill = _Brush;
        };*/
        _Pages.Add(_Page);
    }
    return _Pages;
}

removed now unnecessary resize code from GetWebView

WebViewBrush GetWebViewBrush(WebView webView)
{
    /*// resize width to content
    var _OriginalWidth = webView.Width;
    var _WidthString = webView.InvokeScript("eval",
        new[] { "document.body.scrollWidth.toString()" });
    int _ContentWidth;
    if (!int.TryParse(_WidthString, out _ContentWidth))
        throw new Exception(string.Format("failure/width:{0}", _WidthString));
    webView.Width = _ContentWidth;

    // resize height to content
    var _OriginalHeight = webView.Height;
    var _HeightString = webView.InvokeScript("eval",
        new[] { "document.body.scrollHeight.toString()" });
    int _ContentHeight;
    if (!int.TryParse(_HeightString, out _ContentHeight))
        throw new Exception(string.Format("failure/height:{0}", _HeightString));
    webView.Height = _ContentHeight;

    // create brush
    var _OriginalVisibilty = webView.Visibility;
    webView.Visibility = Windows.UI.Xaml.Visibility.Visible;*/
    var _Brush = new WebViewBrush
    {
        SourceName = webView.Name,
        Stretch = Stretch.Uniform
    };
    _Brush.Redraw();

    // reset, return
    /*webView.Width = _OriginalWidth;
    webView.Height = _OriginalHeight;
    webView.Visibility = _OriginalVisibilty;*/
    return _Brush;
}

This is becoming a seriously epic question.

TL;DR;

Here is the updated project. https://onedrive.live.com/redir?resid=EE99EF9560A6740E!110545&authkey=!AMcnB_0xKRhYHIc&ithint=file%2czip

Load and observe. there are 9 pages in the document.

  • GetWebPages occasionally loads only one page, cropped
  • the rest of the time, GetWebPages loads pages 1-5, part of 6 and 3 blank pages

This is pretty much all you need to know to answer this question.

I've put a bounty on this question. I'm looking for resolution of this issue, and if possible a way to add padding to each page so that the text does not run to the edges.

Community
  • 1
  • 1
roryok
  • 9,325
  • 17
  • 71
  • 138
  • In Windows 8.1 there are a bunch of events for success and failure states, such as LongRunningScriptDetected, UnsafeContentWarningDisplaying, and UnviewableContentIdentified. Try adding those events and seeing if any fire. Another thing to try is the new [`NavigateToLocalStreamUri`](http://msdn.microsoft.com/en-US/library/windows/apps/windows.ui.xaml.controls.webview.navigatetolocalstreamuri.aspx) method. – Nate Diamond Sep 29 '14 at 22:22
  • Thanks Nate. I just added all those events, and a `throw new Exception...` to each one. No exceptions. Again, just a gray screen. I verified that this is not the grid being removed too - I the background color of the page itself is a darker gray. This gray is the "app launch" gray. – roryok Sep 30 '14 at 07:17
  • Have you tried it in different sized screens? Try loading it up in the simulator with different sized screens and see if that changes the number. – Nate Diamond Sep 30 '14 at 18:48

2 Answers2

0

[Update]

The fact that it is cutting off at the exact same place with this new code is extremely strange. This would indicate that the data doesn’t exist in the WebViewBrush. If you render the entire brush to a control does it render everything? If my theory is correct you should only see up to the page 6 cutoff. This is starting to smell like a video driver bug on the Surface pro. The Surface pro uses the Intel video chipset and uses shared i.e. system memory. You should have plenty to render a single instance of the brush (all nine pages). Keep in mind that even on better hardware this approach is still going to likely cause resource problems at large page counts.

If you have a chance can you please try side loading it on another machine with a video card from a different manufacturer to confirm that it does indeed work? I'm going to try to dig up a Surface Pro 1 and see if I can reproduce your findings.

I need to warn you about the known resource pitfalls of XAML printing before you head down that road. The XAML printing subsystem requires that you pass ALL of the pages to it before it will render. It then sends the document as a whole to the printer. This requires that every page be cached as XAML and then every page gets rendered as a bitmap. This all happens in main memory. You likely can get away with twenty pages or so but you will quickly run out of system resources once you get past that. Fifty to a hundred pages will likely cause the runtime to shut you down due to memory pressure. You will get even fewer pages on low resource devices.

Initially I thought your goal was to print HTML pages. The solution below is perfect for that. However, if your ultimate goal is to allow your users to print large documents from your app the real answer (and only solution for Windows Store apps) is to use Direct2D and DirectWrite. These technologies allow you to “stream” the pages to the printer cache. The printer cache is backed by disk and doesn’t cause the same memory pressure problems (although you can still theoretically run out of disk space). Needless to say this is the ideal solution but does require a lot of expertise. You need to know something about C++ / CX as well as Direct2D to really be able to paginate the pages correctly.

Direct2Dapp printing sample

I hope this helps,

James


I took a close look at this today and I think that you are running into is resource limitations. I am not able to reproduce the issue on my workstation class machine and my colleague was not able to reproduce the problem on his workstation class laptop. What I see in your code is that you are creating a new brush for every page. Even though you are transforming the brush the entire WebView is getting transformed into a bitmap, a very big bitmap, for every page. This puts resource constraints on the XAML rich compositor. Since the compositor is D3D based you are likely running out of texture memory on your video card.

I think I have a solution for you. I'm not going to lie it is ugly but its working for me and I hope that it will work for you. Basically I'm rendering the page section via the brush to a hidden rectangle declared in the XAML. I then use RenderTargetBitmap to render the hidden rectangle. This has the effect of only rendering the portion of WebView that we need. Then I'm setting the RenderTargetBitmap as the source to an Image. I add the Images to the pages collection. The pages collection is set to your ItemsControl and and bob's your uncle.

The bulk of the changes are in GetWebPages:

async Task<bool> GetWebPages(WebView webView, Windows.Foundation.Size page)
{
    // how many pages will there be?
    var _Scale = page.Width / webView.Width; // now sourced directly from webview
    var _ScaledHeight = (webView.Height * _Scale); // now sourced directly from webview
    var _PageCount = (double)_ScaledHeight / page.Height;
    _PageCount = _PageCount + ((_PageCount > (int)_PageCount) ? 1 : 0);

    var _Brush = GetWebViewBrush(webView);

    for (int i = 0; i < (int)_PageCount; i++)
    {
        var _TranslateY = -page.Height * i;
        // You can probably optimize this bit
        _RenderSource.Height = page.Height;
        _RenderSource.Width = page.Width;
        Margin = new Thickness(5);

        _Brush.Stretch = Stretch.UniformToFill;
        _Brush.AlignmentY = AlignmentY.Top;
        _Brush.Transform = new TranslateTransform { Y = _TranslateY };
        _RenderSource.Fill = _Brush;

        RenderTargetBitmap rtb = new RenderTargetBitmap();

        await rtb.RenderAsync(_RenderSource);

        var _iPge = new Image
        {
            Source = rtb,
            Margin = new Thickness(10)
        };

        _Pages.Add(_iPge);
    }
    return true;
}

The entire modified project can be found here: http://1drv.ms/1y91iv7

Please give it a try and let me know if it works for you.

PS There are some layout issues and pagenation issues that I didn't try to fix.

I hope this helps,

James

  • I really appreciate the time taken, and I'd love to say this fixes the issue, but its still cutting off at exactly the same place for me. You're probably right about the resources - my dev machine is a 4GB surface pro. I think I'll have to try a completely different approach - converting HTML to XAML, rendering it into a RichTextBlock and printing from there. This isn't what I wanted but it should hopefully get around the memory issues. – roryok Oct 09 '14 at 08:31
0

Not a solution, but since I'm struggling with UWP printing myself, I'd thought I share my findings:

(1) Gray screen:

You use WebViewBrush.Redraw() to capture a "page" of your WebView. However, Redraw() happens asynchronously. Unfortunately we can't use await to wait for the WebViewBrush to capture the WebView.

I suspect you are processing the next page, but WebViewBrush has not yet captured the previous page. That would explain why it works when you add a breakpoint.

(2) Size limitation of WebViewBrush:

I'm seeing something similar For me, WebVewBrush stops working if my WebView is higher than 8000px. I assume it also depends on the width of the view and hence the total number of pixels.

My attempt here: resize the WebView to the page size and scroll from page to page using JavaScript. But this does not solve the gray page problem above.

(3) Memory constraints

Yes, that's a problem. I tried simply using a separate WebView for each page, but the app runs out of memory after 240 pages.

However, I was able to use 500 "plain" pages rendered as Rectangle.

I also tried to load and print a 500 page PDF with MS Edge and this works, so it is possible to print large documents.

For reference, here's my own question (I'll post an update should I ever find a solution):

How to wait for WebViewBrush.Redraw() to finish (UWP printing)?

Mark
  • 6,647
  • 1
  • 45
  • 88