Feature: pngx PDF viewer with updated pdfjs (#4679)

This commit is contained in:
shamoon
2023-12-04 17:17:40 -08:00
committed by GitHub
parent 1a36d8ff23
commit d82e97fab7
16 changed files with 2286 additions and 127 deletions

View 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,

View File

@@ -0,0 +1,3 @@
<div #pdfViewerContainer class="pngx-pdf-viewer-container">
<div class="pdfViewer"></div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -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()
})
})
}
}

View File

@@ -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'

View File

@@ -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)
})
}

View 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">
@@ -189,24 +200,7 @@
<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>
@@ -233,14 +227,7 @@
</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>
@@ -252,3 +239,39 @@
</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>

View 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 {
@@ -41,3 +36,11 @@
textarea.rtl {
direction: rtl;
}
.form-select {
padding-right: 2.5em;
}
.input-group .btn-outline-secondary {
border-color: var(--bs-border-color);
}

View 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', () => {

View 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(