],
"scripts": [],
"allowedCommonJsDependencies": [
- "ng2-pdf-viewer",
"file-saver",
"utif"
],
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 }) => {
moduleNameMapper: {
...esmPreset.moduleNameMapper,
'^src/(.*)': '<rootDir>/src/$1',
+ '^pdfjs-dist/legacy/build/pdf\\.mjs$':
+ '<rootDir>/src/test/mocks/pdfjs-legacy-build-pdf.ts',
+ '^pdfjs-dist/web/pdf_viewer\\.mjs$':
+ '<rootDir>/src/test/mocks/pdfjs-web-pdf_viewer.ts',
},
workerIdleMemoryLimit: '512MB',
reporters: [
"bootstrap": "^5.3.8",
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
- "ng2-pdf-viewer": "^10.4.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^10.1.0",
"ngx-cookie-service": "^21.1.0",
"ngx-device-detector": "^11.0.0",
"ngx-ui-tour-ng-bootstrap": "^18.0.0",
+ "pdfjs-dist": "^5.4.624",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"utif": "^3.1.0",
mime-names:
specifier: ^1.0.0
version: 1.0.0
- ng2-pdf-viewer:
- specifier: ^10.4.0
- version: 10.4.0
ngx-bootstrap-icons:
specifier: ^1.9.3
version: 1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))
ngx-ui-tour-ng-bootstrap:
specifier: ^18.0.0
version: 18.0.0(2a89effa12f6df8cde064aa7713e7e29)
+ pdfjs-dist:
+ specifier: ^5.4.624
+ version: 5.4.624
rxjs:
specifier: ^7.8.2
version: 7.8.2
cpu: [x64]
os: [win32]
+ '@napi-rs/canvas-android-arm64@0.1.90':
+ resolution: {integrity: sha512-3JBULVF+BIgr7yy7Rf8UjfbkfFx4CtXrkJFD1MDgKJ83b56o0U9ciT8ZGTCNmwWkzu8RbNKlyqPP3KYRG88y7Q==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@napi-rs/canvas-darwin-arm64@0.1.90':
+ resolution: {integrity: sha512-L8XVTXl+8vd8u7nPqcX77NyG5RuFdVsJapQrKV9WE3jBayq1aSMht/IH7Dwiz/RNJ86E5ZSg9pyUPFIlx52PZA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@napi-rs/canvas-darwin-x64@0.1.90':
+ resolution: {integrity: sha512-h0ukhlnGhacbn798VWYTQZpf6JPDzQYaow+vtQ2Fat7j7ImDdpg6tfeqvOTO1r8wS+s+VhBIFITC7aA1Aik0ZQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@napi-rs/canvas-linux-arm-gnueabihf@0.1.90':
+ resolution: {integrity: sha512-JCvTl99b/RfdBtgftqrf+5UNF7GIbp7c5YBFZ+Bd6++4Y3phaXG/4vD9ZcF1bw1P4VpALagHmxvodHuQ9/TfTg==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-arm64-gnu@0.1.90':
+ resolution: {integrity: sha512-vbWFp8lrP8NIM5L4zNOwnsqKIkJo0+GIRUDcLFV9XEJCptCc1FY6/tM02PT7GN4PBgochUPB1nBHdji6q3ieyQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-arm64-musl@0.1.90':
+ resolution: {integrity: sha512-8Bc0BgGEeOaux4EfIfNzcRRw0JE+lO9v6RWQFCJNM9dJFE4QJffTf88hnmbOaI6TEMpgWOKipbha3dpIdUqb/g==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-riscv64-gnu@0.1.90':
+ resolution: {integrity: sha512-0iiVDG5IH+gJb/YUrY/pRdbsjcgvwUmeckL/0gShWAA7004ygX2ST69M1wcfyxXrzFYjdF8S/Sn6aCAeBi89XQ==}
+ engines: {node: '>= 10'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-x64-gnu@0.1.90':
+ resolution: {integrity: sha512-SkKmlHMvA5spXuKfh7p6TsScDf7lp5XlMbiUhjdCtWdOS6Qke/A4qGVOciy6piIUCJibL+YX+IgdGqzm2Mpx/w==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-x64-musl@0.1.90':
+ resolution: {integrity: sha512-o6QgS10gAS4vvELGDOOWYfmERXtkVRYFWBCjomILWfMgCvBVutn8M97fsMW5CrEuJI8YuxuJ7U+/DQ9oG93vDA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@napi-rs/canvas-win32-arm64-msvc@0.1.90':
+ resolution: {integrity: sha512-2UHO/DC1oyuSjeCAhHA0bTD9qsg58kknRqjJqRfvIEFtdqdtNTcWXMCT9rQCuJ8Yx5ldhyh2SSp7+UDqD2tXZQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@napi-rs/canvas-win32-x64-msvc@0.1.90':
+ resolution: {integrity: sha512-48CxEbzua5BP4+OumSZdi3+9fNiRO8cGNBlO2bKwx1PoyD1R2AXzPtqd/no1f1uSl0W2+ihOO1v3pqT3USbmgQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@napi-rs/canvas@0.1.90':
+ resolution: {integrity: sha512-vO9j7TfwF9qYCoTOPO39yPLreTRslBVOaeIwhDZkizDvBb0MounnTl0yeWUMBxP4Pnkg9Sv+3eQwpxNUmTwt0w==}
+ engines: {node: '>= 10'}
+
'@napi-rs/nice-android-arm-eabi@1.1.1':
resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==}
engines: {node: '>= 10'}
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
- ng2-pdf-viewer@10.4.0:
- resolution: {integrity: sha512-TPh1oLZoeARggreTG60Sl3ikSn+Z3+At9pLZ0o/vxPjc7mW2ok2XPyl2Oqz7VyP80ipVorldm1hsLPBmNe2zzA==}
-
ngx-bootstrap-icons@1.9.3:
resolution: {integrity: sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==}
engines: {node: '>= 16.18.1', npm: '>= 8.11.0'}
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
+ node-readable-to-web-readable-stream@0.4.2:
+ resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
+
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
- path2d@0.2.2:
- resolution: {integrity: sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==}
- engines: {node: '>=6'}
-
- pdfjs-dist@4.8.69:
- resolution: {integrity: sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==}
- engines: {node: '>=18'}
+ pdfjs-dist@5.4.624:
+ resolution: {integrity: sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==}
+ engines: {node: '>=20.16.0 || >=22.3.0'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true
+ '@napi-rs/canvas-android-arm64@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-darwin-arm64@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-darwin-x64@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm-gnueabihf@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm64-gnu@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm64-musl@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-linux-riscv64-gnu@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-linux-x64-gnu@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-linux-x64-musl@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-win32-arm64-msvc@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas-win32-x64-msvc@0.1.90':
+ optional: true
+
+ '@napi-rs/canvas@0.1.90':
+ optionalDependencies:
+ '@napi-rs/canvas-android-arm64': 0.1.90
+ '@napi-rs/canvas-darwin-arm64': 0.1.90
+ '@napi-rs/canvas-darwin-x64': 0.1.90
+ '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.90
+ '@napi-rs/canvas-linux-arm64-gnu': 0.1.90
+ '@napi-rs/canvas-linux-arm64-musl': 0.1.90
+ '@napi-rs/canvas-linux-riscv64-gnu': 0.1.90
+ '@napi-rs/canvas-linux-x64-gnu': 0.1.90
+ '@napi-rs/canvas-linux-x64-musl': 0.1.90
+ '@napi-rs/canvas-win32-arm64-msvc': 0.1.90
+ '@napi-rs/canvas-win32-x64-msvc': 0.1.90
+ optional: true
+
'@napi-rs/nice-android-arm-eabi@1.1.1':
optional: true
neo-async@2.6.2: {}
- ng2-pdf-viewer@10.4.0:
- dependencies:
- pdfjs-dist: 4.8.69
- tslib: 2.8.1
-
ngx-bootstrap-icons@1.9.3(@angular/common@21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0)):
dependencies:
'@angular/common': 21.1.3(@angular/core@21.1.3(@angular/compiler@21.1.3)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2)
node-int64@0.4.0: {}
+ node-readable-to-web-readable-stream@0.4.2:
+ optional: true
+
node-releases@2.0.27: {}
nopt@9.0.0:
path-to-regexp@8.3.0: {}
- path2d@0.2.2:
- optional: true
-
- pdfjs-dist@4.8.69:
+ pdfjs-dist@5.4.624:
optionalDependencies:
- canvas: 3.0.0
- path2d: 0.2.2
+ '@napi-rs/canvas': 0.1.90
+ node-readable-to-web-readable-stream: 0.4.2
picocolors@1.1.1: {}
}
}
-Object.defineProperty(window, 'open', { value: jest.fn() })
-Object.defineProperty(window, 'localStorage', { value: mock() })
-Object.defineProperty(window, 'sessionStorage', { value: mock() })
-Object.defineProperty(window, 'getComputedStyle', {
+Object.defineProperty(globalThis, 'open', { value: jest.fn() })
+Object.defineProperty(globalThis, 'localStorage', { value: mock() })
+Object.defineProperty(globalThis, 'sessionStorage', { value: mock() })
+Object.defineProperty(globalThis, 'getComputedStyle', {
value: () => ['-webkit-appearance'],
})
Object.defineProperty(navigator, 'clipboard', {
if (!navigator.share) {
Object.defineProperty(navigator, 'share', { value: jest.fn() })
}
-if (!URL.createObjectURL) {
- Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() })
+if (!globalThis.URL.createObjectURL) {
+ Object.defineProperty(globalThis.URL, 'createObjectURL', { value: jest.fn() })
}
-if (!URL.revokeObjectURL) {
- Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
+if (!globalThis.URL.revokeObjectURL) {
+ Object.defineProperty(globalThis.URL, 'revokeObjectURL', { value: jest.fn() })
}
-Object.defineProperty(window, 'ResizeObserver', { value: mock() })
+class MockResizeObserver {
+ private readonly callback: ResizeObserverCallback
+
+ constructor(callback: ResizeObserverCallback) {
+ this.callback = callback
+ }
+
+ observe = jest.fn()
+ unobserve = jest.fn()
+ disconnect = jest.fn()
+
+ trigger = (entries: ResizeObserverEntry[] = []) => {
+ this.callback(entries, this)
+ }
+}
+
+Object.defineProperty(globalThis, 'ResizeObserver', {
+ writable: true,
+ configurable: true,
+ value: MockResizeObserver,
+})
if (typeof IntersectionObserver === 'undefined') {
class MockIntersectionObserver {
takeRecords = jest.fn()
}
- Object.defineProperty(window, 'IntersectionObserver', {
+ Object.defineProperty(globalThis, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
</div>
<div class="col">
<select class="form-select" formControlName="pdfViewerDefaultZoom">
- <option [ngValue]="ZoomSetting.PageWidth" i18n>Fit width</option>
- <option [ngValue]="ZoomSetting.PageFit" i18n>Fit page</option>
+ <option [ngValue]="PdfZoomScale.PageWidth" i18n>Fit width</option>
+ <option [ngValue]="PdfZoomScale.PageFit" i18n>Fit page</option>
</select>
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
</div>
import { SelectComponent } from '../../common/input/select/select.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
+import { PdfZoomScale } from '../../common/pdf-viewer/pdf-viewer.types'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
-import { ZoomSetting } from '../../document-detail/zoom-setting'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
enum SettingsNavIDs {
public readonly GlobalSearchType = GlobalSearchType
- public readonly ZoomSetting = ZoomSetting
+ public readonly PdfZoomScale = PdfZoomScale
public readonly PdfEditorEditMode = PdfEditorEditMode
-<pdf-viewer [src]="pdfSrc" [render-text]="false" zoom="0.4" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
+<pngx-pdf-viewer class="visually-hidden" [src]="pdfSrc" [renderMode]="PdfRenderMode.Single" [page]="1" [selectable]="false" (afterLoadComplete)="pdfLoaded($event)"></pngx-pdf-viewer>
<div class="modal-header">
<h4 class="modal-title">{{ title }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
<span class="placeholder w-100 h-100"></span>
</div>
}
- <pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [original-size]="false" [show-all]="false" [render-text]="false" (page-rendered)="p.loaded = true"></pdf-viewer>
+ <pngx-pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [renderMode]="PdfRenderMode.Single" (rendered)="p.loaded = true"></pngx-pdf-viewer>
} @placeholder {
<div class="placeholder-glow w-100 h-100 z-10">
<span class="placeholder w-100 h-100"></span>
background-color: gray;
height: 240px;
- pdf-viewer {
+ pngx-pdf-viewer {
width: 100%;
height: 100%;
}
}
-::ng-deep .ng2-pdf-viewer-container {
+::ng-deep .pngx-pdf-viewer-container {
overflow: hidden;
}
import { Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
-import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
+import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
+import {
+ PdfRenderMode,
+ PngxPdfDocumentProxy,
+} from '../pdf-viewer/pdf-viewer.types'
import { PdfEditorEditMode } from './pdf-editor-edit-mode'
interface PageOperation {
imports: [
DragDropModule,
FormsModule,
- PdfViewerModule,
NgxBootstrapIconsModule,
+ PngxPdfViewerComponent,
],
})
export class PDFEditorComponent extends ConfirmDialogComponent {
+ PdfRenderMode = PdfRenderMode
public PdfEditorEditMode = PdfEditorEditMode
private documentService = inject(DocumentService)
return this.documentService.getPreviewUrl(this.documentID)
}
- pdfLoaded(pdf: PDFDocumentProxy) {
+ pdfLoaded(pdf: PngxPdfDocumentProxy) {
this.totalPages = pdf.numPages
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
page: i + 1,
--- /dev/null
+<div #container class="pngx-pdf-viewer-container">
+ <div #viewer class="pdfViewer"></div>
+</div>
--- /dev/null
+:host {
+ display: block;
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+:host ::ng-deep .pngx-pdf-viewer-container {
+ position: absolute;
+ inset: 0;
+ overflow: auto;
+}
+
+:host ::ng-deep .pdfViewer {
+ --scale-factor: 1;
+ --page-bg-color: unset;
+ padding-bottom: 0;
+}
+
+:host ::ng-deep .pdfViewer .page {
+ --user-unit: 1;
+ --total-scale-factor: calc(var(--scale-factor) * var(--user-unit));
+ --scale-round-x: 1px;
+ --scale-round-y: 1px;
+ direction: ltr;
+ margin: 0 auto 10px;
+ border: 0;
+ position: relative;
+ overflow: visible;
+ background-clip: content-box;
+ background-color: var(--page-bg-color, rgb(255 255 255));
+}
+
+:host ::ng-deep .pdfViewer > .page:last-of-type {
+ margin-bottom: 0;
+}
+
+:host ::ng-deep .pdfViewer.singlePageView {
+ display: inline-block;
+}
+
+:host ::ng-deep .pdfViewer.singlePageView .page {
+ margin: 0;
+ border: none;
+}
+
+:host ::ng-deep .pdfViewer .canvasWrapper {
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+}
+
+:host ::ng-deep .pdfViewer .canvasWrapper canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0;
+ display: block;
+ width: 100%;
+ height: 100%;
+ contain: content;
+}
+
+:host ::ng-deep .textLayer {
+ position: absolute;
+ text-align: initial;
+ inset: 0;
+ overflow: clip;
+ opacity: 1;
+ line-height: 1;
+ text-size-adjust: none;
+ transform-origin: 0 0;
+ caret-color: CanvasText;
+ z-index: 0;
+ user-select: text;
+ --min-font-size: 1;
+ --text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));
+ --min-font-size-inv: calc(1 / var(--min-font-size));
+}
+
+:host ::ng-deep .textLayer.highlighting {
+ touch-action: none;
+}
+
+:host ::ng-deep .textLayer :is(span, br) {
+ position: absolute;
+ white-space: pre;
+ color: transparent;
+ cursor: text;
+ transform-origin: 0% 0%;
+}
+
+:host ::ng-deep .textLayer > :not(.markedContent),
+:host ::ng-deep .textLayer .markedContent span:not(.markedContent) {
+ z-index: 1;
+ --font-height: 0;
+ font-size: calc(var(--text-scale-factor) * var(--font-height));
+ --scale-x: 1;
+ --rotate: 0deg;
+ transform: rotate(var(--rotate)) scaleX(var(--scale-x))
+ scale(var(--min-font-size-inv));
+}
+
+:host ::ng-deep .textLayer .markedContent {
+ display: contents;
+}
+
+:host ::ng-deep .textLayer span[role='img'] {
+ user-select: none;
+ cursor: default;
+}
+
+:host ::ng-deep .textLayer .highlight {
+ --highlight-bg-color: rgb(180 0 170 / 0.25);
+ --highlight-selected-bg-color: rgb(0 100 0 / 0.25);
+ --highlight-backdrop-filter: none;
+ --highlight-selected-backdrop-filter: none;
+ margin: -1px;
+ padding: 1px;
+ background-color: var(--highlight-bg-color);
+ backdrop-filter: var(--highlight-backdrop-filter);
+ border-radius: 4px;
+}
+
+:host ::ng-deep .appended:is(.textLayer .highlight) {
+ position: initial;
+}
+
+:host ::ng-deep .begin:is(.textLayer .highlight) {
+ border-radius: 4px 0 0 4px;
+}
+
+:host ::ng-deep .end:is(.textLayer .highlight) {
+ border-radius: 0 4px 4px 0;
+}
+
+:host ::ng-deep .middle:is(.textLayer .highlight) {
+ border-radius: 0;
+}
+
+:host ::ng-deep .selected:is(.textLayer .highlight) {
+ background-color: var(--highlight-selected-bg-color);
+}
+
+:host ::ng-deep .textLayer ::selection {
+ background: rgba(30, 100, 255, 0.35);
+}
+
+:host ::ng-deep .annotationLayer {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
--- /dev/null
+import { SimpleChange } from '@angular/core'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
+import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'
+import { PngxPdfViewerComponent } from './pdf-viewer.component'
+import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
+
+describe('PngxPdfViewerComponent', () => {
+ let fixture: ComponentFixture<PngxPdfViewerComponent>
+ let component: PngxPdfViewerComponent
+
+ const initComponent = async (src = 'test.pdf') => {
+ component.src = src
+ fixture.detectChanges()
+ await fixture.whenStable()
+ }
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PngxPdfViewerComponent],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(PngxPdfViewerComponent)
+ component = fixture.componentInstance
+ })
+
+ it('loads a document and emits events', async () => {
+ const loadSpy = jest.fn()
+ const renderedSpy = jest.fn()
+ component.afterLoadComplete.subscribe(loadSpy)
+ component.rendered.subscribe(renderedSpy)
+
+ await initComponent()
+
+ expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
+ '/assets/js/pdf.worker.min.mjs'
+ )
+ const isVisible = (component as any).findController.onIsPageVisible as
+ | (() => boolean)
+ | undefined
+ expect(isVisible?.()).toBe(true)
+ expect(loadSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ numPages: 1 })
+ )
+ expect(renderedSpy).toHaveBeenCalled()
+ expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
+ })
+
+ it('initializes single-page viewer and disables text layer', async () => {
+ component.renderMode = PdfRenderMode.Single
+ component.selectable = false
+
+ await initComponent()
+
+ const viewer = (component as any).pdfViewer as PDFSinglePageViewer & {
+ options: Record<string, unknown>
+ }
+ expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
+ expect(viewer.options.textLayerMode).toBe(0)
+ })
+
+ it('applies zoom, rotation, and page changes', async () => {
+ await initComponent()
+
+ const pageSpy = jest.fn()
+ component.pageChange.subscribe(pageSpy)
+
+ component.zoomScale = PdfZoomScale.PageFit
+ component.zoom = PdfZoomLevel.Two
+ component.rotation = 90
+ component.page = 2
+
+ component.ngOnChanges({
+ zoomScale: new SimpleChange(
+ PdfZoomScale.PageWidth,
+ PdfZoomScale.PageFit,
+ false
+ ),
+ zoom: new SimpleChange(PdfZoomLevel.One, PdfZoomLevel.Two, false),
+ rotation: new SimpleChange(undefined, 90, false),
+ page: new SimpleChange(undefined, 2, false),
+ })
+
+ const viewer = (component as any).pdfViewer as PDFViewer
+ expect(viewer.pagesRotation).toBe(90)
+ expect(viewer.currentPageNumber).toBe(2)
+ expect(pageSpy).toHaveBeenCalledWith(2)
+
+ viewer.currentScale = 1
+ ;(component as any).applyScale()
+ expect(viewer.currentScaleValue).toBe(PdfZoomScale.PageFit)
+ expect(viewer.currentScale).toBe(2)
+
+ const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
+ component.page = 2
+ ;(component as any).lastViewerPage = 2
+ ;(component as any).applyViewerState()
+ expect((component as any).lastViewerPage).toBeUndefined()
+ expect(applyScaleSpy).toHaveBeenCalled()
+ })
+
+ it('dispatches find when search query changes after render', async () => {
+ await initComponent()
+
+ const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
+ const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
+
+ ;(component as any).hasRenderedPage = true
+ component.searchQuery = 'needle'
+ component.ngOnChanges({
+ searchQuery: new SimpleChange('', 'needle', false),
+ })
+
+ expect(dispatchSpy).toHaveBeenCalledWith('find', {
+ query: 'needle',
+ caseSensitive: false,
+ highlightAll: true,
+ phraseSearch: true,
+ })
+
+ component.ngOnChanges({
+ searchQuery: new SimpleChange('needle', 'needle', false),
+ })
+ expect(dispatchSpy).toHaveBeenCalledTimes(1)
+ })
+
+ it('emits error when document load fails', async () => {
+ const errorSpy = jest.fn()
+ component.loadError.subscribe(errorSpy)
+
+ jest.spyOn(pdfjs, 'getDocument').mockImplementationOnce(() => {
+ return {
+ promise: Promise.reject(new Error('boom')),
+ destroy: jest.fn(),
+ } as any
+ })
+
+ await initComponent('bad.pdf')
+
+ expect(errorSpy).toHaveBeenCalled()
+ })
+
+ it('cleans up resources on destroy', async () => {
+ await initComponent()
+
+ const viewer = (component as any).pdfViewer as { cleanup: jest.Mock }
+ const loadingTask = (component as any).loadingTask as unknown as {
+ destroy: jest.Mock
+ }
+ const resizeObserver = (component as any).resizeObserver as unknown as {
+ disconnect: jest.Mock
+ }
+ const eventBus = (component as any).eventBus as { off: jest.Mock }
+
+ jest.spyOn(viewer, 'cleanup')
+ jest.spyOn(loadingTask, 'destroy')
+ jest.spyOn(resizeObserver, 'disconnect')
+ jest.spyOn(eventBus, 'off')
+
+ component.ngOnDestroy()
+
+ expect(eventBus.off).toHaveBeenCalledWith(
+ 'pagerendered',
+ expect.any(Function)
+ )
+ expect(eventBus.off).toHaveBeenCalledWith('pagesinit', expect.any(Function))
+ expect(eventBus.off).toHaveBeenCalledWith(
+ 'pagechanging',
+ expect.any(Function)
+ )
+ expect(resizeObserver.disconnect).toHaveBeenCalled()
+ expect(loadingTask.destroy).toHaveBeenCalled()
+ expect(viewer.cleanup).toHaveBeenCalled()
+ expect((component as any).pdfViewer).toBeUndefined()
+ })
+
+ it('skips work when viewer is missing or has no pages', () => {
+ const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
+ const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
+ ;(component as any).dispatchFindIfReady()
+ expect(dispatchSpy).not.toHaveBeenCalled()
+ ;(component as any).applyViewerState()
+ ;(component as any).applyScale()
+
+ const viewer = new PDFViewer({ eventBus: undefined })
+ viewer.pagesCount = 0
+ ;(component as any).pdfViewer = viewer
+ viewer.currentScale = 5
+ ;(component as any).applyScale()
+ expect(viewer.currentScale).toBe(5)
+ })
+
+ it('returns early on src change in ngOnChanges', () => {
+ const loadSpy = jest.spyOn(component as any, 'loadDocument')
+ const initSpy = jest.spyOn(component as any, 'initViewer')
+ const scaleSpy = jest.spyOn(component as any, 'applyViewerState')
+ const resizeSpy = jest.spyOn(component as any, 'setupResizeObserver')
+
+ component.ngOnChanges({
+ src: new SimpleChange(undefined, 'test.pdf', true),
+ zoomScale: new SimpleChange(
+ PdfZoomScale.PageWidth,
+ PdfZoomScale.PageFit,
+ false
+ ),
+ })
+
+ expect(loadSpy).toHaveBeenCalled()
+ expect(resizeSpy).not.toHaveBeenCalled()
+ expect(initSpy).not.toHaveBeenCalled()
+ expect(scaleSpy).not.toHaveBeenCalled()
+ })
+
+ it('applies viewer state after view init when already loaded', () => {
+ const applySpy = jest.spyOn(component as any, 'applyViewerState')
+ ;(component as any).hasLoaded = true
+ ;(component as any).pdf = { numPages: 1 }
+
+ fixture.detectChanges()
+
+ expect(applySpy).toHaveBeenCalled()
+ })
+
+ it('skips viewer state after view init when no pdf is available', () => {
+ const applySpy = jest.spyOn(component as any, 'applyViewerState')
+ ;(component as any).hasLoaded = true
+
+ fixture.detectChanges()
+
+ expect(applySpy).not.toHaveBeenCalled()
+ })
+
+ it('does not reload when already loaded', async () => {
+ await initComponent()
+
+ const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
+ const callCount = getDocumentSpy.mock.calls.length
+ await (component as any).loadDocument()
+
+ expect(getDocumentSpy).toHaveBeenCalledTimes(callCount)
+ })
+
+ it('runs applyScale on resize observer notifications', async () => {
+ await initComponent()
+
+ const applySpy = jest.spyOn(component as any, 'applyScale')
+ const resizeObserver = (component as any).resizeObserver as {
+ trigger: () => void
+ }
+ resizeObserver.trigger()
+
+ expect(applySpy).toHaveBeenCalled()
+ })
+
+ it('skips page work when no pages are available', async () => {
+ await initComponent()
+
+ const viewer = (component as any).pdfViewer as PDFViewer
+ viewer.pagesCount = 0
+ const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
+
+ component.page = undefined
+ ;(component as any).lastViewerPage = 1
+ ;(component as any).applyViewerState()
+
+ expect(applyScaleSpy).not.toHaveBeenCalled()
+ expect((component as any).lastViewerPage).toBe(1)
+ })
+
+ it('falls back to a default zoom when input is invalid', async () => {
+ await initComponent()
+
+ const viewer = (component as any).pdfViewer as PDFViewer
+ viewer.currentScale = 3
+ component.zoom = 'not-a-number' as PdfZoomLevel
+ ;(component as any).applyScale()
+
+ expect(viewer.currentScale).toBe(3)
+ })
+
+ it('re-initializes viewer on selectable or render mode changes', async () => {
+ await initComponent()
+
+ const initSpy = jest.spyOn(component as any, 'initViewer')
+ component.selectable = false
+ component.renderMode = PdfRenderMode.Single
+
+ component.ngOnChanges({
+ selectable: new SimpleChange(true, false, false),
+ renderMode: new SimpleChange(
+ PdfRenderMode.All,
+ PdfRenderMode.Single,
+ false
+ ),
+ })
+
+ expect(initSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnDestroy,
+ Output,
+ SimpleChanges,
+ ViewChild,
+} from '@angular/core'
+import {
+ getDocument,
+ GlobalWorkerOptions,
+ PDFDocumentLoadingTask,
+ PDFDocumentProxy,
+} from 'pdfjs-dist/legacy/build/pdf.mjs'
+import {
+ EventBus,
+ PDFFindController,
+ PDFLinkService,
+ PDFSinglePageViewer,
+ PDFViewer,
+} from 'pdfjs-dist/web/pdf_viewer.mjs'
+import {
+ PdfRenderMode,
+ PdfSource,
+ PdfZoomLevel,
+ PdfZoomScale,
+ PngxPdfDocumentProxy,
+} from './pdf-viewer.types'
+
+@Component({
+ selector: 'pngx-pdf-viewer',
+ templateUrl: './pdf-viewer.component.html',
+ styleUrl: './pdf-viewer.component.scss',
+})
+export class PngxPdfViewerComponent
+ implements AfterViewInit, OnChanges, OnDestroy
+{
+ @Input() src!: PdfSource
+ @Input() page?: number
+ @Output() pageChange = new EventEmitter<number>()
+ @Input() rotation?: number
+ @Input() renderMode: PdfRenderMode = PdfRenderMode.All
+ @Input() selectable = true
+ @Input() searchQuery = ''
+ @Input() zoom: PdfZoomLevel = PdfZoomLevel.One
+ @Input() zoomScale: PdfZoomScale = PdfZoomScale.PageWidth
+
+ @Output() afterLoadComplete = new EventEmitter<PngxPdfDocumentProxy>()
+ @Output() rendered = new EventEmitter<void>()
+ @Output() loadError = new EventEmitter<unknown>()
+
+ @ViewChild('container', { static: true })
+ private readonly container!: ElementRef<HTMLDivElement>
+
+ @ViewChild('viewer', { static: true })
+ private readonly viewer!: ElementRef<HTMLDivElement>
+
+ private hasLoaded = false
+ private loadingTask?: PDFDocumentLoadingTask
+ private resizeObserver?: ResizeObserver
+ private pdf?: PDFDocumentProxy
+ private pdfViewer?: PDFViewer | PDFSinglePageViewer
+ private hasRenderedPage = false
+ private lastFindQuery = ''
+ private lastViewerPage?: number
+
+ private readonly eventBus = new EventBus()
+ private readonly linkService = new PDFLinkService({ eventBus: this.eventBus })
+ private readonly findController = new PDFFindController({
+ eventBus: this.eventBus,
+ linkService: this.linkService,
+ updateMatchesCountOnProgress: false,
+ })
+
+ private readonly onPageRendered = () => {
+ this.hasRenderedPage = true
+ this.dispatchFindIfReady()
+ this.rendered.emit()
+ }
+ private readonly onPagesInit = () => this.applyScale()
+ private readonly onPageChanging = (evt: { pageNumber: number }) => {
+ // Avoid [(page)] two-way binding re-triggers navigation
+ this.lastViewerPage = evt.pageNumber
+ this.pageChange.emit(evt.pageNumber)
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['src']) {
+ this.hasLoaded = false
+ this.loadDocument()
+ return
+ }
+
+ if (changes['zoomScale']) {
+ this.setupResizeObserver()
+ }
+
+ if (changes['selectable'] || changes['renderMode']) {
+ this.initViewer()
+ }
+
+ if (
+ changes['page'] ||
+ changes['zoom'] ||
+ changes['zoomScale'] ||
+ changes['rotation']
+ ) {
+ this.applyViewerState()
+ }
+
+ if (changes['searchQuery']) {
+ this.dispatchFindIfReady()
+ }
+ }
+
+ ngAfterViewInit(): void {
+ this.setupResizeObserver()
+ this.initViewer()
+ if (!this.hasLoaded) {
+ this.loadDocument()
+ return
+ }
+ if (this.pdf) {
+ this.applyViewerState()
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.eventBus.off('pagerendered', this.onPageRendered)
+ this.eventBus.off('pagesinit', this.onPagesInit)
+ this.eventBus.off('pagechanging', this.onPageChanging)
+ this.resizeObserver?.disconnect()
+ this.loadingTask?.destroy()
+ this.pdfViewer?.cleanup()
+ this.pdfViewer = undefined
+ }
+
+ private async loadDocument(): Promise<void> {
+ if (this.hasLoaded) {
+ return
+ }
+
+ this.hasLoaded = true
+ this.hasRenderedPage = false
+ this.lastFindQuery = ''
+ this.loadingTask?.destroy()
+
+ GlobalWorkerOptions.workerSrc = '/assets/js/pdf.worker.min.mjs'
+ this.loadingTask = getDocument(this.src)
+
+ try {
+ const pdf = await this.loadingTask.promise
+ this.pdf = pdf
+ this.linkService.setDocument(pdf)
+ this.findController.onIsPageVisible = () => true
+ this.pdfViewer?.setDocument(pdf)
+ this.applyViewerState()
+ this.afterLoadComplete.emit(pdf)
+ } catch (err) {
+ this.loadError.emit(err)
+ }
+ }
+
+ private setupResizeObserver(): void {
+ this.resizeObserver?.disconnect()
+ this.resizeObserver = new ResizeObserver(() => {
+ this.applyScale()
+ })
+ this.resizeObserver.observe(this.container.nativeElement)
+ }
+
+ private initViewer(): void {
+ this.viewer.nativeElement.innerHTML = ''
+ this.pdfViewer?.cleanup()
+ this.hasRenderedPage = false
+ this.lastFindQuery = ''
+
+ const textLayerMode = this.selectable === false ? 0 : 1
+ const options = {
+ container: this.container.nativeElement,
+ viewer: this.viewer.nativeElement,
+ eventBus: this.eventBus,
+ linkService: this.linkService,
+ findController: this.findController,
+ textLayerMode,
+ removePageBorders: true,
+ }
+
+ this.pdfViewer =
+ this.renderMode === PdfRenderMode.Single
+ ? new PDFSinglePageViewer(options)
+ : new PDFViewer(options)
+ this.linkService.setViewer(this.pdfViewer)
+
+ this.eventBus.off('pagerendered', this.onPageRendered)
+ this.eventBus.off('pagesinit', this.onPagesInit)
+ this.eventBus.off('pagechanging', this.onPageChanging)
+ this.eventBus.on('pagerendered', this.onPageRendered)
+ this.eventBus.on('pagesinit', this.onPagesInit)
+ this.eventBus.on('pagechanging', this.onPageChanging)
+
+ if (this.pdf) {
+ this.pdfViewer.setDocument(this.pdf)
+ this.applyViewerState()
+ }
+ }
+
+ private applyViewerState(): void {
+ if (!this.pdfViewer) {
+ return
+ }
+ const hasPages = this.pdfViewer.pagesCount > 0
+ if (typeof this.rotation === 'number' && hasPages) {
+ this.pdfViewer.pagesRotation = this.rotation
+ }
+ if (
+ typeof this.page === 'number' &&
+ hasPages &&
+ this.page !== this.lastViewerPage
+ ) {
+ this.pdfViewer.currentPageNumber = this.page
+ }
+ if (this.page === this.lastViewerPage) {
+ this.lastViewerPage = undefined
+ }
+ if (hasPages) {
+ this.applyScale()
+ }
+ this.dispatchFindIfReady()
+ }
+
+ private applyScale(): void {
+ if (!this.pdfViewer) {
+ return
+ }
+ if (this.pdfViewer.pagesCount === 0) {
+ return
+ }
+ const zoomFactor = Number(this.zoom) || 1
+ this.pdfViewer.currentScaleValue = this.zoomScale
+ if (zoomFactor !== 1) {
+ this.pdfViewer.currentScale = this.pdfViewer.currentScale * zoomFactor
+ }
+ }
+
+ private dispatchFindIfReady(): void {
+ if (!this.hasRenderedPage) {
+ return
+ }
+ const query = this.searchQuery.trim()
+ if (query === this.lastFindQuery) {
+ return
+ }
+ this.lastFindQuery = query
+ this.eventBus.dispatch('find', {
+ query,
+ caseSensitive: false,
+ highlightAll: query.length > 0,
+ phraseSearch: true,
+ })
+ }
+}
--- /dev/null
+export type PngxPdfDocumentProxy = {
+ numPages: number
+}
+
+export type PdfSource = string | { url: string; password?: string }
+
+export enum PdfRenderMode {
+ Single = 'single',
+ All = 'all',
+}
+
+export enum PdfZoomScale {
+ PageFit = 'page-fit',
+ PageWidth = 'page-width',
+}
+
+export enum PdfZoomLevel {
+ Quarter = '.25',
+ Half = '.5',
+ ThreeQuarters = '.75',
+ One = '1',
+ OneAndHalf = '1.5',
+ Two = '2',
+ Three = '3',
+}
</div>
}
@if (!requiresPassword) {
- <pdf-viewer
+ <pngx-pdf-viewer
[src]="previewUrl"
- [original-size]="false"
- [show-borders]="false"
- [show-all]="true"
- (text-layer-rendered)="onPageRendered()"
- (error)="onError($event)" #pdfViewer>
- </pdf-viewer>
+ [renderMode]="PdfRenderMode.All"
+ [searchQuery]="documentService.searchQuery"
+ (loadError)="onError($event)">
+ </pngx-pdf-viewer>
}
}
}
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service'
+import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
import { PreviewPopupComponent } from './preview-popup.component'
const doc = {
component.popover.open()
fixture.detectChanges()
expect(fixture.debugElement.query(By.css('object'))).toBeNull()
- expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
+ expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
})
it('should show lock icon on password error', () => {
expect(component.popover.isOpen()).toBeFalsy()
})
- it('should dispatch find event on viewer loaded if searchQuery set', () => {
+ it('should pass searchQuery to viewer', () => {
documentService.searchQuery = 'test'
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
component.popover.open()
- jest.advanceTimersByTime(1000)
fixture.detectChanges()
- // normally setup by pdf-viewer
- jest.replaceProperty(component.pdfViewer, 'eventBus', {
- dispatch: jest.fn(),
- } as any)
- const dispatchSpy = jest.spyOn(component.pdfViewer.eventBus, 'dispatch')
- component.onPageRendered()
- expect(dispatchSpy).toHaveBeenCalledWith('find', {
- query: 'test',
- caseSensitive: false,
- highlightAll: true,
- phraseSearch: true,
- })
+ const viewer = fixture.debugElement.query(
+ By.directive(PngxPdfViewerComponent)
+ )
+ expect(viewer).not.toBeNull()
+ expect(viewer.componentInstance.searchQuery).toBe('test')
})
})
import { HttpClient } from '@angular/common/http'
import { Component, inject, Input, OnDestroy, ViewChild } from '@angular/core'
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
-import { PdfViewerComponent, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first, Subject, takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document'
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service'
+import { PngxPdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
+import { PdfRenderMode } from '../pdf-viewer/pdf-viewer.types'
@Component({
selector: 'pngx-preview-popup',
imports: [
NgbPopoverModule,
DocumentTitlePipe,
- PdfViewerModule,
+ PngxPdfViewerComponent,
SafeUrlPipe,
NgxBootstrapIconsModule,
],
})
export class PreviewPopupComponent implements OnDestroy {
+ PdfRenderMode = PdfRenderMode
private settingsService = inject(SettingsService)
- private documentService = inject(DocumentService)
+ public readonly documentService = inject(DocumentService)
private http = inject(HttpClient)
private _document: Document
@ViewChild('popover') popover: NgbPopover
- @ViewChild('pdfViewer') pdfViewer: PdfViewerComponent
-
mouseOnPreview: boolean = false
popoverClass: string = 'shadow popover-preview'
}
}
- onPageRendered() {
- // Only triggered by the pngx pdf viewer
- if (this.documentService.searchQuery) {
- this.pdfViewer.eventBus.dispatch('find', {
- query: this.documentService.searchQuery,
- caseSensitive: false,
- highlightAll: true,
- phraseSearch: true,
- })
- }
- }
-
mouseEnterPreview() {
this.mouseOnPreview = true
if (!this.popover.isOpen()) {
@case (ContentRenderType.PDF) {
@if (!useNativePdfViewer) {
<div class="preview-sticky pdf-viewer-container">
- <pdf-viewer
+ <pngx-pdf-viewer
[src]="{ url: previewUrl, password: password }"
- [original-size]="false"
- [show-borders]="true"
- [show-all]="true"
+ [renderMode]="PdfRenderMode.All"
[(page)]="previewCurrentPage"
- [zoom-scale]="previewZoomScale"
+ [zoomScale]="previewZoomScale"
[zoom]="previewZoomSetting"
- (error)="onError($event)"
- (after-load-complete)="pdfPreviewLoaded($event)">
- </pdf-viewer>
+ (loadError)="onError($event)"
+ (afterLoadComplete)="pdfPreviewLoaded($event)">
+ </pngx-pdf-viewer>
</div>
} @else {
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
}
.pdf-viewer-container {
- padding-top: 10px;
+ padding: 8px;
background-color: gray;
- pdf-viewer {
+ pngx-pdf-viewer {
width: 100%;
height: 100%;
}
}
-::ng-deep .ng2-pdf-viewer-container .page {
- --page-margin: 0 auto 10px;
- --page-border: 0;
-}
-
.btn-group .dropdown-toggle-split {
border-top-right-radius: inherit;
border-bottom-right-radius: inherit;
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
+import {
+ PdfZoomLevel,
+ PdfZoomScale,
+} from '../common/pdf-viewer/pdf-viewer.types'
import { DocumentDetailComponent } from './document-detail.component'
-import { ZoomSetting } from './zoom-setting'
const doc: Document = {
id: 3,
it('should support zoom controls', () => {
initNormally()
- component.setZoom(ZoomSetting.One) // from select
+ component.setZoom(PdfZoomLevel.One) // from select
expect(component.previewZoomSetting).toEqual('1')
component.increaseZoom()
expect(component.previewZoomSetting).toEqual('1.5')
expect(component.previewZoomSetting).toEqual('2')
component.decreaseZoom()
expect(component.previewZoomSetting).toEqual('1.5')
- component.setZoom(ZoomSetting.One) // from select
+ component.setZoom(PdfZoomLevel.One) // from select
component.decreaseZoom()
expect(component.previewZoomSetting).toEqual('.75')
- component.setZoom(ZoomSetting.PageFit) // from select
+ component.setZoom(PdfZoomScale.PageFit) // 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.setZoom(ZoomSetting.PageFit) // from select
+ component.setZoom(PdfZoomScale.PageFit) // from select
expect(component.previewZoomScale).toEqual('page-fit')
expect(component.previewZoomSetting).toEqual('1')
component.decreaseZoom()
it('should select correct zoom setting in dropdown', () => {
initNormally()
- component.setZoom(ZoomSetting.PageFit)
- expect(component.currentZoom).toEqual(ZoomSetting.PageFit)
- component.setZoom(ZoomSetting.Quarter)
- expect(component.currentZoom).toEqual(ZoomSetting.Quarter)
+ component.setZoom(PdfZoomScale.PageFit)
+ expect(component.currentZoom).toEqual(PdfZoomScale.PageFit)
+ component.setZoom(PdfZoomLevel.Quarter)
+ expect(component.currentZoom).toEqual(PdfZoomLevel.Quarter)
})
it('should support updating notes dynamically', () => {
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, 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', () => {
NgbNavModule,
} from '@ng-bootstrap/ng-bootstrap'
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
-import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DeviceDetectorService } from 'ngx-device-detector'
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode'
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
+import { PngxPdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
+import {
+ PdfRenderMode,
+ PdfZoomLevel,
+ PdfZoomScale,
+ PngxPdfDocumentProxy,
+} from '../common/pdf-viewer/pdf-viewer.types'
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
import { DocumentHistoryComponent } from '../document-history/document-history.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
-import { ZoomSetting } from './zoom-setting'
enum DocumentDetailNavIDs {
Details = 1,
NgbNavModule,
NgbDropdownModule,
NgxBootstrapIconsModule,
- PdfViewerModule,
TextAreaComponent,
RouterModule,
+ PngxPdfViewerComponent,
],
})
export class DocumentDetailComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent
{
- private documentsService = inject(DocumentService)
+ PdfRenderMode = PdfRenderMode
+ documentsService = inject(DocumentService)
private route = inject(ActivatedRoute)
private tagService = inject(TagService)
private correspondentService = inject(CorrespondentService)
previewCurrentPage: number = 1
previewNumPages: number
- previewZoomSetting: ZoomSetting = ZoomSetting.One
- previewZoomScale: ZoomSetting = ZoomSetting.PageWidth
+ previewZoomSetting: PdfZoomLevel = PdfZoomLevel.One
+ previewZoomScale: PdfZoomScale = PdfZoomScale.PageWidth
store: BehaviorSubject<any>
isDirty$: Observable<boolean>
}
ngOnInit(): void {
- this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING))
+ this.setZoom(
+ this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING) as PdfZoomScale
+ )
this.documentForm.valueChanges
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((values) => {
})
}
- pdfPreviewLoaded(pdf: PDFDocumentProxy) {
+ pdfPreviewLoaded(pdf: PngxPdfDocumentProxy) {
this.previewNumPages = pdf.numPages
if (this.password) this.requiresPassword = false
setTimeout(() => {
}
}
- setZoom(setting: ZoomSetting) {
- if (ZoomSetting.PageFit === setting || ZoomSetting.PageWidth === setting) {
+ setZoom(setting: PdfZoomScale | PdfZoomLevel) {
+ if (
+ setting === PdfZoomScale.PageFit ||
+ setting === PdfZoomScale.PageWidth
+ ) {
this.previewZoomScale = setting
- this.previewZoomSetting = ZoomSetting.One
- } else {
- this.previewZoomSetting = setting
- this.previewZoomScale = ZoomSetting.PageWidth
+ this.previewZoomSetting = PdfZoomLevel.One
+ return
}
+ this.previewZoomSetting = setting
+ this.previewZoomScale = PdfZoomScale.PageWidth
}
get zoomSettings() {
- return Object.values(ZoomSetting).filter(
- (setting) => setting !== ZoomSetting.PageWidth
- )
+ return [PdfZoomScale.PageFit, ...Object.values(PdfZoomLevel)]
}
get currentZoom() {
- if (this.previewZoomScale === ZoomSetting.PageFit) {
- return ZoomSetting.PageFit
- } else return this.previewZoomSetting
+ if (this.previewZoomScale === PdfZoomScale.PageFit) {
+ return PdfZoomScale.PageFit
+ }
+ return this.previewZoomSetting
}
- getZoomSettingTitle(setting: ZoomSetting): string {
+ getZoomSettingTitle(setting: PdfZoomScale | PdfZoomLevel): string {
switch (setting) {
- case ZoomSetting.PageFit:
+ case PdfZoomScale.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
+ const zoomLevels = Object.values(PdfZoomLevel)
+ let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
+ if (this.previewZoomScale === PdfZoomScale.PageFit) {
+ currentIndex = zoomLevels.indexOf(PdfZoomLevel.One)
+ }
+ this.previewZoomScale = PdfZoomScale.PageWidth
this.previewZoomSetting =
- Object.values(ZoomSetting)[
- Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1)
- ]
+ zoomLevels[Math.min(zoomLevels.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)]
+ const zoomLevels = Object.values(PdfZoomLevel)
+ let currentIndex = zoomLevels.indexOf(this.previewZoomSetting)
+ if (this.previewZoomScale === PdfZoomScale.PageFit) {
+ currentIndex = zoomLevels.indexOf(PdfZoomLevel.ThreeQuarters)
+ }
+ this.previewZoomScale = PdfZoomScale.PageWidth
+ this.previewZoomSetting = zoomLevels[Math.max(0, currentIndex - 1)]
}
get showPermissions(): boolean {
+++ /dev/null
-export enum ZoomSetting {
- PageFit = 'page-fit',
- PageWidth = 'page-width',
- Quarter = '.25',
- Half = '.5',
- ThreeQuarters = '.75',
- One = '1',
- OneAndHalf = '1.5',
- Two = '2',
- Three = '3',
-}
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
-import { ZoomSetting } from '../components/document-detail/zoom-setting'
+import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types'
import { User } from './user'
export interface UiSettings {
{
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
type: 'string',
- default: ZoomSetting.PageWidth,
+ default: PdfZoomScale.PageWidth,
},
{
key: SETTINGS_KEYS.AI_ENABLED,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
-import { PdfViewerModule } from 'ng2-pdf-viewer'
import {
NgxBootstrapIconsModule,
airplane,
NgbModule,
FormsModule,
ReactiveFormsModule,
- PdfViewerModule,
NgSelectModule,
ColorSliderModule,
DragDropModule,
--- /dev/null
+export class PDFDocumentProxy {
+ numPages = 1
+}
+
+export class PDFDocumentLoadingTask {
+ promise: Promise<PDFDocumentProxy>
+ destroyed = false
+
+ constructor(promise: Promise<PDFDocumentProxy>) {
+ this.promise = promise
+ }
+
+ destroy(): void {
+ this.destroyed = true
+ }
+}
+
+export const GlobalWorkerOptions = {
+ workerSrc: '',
+}
+
+export const getDocument = (_src: unknown): PDFDocumentLoadingTask => {
+ return new PDFDocumentLoadingTask(Promise.resolve(new PDFDocumentProxy()))
+}
--- /dev/null
+type EventHandler = (event?: unknown) => void
+
+export class EventBus {
+ private readonly listeners = new Map<string, Set<EventHandler>>()
+
+ on(eventName: string, listener: EventHandler): void {
+ let listeners = this.listeners.get(eventName)
+ if (!listeners) {
+ listeners = new Set()
+ this.listeners.set(eventName, listeners)
+ }
+ listeners.add(listener)
+ }
+
+ off(eventName: string, listener: EventHandler): void {
+ this.listeners.get(eventName)?.delete(listener)
+ }
+
+ dispatch(eventName: string, event?: unknown): void {
+ this.listeners.get(eventName)?.forEach((listener) => listener(event))
+ }
+}
+
+export class PDFFindController {
+ onIsPageVisible?: () => boolean
+}
+
+export class PDFLinkService {
+ private document?: unknown
+ private viewer?: unknown
+
+ setDocument(document: unknown): void {
+ this.document = document
+ }
+
+ setViewer(viewer: unknown): void {
+ this.viewer = viewer
+ }
+}
+
+class BaseViewer {
+ pagesCount = 0
+ currentScale = 1
+ currentScaleValue: string | number = 1
+ pagesRotation = 0
+ readonly options: Record<string, unknown>
+
+ private readonly eventBus?: EventBus
+ private _currentPageNumber = 1
+
+ constructor(options: { eventBus?: EventBus }) {
+ this.options = options
+ this.eventBus = options.eventBus
+ }
+
+ setDocument(document: { numPages?: number } | null | undefined): void {
+ this.pagesCount = document?.numPages ?? 1
+ this.eventBus?.dispatch('pagesinit', {})
+ this.eventBus?.dispatch('pagerendered', {
+ pageNumber: this._currentPageNumber,
+ })
+ }
+
+ cleanup(): void {
+ this.pagesCount = 0
+ }
+
+ get currentPageNumber(): number {
+ return this._currentPageNumber
+ }
+
+ set currentPageNumber(value: number) {
+ this._currentPageNumber = value
+ this.eventBus?.dispatch('pagechanging', { pageNumber: value })
+ }
+}
+
+export class PDFViewer extends BaseViewer {}
+export class PDFSinglePageViewer extends BaseViewer {}
],
"include": [
"src/**/*.spec.ts",
- "src/**/*.d.ts"
+ "src/**/*.d.ts",
+ "src/test/**/*.ts"
]
}