changeset 1652:fa9e6bf84958

integrating pdf.js into Stone Web viewer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 16 Nov 2020 20:47:53 +0100
parents 00674f3695f2
children 2e3b2ed239b9
files Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebApplication/pdf-viewer.js Applications/StoneWebViewer/WebAssembly/CMakeLists.txt Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake
diffstat 4 files changed, 250 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebApplication/index.html	Fri Nov 13 18:29:17 2020 +0100
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Mon Nov 16 20:47:53 2020 +0100
@@ -586,6 +586,10 @@
             <div v-if="status == 'waiting'" class="wvPaneOverlay">
               [ drop a series here ]
             </div>
+
+            <div v-if="status == 'pdf'" >
+              <pdf-viewer v-bind:prefix="canvasId + '-pdf'"></pdf-viewer>
+            </div>
                 
             <!--div v-if="status == 'video'" class="wvPaneOverlay">
               <video class="wvVideo" autoplay="" loop="" controls="" preload="auto" type="video/mp4"
@@ -606,14 +610,45 @@
     </script>
 
 
+    <script type="text/x-template" id="pdf-viewer">
+      <div style="position:absolute; left:0; top:0; width:100%; height:100%;">
+        <!-- "line-height: 0px" to fit height: https://stackoverflow.com/a/12616341/881731 -->
+        <div v-bind:id="prefix + '-container'"
+             style="position: absolute; left: 0; top: 0; width:100%;height:100%;overflow:auto;line-height: 0px;">
+          <canvas v-bind:id="prefix + '-canvas'"
+                  style="position: absolute; top:0px; left:0px;"></canvas>
+        </div>
+
+        <div class="wv-overlay">
+          <div class="wv-overlay-bottomleft wvPrintExclude">
+            <button class="btn btn-primary" @click="FitWidth()"><i class="fas fa-text-width"></i></button>
+            <button class="btn btn-primary" @click="FitHeight()"><i class="fas fa-text-height"></i></button>
+            <button class="btn btn-primary" @click="ZoomIn()"><i class="fas fa-search-plus"></i></button>
+            <button class="btn btn-primary" @click="ZoomOut()"><i class="fas fa-search-minus"></i></button>
+            <button class="btn btn-primary" @click="PreviousPage()">
+              <i class="fa fa-chevron-circle-left"></i>
+            </button>
+            &nbsp;&nbsp;{{currentPage}} / {{countPages}}&nbsp;&nbsp;
+            <button class="btn btn-primary" @click="NextPage()">
+              <i class="fa fa-chevron-circle-right"></i>
+            </button>
+          </div>
+        </div>
+      </div>
+    </script>
+
+    
+
     <script src="js/jquery-3.4.1.min.js"></script>
     <script src="js/bootstrap.min.js"></script>
     <script src="js/vue.min.js"></script>
     <script src="js/axios.min.js"></script>
+    <script src="js/pdf.js"></script>
     
     <script src="ua-parser.js"></script>
     
     <script src="stone.js"></script>
+    <script src="pdf-viewer.js"></script>   <!-- Must be before inclusion of "app.js" -->
     <script src="app.js"></script>
     <script src="print.js"></script>
   </body>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/StoneWebViewer/WebApplication/pdf-viewer.js	Mon Nov 16 20:47:53 2020 +0100
@@ -0,0 +1,198 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+
+// Loaded via <script> tag, create shortcut to access PDF.js exports.
+var pdfjsLib = window['pdfjs-dist/build/pdf'];
+
+// The workerSrc property shall be specified.
+pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.5.207/build/pdf.worker.min.js';
+
+
+
+var ZOOM_FACTOR = 1.3;
+var FIT_MARGIN = 10;
+
+Vue.component('pdf-viewer', {
+  props: [ 'prefix', 'pdf' ],  // "pdf" must correspond to a "Uint8Array"
+  template: '#pdf-viewer',
+  data: function() {
+    return {
+      container: null,
+      canvas: null,
+      ctx: null,
+      
+      scale: 1,
+      countPages: 0,
+      currentPage: 0,
+      pdfDoc: null,
+      isRendering: false,
+      pageNumPending: null
+    }
+  },
+  watch: {
+    pdf: function(newVal, oldVal) {
+      this.LoadPdf();
+    }
+  },
+  mounted: function() {
+    this.container = document.getElementById(this.prefix + '-container');
+    this.canvas = document.getElementById(this.prefix + '-canvas');
+    this.ctx = this.canvas.getContext('2d');
+
+    if (this.container === null ||
+        this.canvas === null ||
+        this.ctx === null) {
+      alert('Bad viewer configuration');
+    }
+
+    var that = this;
+    this.container.addEventListener('wheel', function(event) {
+      that.MouseWheel(event);
+    });
+  },
+  methods: {
+    NextPage: function() {
+      if (this.pdfDoc !== null &&
+          this.currentPage < this.pdfDoc.numPages) {
+        this.QueueRenderPage(this.currentPage + 1);
+      }
+    },
+    PreviousPage: function() {
+      if (this.pdfDoc !== null &&
+          this.currentPage > 1) {
+        this.QueueRenderPage(this.currentPage - 1);
+      }
+    },
+    FitWidth: function() {
+      if (this.pdfDoc !== null) {
+        var that = this;
+        this.pdfDoc.getPage(this.currentPage).then(function(page) {
+          // https://github.com/mozilla/pdf.js/issues/5628
+          // https://stackoverflow.com/a/21064102/881731
+          // https://stackoverflow.com/a/60008044/881731
+          var scrollbarWidth = window.innerWidth - document.body.clientWidth + FIT_MARGIN;
+          that.scale = (that.container.offsetWidth - scrollbarWidth) / page.getViewport({ scale: 1.0 }).width;
+          that.QueueRenderPage(that.currentPage);
+        });
+      }
+    },
+    FitHeight: function() {
+      if (this.pdfDoc !== null) {
+        var that = this;
+        this.pdfDoc.getPage(this.currentPage).then(function(page) {
+          // The computation below assumes that "line-height: 0px" CSS
+          // on the parent element of the canvas.
+          
+          // https://github.com/mozilla/pdf.js/issues/5628
+          var scrollbarHeight = window.innerHeight - document.body.clientHeight + FIT_MARGIN;
+          that.scale = (that.container.offsetHeight - scrollbarHeight) / page.getViewport({ scale: 1.0 }).height;
+          //that.scale = that.container.clientHeight / page.getViewport({ scale: 1.0 }).height;
+          that.QueueRenderPage(that.currentPage);
+        });
+      }
+    },
+    ZoomIn: function() {
+      this.scale *= ZOOM_FACTOR;
+      this.QueueRenderPage(this.currentPage);  
+    },
+    ZoomOut: function() {
+      this.scale /= ZOOM_FACTOR;
+      this.QueueRenderPage(this.currentPage);  
+    },
+    LoadPdf: function(pdf) {
+      var that = this;
+      pdfjsLib.getDocument(new Uint8Array(this.pdf)).promise.then(function(pdfDoc_) {
+        that.pdfDoc = pdfDoc_;
+        that.currentPage = 0;
+        that.countPages = pdfDoc_.numPages;
+        that.scale = 1;
+        that.isRendering = false;
+        that.pageNumPending = null;
+        
+        // Initial/first page rendering
+        that.RenderPage(1);
+      });
+    },
+    RenderPage: function(pageNum) {
+      var that = this;
+      
+      if (this.pdfDoc !== null &&
+          pageNum >= 1 &&
+          pageNum <= this.countPages) {
+        this.isRendering = true;
+        this.pdfDoc.getPage(pageNum).then(function(page) {
+          var viewport = page.getViewport({scale: that.scale});
+
+          that.canvas.height = viewport.height;
+          that.canvas.width = viewport.width;
+          
+          // Horizontal centering of the canvas. This requires CSS
+          // "position: relative" on the canvas element.
+          if (that.canvas.width < that.container.clientWidth) {
+            that.canvas.style.left = Math.floor((that.container.clientWidth - viewport.width) / 2) + 'px';
+          } else {
+            that.canvas.style.left = '0px';
+          }
+
+          // Render PDF page into canvas context
+          var renderContext = {
+            canvasContext: that.ctx,
+            viewport: viewport
+          };
+          
+          var renderTask = page.render(renderContext);
+
+          // Wait for rendering to finish
+          renderTask.promise.then(function() {
+            that.isRendering = false;
+            that.currentPage = pageNum;
+            if (that.pageNumPending !== null) {
+              // New page rendering is pending
+              that.currentPage = that.pageNumPending;
+              that.pageNumPending = null;
+              that.RenderPage();
+            }
+          });
+        });
+      }
+    },
+    QueueRenderPage: function(pageNum) {
+      if (this.isRendering) {
+        this.pageNumPending = pageNum;
+      } else {
+        this.RenderPage(pageNum);
+      }
+    },
+    MouseWheel: function(event) {
+      if (event.ctrlKey) {
+        if (event.deltaY < 0) {
+          this.ZoomIn();
+        } else if (event.deltaY > 0) {
+          this.ZoomOut();
+        }
+        
+        event.preventDefault();
+      }
+    }
+  }
+});
--- a/Applications/StoneWebViewer/WebAssembly/CMakeLists.txt	Fri Nov 13 18:29:17 2020 +0100
+++ b/Applications/StoneWebViewer/WebAssembly/CMakeLists.txt	Mon Nov 16 20:47:53 2020 +0100
@@ -160,6 +160,7 @@
   ${CMAKE_SOURCE_DIR}/../WebApplication/app.css
   ${CMAKE_SOURCE_DIR}/../WebApplication/app.js
   ${CMAKE_SOURCE_DIR}/../WebApplication/index.html
+  ${CMAKE_SOURCE_DIR}/../WebApplication/pdf-viewer.js
   ${CMAKE_SOURCE_DIR}/../WebApplication/print.js
   ${CMAKE_SOURCE_DIR}/../WebApplication/ua-parser.js  # TODO => Package this from https://github.com/faisalman/ua-parser-js/releases
   ${STONE_WRAPPER}
--- a/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake	Fri Nov 13 18:29:17 2020 +0100
+++ b/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake	Mon Nov 16 20:47:53 2020 +0100
@@ -43,6 +43,18 @@
   "220afd743d9e9643852e31a135a9f3ae"
   "${BASE_URL}/jquery-3.4.1.min.js")
 
+if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/pdfjs)
+  DownloadPackage(
+    "f2e0f7eacd8946bd3111a2d10dceaa72"
+    "${BASE_URL}/web-viewer/pdfjs-2.5.207-dist.zip"
+    "${CMAKE_CURRENT_BINARY_DIR}/build")
+
+  # Reorganize the PDF.js package
+  file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/LICENSE)
+  file(REMOVE_RECURSE ${CMAKE_CURRENT_BINARY_DIR}/web)
+  file(RENAME ${CMAKE_CURRENT_BINARY_DIR}/build ${CMAKE_CURRENT_BINARY_DIR}/pdfjs)
+endif()
+
 
 install(
   FILES
@@ -59,6 +71,10 @@
   ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js
   ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.js
   ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.map
+  ${CMAKE_CURRENT_BINARY_DIR}/pdfjs/pdf.js
+  ${CMAKE_CURRENT_BINARY_DIR}/pdfjs/pdf.js.map
+  ${CMAKE_CURRENT_BINARY_DIR}/pdfjs/pdf.worker.js
+  ${CMAKE_CURRENT_BINARY_DIR}/pdfjs/pdf.worker.js.map
   DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/js
   )