4

I have a web application. It runs in Google Chrome and is not required to work in any other browser.

I have PDF data which has been generated on the server and sent back to the client in an AJAX request. I create a blob from the PDF data. I use window.URL.createObjectURL to create a URL from the blob, which I then load into a window (my preview_window) which has previously been created to show the PDF.

To load the URL, I set preview_window.location.href.

I would like to call revokeObjectURL to avoid wasting more and more resources as new PDFs are generated and previewed in the window. The problem is that calling it immediately after setting preview_window.location.href is too soon, and stops the PDF from being displayed. So I would like to call revokeObjectURL only once the URL has been loaded. I have tried setting preview_window.onload to a callback for this purpose, but it never gets called.

I would like to know:

  1. Is it possible to trigger a callback when the window has loaded the URL, as I am trying to do? How?
  2. Is there another approach to ensure revokeObjectURL gets called in a timely manner?

If I cannot trigger revokeObjectURL when the window finishes loading the URL, I may revoke each URL immediately before generating a new one. But I would rather revoke the URL as soon as it is done loading, if possible.

I have prepared a html file which demonstrates the situation pretty well:

<html>
    <head>
        <title>Show PDF Demo</title>
        <script>
            var build_blob = function(mime_type, data) {
                var buf = new ArrayBuffer(data.length);
                var ia = new Uint8Array(buf);
                for (var i = 0; i < data.length; i++) ia[i] = data.charCodeAt(i);
                var blob = new Blob([ buf ], { type: mime_type });
                return blob;
            };

            window.onload = function(e) {
                document.getElementById('preview_button').onclick = function(e) {

                    // open the window in the onclick handler so we don't trigger popup blocking
                    var preview_window = window.open(null, 'preview_window');

                    // use setTimeout to simulate an asynchronous AJAX request
                    setTimeout(function(e) {
                        var pdf_data = atob(
                            "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2Vz" +
                            "IDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8IC9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+Pgpl" +
                            "bmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+Pgpl" +
                            "bmRvYmoKNCAwIG9iago8PCAvVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAw" +
                            "IDUwMCAyMDBdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8IC9Qcm9jU2V0IDYgMCBSCi9G" +
                            "b250IDw8IC9GMSA3IDAgUiA+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PCAvTGVuZ3RoIDczID4+" +
                            "CnN0cmVhbQpCVAovRjEgMjQgVGYKMTAwIDEwMCBUZAooU01BTEwgVEVTVCBQREYgRklMRSkgVGoK" +
                            "RVQKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqClsvUERGIC9UZXh0XQplbmRvYmoKNyAwIG9iago8" +
                            "PCAvVHlwZSAvRm9udAovU3VidHlwZSAvVHlwZTEKL05hbWUgL0YxCi9CYXNlRm9udCAvSGVsdmV0" +
                            "aWNhCi9FbmNvZGluZyAvTWFjUm9tYW5FbmNvZGluZwo+PgplbmRvYmoKeHJlZgowIDgKMDAwMDAw" +
                            "MDAwMCA2NTUzNSBmCjAwMDAwMDAwMDkgMDAwMDAgbgowMDAwMDAwMDc0IDAwMDAwIG4KMDAwMDAw" +
                            "MDEyMCAwMDAwMCBuCjAwMDAwMDAxNzkgMDAwMDAgbgowMDAwMDAwMzY0IDAwMDAwIG4KMDAwMDAw" +
                            "MDQ2NiAwMDAwMCBuCjAwMDAwMDA0OTYgMDAwMDAgbgp0cmFpbGVyCjw8IC9TaXplIDgKL1Jvb3Qg" +
                            "MSAwIFIKPj4Kc3RhcnR4cmVmCjYyNQolJUVPRg=="
                        );

                        /*
                            Warning: for my Chrome (Version 44.0.2403.155 m), the in-built PDF viewer doesn't seem 
                            to work with a blob when this html page is loaded from the local filesystem.  I have only 
                            got this to work when fetching this page via HTTP.
                        */

                        var pdf_blob = build_blob('application/pdf', pdf_data);

                        var pdf_url = window.URL.createObjectURL(pdf_blob);

                        preview_window.onload = function(e) {
                            console.log("preview_window.onload called");  // never happens
                            window.URL.revokeObjectURL(pdf_url);
                        };

                        preview_window.location.href = pdf_url;
                        console.log("preview_window.location.href set");

                    }, 500);
                };
            };
        </script>
    </head>
    <body>
        <button id="preview_button">Show Preview</button>
    </body>
</html>

Although my demo code above avoids it, I do have jQuery loaded for my application, so if that makes things easier I'm open to using it.

I did find this question in a search, but in that situation the main window ("window") is pointed to a new URL, and the OP never got a response when asking in comments whether it makes a difference if the window came from window.open.

Community
  • 1
  • 1
barryd
  • 317
  • 4
  • 12
  • Why don't you call it on each new call ? – Kaiido Aug 24 '15 at 11:47
  • @Kaiido, I'll do that if I don't find a better way. It will certainly stop accumulation of wasted memory. But it seemed that it should be possible to know when I no longer have to keep the link in existence for the browser, and free the memory sooner. – barryd Aug 24 '15 at 12:48
  • Well I think that your only way to detect if an `open()`ed window has loaded is to inject some code in it that will make its `referer` aware of it. Since you're opening a PDF, which will be reparsed entirely by the browser, I'd say it's your sole option. But maybe someone here will make me lie. – Kaiido Aug 24 '15 at 13:21
  • Is it possible to put an iframe in the window, load the PDF in the iframe, and put a callback on the iframe that will actually get called when the PDF is loaded? – barryd Aug 25 '15 at 09:07
  • @Kaiido, that approach sounds too ambitious for me. If anyone can write a sufficiently authorative-sounding answer stating Chrome can't trigger a callback on loading a PDF into another window, I should be able to accept it and just revoke each URL prior to creating a new one. – barryd Aug 25 '15 at 09:19
  • Your idea was indeed clever and it does work :) – Kaiido Aug 26 '15 at 01:24

1 Answers1

0

As you found out, you can't set open()ed windows' onload event from the opener. You will have to inject some script in the second page that will call its window.opener functions.

But since you are opening a pdf file, the browser will re-parse entirely your page and your injected code will vanish.

The solution, as you found out yourself in the comments, is to inject the blob's url in an iframe, and wait for this iframe's load event.

Here is how :

index.html

    <script>
        // The callback that our pop-up will call when loaded
        function imDone(url){
            window.URL.revokeObjectURL(url);
            }

        var build_blob = function(mime_type, data) {
            var buf = new ArrayBuffer(data.length);
            var ia = new Uint8Array(buf);
            for (var i = 0; i < data.length; i++) ia[i] = data.charCodeAt(i);
            var blob = new Blob([ buf ], { type: mime_type });
            return blob;
        };
        var preview_window=null;
        window.onload = function(e) {
            document.getElementById('preview_button').onclick = function(e) {
              if(preview_window===null || preview_window.closed){
                // open the window in the onclick handler so we don't trigger popup blocking
                preview_window = window.open('html2.html', 'preview_window');
              }
               // avoid reopening the window since it may cache our last blob
              else preview_window.focus();

                // use setTimeout to simulate an asynchronous AJAX request
                setTimeout(function(e) {
                    var pdf_data = /* Your pdf data */

                    var pdf_blob = build_blob('application/pdf', pdf_data);

                    var pdf_url = window.URL.createObjectURL(pdf_blob);
                    // Simple loop if our target document is not ready yet
                    var loopLoad = function(url){
                      var doc = preview_window.document;
                      if(doc){
                        var iframe = doc.querySelector('iframe');
                        if(iframe)iframe.src = url;
                        else setTimeout(function(){loopLoad(url);},200);
                      }
                      else setTimeout(function(){loopLoad(url);},200)
                    };
                    loopLoad(pdf_url);

                }, 0);
            };
        };
    </script>

and the html2.html

<html>
    <head>
        <title>Iframe PDF Demo</title>
        <style>
            body, html, iframe{margin:0; border:0}
        </style>
    </head>
    <body>
    <iframe width="100%" height="100%"></iframe>
    <script>
        document.querySelector('iframe').onload = function(){
            //first check that our src is set
            if(this.src.indexOf('blob')===0)
                // then call index.html's callback
                window.opener.imDone(this.src);
            }
    </script>
    </body>
</html>

Live Demo

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • When I test this on my Chrome, it behaves a bit dodgy, apparently due to a race condition/s. I made it behave okay in practice by only loading html2 when the window is first opened ([updated demo](http://plnkr.co/edit/r2IhNiStxvYtJma00v3A?p=preview)), but there's still a race condition there, as can be seen by reducing the timeout from 500 to 0. The obvious way to avoid the race condition would be... to set the iframe src from a preview_window.onload handler, but of course that won't work. – barryd Aug 26 '15 at 03:23
  • I don't have this bug, does your pdf not load at all? Normally it doesn't matter how you call html2, since the callback will only fire when the iframe has loaded. – Kaiido Aug 26 '15 at 03:28
  • The symptom is that the 2nd and subsequent pushes of the preview button only rarely display the PDF. Mostly they just give a blank window. I'm waiting several seconds after pushing it. I'm quite sure it's because the iframe src of the *old* html2 is set before the new html2 is loaded in the window. For me it's happening with the delay at 500, but I'm pretty sure if you reduce that to 0, you'll see the same as me. – barryd Aug 26 '15 at 04:05
  • Ok, I was able to repro the bug, which I think came from the fact that the target document hadn't loaded before we tried to set the iframe src (fixed with a looping function) and also because the second call to `window.open()` would make the target window to load from cache, thus firing the callback before we told it to set the iframe src to the new url. Edit should fix all of these – Kaiido Aug 26 '15 at 06:35
  • 1
    Okay, so now we're polling to catch when html2 has loaded so we can set the iframe src. Gets the job done and behaves as expected, so I'm going to accept this answer. But you also helped me to realise (with your initial question) that calling revokeObjectURL just before createObjectURL is not a bad solution, so I think I'll be doing that in my application. – barryd Aug 26 '15 at 09:18