mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-09 23:49:29 -06:00
Enhancement: pngx pdf viewer (#12043)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<div #container class="pngx-pdf-viewer-container">
|
||||
<div #viewer class="pdfViewer"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,153 @@
|
||||
: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;
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,266 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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',
|
||||
}
|
||||
Reference in New Issue
Block a user