Show PDF in your application [Updated 16 Jan 2018]

Edit : This article has been updated to be compliant with PDF.js version 1.9.426. The changes are related to the map added next to the js files. And also a translateFragments() to add for dynamic translation.

Long time no see! This article will explain one of the possible way for showing PDF file in your JavaFX application :


Three years ago, when I first implemented a PDF reader in my application, I did not find many options. I finally used IcePDF viewer. But this solution was not that good for several reasons :

  • IcePDF was made in Swing! Therefore I had to wrap the viewer into a SwingNode and frankly, it did not work that well.
  • The toolbar was not customizable and quite hideous.
  • It was displaying some popups when starting in order to save some cache into AppData, so I had to get the sources, disable that popup and rebuild the entire thing. Then I forgot what I did and was not able to upgrade my version.
  • It was very slow for no reason. I displayed my PDF in a side of my application, into a SplitPane, and it was taking forever to close it.
  • Recently, I tried to dig a bit more and see if new options were available. Luckily some solutions arose.

    One solution (the one I’m describing) is to use the Mozilla HTML 5 PDF viewer : pdf.js. If I manage to integrate that viewer into a WebView, I would have a stable, open-source pdf viewer with lots of functionalities!

    I decided to go with that solution because the files that we need weight less than 5Mb and basically nothing has to be done. (Ok maybe some tweaks..)

    Most of what I will say here is inspired by the comment on StackOverFlow made by user enzo : here. But the solution he wrote is not working when you actually deploy your application into a bundle or a JAR.

    Retrieve pdf.js

    So what you need is to download pdf.js dist and place it under src/main/resources.

    If you only want to display the pdf into your IDE, go to next step.

    One of the main problem with pdf.js is that we have relative links inside the javascript files. But the jar protocol does not allow for .. relative location specifiers! Thus, the viewer seems to work when you launch it in your IDE, but as soon as you will package it into a JAR and deploy it somewhere, the viewer will not work!

    So you need to take the pdf.js and pdf.worker.js files out of their build package, and place them with the other javascript files under web package.
    It should look like that :
    packages

    Once it’s done, we need to fix the broken links:
    At the top of viewer.html, replace src="../build/pdf.js" by src="pdf.js"

    In the viewer.js and viewer.js.map, replace PDFJS.workerSrc = '../build/pdf.worker.js'; by PDFJS.workerSrc = 'pdf.worker.js'; and PDFJS.cMapUrl = '../web/cmaps/'; by PDFJS.cMapUrl = 'cmaps/';.

    Now all the links should be working in the IDE and in the JAR.

    Tweaking the viewer

    To prevent pdf.js from opening demo pdf file on startup, open web/viewer.js and clear DEFAULT_URL value.

    var DEFAULT_URL = '';

    Open web/viewer.html and add script block:

    <script src="viewer.js"></script>
        <!-- CUSTOM BLOCK -->
        <script>
        var openFileFromBase64 = function(data) {
            var arr = base64ToArrayBuffer(data);
            console.log(arr);
            PDFViewerApplication.open(arr);
        }
    
        var changeLanguage = function(language) {
             var mozL10n = document.mozL10n || document.webL10n;
             if(mozL10n !== null){ 
                  mozL10n.setLanguage(language);
              }
        }
        
        function base64ToArrayBuffer(base64) {
          var binary_string = window.atob(base64);
          var len = binary_string.length;
          var bytes = new Uint8Array( len );
          for (var i = 0; i < len; i++)        {
              bytes[i] = binary_string.charCodeAt(i);
          }
          return bytes.buffer;
        }
    </script>
    <!-- end of CUSTOM BLOCK -->
    

    The script block we added will allow us to open our PDF file and also to change the language of the PDF viewer.

    For an unknown reason, the setLanguage method has been changed in this commit https://github.com/mozilla/pdf.js/commit/5438ce9b9826fabcdc1638223869dfb17925bd0c#diff-dfc0cf01b6198837e978fe22d149ba95 so that translateFragments is not called anymore when the language is changed, so that the UI is not changed at all.

    So we need to go to viewer.js, find the setLanguage method and modify it that way :

    setLanguage: function setLanguage(lang, callback) {
          loadLocale(lang, function () {
            if (callback) callback();
            translateFragment();
            });
        },
    

    Some of pdf.js functions will not work: open file (cause pdf.js have no access to URL outside JAR), printing etc. To hide corresponding toolbar buttons you can add the following lines to a web.css file added in your resources :

    #toolbarViewerRight {
        display:none;
    }
    

    Launching the WebView and adding a PDF

    Now all you need to do is to create your WebView. Note that the WebView must be created on the FXApplicationThread otherwise an exception will be thrown :

                 WebView webView = new WebView();
                WebEngine engine = webView.getEngine();
                //Change the path according to yours.
                String url = getClass().getResource("pdfjs/web/viewer.html").toExternalForm();
                //We add our stylesheet.
                engine.setUserStyleSheetLocation(getClass().getResource("pdfjs/web.css").toExternalForm());
                engine.setJavaScriptEnabled(true);
                engine.load(url);
    

    Now the idea is to put the PDF file into a byte array. I get my PDF from a server URL so here it is :

    InputStream stream = null;
    try {
           stream = myUrl.openStream();
           //I use IOUtils from org.​apache.​commons.​io
           byte[] data = IOUtils.toByteArray(stream);
           //Base64 from java.util
           String base64 = Base64.getEncoder().encodeToString(data);
           //This must be ran on FXApplicationThread
           webView.getEngine().executeScript("openFileFromBase64('" + base64 + "')");
                            
           } catch (Exception ex) {
                e.printStackTrace();
           }finally {
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException ex) {
                        e.printStackTrace();
                    }
                }
            }
    

    But if you get your PDF from a file, you can get your byte array like that :

    // readFileToByteArray() comes from commons-io library
    byte[] data = FileUtils.readFileToByteArray(new File("/path/to/file"));
    

    Your PDF should be displayed inside your application!

    If you want to change the language of the viewer, simply do like that :

    public void changeLanguage(Locale locale) {
         webView.getEngine().executeScript("changeLanguage('" + locale.toLanguageTag() + "')");
    }
    

    Cheers

    20 thoughts on “Show PDF in your application [Updated 16 Jan 2018]”

    1. hello, nice example. i implemented it in my javafx app, but hit some issues.
      e.g
      stream = myUrl.openStream();—–> which url are you referencing?

      byte[] data = FileUtils.readFileToByteArray(new File(“/path/to/file”));–> after putting the path to my file it reads file not found or does not exist

      and occasionally, i get this error
      JSException: TypeError: undefined is not a function

      PLease help me out

      1. Hi Vernon,

        In my case, the PDF file is sent by a server, so actually I have a String like “http://server.com/?dl=MY_PDF.pdf”, and I simply build an URL out of that String.

        I cannot really help you with your File not found error. Where is located your File? On your computer or inside your project? Try several solutions in debug mode in order to see where the problem could be. Maybe a “\” instead of “/”. You can use java.nio.Paths in order to construct one and use “toFile()” also.

        Finally, I did not encounter the last error you are mentioning.. It’s definitely a Javascript exception so it should be inside PDF.js.

        Please note that I’ve been using this viewer for several months, I have noted that some complex PDF were not shown properly, for examples some big images. I’m looking on the problem but it seems that the WebView is not fully compatible with PDF.js sadly.. The quest to a perfect PDF viewer in JavaFX is still on.

        Sam’

        1. hello,
          thanks for your reply, much appreciated..

          my file is located in my computer, i have tried the file in my documents, and inside the src/web folder also..i encounter same issue..

          here i my block of code for loading the pdf

          private void LoadPDF() throws IOException {
          WebEngine engine = pdViewer.getEngine();
          String url = getClass().getResource(“/web/viewer.html”).toExternalForm();
          engine.setJavaScriptEnabled(true);
          engine.load(url);

          byte[] data = FileUtils.readFileToByteArray(new File(“C:\\Users\\VERNON\\Documents\\NetBeansProjects\\TestRun\\src\\web\\receipt.pdf”));
          String base64 = Base64.getEncoder().encodeToString(data);
          engine.executeScript(“openFileFromBase64(‘” + base64 + “‘)”);

          }

          with the above code i get the following error log:

          Caused by: netscape.javascript.JSException: TypeError: undefined is not a function
          at com.sun.webkit.dom.JSObject.fwkMakeException(JSObject.java:137)
          at com.sun.webkit.WebPage.twkExecuteScript(Native Method)
          at com.sun.webkit.WebPage.executeScript(WebPage.java:1482)
          at javafx.scene.web.WebEngine.executeScript(WebEngine.java:1003)
          at testrun.FXMLDocumentController.LoadPDF(FXMLDocumentController.java:283)
          at testrun.FXMLDocumentController.PDFLOAD(FXMLDocumentController.java:110)

          but with this code below, i get file not found or does not exist

          private void LoadPDF() throws IOException {
          WebEngine engine = pdViewer.getEngine();
          String url = getClass().getResource(“/web/viewer.html”).toExternalForm();
          engine.setJavaScriptEnabled(true);
          engine.load(url);

          byte[] data = FileUtils.readFileToByteArray(new File(“/web/receipt.pdf”));
          String base64 = Base64.getEncoder().encodeToString(data);
          engine.executeScript(“openFileFromBase64(‘” + base64 + “‘)”);

          Note that the location of the pdf is inside my project src/web folder..

          if there is anything you can tell me from my code..it will be very much appreciated

          1. Hi,

            If your File is situated in src/web folder, you should be able to reach it. Try to use the “getRessource” method just like you did for the viewer.
            byte[] data = FileUtils.readFileToByteArray(new File(getClass().getResource(“web/receipt.pdf”).toExternalForm());

            It should work somehow. In your first case, you clearly found the file and indeed, a JavaScript error is thrown after.

            It’s quite tricky to debug it just like that since I don’t have it on my side. Could you upload you application to GitHub or somewhere so that I can fork it and try to see where the error could be?

            Sam’

          2. Here is a solution:
            webengine.load() call is asynchronous. You need to wait until document is loaded before applying javascript

            so wrap load pdf with engine change listener
            engine.getLoadWorker().stateProperty().addListener(
            new ChangeListener() {
            public void changed(ObservableValue ov, State oldState, State newState) {
            if (newState == Worker.State.SUCCEEDED) {
            byte[] dataX = null;
            try {
            dataX = FileUtils.readFileToByteArray(new File(“D:\\Dive.pdf”));
            } catch (IOException ex) {
            Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
            }
            String base64 = Base64.getEncoder().encodeToString(dataX);
            engine.executeScript(“openFileFromBase64(‘” + base64 + “‘)”);

            }
            }
            });

    2. hello,

      were you able to access my project?
      I went through your tutorial all over again, and tried debugging, cant seem to find where the problem is.

      Vernon’

          1. Allright,

            I’ve been able to launch your project. The issue is coming from the fact that you are loading the viewer.html right before loading your File. Thus I believe the pdf viewer is not yet instantiated.

            In the “loadPdf” method, if you move the four line instantiating the viewer.html in the “initialize” method, it should work.

            Please note that the receipt.pdf inside your Jar seems to be corrupted. I cannot open it with Acrobat Reader.

            In LoadPDF, I replaced the data line with that: “byte[] data = FileUtils.readFileToByteArray(new File(“C:\\Users\\MYSELF\\Desktop\\MY_WORKING_PDF.pdf”));”

            You can have a look to another of my blog post here : https://blog.samirhadzic.com/2016/09/12/change-your-application-theme-dynamically/ where I explain how to have a working code for opening files where you are launching from your IDE (FileSystem) and your JAR.

            Sam’

            1. hi

              Thanks so much for this. it actually worked…for some reason i can only read pdf files on my desktop. But am going through your link….
              when i will contact you again.
              good job and thanks again

              Vernon

    3. Hi Sam,

      What a great article! It has helped me tremendously, thanks for that. I hope you can help me also with this nasty problem that I’m experiencing with webview and pdf.js.

      Now, I want to show also electronic signatures in the viewer. By default this is disabled but there is a easy workaround that works by commenting some lines in pdf.worker.js (search for: “‘Sig'”).

      The problem is that the webview is somehow caching the js file and I am not succesfull in showing the electronic signature from my application. When I launch the viewer from firefox the signature is there however.

      My investigation online was not very successful. The webview’s cache should be deleted upon restarting the application but I don’t see that happening. Moreover there is no direct way to purge caches. Maybe you have another insight that may be of any help?

      Thanks in advance!

      Best
      Enes

    4. Hi Sam, great article, almost there, but I’m getting a “Warning: Setting up fake worker” message logged to my console, webview is displayed, no error, but the pdf isnt!!. Please help out. Plus can you guys just attach your emails?

    5. Hi, Samir
      Thanks for the article. It’s very useful.

      However, I faced two problems. The first one related with fonts. The fonts of pdf document https://mozilla.github.io/pdf.js/web/viewer.html look different in a Firefox and in a JavaFX WebView. So should I add some fonts to my JavaFX application?

      And the second question about replacing links in pdf.js.map and pdf.worker.js.map. Could you explain what links I have to replace?

      1. Hi Nick,

        Sorry for the delay, I really need to trigger email notification for comments 😉

        Regarding your issue on fonts, I confess that I have not investigate it. I’m not sure that PDF.js embed fonts. I know that some PDF can embed some fonts, but maybe JavaFX is unable to render them? I know for sure that JavaFX is unable to synthesize italic and bold fonts from original one. If you find anything useful on that, please tell me so that I can update the article.

        On the second issue, I’ve updated my post. I believe there is nothing to modify in pdf.worker.js.map and pdf.js.map. The only modification needed seems to do the exact modification on viewer.js and viewer.js.map.

    6. Hi Samir,
      Don’t know how to thank you. I just blindly did what you have mentioned and it worked like a charm on first hit. Kudos Sam !!! Thanks a ton for this article. 🙂
      Just a quick query, do you know how we can tweak PDF.js to pre supply the search term and highlight the words on render of pdf ? I assume I need to trigger some javascript function from java code by supplying the search term. Any help is highly appreciated.
      Thanks,
      Sai Dandem.

      1. Hi,

        I’m glad this article helped you.
        Regarding your question, I’m afraid I’m no expert on PDF.js itself. You should go to the official documentation or on their github (https://github.com/mozilla/pdf.js/). Maybe you could find some help over there.
        The trick is to get the right javascript method, and then call it from JavaFX like I did with the “changeLanguage” method. If you find the solution, don’t hesitate to ping me so I can add it to the article.
        Regards,

    Leave a Reply

    Your email address will not be published. Required fields are marked *