From 737ed8417472b9c5084f963de95e3f87779dd7b2 Mon Sep 17 00:00:00 2001
From: Yury Delendik <ydelendik@mozilla.com>
Date: Mon, 9 Apr 2012 22:20:57 -0700
Subject: [PATCH] Initial API implementation

---
 src/api.js      | 141 +++++++++++++++++++++++++++++++
 src/util.js     |  17 +++-
 web/viewer.html |   1 +
 web/viewer.js   | 220 +++++++++++++++++++++++++++---------------------
 4 files changed, 281 insertions(+), 98 deletions(-)
 create mode 100644 src/api.js

diff --git a/src/api.js b/src/api.js
new file mode 100644
index 000000000..a8bb5fb65
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,141 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+(function pdfApiWrapper() {
+  function PdfPageWrapper(page) {
+    this.page = page;
+  }
+  PdfPageWrapper.prototype = {
+    get width() {
+      return this.page.width;
+    },
+    get height() {
+      return this.page.height;
+    },
+    get stats() {
+      return this.page.stats;
+    },
+    get ref() {
+      return this.page.ref;
+    },
+    get view() {
+      return this.page.view;
+    },
+    rotatePoint: function(x, y) {
+      return this.page.rotatePoint(x, y);
+    },
+    getAnnotations: function() {
+      var promise = new PDFJS.Promise();
+      var annotations = this.page.getAnnotations();
+      promise.resolve(annotations);
+      return promise;
+    },
+    render: function(renderContext) {
+      var promise = new PDFJS.Promise();
+      this.page.startRendering(renderContext.canvasContext,
+        function complete(error) {
+          if (error)
+            promise.reject(error);
+          else
+            promise.resolve();
+        },
+        renderContext.textLayer);
+      return promise;
+    },
+    getTextContent: function() {
+      var promise = new PDFJS.Promise();
+      var textContent = 'page text'; // not implemented
+      promise.resolve(textContent);
+      return promise;
+    },
+    getOperationList: function() {
+      var promise = new PDFJS.Promise();
+      var operationList = { // not implemented
+        dependencyFontsID: null,
+        operatorList: null
+      };
+      promise.resolve(operationList);
+      return promise;
+    }
+  };
+
+  function PdfDocumentWrapper(pdf) {
+    this.pdf = pdf;
+  }
+  PdfDocumentWrapper.prototype = {
+    get numPages() {
+      return this.pdf.numPages;
+    },
+    get fingerprint() {
+      return this.pdf.fingerPrint;
+    },
+    getPage: function(number) {
+      var promise = new PDFJS.Promise();
+      var page = this.pdf.getPage(number);
+      promise.resolve(new PdfPageWrapper(page));
+      return promise;
+    },
+    getDestinations: function() {
+      var promise = new PDFJS.Promise();
+      var destinations = this.pdf.catalog.destinations;
+      promise.resolve(destinations);
+      return promise;
+    },
+    getOutline: function() {
+      var promise = new PDFJS.Promise();
+      var outline = this.pdf.catalog.documentOutline;
+      promise.resolve(outline);
+      return promise;
+    },
+    getMetadata: function() {
+      var promise = new PDFJS.Promise();
+      var info = this.pdf.info;
+      var metadata = this.pdf.catalog.metadata;
+      promise.resolve(info, metadata ? new PDFJS.Metadata(metadata) : null);
+      return promise;
+    }
+  };
+
+  PDFJS.getDocument = function getDocument(source) {
+    var promise = new PDFJS.Promise();
+    if (typeof source === 'string') {
+      // fetch url
+      PDFJS.getPdf(
+        {
+          url: source,
+          progress: function getPdfProgress(evt) {
+            if (evt.lengthComputable)
+              promise.progress({
+                loaded: evt.loaded,
+                total: evt.total
+              });
+          },
+          error: function getPdfError(e) {
+            promise.reject('Unexpected server response of ' +
+              e.target.status + '.');
+          }
+        },
+        function getPdfLoad(data) {
+          var pdf = null;
+          try {
+            pdf = new PDFJS.PDFDoc(data);
+          } catch (e) {
+            promise.reject('An error occurred while reading the PDF.', e);
+          }
+          if (pdf)
+            promise.resolve(new PdfDocumentWrapper(pdf));
+        });
+    } else {
+      // assuming the source is array, instantiating directly from it
+      var pdf = null;
+      try {
+        pdf = new PDFJS.PDFDoc(source);
+      } catch (e) {
+        promise.reject('An error occurred while reading the PDF.', e);
+      }
+      if (pdf)
+        promise.resolve(new PdfDocumentWrapper(pdf));
+    }
+    return promise;
+  };
+})();
diff --git a/src/util.js b/src/util.js
index de7f3c1d5..30fc799f9 100644
--- a/src/util.js
+++ b/src/util.js
@@ -275,7 +275,7 @@ function isPDFFunction(v) {
  * can be set. If any of these happens twice or the data is required before
  * it was set, an exception is throw.
  */
-var Promise = (function PromiseClosure() {
+var Promise = PDFJS.Promise = (function PromiseClosure() {
   var EMPTY_PROMISE = {};
 
   /**
@@ -297,6 +297,7 @@ var Promise = (function PromiseClosure() {
     }
     this.callbacks = [];
     this.errbacks = [];
+    this.progressbacks = [];
   };
   /**
    * Builds a promise that is resolved when all the passed in promises are
@@ -312,7 +313,7 @@ var Promise = (function PromiseClosure() {
       deferred.resolve(results);
       return deferred;
     }
-    for (var i = 0; i < unresolved; ++i) {
+    for (var i = 0, ii = promises.length; i < ii; ++i) {
       var promise = promises[i];
       promise.then((function(i) {
         return function(value) {
@@ -376,6 +377,13 @@ var Promise = (function PromiseClosure() {
       }
     },
 
+    progress: function Promise_progress(data) {
+      var callbacks = this.progressbacks;
+      for (var i = 0, ii = callbacks.length; i < ii; i++) {
+        callbacks[i].call(null, data);
+      }
+    },
+
     reject: function Promise_reject(reason) {
       if (this.isRejected) {
         error('A Promise can be rejected only once ' + this.name);
@@ -393,7 +401,7 @@ var Promise = (function PromiseClosure() {
       }
     },
 
-    then: function Promise_then(callback, errback) {
+    then: function Promise_then(callback, errback, progressback) {
       if (!callback) {
         error('Requiring callback' + this.name);
       }
@@ -410,6 +418,9 @@ var Promise = (function PromiseClosure() {
         if (errback)
           this.errbacks.push(errback);
       }
+
+      if (progressback)
+        this.progressbacks.push(progressback);
     }
   };
 
diff --git a/web/viewer.html b/web/viewer.html
index d275f77c1..ef61ce697 100644
--- a/web/viewer.html
+++ b/web/viewer.html
@@ -11,6 +11,7 @@
         <!-- PDFJSSCRIPT_INCLUDE_BUILD -->
         <script type="text/javascript" src="../src/core.js"></script> <!-- PDFJSSCRIPT_REMOVE_CORE -->
         <script type="text/javascript" src="../src/util.js"></script>  <!-- PDFJSSCRIPT_REMOVE_CORE -->
+        <script type="text/javascript" src="../src/api.js"></script>  <!-- PDFJSSCRIPT_REMOVE_CORE -->
         <script type="text/javascript" src="../src/metadata.js"></script>  <!-- PDFJSSCRIPT_REMOVE_CORE -->
         <script type="text/javascript" src="../src/canvas.js"></script>  <!-- PDFJSSCRIPT_REMOVE_CORE -->
         <script type="text/javascript" src="../src/obj.js"></script>  <!-- PDFJSSCRIPT_REMOVE_CORE -->
diff --git a/web/viewer.js b/web/viewer.js
index 3587c96bd..3ca4f805f 100644
--- a/web/viewer.js
+++ b/web/viewer.js
@@ -318,27 +318,25 @@ var PDFView = {
     }
 
     var self = this;
-    PDFJS.getPdf(
-      {
-        url: url,
-        progress: function getPdfProgress(evt) {
-          if (evt.lengthComputable)
-            self.progress(evt.loaded / evt.total);
-        },
-        error: function getPdfError(e) {
-          var loadingIndicator = document.getElementById('loading');
-          loadingIndicator.textContent = 'Error';
-          var moreInfo = {
-            message: 'Unexpected server response of ' + e.target.status + '.'
-          };
-          self.error('An error occurred while loading the PDF.', moreInfo);
-        }
-      },
-      function getPdfLoad(data) {
-        self.loading = true;
-        self.load(data, scale);
+    self.loading = true;
+    PDFJS.getDocument(url).then(
+      function getDocumentCallback(pdfDocument) {
+        self.load(pdfDocument, scale);
         self.loading = false;
-      });
+      },
+      function getDocumentError(message, exception) {
+        var loadingIndicator = document.getElementById('loading');
+        loadingIndicator.textContent = 'Error';
+        var moreInfo = {
+          message: message
+        };
+        self.error('An error occurred while loading the PDF.', moreInfo);
+        self.loading = false;
+      },
+      function getDocumentProgress(progressData) {
+        self.progress(progressData.loaded / progressData.total);
+      }
+    );
   },
 
   download: function pdfViewDownload() {
@@ -461,7 +459,7 @@ var PDFView = {
     PDFView.loadingBar.percent = percent;
   },
 
-  load: function pdfViewLoad(data, scale) {
+  load: function pdfViewLoad(pdfDocument, scale) {
     function bindOnAfterDraw(pageView, thumbnailView) {
       // when page is painted, using the image as thumbnail base
       pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() {
@@ -489,14 +487,8 @@ var PDFView = {
     while (container.hasChildNodes())
       container.removeChild(container.lastChild);
 
-    var pdf;
-    try {
-      pdf = new PDFJS.PDFDoc(data);
-    } catch (e) {
-      this.error('An error occurred while reading the PDF.', e);
-    }
-    var pagesCount = pdf.numPages;
-    var id = pdf.fingerprint;
+    var pagesCount = pdfDocument.numPages;
+    var id = pdfDocument.fingerprint;
     var storedHash = null;
     document.getElementById('numPages').textContent = pagesCount;
     document.getElementById('pageNumber').max = pagesCount;
@@ -514,30 +506,68 @@ var PDFView = {
     var pages = this.pages = [];
     var pagesRefMap = {};
     var thumbnails = this.thumbnails = [];
-    for (var i = 1; i <= pagesCount; i++) {
-      var page = pdf.getPage(i);
-      var pageView = new PageView(container, page, i, page.width, page.height,
-                                  page.stats, this.navigateTo.bind(this));
-      var thumbnailView = new ThumbnailView(sidebar, page, i,
-                                            page.width / page.height);
-      bindOnAfterDraw(pageView, thumbnailView);
+    var pagePromises = [];
+    for (var i = 1; i <= pagesCount; i++)
+      pagePromises.push(pdfDocument.getPage(i));
+    var self = this;
+    var pagesPromise = PDFJS.Promise.all(pagePromises);
+    pagesPromise.then(function(promisedPages) {
+      for (var i = 1; i <= pagesCount; i++) {
+        var page = promisedPages[i - 1];
+        var pageView = new PageView(container, page, i, page.width, page.height,
+                                    page.stats, self.navigateTo.bind(self));
+        var thumbnailView = new ThumbnailView(sidebar, page, i,
+                                              page.width / page.height);
+        bindOnAfterDraw(pageView, thumbnailView);
 
-      pages.push(pageView);
-      thumbnails.push(thumbnailView);
-      var pageRef = page.ref;
-      pagesRefMap[pageRef.num + ' ' + pageRef.gen + ' R'] = i;
-    }
+        pages.push(pageView);
+        thumbnails.push(thumbnailView);
+        var pageRef = page.ref;
+        pagesRefMap[pageRef.num + ' ' + pageRef.gen + ' R'] = i;
+      }
 
-    this.pagesRefMap = pagesRefMap;
-    this.destinations = pdf.catalog.destinations;
+      self.pagesRefMap = pagesRefMap;
+    });
 
-    if (pdf.catalog.documentOutline) {
-      this.outline = new DocumentOutlineView(pdf.catalog.documentOutline);
-      var outlineSwitchButton = document.getElementById('outlineSwitch');
-      outlineSwitchButton.removeAttribute('disabled');
-      this.switchSidebarView('outline');
-    }
+    var destinationsPromise = pdfDocument.getDestinations();
+    destinationsPromise.then(function(destinations) {
+      self.destinations = destinations;
+    });
 
+    // outline and initial view depends on destinations and pagesRefMap
+    PDFJS.Promise.all([pagesPromise, destinationsPromise]).then(function() {
+      pdfDocument.getOutline().then(function(outline) {
+        if (!outline)
+          return;
+
+        self.outline = new DocumentOutlineView(outline);
+        var outlineSwitchButton = document.getElementById('outlineSwitch');
+        outlineSwitchButton.removeAttribute('disabled');
+        self.switchSidebarView('outline');
+      });
+
+      self.setInitialView(storedHash, scale);
+    });
+
+    pdfDocument.getMetadata().then(function(info, metadata) {
+      self.documentInfo = info;
+      self.metadata = metadata;
+
+      var pdfTitle;
+      if (metadata) {
+        if (metadata.has('dc:title'))
+          pdfTitle = metadata.get('dc:title');
+      }
+
+      if (!pdfTitle && info && info['Title'])
+        pdfTitle = info['Title'];
+
+      if (pdfTitle)
+        document.title = pdfTitle + ' - ' + document.title;
+    });
+  },
+
+  setInitialView: function pdfViewSetInitialView(storedHash, scale) {
     // Reset the current scale, as otherwise the page's scale might not get
     // updated if the zoom level stayed the same.
     this.currentScale = 0;
@@ -558,24 +588,6 @@ var PDFView = {
       // Setting the default one.
       this.parseScale(kDefaultScale, true);
     }
-
-    this.metadata = null;
-    var metadata = pdf.catalog.metadata;
-    var info = this.documentInfo = pdf.info;
-    var pdfTitle;
-
-    if (metadata) {
-      this.metadata = metadata = new PDFJS.Metadata(metadata);
-
-      if (metadata.has('dc:title'))
-        pdfTitle = metadata.get('dc:title');
-    }
-
-    if (!pdfTitle && info && info['Title'])
-      pdfTitle = info['Title'];
-
-    if (pdfTitle)
-      document.title = pdfTitle + ' - ' + document.title;
   },
 
   setHash: function pdfViewSetHash(hash) {
@@ -711,12 +723,12 @@ var PDFView = {
   }
 };
 
-var PageView = function pageView(container, content, id, pageWidth, pageHeight,
+var PageView = function pageView(container, pdfPage, id, pageWidth, pageHeight,
                                  stats, navigateTo) {
   this.id = id;
-  this.content = content;
+  this.pdfPage = pdfPage;
 
-  var view = this.content.view;
+  var view = pdfPage.view;
   this.x = view.x;
   this.y = view.y;
   this.width = view.width;
@@ -748,7 +760,7 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight,
     div.appendChild(this.loadingIconDiv);
   };
 
-  function setupAnnotations(content, scale) {
+  function setupAnnotations(pdfPage, scale) {
     function bindLink(link, dest) {
       link.href = PDFView.getDestinationHash(dest);
       link.onclick = function pageViewSetupLinksOnclick() {
@@ -809,29 +821,30 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight,
       return container;
     }
 
-    var items = content.getAnnotations();
-    for (var i = 0; i < items.length; i++) {
-      var item = items[i];
-      switch (item.type) {
-        case 'Link':
-          var link = createElementWithStyle('a', item);
-          link.href = item.url || '';
-          if (!item.url)
-            bindLink(link, ('dest' in item) ? item.dest : null);
-          div.appendChild(link);
-          break;
-        case 'Text':
-          var comment = createCommentAnnotation(item.name, item);
-          if (comment)
-            div.appendChild(comment);
-          break;
+    pdfPage.getAnnotations().then(function(items) {
+      for (var i = 0; i < items.length; i++) {
+        var item = items[i];
+        switch (item.type) {
+          case 'Link':
+            var link = createElementWithStyle('a', item);
+            link.href = item.url || '';
+            if (!item.url)
+              bindLink(link, ('dest' in item) ? item.dest : null);
+            div.appendChild(link);
+            break;
+          case 'Text':
+            var comment = createCommentAnnotation(item.name, item);
+            if (comment)
+              div.appendChild(comment);
+            break;
+        }
       }
-    }
+    });
   }
 
   this.getPagePoint = function pageViewGetPagePoint(x, y) {
     var scale = PDFView.currentScale;
-    return this.content.rotatePoint(x / scale, y / scale);
+    return this.pdfPage.rotatePoint(x / scale, y / scale);
   };
 
   this.scrollIntoView = function pageViewScrollIntoView(dest) {
@@ -879,8 +892,8 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight,
       }
 
       var boundingRect = [
-        this.content.rotatePoint(x, y),
-        this.content.rotatePoint(x + width, y + height)
+        this.pdfPage.rotatePoint(x, y),
+        this.pdfPage.rotatePoint(x + width, y + height)
       ];
 
       if (scale && scale !== PDFView.currentScale)
@@ -948,7 +961,7 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight,
     // Rendering area
 
     var self = this;
-    this.content.startRendering(ctx, function pageViewDrawCallback(error) {
+    function pageViewDrawCallback(error) {
       if (self.loadingIconDiv) {
         div.removeChild(self.loadingIconDiv);
         delete self.loadingIconDiv;
@@ -964,9 +977,22 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight,
 
       cache.push(self);
       callback();
-    }, textLayer);
+    }
 
-    setupAnnotations(this.content, this.scale);
+    var renderContext = {
+      canvasContext: ctx,
+      textLayer: textLayer
+    };
+    this.pdfPage.render(renderContext).then(
+      function pdfPageRenderCallback() {
+        pageViewDrawCallback(null);
+      },
+      function pdfPageRenderError(error) {
+        pageViewDrawCallback(error);
+      }
+    );
+
+    setupAnnotations(this.pdfPage, this.scale);
     div.setAttribute('data-loaded', true);
   };
 
@@ -1397,7 +1423,11 @@ window.addEventListener('change', function webViewerChange(evt) {
 
     for (var i = 0; i < data.length; i++)
       uint8Array[i] = data.charCodeAt(i);
-    PDFView.load(uint8Array);
+
+    // TODO using blob instead?
+    PDFJS.getDocument(uint8Array).then(function(pdfDocument) {
+      PDFView.load(pdfDocument);
+    });
   };
 
   // Read as a binary string since "readAsArrayBuffer" is not yet