Show PDF in your application [Updated June 2021]

Edit : This article has been updated to be compliant with PDF.js version 2.8.335. For Java 8 and at least 11 you need to use the “Prebuilt (for older browsers)” version of PDFJS because the WebView does not seem to support ES6 features used in the “normal” PDFJS version. This tutorial has been tested using Java 8, 11, and early release of OpenJFX 17.

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 stable version and place it under src/main/resources. For Java 8 and at least 11, use the “Prebuilt (for older browsers)” version as it is guaranteed to work with the WebView. Even with OpenJFX 17, you can use the older browsers version as it will be working.

    You can delete viewer.js.map as we won’t need it. You can also delete the sample pdf file compressed.tracemonkey-pldi-09.pdf as we won’t need it neither.
    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 viewer.js, replace

    workerSrc: {
        value: "../build/pdf.worker.js",
        kind: OptionKind.WORKER
      }
    

    by

    workerSrc: {
        value: "pdf.worker.js",
        kind: OptionKind.WORKER
      }
    

    and

    cMapUrl: {
        value: "../web/cmaps/",
        kind: OptionKind.API
      },
    

    by

    cMapUrl: {
        value: "cmaps/",
        kind: OptionKind.API
      },
    

    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 defaultUrl value.

    defaultUrl: {
        value: "",
        kind: OptionKind.VIEWER
      },
    

    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 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 (because 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();
                // If the above is not working, one may try getClass().getResource("pdfjs/web/viewer.html").toURI().toString();
                // 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) {
                ex.printStackTrace();
           } finally {
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            }
    

    (This code should be called when the webView.getEngine().getLoadWorker().getState() is in READY or SUCCEEDED state)
    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() + "')");
    }
    

    A full working example can be found here https://github.com/Searen1984/pdfjs4JFX. (Courtesy of Alexander Schmidts, thanks!)

    Cheers

    43 thoughts on “Show PDF in your application [Updated June 2021]”

    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 + “‘)”);

            }
            }
            });

            1. Be sure to remove the listener once the page is loaded if you don’t need it.

              final AtomicReference<ChangeListener> ref = new AtomicReference();
              final ChangeListener listener = (observable, oldValue, newValue) -> {
              if (newValue == State.SUCCEEDED) {
              try {
              final byte[] data = FileUtils.readFileToByteArray(fileHandle);
              final String base64 = Base64.getEncoder().encodeToString(data);
              webEngine.executeScript(“openFileFromBase64(‘” + base64 + “‘)”);
              } catch (final Exception e) {
              logger.error(“Failed to load pdf”, e);
              } finally {
              webEngine.getLoadWorker().stateProperty().removeListener(ref.get());
              }
              }
              };
              ref.set(listener);
              webEngine.getLoadWorker().stateProperty().addListener(listener);

    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,

    7. Hi Max, Thanks for this article.
      I have been trying to follow the steps you described but still cant get it to work: I get the following exception:
      Caused by: netscape.javascript.JSException: ReferenceError: Can’t find variable: PDFViewerApplication
      do you have any idea?

      1. Hum that does not ring any bells to me.

        PDFViewerApplication seems to be declared in viewer.js.
        Where does this exception comes from? From the openFileFromBase64 method in viewer.html ? Maybe you’re calling this method too early before viewer.js is fully initialized?

        1. Yeah its from openFileFromBase64 . In fact I load the viewer in the css in the initialize method. I use a button to load a file from the computer and execute the script

          1. sorry i meant: In fact I load the viewer and the css in the initialize method. I use a button to load a file from the computer and execute the script

            1. I had the same issue as Eric. For me it was a dumb mistake in “viewer.html”. I used
              src=”../pdf.js”
              when I should have been using
              src=”pdf.js”

    8. Hi,

      Thank you very much for this post. So far it is the best solution I’ve found to view PDF files.
      However, I have some issues and I was wondering whether you would be able to help.

      I am using JavaFX 11 and pdfjs-2.1.266-dist. I think I followed all the steps and if I launch the viewer.html in Chrome (via IntelliJ webserver to view HTML files) and call openFileFromBase64(‘…’) my file renders just fine. When I pass the same file to WebView parts of text in my PDF are simply floating at the beginning of PDF file. Everything else, apart selecting text which, I think, requires listeners to be handled anyway, appears to work fine. Some other PDF files seem to render fine as well. My guess would be that some JS functions for rendering have not been loaded and possibly that’s due to relative path as well.

      Has anyone come across a similar issue or has any suggestions of what could be going wrong?

    9. I’ve tried PdfJs as you described it. It runs fine in my Eclipse environment. Then I package the java code in a jar, but keeping Pdf.js files outside the jar to avoid issues with loading files from a jar. No suddenly my JVM crashes as soon as I load the viewer.html in the WebView. It has something to do with the viewer.js but no idea what. There is nothing in the log nor in the javascript console log; probably because the JVM crashes hard and has no change to produce any l0gging..
      Does anyone have any suggestions? (I run OpenJDK JavaFX version 11.0.2, but also tried older and newer versions; all with the same result.)

      1. That’s odd. If the JVM crash, you should report it to the JavaFX team. You should reproduce the issue in the smallest way possible so that they could investigate on that.
        Regards,

        1. I found a workaround for the crashing JVM that suits me; (and perhaps others too).
          My requirements are low: just to display a PDF in my JavaFX application.
          I now use IcePDF core to generate a java.awt.image.BufferedImage for every page of my PDF.
          See: https://www.icesoft.org/wiki/display/PDF/Converting+PDF+Page+Renderings

          Then I convert this to javaFX:
          javafx.scene.image.Image fxImage = SwingFXUtils.toFXImage(bufferedImage, null);
          Now I wrap this in a javafx.scene.image.ImageView to display the page in my application.

    10. Hello. I tried it corresponding to the instructions. However, I am running into “can’t find variable: PDFViewerApplication”. I checked the paths and I made sure that the custom script is loaded after the script viewer.js. It seems like the problem occurs with the latest release v2.7.570. Could you please check this? I spent hours and I do not know what to do anymore…
      Regards

      1. Hello,

        Have you taken the “Prebuilt (for older browsers)” version? I’ve tested the normal one and it’s indeed not working. I just tested with 2.7.270 for older browsers and it seems to work (using Java 8). I will update the article since the variable have changed a bit.

        1. Hello,

          I could solve it and I can confirm.

          The solution: Just use the legacy build, e.g. pdfjs-2.8.335-legacy-dist.zip from https://github.com/mozilla/pdf.js/releases/tag/v2.8.335.

          The problem (my buest guess): Starting with v2.6.x the given approach did not work anymore but there was no error message. Only starting with v2.7.x I also got the error message saying that “can’t find variable: PDFViewerApplication”. Using the latest legace build works. Therefore, PDF.JS must be using some JavaScript features starting with build v2.6.x which are not supported from my OpenJFX version 11.0.2. Fast Googling brought me to this discussion:
          https://stackoverflow.com/questions/54344423/javafx-11-not-supporting-ecmascript6-and-css3

          So one guess is that PDF.JS started using some ES6 features with build v2.6.x which are not supported and might be supported with later Java versions. In detail it seems like the used WebKit of JavaFX 11.0.2: WebKit Version 606.1 (Safari 12.x) seems to miss some features. But I did not try with a later Java version to get more confidence on this best guess.

          1. I’m happy you found a solution. Thanks for the investigation.

            I’ll update the article to clear that out. I will also test in the upcoming weeks the solution using OpenJDK 16 and OpenJFX 16 to see if it’s possible to use the “normal” PDFJS. I’ll then provide an update on the article explaining both solutions.

          2. I’ve updated the article. However, I tested the normal PDFJS version “2.8.335” and I can make it work with OpenJDK 16 and OpenJFX 16, but also with OpenJDK 11 and OpenJFX 11 apparently.

            So I’m unsure whether I did something wrong and I think I’m using 11.0.2 version but I’m not, or if you forget a step?

            1. Hello again,

              I tried exactly according to your tutorial and actually the Java code does not compile. So I fixed it and then I got the error “ReferenceError: Can’t find variable: PDFViewerApplication” again with Java 11. I guess you were either using the legacy build or the newer JDK. To make this easier I put two projects on GitHub. One with legacy and one with non-legacy pdfjs build. But first here are the problems I found for the Java source code using your tutorial:
              1. The exceptions on line 12 and 18 should be “ex…” and not only “e…”
              2. The referenced resource “pdfjs/web/viewer.html” should be “/web/viewer.html”
              3. The referenced resource “pdfjs/web.css” should be “/web/viewer.css”
              4. In a jar viewer.html (and perhaps also viewer.css) cannot be found using external path so I used this instead:
              getClass().getResource(“/web/viewer.html”).toURI().toString();
              getClass().getResource(“/web/viewer.css”).toURI().toString();
              5. The javascript to load the pdf must only be executed in the ready state of the worker load state

              To make it easier for you to check the “PDFViewerApplication” error here is the Github project which uses the normal pdfjs build 2.8.335 and Java 11:
              https://github.com/Searen1984/pdfjs_no_legacy_build

              And I made also a working example according to your tutorial and my mentioned changes above which you could reference in your article and also copy the code from:
              https://github.com/Searen1984/pdfjs4JFX

          3. You are right, I did not run the thing with the proper JDK. I do not know when the webview becomes compatible with PDFJS but I’ve tested with JDK 16 and it’s working.

            I have updated a bit the article including your modifications and also a link to the GitHub page for the example.

            Thanks!

            1. Thanks for this excellent article! When I build the linked github project using build/build artifact to create a single executable jar. The PDF takes ages to load. However, when running through intellij it is almost instant. Do you know how I can speed this up in a single executable Jar?

            2. Hi. I think there might be different reasons possible. I do not have so much time to support but here are some points I could think of:
              1. IntelliJ might provide some packages which are loaded during startup of IntelliJ. When you execute the Jar those packages are loaded also during startup which might be the time consumption you see. So I would try to execute what you are doing in your Jar twice during the same JRE instance and measure the time. If the second time is fast then you know that the first time something was loaded.
              2. I could also think of some security issues, e.g. IntelliJ could have specific allowances configured in your virus scanner. Therefore, the Jar might be checked first before or during execution if it loads something. I would try to check the same as above.

              Best wishes,
              Alex

            3. That’s odd. I did not experienced that. Maybe you should check on Visual VM or other profiling tool to see what could be the issue?

    Leave a Reply to Sai Pradeep Dandem Cancel reply

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