How to extract text from a PDF in JavaScript

Viewed 94376

I wonder if is possible to get the text inside of a PDF file by using only Javascript? If yes, can anyone show me how?

I know there are some server-side java, c#, etc libraries but I would prefer not using a server. thanks

8 Answers

Just leaving here a full working sample.

<html>
    <head>
        <script src="https://npmcdn.com/pdfjs-dist/build/pdf.js"></script>
    </head>
    <body>
        <input id="pdffile" name="pdffile" type="file" />
        <button id="btn" onclick="convert()">Process</button>
        <div id="result"></div>
    </body>
</html>

<script>

    function convert() {
        var fr=new FileReader();
        var pdff = new Pdf2TextClass();
        fr.onload=function(){
            pdff.pdfToText(fr.result, null, (text) => { document.getElementById('result').innerText += text; });
        }
        fr.readAsDataURL(document.getElementById('pdffile').files[0])
        
    }

    function Pdf2TextClass() {
        var self = this;
        this.complete = 0;

        this.pdfToText = function (data, callbackPageDone, callbackAllDone) {
            console.assert(data instanceof ArrayBuffer || typeof data == 'string');
            var loadingTask = pdfjsLib.getDocument(data);
            loadingTask.promise.then(function (pdf) {


                var total = pdf._pdfInfo.numPages;
                //callbackPageDone( 0, total );        
                var layers = {};
                for (i = 1; i <= total; i++) {
                    pdf.getPage(i).then(function (page) {
                        var n = page.pageNumber;
                        page.getTextContent().then(function (textContent) {

                            //console.log(textContent.items[0]);0
                            if (null != textContent.items) {
                                var page_text = "";
                                var last_block = null;
                                for (var k = 0; k < textContent.items.length; k++) {
                                    var block = textContent.items[k];
                                    if (last_block != null && last_block.str[last_block.str.length - 1] != ' ') {
                                        if (block.x < last_block.x)
                                            page_text += "\r\n";
                                        else if (last_block.y != block.y && (last_block.str.match(/^(\s?[a-zA-Z])$|^(.+\s[a-zA-Z])$/) == null))
                                            page_text += ' ';
                                    }
                                    page_text += block.str;
                                    last_block = block;
                                }

                                textContent != null && console.log("page " + n + " finished."); //" content: \n" + page_text);
                                layers[n] = page_text + "\n\n";
                            }
                            ++self.complete;
                            //callbackPageDone( self.complete, total );
                            if (self.complete == total) {
                                window.setTimeout(function () {
                                    var full_text = "";
                                    var num_pages = Object.keys(layers).length;
                                    for (var j = 1; j <= num_pages; j++)
                                        full_text += layers[j];
                                    callbackAllDone(full_text);
                                }, 1000);
                            }
                        }); // end  of page.getTextContent().then
                    }); // end of page.then
                } // of for
            });
        }; // end of pdfToText()
    }; // end of class

</script>

Note: This code assumes you're using nodejs. That means you're parsing a local file instead of one from a web page since the original question doesn't explicitly ask about parsing pdfs on a web page.

@gm2008's answer was a great starting point (please read it and its comments for more info), but needed some updates (08/19) and had some unused code. I also like examples that are more full. There's more refactoring and tweaking that could be done (e.g. with await), but for now it's as close to that original answer as it could be.

As before, this uses Mozilla's PDFjs library. The npmjs package is at https://www.npmjs.com/package/pdfjs-dist.

In my experience, this doesn't do well in finding where to put spaces, but that's a problem for another time.

[Edit: I believe the update to the use of .transform has restored the whitespace as it originally behaved.]

// This file is called myPDFfileToText.js and is in the root folder
let PDFJS = require('pdfjs-dist');

let pathToPDF = 'path/to/myPDFfileToText.pdf';

let toText = Pdf2TextObj();
let onPageDone = function() {}; // don't want to do anything between pages
let onFinish = function(fullText) { console.log(fullText) };
toText.pdfToText(pathToPDF, onPageDone, onFinish);

function Pdf2TextObj() {
    let self = this;
    this.complete = 0;

    /**
     *
     * @param path Path to the pdf file.
     * @param callbackPageDone To inform the progress each time
     *        when a page is finished. The callback function's input parameters are:
     *        1) number of pages done.
     *        2) total number of pages in file.
     *        3) the `page` object itself or null.
     * @param callbackAllDone Called after all text has been collected. Input parameters:
     *        1) full text of parsed pdf.
     *
     */
    this.pdfToText = function(path, callbackPageDone, callbackAllDone) {
        // console.assert(typeof path == 'string');
        PDFJS.getDocument(path).promise.then(function(pdf) {

            let total = pdf.numPages;
            callbackPageDone(0, total, null);

            let pages = {};
            // For some (pdf?) reason these don't all come in consecutive
            // order. That's why they're stored as an object and then
            // processed one final time at the end.
            for (let pagei = 1; pagei <= total; pagei++) {
                pdf.getPage(pagei).then(function(page) {
                    let pageNumber = page.pageNumber;
                    page.getTextContent().then(function(textContent) {
                        if (null != textContent.items) {
                            let page_text = "";
                            let last_item = null;
                            for (let itemsi = 0; itemsi < textContent.items.length; itemsi++) {
                                let item = textContent.items[itemsi];
                                // I think to add whitespace properly would be more complex and
                                // would require two loops.
                                if (last_item != null && last_item.str[last_item.str.length - 1] != ' ') {
                                    let itemX = item.transform[5]
                                    let lastItemX = last_item.transform[5]
                                    let itemY = item.transform[4]
                                    let lastItemY = last_item.transform[4]
                                    if (itemX < lastItemX)
                                        page_text += "\r\n";
                                    else if (itemY != lastItemY && (last_item.str.match(/^(\s?[a-zA-Z])$|^(.+\s[a-zA-Z])$/) == null))
                                        page_text += ' ';
                                } // ends if may need to add whitespace

                                page_text += item.str;
                                last_item = item;
                            } // ends for every item of text

                            textContent != null && console.log("page " + pageNumber + " finished.") // " content: \n" + page_text);
                            pages[pageNumber] = page_text + "\n\n";
                        } // ends if has items

                        ++self.complete;

                        callbackPageDone(self.complete, total, page);


                        // If all done, put pages in order and combine all
                        // text, then pass that to the callback
                        if (self.complete == total) {
                            // Using `setTimeout()` isn't a stable way of making sure 
                            // the process has finished. Watch out for missed pages.
                            // A future version might do this with promises.
                            setTimeout(function() {
                                let full_text = "";
                                let num_pages = Object.keys(pages).length;
                                for (let pageNum = 1; pageNum <= num_pages; pageNum++)
                                    full_text += pages[pageNum];
                                callbackAllDone(full_text);
                            }, 1000);
                        }
                    }); // ends page.getTextContent().then
                }); // ends page.then
            } // ends for every page
        });
    }; // Ends pdfToText()

    return self;
}; // Ends object factory

Run in the terminal:

node myPDFfileToText.js

Updated 02/2021

<script src="https://npmcdn.com/pdfjs-dist/build/pdf.js"></script>
    <script>
    
function Pdf2TextClass(){
    var self = this;
    this.complete = 0;

    this.pdfToText = function(data, callbackPageDone, callbackAllDone){
    console.assert( data  instanceof ArrayBuffer  || typeof data == 'string' );
    var loadingTask = pdfjsLib.getDocument(data);
    loadingTask.promise.then(function(pdf) {


    var total = pdf._pdfInfo.numPages;
    //callbackPageDone( 0, total );        
    var layers = {};        
    for (i = 1; i <= total; i++){
       pdf.getPage(i).then( function(page){
       var n = page.pageNumber;
       page.getTextContent().then( function(textContent){
       
       //console.log(textContent.items[0]);0
         if( null != textContent.items ){
           var page_text = "";
           var last_block = null;
           for( var k = 0; k < textContent.items.length; k++ ){
               var block = textContent.items[k];
               if( last_block != null && last_block.str[last_block.str.length-1] != ' '){
                   if( block.x < last_block.x )
                       page_text += "\r\n"; 
                   else if ( last_block.y != block.y && ( last_block.str.match(/^(\s?[a-zA-Z])$|^(.+\s[a-zA-Z])$/) == null ))
                       page_text += ' ';
               }
               page_text += block.str;
               last_block = block;
           }

           textContent != null && console.log("page " + n + " finished."); //" content: \n" + page_text);
           layers[n] =  page_text + "\n\n";
         }
         ++ self.complete;
         //callbackPageDone( self.complete, total );
         if (self.complete == total){
           window.setTimeout(function(){
             var full_text = "";
             var num_pages = Object.keys(layers).length;
             for( var j = 1; j <= num_pages; j++)
                 full_text += layers[j] ;
             console.log(full_text);
           }, 1000);              
         }
       }); // end  of page.getTextContent().then
     }); // end of page.then
   } // of for
 });
}; // end of pdfToText()
}; // end of class
var pdff = new Pdf2TextClass();
pdff.pdfToText('PDF_URL');
    </script>
Related