]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Feature: pngx PDF viewer with updated pdfjs (#4679)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 5 Dec 2023 01:17:40 +0000 (17:17 -0800)
committerGitHub <noreply@github.com>
Tue, 5 Dec 2023 01:17:40 +0000 (17:17 -0800)
16 files changed:
src-ui/.eslintrc.json
src-ui/angular.json
src-ui/e2e/document-detail/document-detail.spec.ts
src-ui/jest.config.js
src-ui/package-lock.json
src-ui/package.json
src-ui/src/app/app.module.ts
src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.html [new file with mode: 0644]
src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.scss [new file with mode: 0644]
src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.ts [new file with mode: 0644]
src-ui/src/app/components/common/pdf-viewer/typings.ts [new file with mode: 0644]
src-ui/src/app/components/common/pdf-viewer/utils/event-bus-utils.ts [new file with mode: 0644]
src-ui/src/app/components/document-detail/document-detail.component.html
src-ui/src/app/components/document-detail/document-detail.component.scss
src-ui/src/app/components/document-detail/document-detail.component.spec.ts
src-ui/src/app/components/document-detail/document-detail.component.ts

index 12b35ce26133f05a1a0cebf1d27bba3f9c82cff4..37f9c7c12237f585b5ed051bcd0fadd5d59b1dfb 100644 (file)
@@ -1,7 +1,8 @@
 {
   "root": true,
   "ignorePatterns": [
-    "projects/**/*"
+    "projects/**/*",
+    "/src/app/components/common/pdf-viewer/**"
   ],
   "overrides": [
     {
index 1fef2fe7d23d305afa94cfb8ba2c2c9c766b1c2c..9a73922c70c559e627debb85c214522fdc0550c4 100644 (file)
@@ -65,7 +65,7 @@
               "src/assets",
               "src/manifest.webmanifest",
               {
-                "glob": "pdf.worker.min.js",
+                "glob": "{pdf.worker.min.js,pdf.min.js}",
                 "input": "node_modules/pdfjs-dist/build/",
                 "output": "/assets/js/"
               }
@@ -75,7 +75,8 @@
             ],
             "scripts": [],
             "allowedCommonJsDependencies": [
-              "ng2-pdf-viewer"
+              "pdfjs-dist",
+              "pdfjs-dist/web/pdf_viewer"
             ],
             "vendorChunk": true,
             "extractLicenses": false,
                 {
                   "type": "anyComponentStyle",
                   "maximumWarning": "6kb",
-                  "maximumError": "10kb"
+                  "maximumError": "30kb"
                 }
               ]
             },
index 1bc2b6aa911a5323b0b4521679aa56c159105d90..e40b88cccfb8255296152d806746ce07e76b8f26 100644 (file)
@@ -79,7 +79,7 @@ test('should show a mobile preview', async ({ page }) => {
   await page.setViewportSize({ width: 400, height: 1000 })
   await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
   await page.getByRole('tab', { name: 'Preview' }).click()
-  await page.waitForSelector('pdf-viewer')
+  await page.waitForSelector('pngx-pdf-viewer')
 })
 
 test('should show a list of notes', async ({ page }) => {
index 78edc082247c93d5b1f0d8c517cab71d0013aa97..b544a4767dc2263675d8e76ef4d6deca69cedc82 100644 (file)
@@ -7,6 +7,7 @@ module.exports = {
     'abstract-name-filter-service',
     'abstract-paperless-service',
   ],
+  coveragePathIgnorePatterns: ['/src/app/components/common/pdf-viewer/*'],
   transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
   moduleNameMapper: {
     '^src/(.*)': '<rootDir>/src/$1',
index cf0882708b00ea3ce1247da4856a3d847a6535a5..4e898bf817ddce304b67d4e02aea6466e2eea050 100644 (file)
         "bootstrap": "^5.3.2",
         "file-saver": "^2.0.5",
         "mime-names": "^1.0.0",
-        "ng2-pdf-viewer": "^10.0.0",
         "ngx-color": "^9.0.0",
         "ngx-cookie-service": "^16.0.1",
         "ngx-file-drop": "^16.0.0",
         "ngx-ui-tour-ng-bootstrap": "^13.0.6",
+        "pdfjs-dist": "^3.11.174",
         "rxjs": "^7.8.1",
         "tslib": "^2.6.2",
         "uuid": "^9.0.1",
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
       "dev": true
     },
+    "node_modules/@mapbox/node-pre-gyp": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+      "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.0",
+        "https-proxy-agent": "^5.0.0",
+        "make-dir": "^3.1.0",
+        "node-fetch": "^2.6.7",
+        "nopt": "^5.0.0",
+        "npmlog": "^5.0.1",
+        "rimraf": "^3.0.2",
+        "semver": "^7.3.5",
+        "tar": "^6.1.11"
+      },
+      "bin": {
+        "node-pre-gyp": "bin/node-pre-gyp"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+      "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+      "optional": true,
+      "dependencies": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^3.6.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+      "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+      "optional": true,
+      "dependencies": {
+        "aproba": "^1.0.3 || ^2.0.0",
+        "color-support": "^1.1.2",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.1",
+        "object-assign": "^4.1.1",
+        "signal-exit": "^3.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wide-align": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+      "optional": true,
+      "dependencies": {
+        "semver": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "optional": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+      "optional": true,
+      "dependencies": {
+        "abbrev": "1"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+      "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+      "optional": true,
+      "dependencies": {
+        "are-we-there-yet": "^2.0.0",
+        "console-control-strings": "^1.1.0",
+        "gauge": "^3.0.0",
+        "set-blocking": "^2.0.0"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "optional": true
+    },
     "node_modules/@ng-bootstrap/ng-bootstrap": {
       "version": "15.1.2",
       "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-15.1.2.tgz",
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/accepts": {
       "version": "1.3.8",
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
       "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "debug": "4"
       },
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
       "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/are-we-there-yet": {
       "version": "3.0.1",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/base64-js": {
       "version": "1.5.1",
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
         }
       ]
     },
+    "node_modules/canvas": {
+      "version": "2.11.2",
+      "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz",
+      "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==",
+      "hasInstallScript": true,
+      "optional": true,
+      "dependencies": {
+        "@mapbox/node-pre-gyp": "^1.0.0",
+        "nan": "^2.17.0",
+        "simple-get": "^3.0.3"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
       "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
-      "dev": true,
+      "devOptional": true,
       "engines": {
         "node": ">=10"
       }
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
       "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
-      "dev": true,
+      "devOptional": true,
       "bin": {
         "color-support": "bin.js"
       }
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/concurrently": {
       "version": "8.2.2",
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
       "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/content-disposition": {
       "version": "0.5.4",
       "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
       "dev": true
     },
+    "node_modules/decompress-response": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
+      "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
+      "optional": true,
+      "dependencies": {
+        "mimic-response": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/dedent": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
       "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/depd": {
       "version": "2.0.0",
         "npm": "1.2.8000 || >= 1.4.16"
       }
     },
+    "node_modules/detect-libc": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/detect-newline": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
         "url": "https://github.com/fb55/domhandler?sponsor=1"
       }
     },
-    "node_modules/dommatrix": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz",
-      "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==",
-      "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix."
-    },
     "node_modules/domutils": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/fsevents": {
       "version": "2.3.2",
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
       "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/hasown": {
       "version": "2.0.0",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
       "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "agent-base": "6",
         "debug": "4"
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "once": "^1.3.0",
         "wrappy": "1"
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/ini": {
       "version": "4.1.1",
         "node": ">=6"
       }
     },
+    "node_modules/mimic-response": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
+      "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/mini-css-extract-plugin": {
       "version": "2.7.6",
       "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz",
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "brace-expansion": "^1.1.7"
       },
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
       "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
-      "dev": true,
+      "devOptional": true,
       "engines": {
         "node": ">=8"
       }
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
       "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "minipass": "^3.0.0",
         "yallist": "^4.0.0"
       "version": "3.3.6",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
       "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "yallist": "^4.0.0"
       },
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/mkdirp": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
       "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
-      "dev": true,
+      "devOptional": true,
       "bin": {
         "mkdirp": "bin/cmd.js"
       },
       "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
       "dev": true
     },
+    "node_modules/nan": {
+      "version": "2.18.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
+      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
+      "optional": true
+    },
     "node_modules/nanoid": {
       "version": "3.3.6",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
       "dev": true
     },
-    "node_modules/ng2-pdf-viewer": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.0.0.tgz",
-      "integrity": "sha512-zEefcAsTpDoxFceQYs3ycPMaUAkt5UX4OcTstVQoNqRK6w+vOY+V8z8aFCuBwnt+7iN1EHaIpquOf4S9mWc04g==",
-      "dependencies": {
-        "pdfjs-dist": "~2.16.105",
-        "tslib": "^2.3.0"
-      },
-      "peerDependencies": {
-        "pdfjs-dist": "~2.16.105"
-      }
-    },
     "node_modules/ngx-color": {
       "version": "9.0.0",
       "resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",
       "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
       "dev": true
     },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "optional": true,
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-fetch/node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+      "optional": true
+    },
+    "node_modules/node-fetch/node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+      "optional": true
+    },
+    "node_modules/node-fetch/node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "optional": true,
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
     "node_modules/node-forge": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
         "node": ">=8"
       }
     },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/object-inspect": {
       "version": "1.13.1",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "wrappy": "1"
       }
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "dev": true,
+      "devOptional": true,
       "engines": {
         "node": ">=0.10.0"
       }
         "node": ">=8"
       }
     },
+    "node_modules/path2d-polyfill": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz",
+      "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/pdfjs-dist": {
-      "version": "2.16.105",
-      "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz",
-      "integrity": "sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==",
-      "dependencies": {
-        "dommatrix": "^1.0.3",
-        "web-streams-polyfill": "^3.2.1"
-      },
-      "peerDependencies": {
-        "worker-loader": "^3.0.8"
+      "version": "3.11.174",
+      "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
+      "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
+      "engines": {
+        "node": ">=18"
       },
-      "peerDependenciesMeta": {
-        "worker-loader": {
-          "optional": true
-        }
+      "optionalDependencies": {
+        "canvas": "^2.11.2",
+        "path2d-polyfill": "^2.0.1"
       }
     },
     "node_modules/picocolors": {
       "version": "3.6.2",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
       "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "inherits": "^2.0.3",
         "string_decoder": "^1.1.1",
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "glob": "^7.1.3"
       },
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
       "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
-      "dev": true,
+      "devOptional": true,
       "funding": [
         {
           "type": "github",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/set-function-length": {
       "version": "1.1.1",
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
+    "node_modules/simple-concat": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+      "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "optional": true
+    },
+    "node_modules/simple-get": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
+      "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
+      "optional": true,
+      "dependencies": {
+        "decompress-response": "^4.2.0",
+        "once": "^1.3.1",
+        "simple-concat": "^1.0.0"
+      }
+    },
     "node_modules/sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
       "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "safe-buffer": "~5.2.0"
       }
       "version": "6.1.15",
       "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz",
       "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "chownr": "^2.0.0",
         "fs-minipass": "^2.0.0",
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
       "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "minipass": "^3.0.0"
       },
       "version": "3.3.6",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
       "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "yallist": "^4.0.0"
       },
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/terser": {
       "version": "5.19.2",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/utils-merge": {
       "version": "1.0.1",
         "defaults": "^1.0.3"
       }
     },
-    "node_modules/web-streams-polyfill": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
-      "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
-      "engines": {
-        "node": ">= 8"
-      }
-    },
     "node_modules/webidl-conversions": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
       "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "string-width": "^1.0.2 || 2 || 3 || 4"
       }
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/write-file-atomic": {
       "version": "4.0.2",
index 7f07203886fd50b305a5993d9671bb86970d7d70..a2c94084c97b599e9924ab227339a90ff2cba8de 100644 (file)
     "bootstrap": "^5.3.2",
     "file-saver": "^2.0.5",
     "mime-names": "^1.0.0",
-    "ng2-pdf-viewer": "^10.0.0",
     "ngx-color": "^9.0.0",
     "ngx-cookie-service": "^16.0.1",
     "ngx-file-drop": "^16.0.0",
     "ngx-ui-tour-ng-bootstrap": "^13.0.6",
+    "pdfjs-dist": "^3.11.174",
     "rxjs": "^7.8.1",
     "tslib": "^2.6.2",
     "uuid": "^9.0.1",
index 684cf6d18f8d09ee60f1f13fc0bfb45ee3b9a960..8d7ea5663091eeddbbf5889d39e551d4e8b67bd6 100644 (file)
@@ -51,7 +51,6 @@ import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-v
 import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
 import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
 import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
-import { PdfViewerModule } from 'ng2-pdf-viewer'
 import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
 import { YesNoPipe } from './pipes/yes-no.pipe'
 import { FileSizePipe } from './pipes/file-size.pipe'
@@ -106,6 +105,7 @@ import { CustomFieldsComponent } from './components/manage/custom-fields/custom-
 import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
 import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
 import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
+import { PdfViewerComponent } from './components/common/pdf-viewer/pdf-viewer.component'
 
 import localeAf from '@angular/common/locales/af'
 import localeAr from '@angular/common/locales/ar'
@@ -258,6 +258,7 @@ function initializeApp(settings: SettingsService) {
     CustomFieldEditDialogComponent,
     CustomFieldsDropdownComponent,
     ProfileEditDialogComponent,
+    PdfViewerComponent,
   ],
   imports: [
     BrowserModule,
@@ -267,7 +268,6 @@ function initializeApp(settings: SettingsService) {
     FormsModule,
     ReactiveFormsModule,
     NgxFileDropModule,
-    PdfViewerModule,
     NgSelectModule,
     ColorSliderModule,
     TourNgBootstrapModule,
diff --git a/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.html b/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.html
new file mode 100644 (file)
index 0000000..cd2c85a
--- /dev/null
@@ -0,0 +1,3 @@
+<div #pdfViewerContainer class="pngx-pdf-viewer-container">
+  <div class="pdfViewer"></div>
+</div>
diff --git a/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.scss b/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.scss
new file mode 100644 (file)
index 0000000..408bbb0
--- /dev/null
@@ -0,0 +1,1014 @@
+/**
+ * This file is taken and modified from https://github.com/VadimDez/ng2-pdf-viewer/blob/10.0.0/src/app/pdf-viewer/pdf-viewer.component.scss
+ * Created by vadimdez on 21/06/16.
+ */
+.pngx-pdf-viewer-container {
+    overflow-x: auto;
+    position: absolute;
+    height: 100%;
+    width: 100%;
+    -webkit-overflow-scrolling: touch;
+  }
+
+  :host {
+    display: block;
+    position: relative;
+  }
+
+  :host ::ng-deep {
+    /* Copyright 2014 Mozilla Foundation
+   *
+   * Licensed under the Apache License, Version 2.0 (the "License");
+   * you may not use this file except in compliance with the License.
+   * You may obtain a copy of the License at
+   *
+   *     http://www.apache.org/licenses/LICENSE-2.0
+   *
+   * Unless required by applicable law or agreed to in writing, software
+   * distributed under the License is distributed on an "AS IS" BASIS,
+   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   * See the License for the specific language governing permissions and
+   * limitations under the License.
+   */
+    --pdfViewer-padding-bottom: 0;
+    --page-margin: 1px auto -8px;
+    --page-border: none;
+    --spreadHorizontalWrapped-margin-LR: -3.5px;
+    --viewer-container-height: 0;
+    --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
+    --xfa-unfocused-field-background: var(
+      --annotation-unfocused-field-background
+    );
+    --page-border-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAA1ElEQVQ4jbWUWw6EIAxFy2NFs/8NzR4UJhpqLsdi5mOmSSMUOfYWqv3S0gMr4XlYH/64gZa/gN3ANYA7KAXALt4ktoQ5MI9YxqaG8bWmsIysMuT6piSQCa4whZThCu8CM4zP9YJaKci9jicPq3NcBWYoPMGUlhG7ivtkB+gVyFY75wXghOvh8t5mto1Mdim6e+MBqH6XsY+YAwjpq3vGF7weTWQptLEDVCZvPTMl5JZZsdh47FHW6qFMyvLYqjcnmdFfY9Xk/KDOlzCusX2mi/ofM7MPkzBcSp4Q1/wAAAAASUVORK5CYII=')
+      9 9 repeat;
+    --scale-factor: 1;
+
+    --focus-outline: solid 2px blue;
+    --hover-outline: dashed 2px blue;
+    --freetext-line-height: 1.35;
+    --freetext-padding: 2px;
+    --editorInk-editing-cursor: pointer;
+
+    @media screen and (forced-colors: active) {
+      --pdfViewer-padding-bottom: 9px;
+      --page-margin: 8px auto -1px;
+      --page-border: 1px solid CanvasText;
+      --page-border-image: none;
+      --spreadHorizontalWrapped-margin-LR: 3.5px;
+    }
+
+    @media (forced-colors: active) {
+      --focus-outline: solid 3px ButtonText;
+      --hover-outline: dashed 3px ButtonText;
+    }
+
+    .textLayer {
+      position: absolute;
+      text-align: initial;
+      left: 0;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      overflow: hidden;
+      opacity: 0.2;
+      line-height: 1;
+      -webkit-text-size-adjust: none;
+      -moz-text-size-adjust: none;
+      text-size-adjust: none;
+      forced-color-adjust: none;
+    }
+
+    .textLayer span,
+    .textLayer br {
+      color: transparent;
+      position: absolute;
+      white-space: pre;
+      cursor: text;
+      transform-origin: 0% 0%;
+    }
+
+    /* Only necessary in Google Chrome, see issue 14205, and most unfortunately
+     * the problem doesn't show up in "text" reference tests. */
+    .textLayer span.markedContent {
+      top: 0;
+      height: 0;
+    }
+
+    .textLayer .highlight {
+      margin: -1px;
+      padding: 1px;
+      background-color: rgba(180, 0, 170, 1);
+      border-radius: 4px;
+    }
+
+    .textLayer .highlight.appended {
+      position: initial;
+    }
+
+    .textLayer .highlight.begin {
+      border-radius: 4px 0 0 4px;
+    }
+
+    .textLayer .highlight.end {
+      border-radius: 0 4px 4px 0;
+    }
+
+    .textLayer .highlight.middle {
+      border-radius: 0;
+    }
+
+    .textLayer .highlight.selected {
+      background-color: rgba(0, 100, 0, 1);
+    }
+
+    .textLayer ::-moz-selection {
+      background: rgba(0, 0, 255, 1);
+    }
+
+    .textLayer ::selection {
+      background: rgba(0, 0, 255, 1);
+    }
+
+    /* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
+    .textLayer br::-moz-selection {
+      background: transparent;
+    }
+
+    .textLayer br::selection {
+      background: transparent;
+    }
+
+    .textLayer .endOfContent {
+      display: block;
+      position: absolute;
+      left: 0;
+      top: 100%;
+      right: 0;
+      bottom: 0;
+      z-index: -1;
+      cursor: default;
+      -webkit-user-select: none;
+      -moz-user-select: none;
+      user-select: none;
+    }
+
+    .textLayer .endOfContent.active {
+      top: 0;
+    }
+
+    @media (forced-colors: active) {
+      .annotationLayer .textWidgetAnnotation input:required,
+      .annotationLayer .textWidgetAnnotation textarea:required,
+      .annotationLayer .choiceWidgetAnnotation select:required,
+      .annotationLayer .buttonWidgetAnnotation.checkBox input:required,
+      .annotationLayer .buttonWidgetAnnotation.radioButton input:required {
+        outline: 1.5px solid selectedItem;
+      }
+    }
+
+    .annotationLayer {
+      position: absolute;
+      top: 0;
+      left: 0;
+      pointer-events: none;
+      transform-origin: 0 0;
+    }
+
+    .annotationLayer section {
+      position: absolute;
+      text-align: initial;
+      pointer-events: auto;
+      box-sizing: border-box;
+      transform-origin: 0 0;
+    }
+
+    .annotationLayer .linkAnnotation > a,
+    .annotationLayer .buttonWidgetAnnotation.pushButton > a {
+      position: absolute;
+      font-size: 1em;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.pushButton > canvas {
+      width: 100%;
+      height: 100%;
+    }
+
+    .annotationLayer .linkAnnotation > a:hover,
+    .annotationLayer .buttonWidgetAnnotation.pushButton > a:hover {
+      opacity: 0.2;
+      background: rgba(255, 255, 0, 1);
+      box-shadow: 0 2px 10px rgba(255, 255, 0, 1);
+    }
+
+    .annotationLayer .textAnnotation img {
+      position: absolute;
+      cursor: pointer;
+      width: 100%;
+      height: 100%;
+    }
+
+    .annotationLayer .textWidgetAnnotation input,
+    .annotationLayer .textWidgetAnnotation textarea,
+    .annotationLayer .choiceWidgetAnnotation select,
+    .annotationLayer .buttonWidgetAnnotation.checkBox input,
+    .annotationLayer .buttonWidgetAnnotation.radioButton input {
+      background-image: var(--annotation-unfocused-field-background);
+      border: 1px solid transparent;
+      box-sizing: border-box;
+      font: calc(9px * var(--scale-factor)) sans-serif;
+      height: 100%;
+      margin: 0;
+      vertical-align: top;
+      width: 100%;
+    }
+
+    .annotationLayer .textWidgetAnnotation input:required,
+    .annotationLayer .textWidgetAnnotation textarea:required,
+    .annotationLayer .choiceWidgetAnnotation select:required,
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:required,
+    .annotationLayer .buttonWidgetAnnotation.radioButton input:required {
+      outline: 1.5px solid red;
+    }
+
+    .annotationLayer .choiceWidgetAnnotation select option {
+      padding: 0;
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.radioButton input {
+      border-radius: 50%;
+    }
+
+    .annotationLayer .textWidgetAnnotation textarea {
+      resize: none;
+    }
+
+    .annotationLayer .textWidgetAnnotation input[disabled],
+    .annotationLayer .textWidgetAnnotation textarea[disabled],
+    .annotationLayer .choiceWidgetAnnotation select[disabled],
+    .annotationLayer .buttonWidgetAnnotation.checkBox input[disabled],
+    .annotationLayer .buttonWidgetAnnotation.radioButton input[disabled] {
+      background: none;
+      border: 1px solid transparent;
+      cursor: not-allowed;
+    }
+
+    .annotationLayer .textWidgetAnnotation input:hover,
+    .annotationLayer .textWidgetAnnotation textarea:hover,
+    .annotationLayer .choiceWidgetAnnotation select:hover,
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:hover,
+    .annotationLayer .buttonWidgetAnnotation.radioButton input:hover {
+      border: 1px solid rgba(0, 0, 0, 1);
+    }
+
+    .annotationLayer .textWidgetAnnotation input:focus,
+    .annotationLayer .textWidgetAnnotation textarea:focus,
+    .annotationLayer .choiceWidgetAnnotation select:focus {
+      background: none;
+      border: 1px solid transparent;
+    }
+
+    .annotationLayer .textWidgetAnnotation input :focus,
+    .annotationLayer .textWidgetAnnotation textarea :focus,
+    .annotationLayer .choiceWidgetAnnotation select :focus,
+    .annotationLayer .buttonWidgetAnnotation.checkBox :focus,
+    .annotationLayer .buttonWidgetAnnotation.radioButton :focus {
+      background-image: none;
+      background-color: transparent;
+      outline: auto;
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before,
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after,
+    .annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before {
+      background-color: CanvasText;
+      content: '';
+      display: block;
+      position: absolute;
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before,
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after {
+      height: 80%;
+      left: 45%;
+      width: 1px;
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before {
+      transform: rotate(45deg);
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after {
+      transform: rotate(-45deg);
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before {
+      border-radius: 50%;
+      height: 50%;
+      left: 30%;
+      top: 20%;
+      width: 50%;
+    }
+
+    .annotationLayer .textWidgetAnnotation input.comb {
+      font-family: monospace;
+      padding-left: 2px;
+      padding-right: 0;
+    }
+
+    .annotationLayer .textWidgetAnnotation input.comb:focus {
+      /*
+       * Letter spacing is placed on the right side of each character. Hence, the
+       * letter spacing of the last character may be placed outside the visible
+       * area, causing horizontal scrolling. We avoid this by extending the width
+       * when the element has focus and revert this when it loses focus.
+       */
+      width: 103%;
+    }
+
+    .annotationLayer .buttonWidgetAnnotation.checkBox input,
+    .annotationLayer .buttonWidgetAnnotation.radioButton input {
+      -webkit-appearance: none;
+      -moz-appearance: none;
+      appearance: none;
+    }
+
+    .annotationLayer .popupTriggerArea {
+      height: 100%;
+      width: 100%;
+    }
+
+    .annotationLayer .popupWrapper {
+      position: absolute;
+      font-size: calc(9px * var(--scale-factor));
+      width: 100%;
+      min-width: calc(180px * var(--scale-factor));
+      pointer-events: none;
+    }
+
+    .annotationLayer .popup {
+      position: absolute;
+      max-width: calc(180px * var(--scale-factor));
+      background-color: rgba(255, 255, 153, 1);
+      box-shadow: 0 calc(2px * var(--scale-factor))
+        calc(5px * var(--scale-factor)) rgba(136, 136, 136, 1);
+      border-radius: calc(2px * var(--scale-factor));
+      padding: calc(6px * var(--scale-factor));
+      margin-left: calc(5px * var(--scale-factor));
+      cursor: pointer;
+      font: message-box;
+      white-space: normal;
+      word-wrap: break-word;
+      pointer-events: auto;
+    }
+
+    .annotationLayer .popup > * {
+      font-size: calc(9px * var(--scale-factor));
+    }
+
+    .annotationLayer .popup h1 {
+      display: inline-block;
+    }
+
+    .annotationLayer .popupDate {
+      display: inline-block;
+      margin-left: calc(5px * var(--scale-factor));
+    }
+
+    .annotationLayer .popupContent {
+      border-top: 1px solid rgba(51, 51, 51, 1);
+      margin-top: calc(2px * var(--scale-factor));
+      padding-top: calc(2px * var(--scale-factor));
+    }
+
+    .annotationLayer .richText > * {
+      white-space: pre-wrap;
+      font-size: calc(9px * var(--scale-factor));
+    }
+
+    .annotationLayer .highlightAnnotation,
+    .annotationLayer .underlineAnnotation,
+    .annotationLayer .squigglyAnnotation,
+    .annotationLayer .strikeoutAnnotation,
+    .annotationLayer .freeTextAnnotation,
+    .annotationLayer .lineAnnotation svg line,
+    .annotationLayer .squareAnnotation svg rect,
+    .annotationLayer .circleAnnotation svg ellipse,
+    .annotationLayer .polylineAnnotation svg polyline,
+    .annotationLayer .polygonAnnotation svg polygon,
+    .annotationLayer .caretAnnotation,
+    .annotationLayer .inkAnnotation svg polyline,
+    .annotationLayer .stampAnnotation,
+    .annotationLayer .fileAttachmentAnnotation {
+      cursor: pointer;
+    }
+
+    .annotationLayer section svg {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+    }
+
+    .annotationLayer .annotationTextContent {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      opacity: 0;
+      color: transparent;
+      -webkit-user-select: none;
+      -moz-user-select: none;
+      user-select: none;
+      pointer-events: none;
+    }
+
+    .annotationLayer .annotationTextContent span {
+      width: 100%;
+      display: inline-block;
+    }
+
+    @media (forced-colors: active) {
+      .xfaLayer *:required {
+        outline: 1.5px solid selectedItem;
+      }
+    }
+
+    .xfaLayer .highlight {
+      margin: -1px;
+      padding: 1px;
+      background-color: rgba(239, 203, 237, 1);
+      border-radius: 4px;
+    }
+
+    .xfaLayer .highlight.appended {
+      position: initial;
+    }
+
+    .xfaLayer .highlight.begin {
+      border-radius: 4px 0 0 4px;
+    }
+
+    .xfaLayer .highlight.end {
+      border-radius: 0 4px 4px 0;
+    }
+
+    .xfaLayer .highlight.middle {
+      border-radius: 0;
+    }
+
+    .xfaLayer .highlight.selected {
+      background-color: rgba(203, 223, 203, 1);
+    }
+
+    .xfaLayer ::-moz-selection {
+      background: rgba(0, 0, 255, 1);
+    }
+
+    .xfaLayer ::selection {
+      background: rgba(0, 0, 255, 1);
+    }
+
+    .xfaPage {
+      overflow: hidden;
+      position: relative;
+    }
+
+    .xfaContentarea {
+      position: absolute;
+    }
+
+    .xfaPrintOnly {
+      display: none;
+    }
+
+    .xfaLayer {
+      position: absolute;
+      text-align: initial;
+      top: 0;
+      left: 0;
+      transform-origin: 0 0;
+      line-height: 1.2;
+    }
+
+    .xfaLayer * {
+      color: inherit;
+      font: inherit;
+      font-style: inherit;
+      font-weight: inherit;
+      font-kerning: inherit;
+      letter-spacing: -0.01px;
+      text-align: inherit;
+      text-decoration: inherit;
+      box-sizing: border-box;
+      background-color: transparent;
+      padding: 0;
+      margin: 0;
+      pointer-events: auto;
+      line-height: inherit;
+    }
+
+    .xfaLayer *:required {
+      outline: 1.5px solid red;
+    }
+
+    .xfaLayer div {
+      pointer-events: none;
+    }
+
+    .xfaLayer svg {
+      pointer-events: none;
+    }
+
+    .xfaLayer svg * {
+      pointer-events: none;
+    }
+
+    .xfaLayer a {
+      color: blue;
+    }
+
+    .xfaRich li {
+      margin-left: 3em;
+    }
+
+    .xfaFont {
+      color: black;
+      font-weight: normal;
+      font-kerning: none;
+      font-size: 10px;
+      font-style: normal;
+      letter-spacing: 0;
+      text-decoration: none;
+      vertical-align: 0;
+    }
+
+    .xfaCaption {
+      overflow: hidden;
+      flex: 0 0 auto;
+    }
+
+    .xfaCaptionForCheckButton {
+      overflow: hidden;
+      flex: 1 1 auto;
+    }
+
+    .xfaLabel {
+      height: 100%;
+      width: 100%;
+    }
+
+    .xfaLeft {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+    }
+
+    .xfaRight {
+      display: flex;
+      flex-direction: row-reverse;
+      align-items: center;
+    }
+
+    .xfaLeft > .xfaCaption,
+    .xfaLeft > .xfaCaptionForCheckButton,
+    .xfaRight > .xfaCaption,
+    .xfaRight > .xfaCaptionForCheckButton {
+      max-height: 100%;
+    }
+
+    .xfaTop {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-start;
+    }
+
+    .xfaBottom {
+      display: flex;
+      flex-direction: column-reverse;
+      align-items: flex-start;
+    }
+
+    .xfaTop > .xfaCaption,
+    .xfaTop > .xfaCaptionForCheckButton,
+    .xfaBottom > .xfaCaption,
+    .xfaBottom > .xfaCaptionForCheckButton {
+      width: 100%;
+    }
+
+    .xfaBorder {
+      background-color: transparent;
+      position: absolute;
+      pointer-events: none;
+    }
+
+    .xfaWrapped {
+      width: 100%;
+      height: 100%;
+    }
+
+    .xfaTextfield:focus,
+    .xfaSelect:focus {
+      background-image: none;
+      background-color: transparent;
+      outline: auto;
+      outline-offset: -1px;
+    }
+
+    .xfaCheckbox:focus,
+    .xfaRadio:focus {
+      outline: auto;
+    }
+
+    .xfaTextfield,
+    .xfaSelect {
+      height: 100%;
+      width: 100%;
+      flex: 1 1 auto;
+      border: none;
+      resize: none;
+      background-image: var(--xfa-unfocused-field-background);
+    }
+
+    .xfaTop > .xfaTextfield,
+    .xfaTop > .xfaSelect,
+    .xfaBottom > .xfaTextfield,
+    .xfaBottom > .xfaSelect {
+      flex: 0 1 auto;
+    }
+
+    .xfaButton {
+      cursor: pointer;
+      width: 100%;
+      height: 100%;
+      border: none;
+      text-align: center;
+    }
+
+    .xfaLink {
+      width: 100%;
+      height: 100%;
+      position: absolute;
+      top: 0;
+      left: 0;
+    }
+
+    .xfaCheckbox,
+    .xfaRadio {
+      width: 100%;
+      height: 100%;
+      flex: 0 0 auto;
+      border: none;
+    }
+
+    .xfaRich {
+      white-space: pre-wrap;
+      width: 100%;
+      height: 100%;
+    }
+
+    .xfaImage {
+      -o-object-position: left top;
+      object-position: left top;
+      -o-object-fit: contain;
+      object-fit: contain;
+      width: 100%;
+      height: 100%;
+    }
+
+    .xfaLrTb,
+    .xfaRlTb,
+    .xfaTb {
+      display: flex;
+      flex-direction: column;
+      align-items: stretch;
+    }
+
+    .xfaLr {
+      display: flex;
+      flex-direction: row;
+      align-items: stretch;
+    }
+
+    .xfaRl {
+      display: flex;
+      flex-direction: row-reverse;
+      align-items: stretch;
+    }
+
+    .xfaTb > div {
+      justify-content: left;
+    }
+
+    .xfaPosition {
+      position: relative;
+    }
+
+    .xfaArea {
+      position: relative;
+    }
+
+    .xfaValignMiddle {
+      display: flex;
+      align-items: center;
+    }
+
+    .xfaTable {
+      display: flex;
+      flex-direction: column;
+      align-items: stretch;
+    }
+
+    .xfaTable .xfaRow {
+      display: flex;
+      flex-direction: row;
+      align-items: stretch;
+    }
+
+    .xfaTable .xfaRlRow {
+      display: flex;
+      flex-direction: row-reverse;
+      align-items: stretch;
+      flex: 1;
+    }
+
+    .xfaTable .xfaRlRow > div {
+      flex: 1;
+    }
+
+    .xfaNonInteractive input,
+    .xfaNonInteractive textarea,
+    .xfaDisabled input,
+    .xfaDisabled textarea,
+    .xfaReadOnly input,
+    .xfaReadOnly textarea {
+      background: initial;
+    }
+
+    @media print {
+      .xfaTextfield,
+      .xfaSelect {
+        background: transparent;
+      }
+
+      .xfaSelect {
+        -webkit-appearance: none;
+        -moz-appearance: none;
+        appearance: none;
+        text-indent: 1px;
+        text-overflow: '';
+      }
+    }
+
+    [data-editor-rotation='90'] {
+      transform: rotate(90deg);
+    }
+    [data-editor-rotation='180'] {
+      transform: rotate(180deg);
+    }
+    [data-editor-rotation='270'] {
+      transform: rotate(270deg);
+    }
+
+    .annotationEditorLayer {
+      background: transparent;
+      position: absolute;
+      top: 0;
+      left: 0;
+      font-size: calc(100px * var(--scale-factor));
+      transform-origin: 0 0;
+    }
+
+    .annotationEditorLayer .selectedEditor {
+      outline: var(--focus-outline);
+      resize: none;
+    }
+
+    .annotationEditorLayer .freeTextEditor {
+      position: absolute;
+      background: transparent;
+      border-radius: 3px;
+      padding: calc(var(--freetext-padding) * var(--scale-factor));
+      resize: none;
+      width: auto;
+      height: auto;
+      z-index: 1;
+      transform-origin: 0 0;
+      touch-action: none;
+    }
+
+    .annotationEditorLayer .freeTextEditor .internal {
+      background: transparent;
+      border: none;
+      top: 0;
+      left: 0;
+      overflow: visible;
+      white-space: nowrap;
+      resize: none;
+      font: 10px sans-serif;
+      line-height: var(--freetext-line-height);
+    }
+
+    .annotationEditorLayer .freeTextEditor .overlay {
+      position: absolute;
+      display: none;
+      background: transparent;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+
+    .annotationEditorLayer .freeTextEditor .overlay.enabled {
+      display: block;
+    }
+
+    .annotationEditorLayer .freeTextEditor .internal:empty::before {
+      content: attr(default-content);
+      color: gray;
+    }
+
+    .annotationEditorLayer .freeTextEditor .internal:focus {
+      outline: none;
+    }
+
+    .annotationEditorLayer .inkEditor.disabled {
+      resize: none;
+    }
+
+    .annotationEditorLayer .inkEditor.disabled.selectedEditor {
+      resize: horizontal;
+    }
+
+    .annotationEditorLayer .freeTextEditor:hover:not(.selectedEditor),
+    .annotationEditorLayer .inkEditor:hover:not(.selectedEditor) {
+      outline: var(--hover-outline);
+    }
+
+    .annotationEditorLayer .inkEditor {
+      position: absolute;
+      background: transparent;
+      border-radius: 3px;
+      overflow: auto;
+      width: 100%;
+      height: 100%;
+      z-index: 1;
+      transform-origin: 0 0;
+      cursor: auto;
+    }
+
+    .annotationEditorLayer .inkEditor.editing {
+      resize: none;
+      cursor: var(--editorInk-editing-cursor), pointer;
+    }
+
+    .annotationEditorLayer .inkEditor .inkEditorCanvas {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      touch-action: none;
+    }
+
+    [data-main-rotation='90'] {
+      transform: rotate(90deg) translateY(-100%);
+    }
+    [data-main-rotation='180'] {
+      transform: rotate(180deg) translate(-100%, -100%);
+    }
+    [data-main-rotation='270'] {
+      transform: rotate(270deg) translateX(-100%);
+    }
+
+    .pdfViewer {
+      padding-bottom: var(--pdfViewer-padding-bottom);
+    }
+
+    .pdfViewer .canvasWrapper {
+      overflow: hidden;
+    }
+
+    .pdfViewer .page {
+      direction: ltr;
+      width: 816px;
+      height: 1056px;
+      margin: var(--page-margin);
+      position: relative;
+      overflow: visible;
+      border: var(--page-border);
+      -o-border-image: var(--page-border-image);
+      border-image: var(--page-border-image);
+      background-clip: content-box;
+      background-color: rgba(255, 255, 255, 1);
+    }
+
+    .pdfViewer .dummyPage {
+      position: relative;
+      width: 0;
+      height: var(--viewer-container-height);
+    }
+
+    .pdfViewer.removePageBorders .page {
+      margin: 0 auto 10px;
+      border: none;
+    }
+
+    .pdfViewer.singlePageView {
+      display: inline-block;
+    }
+
+    .pdfViewer.singlePageView .page {
+      margin: 0;
+      border: none;
+    }
+
+    .pdfViewer.scrollHorizontal,
+    .pdfViewer.scrollWrapped,
+    .spread {
+      margin-left: 3.5px;
+      margin-right: 3.5px;
+      text-align: center;
+    }
+
+    .pdfViewer.scrollHorizontal,
+    .spread {
+      white-space: nowrap;
+    }
+
+    .pdfViewer.removePageBorders,
+    .pdfViewer.scrollHorizontal .spread,
+    .pdfViewer.scrollWrapped .spread {
+      margin-left: 0;
+      margin-right: 0;
+    }
+
+    .spread .page,
+    .spread .dummyPage,
+    .pdfViewer.scrollHorizontal .page,
+    .pdfViewer.scrollWrapped .page,
+    .pdfViewer.scrollHorizontal .spread,
+    .pdfViewer.scrollWrapped .spread {
+      display: inline-block;
+      vertical-align: middle;
+    }
+
+    .spread .page,
+    .pdfViewer.scrollHorizontal .page,
+    .pdfViewer.scrollWrapped .page {
+      margin-left: var(--spreadHorizontalWrapped-margin-LR);
+      margin-right: var(--spreadHorizontalWrapped-margin-LR);
+    }
+
+    .pdfViewer.removePageBorders .spread .page,
+    .pdfViewer.removePageBorders.scrollHorizontal .page,
+    .pdfViewer.removePageBorders.scrollWrapped .page {
+      margin-left: 5px;
+      margin-right: 5px;
+    }
+
+    .pdfViewer .page canvas {
+      margin: 0;
+      display: block;
+    }
+
+    .pdfViewer .page canvas[hidden] {
+      display: none;
+    }
+
+    .pdfViewer .page .loadingIcon {
+      position: absolute;
+      display: block;
+      left: 0;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      background: url('data:image/gif;base64,R0lGODlhGAAYAPQQAM7Ozvr6+uDg4LCwsOjo6I6OjsjIyJycnNjY2KioqMDAwPLy8nZ2doaGhri4uGhoaP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/ilPcHRpbWl6ZWQgd2l0aCBodHRwczovL2V6Z2lmLmNvbS9vcHRpbWl6ZQAh+QQJBwAQACwAAAAAGAAYAAAFmiAkjiTkOGVaBgjZNGSgkgKjjM8zLoI8iy+BKCdiCX8iBeMAhEEIPRXLxViYUE9CbCQoFAzFhHY3zkaT3oPvBz1zE4UBsr1eWZH4vAowOBwGAHk8AoQLfH6Agm0Ed3qOAXWOIgQKiWyFJQgDgJEpdG+WEACNEFNFmKVlVzJQk6qdkwqBoi1mebJ3ALNGeIZHtGSwNDS1RZKueCEAIfkECQcAEAAsAAAAABgAGAAABZcgJI4kpChlWgYCWRQkEKgjURgjw4zOg9CjVwuiEyEeO6CxkBC9nA+HiuUqLEyoBZI0Mx4SAFFgQCDZuguBoGv6Dtg0gvpqdhxQQDkBzuUr/4A1JwMKP39pc2mDhYCIc4GQYn6QCwCMeY91l0p6dBAEJ0OfcFRimZ91Mwt0alxxAIZyRmuAsKxDLKKvZbM1tJxmvGKRpn8hACH5BAkHABAALAAAAAAYABgAAAWhICSOJGQYZVoGAnkcJBKoI3EAY1GMCtPSosSBINKJBIwGkHdwBGGQA0OhYpEGQxNqkYzNIITBACEKKBaxxNfBeOCO4vMy0Hg8nDHFeCktkKtfNAtoS4UqAicKBj9zBAKPC4iKi4aRkISGmWWBmjUIAIyHkCUEAKCVo2WmREecVqoCgZhgP4NHrGWCj7e3szSpuxAsoVWxnp6cVV4kyZW+KSEAIfkECQcAEAAsAAAAABgAGAAABZkgJI4kBABlWgYEOQykEKgjMSDjcYxG0dKi108nEhQKQN4rCIMkCgbawjWYnSCLY2yGVSgEooBhWqsGGwxc0RtNBgoMhmJ1QgETjANYFeBKyUmBKQQIdT9JDmgPDQ6EhoKJD4sOgpWWgiwChyqEBH5hmptSoSOZgJ4kLKWkYTF7C2SaqaM/hEWygay4mYG8t6uffFuzl1iANCEAIfkECQcAEAAsAAAAABgAGAAABZ0gJI4khCBlmhKkopBCoI6LIozDMAIHO4uuBVBnOiR+I4FrCDwAZsKdQnaCLIwwmRUA8JmioprWUCjcwlwUMnAoG0qL03k2KCS8cC0UjOzDCQKBfHQFDAwFU4CCfgqFhy9+kZJWgzSKSAcPZn+BfQENDw8OljGWJAFeDoZPYTBnC1GdSXqnsoBolSulX2GyP6hgvnG0KrS3NJNhuSQhACH5BAkHABAALAAAAAAYABgAAAWaICSOJCQIZZoupGGQRKCOC0CMijIiwz2LABtQZxoMfjQhxAXszWQ7gOwECRhh0MCJJRJARTUoIHFAgbfI6uBwAJS01J/i4PClVYHvfV8lbLlIBmwFbQt+aGmChG18jXeGT4dICQxlb4g/AQUMDER9XjR6BAdiDQwINDBmkAsPDVh4cX4imw53iLKuaVqAcUsPqEiidkt6j4AzIQAh+QQJBwAQACwAAAAAGAAYAAAFmSAkjiREEGWaBiSCtCoZCMsIAKOg1LEo0KKbaKFQ9EYLoOkFuQlirNxzCQkUW9GZ0hQd4nyDAWr4G/esYSbyZFYZwu3jqiuvr8u8I2BwOAwASXh1e31/doeHC3klWnElfAlTd46MfQUGk2stCVEGBQWSdCciDg5VDAVYKoEiDQ0iBwxGcj9RDw8+qHIzebc2DJJQJK6qiKVyIQAh+QQJBwAQACwAAAAAGAAYAAAFmSAkjiS0LGWaBiRBtCoZCKgoCCMB1DF0sz6cCQDo5W62l28XAyZFpyECBv3lnCbhUqHMIo0Qg4Jbmn1jRCa4iV27TzfXGjEecOFWMN1OdvvfPGUuXSoKBw6EXokrAwcHRVU0UAeEBANAAAmUI1gNDyhjJgUHLW0iDg8FIqOnBQZrDA9TELE2rEYIDw4jta2LMpCrqld/YQpgIQAh+QQJBwAQACwAAAAAGAAYAAAFmyAkjiS0LGWaBiRBkKw6BgIqCsJcyyMe4yJajhcEml5H26o1PN2QQd3uFiv2AADlAgflIbDdZLgkABOJgep5LfWty4p4zeU+w+XsvJWXliEKDwdEBgMKYQ4PDw1qK3EDCCMAiQ5BCV0LCj+FSDQkgCgGBiYHAy2MIgoMghAHqw4HAGsNDEMFBTekdgwKI7aRB2MwkL2rVHoQoWchACH5BAkHABAALAAAAAAYABgAAAWWICSOJLQsZZoGJEGQrDoGAioKwlzLIx7jIlqOFwSaXkfbqjU83ZBB3e4WK0qrCxyU55peid0qcUwuixyNx6PhILsAcAJazXYj4lvz2MkLiFsHDAlEcABKZwwMBX8pBgoKQxAIigpBA1sLBj+PSDQkB4uSACYDlTMyBgWDEKVnl2QFBUigN61gBQYjtLV5JZ4jtlR6omMhACH5BAkHABAALAAAAAAYABgAAAWaICSOJLQsZZoGJEGQrDoGAioKwlzLIx7jIlqOFwSaXkdbidYanm7I4AjwYDh6saJuJ3JUG1mZi9srPA7EcRimJLrfJYWZUVC8TziXnEG3u/E+cIJaPAFrPQl1aQAIbRAGBZGHJQiMUQKRBkEKbQsAPZaEXQcslSYKmjMyAAdXj34ACkNEiUgDA5t+PAQHn6Ogjkuzry2DNwhuIQAh+QQFBwAQACwAAAAAGAAYAAAFnCAkjiS0LGVaBgBJEGSguo8zCsK4CPIsMg+ECCcKEH0ix6MwhJl4KiOp8UCdmrEbo6EoHpxF8A6aBBZ6vhf5dmAkkGr0CoWs21WGQ2FvsI9xC3l7B311fy93iWGKJQQOhHCAJQB6A3IqcWwJLU90i2FkUiMKlhBELEI6MwgDXRAGhQgAYD6tTqRFAJxpA6mvrqazSKJJhUWMpjlIIQA7')
+        center no-repeat;
+    }
+
+    .pdfViewer .page .loadingIcon.notVisible {
+      background: none;
+    }
+
+    .pdfViewer.enablePermissions .textLayer span {
+      -webkit-user-select: none !important;
+      -moz-user-select: none !important;
+      user-select: none !important;
+      cursor: not-allowed;
+    }
+
+    .pdfPresentationMode .pdfViewer {
+      padding-bottom: 0;
+    }
+
+    .pdfPresentationMode .spread {
+      margin: 0;
+    }
+
+    .pdfPresentationMode .pdfViewer .page {
+      margin: 0 auto;
+      border: 2px solid transparent;
+    }
+  }
diff --git a/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.ts b/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.ts
new file mode 100644 (file)
index 0000000..f9e7eb0
--- /dev/null
@@ -0,0 +1,599 @@
+/**
+ * This file is taken and modified from https://github.com/VadimDez/ng2-pdf-viewer/blob/10.0.0/src/app/pdf-viewer/pdf-viewer.component.ts
+ * Created by vadimdez on 21/06/16.
+ */
+import {
+  Component,
+  Input,
+  Output,
+  ElementRef,
+  EventEmitter,
+  OnChanges,
+  SimpleChanges,
+  OnInit,
+  OnDestroy,
+  ViewChild,
+  AfterViewChecked,
+  NgZone,
+} from '@angular/core'
+import { from, fromEvent, Subject } from 'rxjs'
+import { debounceTime, filter, takeUntil } from 'rxjs/operators'
+import * as PDFJS from 'pdfjs-dist'
+import * as PDFJSViewer from 'pdfjs-dist/web/pdf_viewer'
+
+import { createEventBus } from './utils/event-bus-utils'
+
+import type {
+  PDFSource,
+  PDFPageProxy,
+  PDFProgressData,
+  PDFDocumentProxy,
+  PDFDocumentLoadingTask,
+  PDFViewerOptions,
+  ZoomScale,
+} from './typings'
+import { PDFSinglePageViewer } from 'pdfjs-dist/web/pdf_viewer'
+
+PDFJS['verbosity'] = PDFJS.VerbosityLevel.ERRORS
+
+// Yea this is a straight hack
+declare global {
+  interface WeakKeyTypes {
+    symbol: Object
+  }
+
+  type WeakKey = WeakKeyTypes[keyof WeakKeyTypes]
+}
+
+export enum RenderTextMode {
+  DISABLED,
+  ENABLED,
+  ENHANCED,
+}
+
+@Component({
+  selector: 'pngx-pdf-viewer',
+  templateUrl: './pdf-viewer.component.html',
+  styleUrls: ['./pdf-viewer.component.scss'],
+})
+export class PdfViewerComponent
+  implements OnChanges, OnInit, OnDestroy, AfterViewChecked
+{
+  static CSS_UNITS = 96.0 / 72.0
+  static BORDER_WIDTH = 9
+
+  @ViewChild('pdfViewerContainer')
+  pdfViewerContainer!: ElementRef<HTMLDivElement>
+
+  public eventBus!: PDFJSViewer.EventBus
+  public pdfLinkService!: PDFJSViewer.PDFLinkService
+  public pdfViewer!: PDFJSViewer.PDFViewer | PDFSinglePageViewer
+
+  private isVisible = false
+
+  private _cMapsUrl =
+    typeof PDFJS !== 'undefined'
+      ? `https://unpkg.com/pdfjs-dist@${(PDFJS as any).version}/cmaps/`
+      : null
+  private _imageResourcesPath =
+    typeof PDFJS !== 'undefined'
+      ? `https://unpkg.com/pdfjs-dist@${(PDFJS as any).version}/web/images/`
+      : undefined
+  private _renderText = true
+  private _renderTextMode: RenderTextMode = RenderTextMode.ENABLED
+  private _stickToPage = false
+  private _originalSize = true
+  private _pdf: PDFDocumentProxy | undefined
+  private _page = 1
+  private _zoom = 1
+  private _zoomScale: ZoomScale = 'page-width'
+  private _rotation = 0
+  private _showAll = true
+  private _canAutoResize = true
+  private _fitToPage = false
+  private _externalLinkTarget = 'blank'
+  private _showBorders = false
+  private lastLoaded!: string | Uint8Array | PDFSource | null
+  private _latestScrolledPage!: number
+
+  private resizeTimeout: number | null = null
+  private pageScrollTimeout: number | null = null
+  private isInitialized = false
+  private loadingTask?: PDFDocumentLoadingTask | null
+  private destroy$ = new Subject<void>()
+
+  @Output('after-load-complete') afterLoadComplete =
+    new EventEmitter<PDFDocumentProxy>()
+  @Output('page-rendered') pageRendered = new EventEmitter<CustomEvent>()
+  @Output('pages-initialized') pageInitialized = new EventEmitter<CustomEvent>()
+  @Output('text-layer-rendered') textLayerRendered =
+    new EventEmitter<CustomEvent>()
+  @Output('error') onError = new EventEmitter<any>()
+  @Output('on-progress') onProgress = new EventEmitter<PDFProgressData>()
+  @Output() pageChange: EventEmitter<number> = new EventEmitter<number>(true)
+  @Input() src?: string | Uint8Array | PDFSource
+
+  @Input('c-maps-url')
+  set cMapsUrl(cMapsUrl: string) {
+    this._cMapsUrl = cMapsUrl
+  }
+
+  @Input('page')
+  set page(_page: number | string | any) {
+    _page = parseInt(_page, 10) || 1
+    const originalPage = _page
+
+    if (this._pdf) {
+      _page = this.getValidPageNumber(_page)
+    }
+
+    this._page = _page
+    if (originalPage !== _page) {
+      this.pageChange.emit(_page)
+    }
+  }
+
+  @Input('render-text')
+  set renderText(renderText: boolean) {
+    this._renderText = renderText
+  }
+
+  @Input('render-text-mode')
+  set renderTextMode(renderTextMode: RenderTextMode) {
+    this._renderTextMode = renderTextMode
+  }
+
+  @Input('original-size')
+  set originalSize(originalSize: boolean) {
+    this._originalSize = originalSize
+  }
+
+  @Input('show-all')
+  set showAll(value: boolean) {
+    this._showAll = value
+  }
+
+  @Input('stick-to-page')
+  set stickToPage(value: boolean) {
+    this._stickToPage = value
+  }
+
+  @Input('zoom')
+  set zoom(value: number) {
+    if (value <= 0) {
+      return
+    }
+
+    this._zoom = value
+  }
+
+  get zoom() {
+    return this._zoom
+  }
+
+  @Input('zoom-scale')
+  set zoomScale(value: ZoomScale) {
+    this._zoomScale = value
+  }
+
+  get zoomScale() {
+    return this._zoomScale
+  }
+
+  @Input('rotation')
+  set rotation(value: number) {
+    if (!(typeof value === 'number' && value % 90 === 0)) {
+      console.warn('Invalid pages rotation angle.')
+      return
+    }
+
+    this._rotation = value
+  }
+
+  @Input('external-link-target')
+  set externalLinkTarget(value: string) {
+    this._externalLinkTarget = value
+  }
+
+  @Input('autoresize')
+  set autoresize(value: boolean) {
+    this._canAutoResize = Boolean(value)
+  }
+
+  @Input('fit-to-page')
+  set fitToPage(value: boolean) {
+    this._fitToPage = Boolean(value)
+  }
+
+  @Input('show-borders')
+  set showBorders(value: boolean) {
+    this._showBorders = Boolean(value)
+  }
+
+  static getLinkTarget(type: string) {
+    switch (type) {
+      case 'blank':
+        return (PDFJSViewer as any).LinkTarget.BLANK
+      case 'none':
+        return (PDFJSViewer as any).LinkTarget.NONE
+      case 'self':
+        return (PDFJSViewer as any).LinkTarget.SELF
+      case 'parent':
+        return (PDFJSViewer as any).LinkTarget.PARENT
+      case 'top':
+        return (PDFJSViewer as any).LinkTarget.TOP
+    }
+
+    return null
+  }
+
+  constructor(
+    private element: ElementRef<HTMLElement>,
+    private ngZone: NgZone
+  ) {
+    PDFJS.GlobalWorkerOptions['workerSrc'] = '/assets/js/pdf.worker.min.js'
+  }
+
+  ngAfterViewChecked(): void {
+    if (this.isInitialized) {
+      return
+    }
+
+    const offset = this.pdfViewerContainer.nativeElement.offsetParent
+
+    if (this.isVisible === true && offset == null) {
+      this.isVisible = false
+      return
+    }
+
+    if (this.isVisible === false && offset != null) {
+      this.isVisible = true
+
+      setTimeout(() => {
+        this.initialize()
+        this.ngOnChanges({ src: this.src } as any)
+      })
+    }
+  }
+
+  ngOnInit() {
+    this.initialize()
+    this.setupResizeListener()
+  }
+
+  ngOnDestroy() {
+    this.clear()
+    this.destroy$.next()
+    this.loadingTask = null
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (!this.isVisible) {
+      return
+    }
+
+    if ('src' in changes) {
+      this.loadPDF()
+    } else if (this._pdf) {
+      if ('renderText' in changes || 'showAll' in changes) {
+        this.setupViewer()
+        this.resetPdfDocument()
+      }
+      if ('page' in changes) {
+        const { page } = changes
+        if (page.currentValue === this._latestScrolledPage) {
+          return
+        }
+
+        // New form of page changing: The viewer will now jump to the specified page when it is changed.
+        // This behavior is introduced by using the PDFSinglePageViewer
+        this.pdfViewer.scrollPageIntoView({ pageNumber: this._page })
+      }
+
+      this.update()
+    }
+  }
+
+  public updateSize() {
+    from(
+      this._pdf!.getPage(
+        this.pdfViewer.currentPageNumber
+      ) as unknown as Promise<PDFPageProxy>
+    )
+      .pipe(takeUntil(this.destroy$))
+      .subscribe({
+        next: (page: PDFPageProxy) => {
+          const rotation = this._rotation + page.rotate
+          const viewportWidth =
+            (page as any).getViewport({
+              scale: this._zoom,
+              rotation,
+            }).width * PdfViewerComponent.CSS_UNITS
+          let scale = this._zoom
+          let stickToPage = true
+
+          // Scale the document when it shouldn't be in original size or doesn't fit into the viewport
+          if (
+            !this._originalSize ||
+            (this._fitToPage &&
+              viewportWidth > this.pdfViewerContainer.nativeElement.clientWidth)
+          ) {
+            const viewPort = (page as any).getViewport({ scale: 1, rotation })
+            scale = this.getScale(viewPort.width, viewPort.height)
+            stickToPage = !this._stickToPage
+          }
+
+          setTimeout(() => {
+            this.pdfViewer.currentScale = scale
+          })
+        },
+      })
+  }
+
+  public clear() {
+    if (this.loadingTask && !this.loadingTask.destroyed) {
+      this.loadingTask.destroy()
+    }
+
+    if (this._pdf) {
+      this._latestScrolledPage = 0
+      this._pdf.destroy()
+      this._pdf = undefined
+    }
+  }
+
+  private getPDFLinkServiceConfig() {
+    const linkTarget = PdfViewerComponent.getLinkTarget(
+      this._externalLinkTarget
+    )
+
+    if (linkTarget) {
+      return { externalLinkTarget: linkTarget }
+    }
+
+    return {}
+  }
+
+  private initEventBus() {
+    this.eventBus = createEventBus(PDFJSViewer, this.destroy$)
+
+    fromEvent<CustomEvent>(this.eventBus, 'pagerendered')
+      .pipe(takeUntil(this.destroy$))
+      .subscribe((event) => {
+        this.pageRendered.emit(event)
+      })
+
+    fromEvent<CustomEvent>(this.eventBus, 'pagesinit')
+      .pipe(takeUntil(this.destroy$))
+      .subscribe((event) => {
+        this.pageInitialized.emit(event)
+      })
+
+    fromEvent(this.eventBus, 'pagechanging')
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(({ pageNumber }: any) => {
+        if (this.pageScrollTimeout) {
+          clearTimeout(this.pageScrollTimeout)
+        }
+
+        this.pageScrollTimeout = window.setTimeout(() => {
+          this._latestScrolledPage = pageNumber
+          this.pageChange.emit(pageNumber)
+        }, 100)
+      })
+
+    fromEvent<CustomEvent>(this.eventBus, 'textlayerrendered')
+      .pipe(takeUntil(this.destroy$))
+      .subscribe((event) => {
+        this.textLayerRendered.emit(event)
+      })
+  }
+
+  private initPDFServices() {
+    this.pdfLinkService = new PDFJSViewer.PDFLinkService({
+      eventBus: this.eventBus,
+      ...this.getPDFLinkServiceConfig(),
+    })
+  }
+
+  private getPDFOptions(): PDFViewerOptions {
+    return {
+      eventBus: this.eventBus,
+      container: this.element.nativeElement.querySelector('div')!,
+      removePageBorders: !this._showBorders,
+      linkService: this.pdfLinkService,
+      textLayerMode: this._renderText
+        ? this._renderTextMode
+        : RenderTextMode.DISABLED,
+      imageResourcesPath: this._imageResourcesPath,
+    }
+  }
+
+  private setupViewer() {
+    PDFJS['disableTextLayer'] = !this._renderText
+
+    this.initPDFServices()
+
+    if (this._showAll) {
+      this.pdfViewer = new PDFJSViewer.PDFViewer(this.getPDFOptions())
+    } else {
+      this.pdfViewer = new PDFJSViewer.PDFSinglePageViewer(this.getPDFOptions())
+    }
+    this.pdfLinkService.setViewer(this.pdfViewer)
+
+    this.pdfViewer._currentPageNumber = this._page
+  }
+
+  private getValidPageNumber(page: number): number {
+    if (page < 1) {
+      return 1
+    }
+
+    if (page > this._pdf!.numPages) {
+      return this._pdf!.numPages
+    }
+
+    return page
+  }
+
+  private getDocumentParams() {
+    const srcType = typeof this.src
+
+    if (!this._cMapsUrl) {
+      return this.src
+    }
+
+    const params: any = {
+      cMapUrl: this._cMapsUrl,
+      cMapPacked: true,
+      enableXfa: true,
+    }
+
+    if (srcType === 'string') {
+      params.url = this.src
+    } else if (srcType === 'object') {
+      if ((this.src as any).byteLength !== undefined) {
+        params.data = this.src
+      } else {
+        Object.assign(params, this.src)
+      }
+    }
+
+    return params
+  }
+
+  private loadPDF() {
+    if (!this.src) {
+      return
+    }
+
+    if (this.lastLoaded === this.src) {
+      this.update()
+      return
+    }
+
+    this.clear()
+
+    this.setupViewer()
+
+    this.loadingTask = PDFJS.getDocument(this.getDocumentParams())
+
+    this.loadingTask!.onProgress = (progressData: PDFProgressData) => {
+      this.onProgress.emit(progressData)
+    }
+
+    const src = this.src
+
+    from(this.loadingTask!.promise as Promise<PDFDocumentProxy>)
+      .pipe(takeUntil(this.destroy$))
+      .subscribe({
+        next: (pdf) => {
+          this._pdf = pdf
+          this.lastLoaded = src
+
+          this.afterLoadComplete.emit(pdf)
+          this.resetPdfDocument()
+
+          this.update()
+        },
+        error: (error) => {
+          this.lastLoaded = null
+          this.onError.emit(error)
+        },
+      })
+  }
+
+  private update() {
+    this.page = this._page
+
+    this.render()
+  }
+
+  private render() {
+    this._page = this.getValidPageNumber(this._page)
+
+    if (
+      this._rotation !== 0 ||
+      this.pdfViewer.pagesRotation !== this._rotation
+    ) {
+      setTimeout(() => {
+        this.pdfViewer.pagesRotation = this._rotation
+      })
+    }
+
+    if (this._stickToPage) {
+      setTimeout(() => {
+        this.pdfViewer.currentPageNumber = this._page
+      })
+    }
+
+    this.updateSize()
+  }
+
+  private getScale(viewportWidth: number, viewportHeight: number) {
+    const borderSize = this._showBorders
+      ? 2 * PdfViewerComponent.BORDER_WIDTH
+      : 0
+    const pdfContainerWidth =
+      this.pdfViewerContainer.nativeElement.clientWidth - borderSize
+    const pdfContainerHeight =
+      this.pdfViewerContainer.nativeElement.clientHeight - borderSize
+
+    if (
+      pdfContainerHeight === 0 ||
+      viewportHeight === 0 ||
+      pdfContainerWidth === 0 ||
+      viewportWidth === 0
+    ) {
+      return 1
+    }
+
+    let ratio = 1
+    switch (this._zoomScale) {
+      case 'page-fit':
+        ratio = Math.min(
+          pdfContainerHeight / viewportHeight,
+          pdfContainerWidth / viewportWidth
+        )
+        break
+      case 'page-height':
+        ratio = pdfContainerHeight / viewportHeight
+        break
+      case 'page-width':
+      default:
+        ratio = pdfContainerWidth / viewportWidth
+        break
+    }
+
+    return (this._zoom * ratio) / PdfViewerComponent.CSS_UNITS
+  }
+
+  private resetPdfDocument() {
+    this.pdfLinkService.setDocument(this._pdf, null)
+    this.pdfViewer.setDocument(this._pdf!)
+  }
+
+  private initialize(): void {
+    if (!this.isVisible) {
+      return
+    }
+
+    this.isInitialized = true
+    this.initEventBus()
+    this.setupViewer()
+  }
+
+  private setupResizeListener(): void {
+    this.ngZone.runOutsideAngular(() => {
+      fromEvent(window, 'resize')
+        .pipe(
+          debounceTime(100),
+          filter(() => this._canAutoResize && !!this._pdf),
+          takeUntil(this.destroy$)
+        )
+        .subscribe(() => {
+          this.updateSize()
+        })
+    })
+  }
+}
diff --git a/src-ui/src/app/components/common/pdf-viewer/typings.ts b/src-ui/src/app/components/common/pdf-viewer/typings.ts
new file mode 100644 (file)
index 0000000..8dad45f
--- /dev/null
@@ -0,0 +1,17 @@
+export type PDFPageProxy =
+  import('pdfjs-dist/types/src/display/api').PDFPageProxy
+export type PDFSource =
+  import('pdfjs-dist/types/src/display/api').DocumentInitParameters
+export type PDFDocumentProxy =
+  import('pdfjs-dist/types/src/display/api').PDFDocumentProxy
+export type PDFDocumentLoadingTask =
+  import('pdfjs-dist/types/src/display/api').PDFDocumentLoadingTask
+export type PDFViewerOptions =
+  import('pdfjs-dist/types/web/pdf_viewer').PDFViewerOptions
+
+export interface PDFProgressData {
+  loaded: number
+  total: number
+}
+
+export type ZoomScale = 'page-height' | 'page-fit' | 'page-width'
diff --git a/src-ui/src/app/components/common/pdf-viewer/utils/event-bus-utils.ts b/src-ui/src/app/components/common/pdf-viewer/utils/event-bus-utils.ts
new file mode 100644 (file)
index 0000000..8ae5011
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * This file is taken and modified from https://github.com/VadimDez/ng2-pdf-viewer/blob/10.0.0/src/app/pdf-viewer/utils/event-bus-utils.ts
+ * Created by vadimdez on 21/06/16.
+ */
+import { fromEvent, Subject } from 'rxjs'
+import { takeUntil } from 'rxjs/operators'
+
+import type { EventBus } from 'pdfjs-dist/web/pdf_viewer'
+
+// interface EventBus {
+//   on(eventName: string, listener: Function): void;
+//   off(eventName: string, listener: Function): void;
+//   _listeners: any;
+//   dispatch(eventName: string, data: Object): void;
+//   _on(eventName: any, listener: any, options?: null): void;
+//   _off(eventName: any, listener: any, options?: null): void;
+// }
+
+export function createEventBus(pdfJsViewer: any, destroy$: Subject<void>) {
+  const globalEventBus: EventBus = new pdfJsViewer.EventBus()
+  attachDOMEventsToEventBus(globalEventBus, destroy$)
+  return globalEventBus
+}
+
+function attachDOMEventsToEventBus(
+  eventBus: EventBus,
+  destroy$: Subject<void>
+): void {
+  fromEvent(eventBus, 'documentload')
+    .pipe(takeUntil(destroy$))
+    .subscribe(() => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('documentload', true, true, {})
+      window.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'pagerendered')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ pageNumber, cssTransform, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('pagerendered', true, true, {
+        pageNumber,
+        cssTransform,
+      })
+      source.div.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'textlayerrendered')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ pageNumber, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('textlayerrendered', true, true, { pageNumber })
+      source.textLayerDiv?.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'pagechanging')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ pageNumber, source }: any) => {
+      const event = document.createEvent('UIEvents') as any
+      event.initEvent('pagechanging', true, true)
+      /* tslint:disable:no-string-literal */
+      event['pageNumber'] = pageNumber
+      source.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'pagesinit')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('pagesinit', true, true, null)
+      source.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'pagesloaded')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ pagesCount, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('pagesloaded', true, true, { pagesCount })
+      source.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'scalechange')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ scale, presetValue, source }: any) => {
+      const event = document.createEvent('UIEvents') as any
+      event.initEvent('scalechange', true, true)
+      /* tslint:disable:no-string-literal */
+      event['scale'] = scale
+      /* tslint:disable:no-string-literal */
+      event['presetValue'] = presetValue
+      source.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'updateviewarea')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ location, source }: any) => {
+      const event = document.createEvent('UIEvents') as any
+      event.initEvent('updateviewarea', true, true)
+      event['location'] = location
+      source.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'find')
+    .pipe(takeUntil(destroy$))
+    .subscribe(
+      ({
+        source,
+        type,
+        query,
+        phraseSearch,
+        caseSensitive,
+        highlightAll,
+        findPrevious,
+      }: any) => {
+        if (source === window) {
+          return // event comes from FirefoxCom, no need to replicate
+        }
+        const event = document.createEvent('CustomEvent')
+        event.initCustomEvent('find' + type, true, true, {
+          query,
+          phraseSearch,
+          caseSensitive,
+          highlightAll,
+          findPrevious,
+        })
+        window.dispatchEvent(event)
+      }
+    )
+
+  fromEvent(eventBus, 'attachmentsloaded')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ attachmentsCount, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('attachmentsloaded', true, true, {
+        attachmentsCount,
+      })
+      source.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'sidebarviewchanged')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ view, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('sidebarviewchanged', true, true, { view })
+      source.outerContainer.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'pagemode')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ mode, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('pagemode', true, true, { mode })
+      source.pdfViewer.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'namedaction')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ action, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('namedaction', true, true, { action })
+      source.pdfViewer.container.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'presentationmodechanged')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ active, switchInProgress }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('presentationmodechanged', true, true, {
+        active,
+        switchInProgress,
+      })
+      window.dispatchEvent(event)
+    })
+
+  fromEvent(eventBus, 'outlineloaded')
+    .pipe(takeUntil(destroy$))
+    .subscribe(({ outlineCount, source }: any) => {
+      const event = document.createEvent('CustomEvent')
+      event.initCustomEvent('outlineloaded', true, true, { outlineCount })
+      source.container.dispatchEvent(event)
+    })
+}
index 19ee955ae6d355b7c8e9c5d736e5af31de5c4fe2..8b50e6f2e61cf9e3381116db425b8ee5b815cdc2 100644 (file)
@@ -1,9 +1,20 @@
 <pngx-page-header [(title)]="title">
-    <div class="input-group input-group-sm me-5 d-none d-md-flex" *ngIf="getContentType() === 'application/pdf' && !useNativePdfViewer">
-      <div class="input-group-text" i18n>Page</div>
-      <input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
-      <div class="input-group-text" i18n>of {{previewNumPages}}</div>
-    </div>
+    <ng-container *ngIf="getContentType() === 'application/pdf' && !useNativePdfViewer">
+        <div class="input-group input-group-sm me-2 d-none d-md-flex">
+            <div class="input-group-text" i18n>Page</div>
+            <input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
+            <div class="input-group-text" i18n>of {{previewNumPages}}</div>
+        </div>
+        <div class="input-group input-group-sm me-5 d-none d-md-flex">
+            <button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
+            <select class="form-select" (change)="onZoomSelect($event)">
+                <option *ngFor="let setting of zoomSettings" [value]="setting" [selected]="previewZoomSetting === setting">
+                    {{ getZoomSettingTitle(setting) }}
+                </option>
+            </select>
+            <button class="btn btn-outline-secondary" (click)="increaseZoom()" i18n>+</button>
+        </div>
+    </ng-container>
 
     <button type="button" class="btn btn-sm btn-outline-danger me-4" (click)="delete()" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
         <svg class="buttonicon" fill="currentColor">
                 <li [ngbNavItem]="DocumentDetailNavIDs.Preview" class="d-md-none">
                     <a ngbNavLink i18n>Preview</a>
                     <ng-template ngbNavContent *ngIf="!pdfPreview.offsetParent">
-                        <div class="position-relative">
-                            <ng-container *ngIf="getContentType() === 'application/pdf'">
-                                <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
-                                    <pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
-                                </div>
-                                <ng-template #nativePdfViewer>
-                                    <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
-                                </ng-template>
-                            </ng-container>
-                            <ng-container *ngIf="getContentType() === 'text/plain'">
-                                <object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-light overflow-auto" width="100%"></object>
-                            </ng-container>
-                            <div *ngIf="requiresPassword" class="password-prompt">
-                                <form>
-                                    <input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
-                                </form>
-                            </div>
-                        </div>
+                        <ng-container *ngTemplateOutlet="previewContent"></ng-container>
                     </ng-template>
                 </li>
 
     </div>
 
     <div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
-        <ng-container *ngIf="getContentType() === 'application/pdf'">
-            <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
-                <pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
-            </div>
-            <ng-template #nativePdfViewer>
-                <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
-            </ng-template>
-        </ng-container>
+        <ng-container *ngTemplateOutlet="previewContent"></ng-container>
         <ng-container *ngIf="renderAsPlainText">
             <div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
         </ng-container>
     </div>
 
 </div>
+
+ <ng-template #previewContent>
+     <div *ngIf="!metadata" class="w-100 h-100 d-flex align-items-center justify-content-center">
+         <div>
+             <div class="spinner-border spinner-border-sm me-2" role="status"></div>
+             <ng-container i18n>Loading...</ng-container>
+         </div>
+     </div>
+     <ng-container *ngIf="getContentType() === 'application/pdf'">
+         <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
+            <pngx-pdf-viewer
+                [src]="{ url: previewUrl, password: password }"
+                [original-size]="false"
+                [show-borders]="true"
+                [show-all]="true"
+                [(page)]="previewCurrentPage"
+                [zoom-scale]="previewZoomScale"
+                [zoom]="previewZoomSetting"
+                [render-text-mode]="2"
+                (error)="onError($event)"
+                (after-load-complete)="pdfPreviewLoaded($event)">
+            </pngx-pdf-viewer>
+         </div>
+         <ng-template #nativePdfViewer>
+             <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
+         </ng-template>
+     </ng-container>
+     <ng-container *ngIf="renderAsPlainText">
+         <div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
+     </ng-container>
+     <div *ngIf="showPasswordField" class="password-prompt">
+         <form>
+             <input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
+         </form>
+     </div>
+ </ng-template>
index 9e2aef4ac8db5b0e6556e597f4f7f73a1b0bd832..ba9b0a5ea7c1de4702994071ef1f4a523a4c50d6 100644 (file)
@@ -7,19 +7,14 @@
 .pdf-viewer-container {
   background-color: gray;
 
-  pdf-viewer {
+  pngx-pdf-viewer {
     width: 100%;
     height: 100%;
   }
 }
 
-::ng-deep .ng2-pdf-viewer-container .page {
-  --page-margin: 1px 0 10px;
-  width: 100% !important;
-}
-
-::ng-deep .ng2-pdf-viewer-container .page:last-child {
-  --page-margin: 1px 0 20px;
+::ng-deep .pngx-pdf-viewer-container .page {
+  --page-margin: 10px auto;
 }
 
 ::ng-deep .ng-select-taggable {
 textarea.rtl {
   direction: rtl;
 }
+
+.form-select {
+  padding-right: 2.5em;
+}
+
+.input-group .btn-outline-secondary {
+  border-color: var(--bs-border-color);
+}
index 4c3f6004d0abeb30561d3ca846d29483c341fe5f..4a9d01cc269147235d680f66a544a7e077f6894a 100644 (file)
@@ -19,7 +19,6 @@ import {
   NgbDateStruct,
 } from '@ng-bootstrap/ng-bootstrap'
 import { NgSelectModule } from '@ng-select/ng-select'
-import { PdfViewerComponent } from 'ng2-pdf-viewer'
 import { of, throwError } from 'rxjs'
 import { routes } from 'src/app/app-routing.module'
 import {
@@ -70,6 +69,7 @@ import { ShareLinksDropdownComponent } from '../common/share-links-dropdown/shar
 import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
 import { PaperlessCustomFieldDataType } from 'src/app/data/paperless-custom-field'
 import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
+import { PdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
 
 const doc: PaperlessDocument = {
   id: 3,
@@ -160,10 +160,10 @@ describe('DocumentDetailComponent', () => {
         PermissionsFormComponent,
         SafeHtmlPipe,
         ConfirmDialogComponent,
-        PdfViewerComponent,
         SafeUrlPipe,
         ShareLinksDropdownComponent,
         CustomFieldsDropdownComponent,
+        PdfViewerComponent,
       ],
       providers: [
         DocumentTitlePipe,
@@ -683,6 +683,35 @@ describe('DocumentDetailComponent', () => {
     expect(component.previewNumPages).toEqual(1000)
   })
 
+  it('should support zoom controls', () => {
+    initNormally()
+    component.onZoomSelect({ target: { value: '1' } } as any) // from select
+    expect(component.previewZoomSetting).toEqual('1')
+    component.increaseZoom()
+    expect(component.previewZoomSetting).toEqual('1.5')
+    component.increaseZoom()
+    expect(component.previewZoomSetting).toEqual('2')
+    component.decreaseZoom()
+    expect(component.previewZoomSetting).toEqual('1.5')
+    component.onZoomSelect({ target: { value: '1' } } as any) // from select
+    component.decreaseZoom()
+    expect(component.previewZoomSetting).toEqual('.75')
+
+    component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select
+    expect(component.previewZoomScale).toEqual('page-fit')
+    expect(component.previewZoomSetting).toEqual('1')
+    component.increaseZoom()
+    expect(component.previewZoomSetting).toEqual('1.5')
+    expect(component.previewZoomScale).toEqual('page-width')
+
+    component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select
+    expect(component.previewZoomScale).toEqual('page-fit')
+    expect(component.previewZoomSetting).toEqual('1')
+    component.decreaseZoom()
+    expect(component.previewZoomSetting).toEqual('.5')
+    expect(component.previewZoomScale).toEqual('page-width')
+  })
+
   it('should support updating notes dynamically', () => {
     const notes = [
       {
@@ -806,7 +835,7 @@ describe('DocumentDetailComponent', () => {
     jest.spyOn(settingsService, 'get').mockReturnValue(false)
     expect(component.useNativePdfViewer).toBeFalsy()
     fixture.detectChanges()
-    expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
+    expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
   })
 
   it('should display native pdf viewer if enabled', () => {
index 368f1e632fdb0407a029231dd865bf30ca43dae3..ef2728c3140b17defab312f02a7df31750af0804 100644 (file)
@@ -21,7 +21,6 @@ import { DocumentService } from 'src/app/services/rest/document.service'
 import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
 import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
 import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
-import { PDFDocumentProxy } from 'ng2-pdf-viewer'
 import { ToastService } from 'src/app/services/toast.service'
 import { TextComponent } from '../common/input/text/text.component'
 import { SettingsService } from 'src/app/services/settings.service'
@@ -69,6 +68,7 @@ import {
 } from 'src/app/data/paperless-custom-field'
 import { PaperlessCustomFieldInstance } from 'src/app/data/paperless-custom-field-instance'
 import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
+import { PDFDocumentProxy } from '../common/pdf-viewer/typings'
 
 enum DocumentDetailNavIDs {
   Details = 1,
@@ -79,6 +79,18 @@ enum DocumentDetailNavIDs {
   Permissions = 6,
 }
 
+enum ZoomSetting {
+  PageFit = 'page-fit',
+  PageWidth = 'page-width',
+  Quarter = '.25',
+  Half = '.5',
+  ThreeQuarters = '.75',
+  One = '1',
+  OneAndHalf = '1.5',
+  Two = '2',
+  Three = '3',
+}
+
 @Component({
   selector: 'pngx-document-detail',
   templateUrl: './document-detail.component.html',
@@ -130,6 +142,8 @@ export class DocumentDetailComponent
 
   previewCurrentPage: number = 1
   previewNumPages: number = 1
+  previewZoomSetting: ZoomSetting = ZoomSetting.One
+  previewZoomScale: ZoomSetting = ZoomSetting.PageWidth
 
   store: BehaviorSubject<any>
   isDirty$: Observable<boolean>
@@ -744,6 +758,54 @@ export class DocumentDetailComponent
     }
   }
 
+  onZoomSelect(event: Event) {
+    const setting = (event.target as HTMLSelectElement)?.value as ZoomSetting
+    if (ZoomSetting.PageFit === setting) {
+      this.previewZoomSetting = ZoomSetting.One
+      this.previewZoomScale = setting
+    } else {
+      this.previewZoomScale = ZoomSetting.PageWidth
+      this.previewZoomSetting = setting
+    }
+  }
+
+  get zoomSettings() {
+    return Object.values(ZoomSetting).filter(
+      (setting) => setting !== ZoomSetting.PageWidth
+    )
+  }
+
+  getZoomSettingTitle(setting: ZoomSetting): string {
+    switch (setting) {
+      case ZoomSetting.PageFit:
+        return $localize`Page Fit`
+      default:
+        return `${parseFloat(setting) * 100}%`
+    }
+  }
+
+  increaseZoom(): void {
+    let currentIndex = Object.values(ZoomSetting).indexOf(
+      this.previewZoomSetting
+    )
+    if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 5
+    this.previewZoomScale = ZoomSetting.PageWidth
+    this.previewZoomSetting =
+      Object.values(ZoomSetting)[
+        Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1)
+      ]
+  }
+
+  decreaseZoom(): void {
+    let currentIndex = Object.values(ZoomSetting).indexOf(
+      this.previewZoomSetting
+    )
+    if (this.previewZoomScale === ZoomSetting.PageFit) currentIndex = 4
+    this.previewZoomScale = ZoomSetting.PageWidth
+    this.previewZoomSetting =
+      Object.values(ZoomSetting)[Math.max(2, currentIndex - 1)]
+  }
+
   get showPermissions(): boolean {
     return (
       this.permissionsService.currentUserCan(