19

I'm new to ES6 and Promise. I'm trying pdf.js to extract texts from all pages of a pdf file into a string array. And when extraction is done, I want to parse the array somehow. Say pdf file(passed via typedarray correctly) has 4 pages and my code is:

let str = [];
PDFJS.getDocument(typedarray).then(function(pdf) {
  for(let i = 1; i <= pdf.numPages; i++) {
    pdf.getPage(i).then(function(page) {
      page.getTextContent().then(function(textContent) {
        for(let j = 0; j < textContent.items.length; j++) {
          str.push(textContent.items[j].str);
        }
        parse(str);
      });
    });
  }
});

It manages to work, but, of course, the problem is my parse function is called 4 times. I just want to call parse only after all 4-pages-extraction is done.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Sangbok Lee
  • 2,132
  • 3
  • 15
  • 33
  • 1
    Similar to http://stackoverflow.com/a/40494019/1765767 -- collect page promises using Promise.all and don't forget to chain then's. – async5 Nov 16 '16 at 16:24
  • @async5 It works! I first tried [this](http://stackoverflow.com/a/28875245/6153990) and it worked with slight modification, but the answer you provided looks more correct. Please reply it as an answer so that I can accept it. Thank you! – Sangbok Lee Nov 17 '16 at 15:26

6 Answers6

29

Similar to https://stackoverflow.com/a/40494019/1765767 -- collect page promises using Promise.all and don't forget to chain then's:

function gettext(pdfUrl){
  var pdf = pdfjsLib.getDocument(pdfUrl);
  return pdf.then(function(pdf) { // get all pages text
    var maxPages = pdf.pdfInfo.numPages;
    var countPromises = []; // collecting all page promises
    for (var j = 1; j <= maxPages; j++) {
      var page = pdf.getPage(j);

      var txt = "";
      countPromises.push(page.then(function(page) { // add page promise
        var textContent = page.getTextContent();
        return textContent.then(function(text){ // return content promise
          return text.items.map(function (s) { return s.str; }).join(''); // value page text 
        });
      }));
    }
    // Wait for all pages and join text
    return Promise.all(countPromises).then(function (texts) {
      return texts.join('');
    });
  });
}

// waiting on gettext to finish completion, or error
gettext("https://cdn.mozilla.net/pdfjs/tracemonkey.pdf").then(function (text) {
  alert('parse ' + text);
}, 
function (reason) {
  console.error(reason);
});
<script src="https://npmcdn.com/pdfjs-dist/build/pdf.js"></script>
Ben Gripka
  • 16,012
  • 6
  • 45
  • 41
async5
  • 2,505
  • 1
  • 20
  • 27
21

A bit more cleaner version of @async5 and updated according to the latest version of "pdfjs-dist": "^2.0.943"

import PDFJS from "pdfjs-dist";
import PDFJSWorker from "pdfjs-dist/build/pdf.worker.js"; // add this to fit 2.3.0

PDFJS.disableTextLayer = true;
PDFJS.disableWorker = true; // not availaible anymore since 2.3.0 (see imports)

const getPageText = async (pdf: Pdf, pageNo: number) => {
  const page = await pdf.getPage(pageNo);
  const tokenizedText = await page.getTextContent();
  const pageText = tokenizedText.items.map(token => token.str).join("");
  return pageText;
};

/* see example of a PDFSource below */
export const getPDFText = async (source: PDFSource): Promise<string> => {
  Object.assign(window, {pdfjsWorker: PDFJSWorker}); // added to fit 2.3.0
  const pdf: Pdf = await PDFJS.getDocument(source).promise;
  const maxPages = pdf.numPages;
  const pageTextPromises = [];
  for (let pageNo = 1; pageNo <= maxPages; pageNo += 1) {
    pageTextPromises.push(getPageText(pdf, pageNo));
  }
  const pageTexts = await Promise.all(pageTextPromises);
  return pageTexts.join(" ");
};

This is the corresponding typescript declaration file that I have used if anyone needs it.

declare module "pdfjs-dist";

type TokenText = {
  str: string;
};

type PageText = {
  items: TokenText[];
};

type PdfPage = {
  getTextContent: () => Promise<PageText>;
};

type Pdf = {
  numPages: number;
  getPage: (pageNo: number) => Promise<PdfPage>;
};

type PDFSource = Buffer | string;

declare module 'pdfjs-dist/build/pdf.worker.js'; // needed in 2.3.0

Example of how to get a PDFSource from a File with Buffer (from node types) :

file.arrayBuffer().then((ab: ArrayBuffer) => {
  const pdfSource: PDFSource = Buffer.from(ab);
});
j3ff
  • 5,719
  • 8
  • 38
  • 51
Atul
  • 2,170
  • 23
  • 24
7

Here's a shorter (not necessarily better) version:

async function getPdfText(data) {
    let doc = await pdfjsLib.getDocument({data}).promise;
    let pageTexts = Array.from({length: doc.numPages}, async (v,i) => {
        return (await (await doc.getPage(i+1)).getTextContent()).items.map(token => token.str).join('');
    });
    return (await Promise.all(pageTexts)).join('');
}

Here, data is a string or ArrayBuffer (or you could change it to take the url, etc., instead).

So, just import pdf.js:

let pdfjsLib = await import("https://cdn.jsdelivr.net/npm/pdfjs-dist@3.6.172/+esm").then(m => m.default);
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.6.172/build/pdf.worker.min.js";
// or via npm, unpkg, etc.

And use it like this:

let buffer = await fetch("https://arxiv.org/pdf/2305.07617.pdf").then(r => r.arrayBuffer());
let text = await getPdfText(buffer);
joe
  • 3,752
  • 1
  • 32
  • 41
voracity
  • 811
  • 1
  • 10
  • 5
2

Here's another Typescript version with await and Promise.all based on the other answers:

import { getDocument } from "pdfjs-dist";
import {
  DocumentInitParameters,
  PDFDataRangeTransport,
  TypedArray,
} from "pdfjs-dist/types/display/api";

export const getPdfText = async (
  src: string | TypedArray | DocumentInitParameters | PDFDataRangeTransport
): Promise<string> => {
  const pdf = await getDocument(src).promise;

  const pageList = await Promise.all(Array.from({ length: pdf.numPages }, (_, i) => pdf.getPage(i + 1)));

  const textList = await Promise.all(pageList.map((p) => p.getTextContent()));

  return textList
    .map(({ items }) => items.map(({ str }) => str).join(""))
    .join("");
};
Westy92
  • 19,087
  • 4
  • 72
  • 54
audunru
  • 41
  • 3
1

I wouldn't know how to do it either, but thanks to async5 I did it. I copied his code and updated it to the new version of pdf.js. I made minimal corrections and also took the liberty of not grouping all the pages into a single string. In addition, I used a regular expression that removes many of the empty spaces that PDF unfortunately ends up creating (it does not solve all cases, but the vast majority). The way I did it should be the way that most will feel comfortable working, however, feel free to remove the regex or make any other changes.

// pdf-to-text.js v1, require pdf.js ( https://mozilla.github.io/pdf.js/getting_started/#download )
// load pdf.js and pdf.worker.js
function pdfToText(url, separator = ' ') {
    let pdf = pdfjsLib.getDocument(url);
    return pdf.promise.then(function(pdf) { // get all pages text
        let maxPages = pdf._pdfInfo.numPages;
        let countPromises = []; // collecting all page promises
        for (let i = 1; i <= maxPages; i++) {
            let page = pdf.getPage(i);
            countPromises.push(page.then(function(page) { // add page promise
                let textContent = page.getTextContent();
                return textContent.then(function(text) { // return content promise
                    return text.items.map(function(obj) {
                        return obj.str;
                    }).join(separator); // value page text
                });
            }));
        };
        // wait for all pages and join text
        return Promise.all(countPromises).then(function(texts) {
            for(let i = 0; i < texts.length; i++){
                texts[i] = texts[i].replace(/\s+/g, ' ').trim();
            };
            return texts;
        });
    });
};

// example of use:
// waiting on pdfToText to finish completion, or error
pdfToText('files/pdf-name.pdf').then(function(pdfTexts) {
    console.log(pdfTexts);
    // RESULT: ['TEXT-OF-PAGE-1', 'TEXT-OF-PAGE-2', ...]
}, function(reason) {
    console.error(reason);
});
Luis Lobo
  • 489
  • 4
  • 7
0

If you use the PDFViewer component, here is my solution that doesn't involve any promise or asynchrony:

function getDocumentText(viewer) {
    let text = '';
    for (let i = 0; i < viewer.pagesCount; i++) {
        const { textContentItemsStr } = viewer.getPageView(i).textLayer;
        for (let item of textContentItemsStr)
            text += item;
    }
    return text;
}
JacopoStanchi
  • 1,962
  • 5
  • 33
  • 61