From edc53c7971f412a17fbfcf6397c9e96362bf517b Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Thu, 19 May 2022 16:35:50 +0300 Subject: [PATCH] RED-3988: replace old pdf viewer --- .../annotation-actions.component.ts | 20 +- .../annotations-list.component.ts | 4 +- .../file-workload.component.html | 2 +- .../file-workload/file-workload.component.ts | 8 +- .../pdf-viewer/pdf-viewer.component.html | 54 +++-- .../pdf-viewer/pdf-viewer.component.scss | 12 +- .../pdf-viewer/pdf-viewer.component.ts | 72 ++---- .../file-preview-screen.component.html | 4 +- .../file-preview-screen.component.ts | 60 +++-- .../services/annotation-actions.service.ts | 42 ++-- .../services/annotation-draw.service.ts | 34 +-- .../services/file-preview-state.service.ts | 12 +- .../services/page-rotation.service.ts | 8 +- .../services/pdf-viewer.service.ts | 182 ++------------ .../file-preview/services/skipped.service.ts | 6 +- .../file-preview/services/stamp.service.ts | 16 +- .../file-preview/services/tooltips.service.ts | 4 +- .../services/viewer-header-config.service.ts | 8 +- .../modules/file-preview/utils/constants.ts | 33 --- .../reusable-pdf-viewer/constants.ts | 60 +++++ .../reusable-pdf-viewer.component.ts | 16 +- .../reusable-pdf-viewer.service.ts | 223 ++++++++++++++---- .../components/reusable-pdf-viewer/types.ts | 5 + 23 files changed, 431 insertions(+), 454 deletions(-) create mode 100644 apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts create mode 100644 apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts index 4012450a6..6321fa68d 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts @@ -7,11 +7,11 @@ import { UserService } from '@services/user.service'; import { AnnotationReferencesService } from '../../services/annotation-references.service'; import { MultiSelectService } from '../../services/multi-select.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service'; -import { HelpModeService, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui'; -import { PdfViewer } from '../../services/pdf-viewer.service'; +import { HelpModeService } from '@iqser/common-ui'; import { FileDataService } from '../../services/file-data.service'; import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service'; import { ViewModeService } from '../../services/view-mode.service'; +import { ReusablePdfViewer } from '../../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; export const AnnotationButtonTypes = { dark: 'dark', @@ -40,7 +40,7 @@ export class AnnotationActionsComponent implements OnChanges { readonly helpModeService: HelpModeService, private readonly _changeRef: ChangeDetectorRef, private readonly _userService: UserService, - private readonly _pdf: PdfViewer, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _state: FilePreviewStateService, private readonly _permissionsService: PermissionsService, private readonly _fileDataService: FileDataService, @@ -59,11 +59,7 @@ export class AnnotationActionsComponent implements OnChanges { } get viewerAnnotations() { - if (this._pdf.annotationManager) { - return this._annotations.map(a => this._pdf.annotationManager.getAnnotationById(a.id)); - } else { - return []; - } + return this._reusablePdf.getAnnotationsById(this._annotations.map(a => a.id)); } get isVisible() { @@ -98,15 +94,15 @@ export class AnnotationActionsComponent implements OnChanges { hideAnnotation($event: MouseEvent) { $event.stopPropagation(); - this._pdf.annotationManager.hideAnnotations(this.viewerAnnotations); - this._pdf.annotationManager.deselectAllAnnotations(); + this._reusablePdf.hideAnnotations(this.viewerAnnotations); + this._reusablePdf.deselectAnnotations(); this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, true); } showAnnotation($event: MouseEvent) { $event.stopPropagation(); - this._pdf.annotationManager.showAnnotations(this.viewerAnnotations); - this._pdf.annotationManager.deselectAllAnnotations(); + this._reusablePdf.showAnnotations(this.viewerAnnotations); + this._reusablePdf.deselectAnnotations(); this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, false); } diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts index e67ddac59..b1b7c6b32 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts @@ -18,6 +18,7 @@ import { ViewModeService } from '../../services/view-mode.service'; import { BehaviorSubject } from 'rxjs'; import { TextHighlightsGroup } from '@red/domain'; import { PdfViewer } from '../../services/pdf-viewer.service'; +import { ReusablePdfViewer } from '../../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; @Component({ selector: 'redaction-annotations-list', @@ -41,6 +42,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O private readonly _userPreferenceService: UserPreferenceService, private readonly _viewModeService: ViewModeService, private readonly _pdf: PdfViewer, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _listingService: ListingService, readonly annotationReferencesService: AnnotationReferencesService, ) { @@ -69,7 +71,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O this.pagesPanelActive.emit(false); if (this._listingService.isSelected(annotation)) { - this._pdf.deselectAnnotations([annotation]); + this._reusablePdf.deselectAnnotations([annotation]); } else { const canMultiSelect = this._multiSelectService.isEnabled; if (canMultiSelect && ($event?.ctrlKey || $event?.metaKey) && this._listingService.selected.length > 0) { diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html index c9f517c59..5307b1111 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html @@ -48,7 +48,7 @@
diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts index 178a1a1fa..67e343f19 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts @@ -37,6 +37,7 @@ import { ViewModeService } from '../../services/view-mode.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { FileDataService } from '../../services/file-data.service'; import { PdfViewer } from '../../services/pdf-viewer.service'; +import { ReusablePdfViewer } from '../../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape']; const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; @@ -78,10 +79,11 @@ export class FileWorkloadComponent { readonly viewModeService: ViewModeService, readonly listingService: ListingService, readonly pdf: PdfViewer, + readonly reusablePdf: ReusablePdfViewer, private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _annotationProcessingService: AnnotationProcessingService, ) { - this.pdf.currentPage$.pipe(takeWhile(() => !!this)).subscribe(pageNumber => { + this.reusablePdf.currentPage$.pipe(takeWhile(() => !!this)).subscribe(pageNumber => { this._scrollViews(); this.scrollAnnotationsToPage(pageNumber, 'always'); }); @@ -134,7 +136,7 @@ export class FileWorkloadComponent { return this.multiSelectService.inactive$.pipe( tap(value => { if (value) { - this.pdf.deselectAllAnnotations(); + this.reusablePdf.deselectAnnotations(); } }), shareDistinctLast(), @@ -183,7 +185,7 @@ export class FileWorkloadComponent { } deselectAllOnActivePage(): void { - this.pdf.deselectAnnotations(this.activeAnnotations); + this.reusablePdf.deselectAnnotations(this.activeAnnotations); } @HostListener('window:keyup', ['$event']) diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.html b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.html index 083b15513..b1c080e95 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.html @@ -1,29 +1,31 @@ -
-
-
+ + - + @@ -88,7 +86,7 @@ ; fullScreen = false; - displayPdfViewer = false; readonly canPerformAnnotationActions$: Observable; readonly fileId = this.state.fileId; readonly dossierId = this.state.dossierId; readonly file$ = this.state.file$.pipe(tap(file => this._fileDataService.loadAnnotations(file))); - ready = false; @ViewChild(FileWorkloadComponent) readonly workloadComponent: FileWorkloadComponent; private _lastPage: string; @ViewChild('annotationFilterTemplate', { @@ -79,6 +78,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni constructor( readonly pdf: PdfViewer, + readonly reusablePdf: ReusablePdfViewer, readonly documentInfoService: DocumentInfoService, readonly state: FilePreviewStateService, readonly listingService: ListingService, @@ -143,7 +143,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni async updateViewMode(): Promise { this._logger.info(`[PDF] Update ${this._viewModeService.viewMode} view mode`); - const annotations = this.pdf.getAnnotations(a => a.getCustomData('redact-manager')); + const annotations = this.reusablePdf.getAnnotations(a => Boolean(a.getCustomData('redact-manager'))); const redactions = annotations.filter(a => a.getCustomData('redaction')); switch (this._viewModeService.viewMode) { @@ -156,8 +156,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni .filter(a => !ocrAnnotationIds.includes(a.Id)); const nonStandardEntries = annotations.filter(a => a.getCustomData('changeLogRemoved') === 'true'); this._setAnnotationsOpacity(standardEntries, true); - this.pdf.showAnnotations(standardEntries); - this.pdf.hideAnnotations(nonStandardEntries); + this.reusablePdf.showAnnotations(standardEntries); + this.reusablePdf.hideAnnotations(nonStandardEntries); break; } case 'DELTA': { @@ -165,21 +165,21 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni const nonChangeLogEntries = annotations.filter(a => a.getCustomData('changeLog') === 'false'); this._setAnnotationsColor(redactions, 'annotationColor'); this._setAnnotationsOpacity(changeLogEntries, true); - this.pdf.showAnnotations(changeLogEntries); - this.pdf.hideAnnotations(nonChangeLogEntries); + this.reusablePdf.showAnnotations(changeLogEntries); + this.reusablePdf.hideAnnotations(nonChangeLogEntries); break; } case 'REDACTED': { const nonRedactionEntries = annotations.filter(a => a.getCustomData('redaction') === 'false'); this._setAnnotationsOpacity(redactions); this._setAnnotationsColor(redactions, 'redactionColor'); - this.pdf.showAnnotations(redactions); - this.pdf.hideAnnotations(nonRedactionEntries); + this.reusablePdf.showAnnotations(redactions); + this.reusablePdf.hideAnnotations(nonRedactionEntries); break; } case 'TEXT_HIGHLIGHTS': { this._loadingService.start(); - this.pdf.hideAnnotations(annotations); + this.reusablePdf.hideAnnotations(annotations); const highlights = await this._fileDataService.loadTextHighlights(); await this._annotationDrawService.draw(highlights); this._loadingService.stop(); @@ -192,7 +192,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni ngOnDetach(): void { this._pageRotationService.clearRotations(); - this.displayPdfViewer = false; + this.reusablePdf.closeDocument(); super.ngOnDetach(); this._changeDetectorRef.markForCheck(); } @@ -204,6 +204,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._viewModeService.compareMode = false; this._viewModeService.switchToStandard(); + this.state.reloadBlob(); await this.ngOnInit(); await this._fileDataService.loadRedactionLog(); @@ -218,7 +219,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return this._handleDeletedFile(); } - this.ready = false; this._loadingService.start(); await this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId); this._subscribeToFileUpdates(); @@ -228,8 +228,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni const reanalyzeFiles = reanalysisService.reanalyzeFilesForDossier([file], this.dossierId, { force: true }); await firstValueFrom(reanalyzeFiles); } - - this.displayPdfViewer = true; } handleAnnotationSelected(annotationIds: string[]) { @@ -251,9 +249,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni null, { manualRedactionEntryWrapper, dossierId: this.dossierId, file }, (wrappers: ManualRedactionEntryWrapper[]) => { - const selectedAnnotations = this.pdf.annotationManager.getSelectedAnnotations(); + const selectedAnnotations = this.reusablePdf.annotationManager.getSelectedAnnotations(); if (selectedAnnotations.length > 0) { - this.pdf.deleteAnnotations([selectedAnnotations[0].Id]); + this.reusablePdf.deleteAnnotations([selectedAnnotations[0].Id]); } const manualRedactionService = this._injector.get(ManualRedactionService); const add$ = manualRedactionService.addAnnotation( @@ -311,13 +309,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni @Debounce(100) async viewerReady() { - this.ready = true; - this._setExcludedPageStyles(); - - this.pdf.documentViewer.addEventListener('pageComplete', () => { - this._setExcludedPageStyles(); - }); - // Go to initial page from query params const pageNumber: string = this._lastPage || this._activatedRoute.snapshot.queryParams.page; if (pageNumber) { @@ -356,8 +347,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } loadAnnotations() { - const documentLoaded$ = this.pdf.documentLoaded$.pipe(tap(() => this.viewerReady())); - const currentPageAnnotations$ = combineLatest([this.pdf.currentPage$, this._fileDataService.annotations$]).pipe( + const documentLoaded$ = this.reusablePdf.show$.pipe( + filter(s => s), + tap(() => this.viewerReady()), + ); + const currentPageAnnotations$ = combineLatest([this.reusablePdf.currentPage$, this._fileDataService.annotations$]).pipe( map(([page, annotations]) => annotations.filter(annotation => annotation.pageNumber === page)), ); let start; @@ -382,12 +376,12 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } this._logger.info('[ANNOTATIONS] To delete: ', annotationsToDelete); - this.pdf.deleteAnnotations(annotationsToDelete.map(annotation => annotation.id)); + this.reusablePdf.deleteAnnotations(annotationsToDelete.map(annotation => annotation.id)); } drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) { let annotationsToDraw: readonly AnnotationWrapper[]; - const annotationsList = this.pdf.annotationManager.getAnnotationsList(); + const annotationsList = this.reusablePdf.getAnnotations(); const ann = annotationsList.map(a => oldAnnotations.find(oldAnnotation => oldAnnotation.id === a.Id)); const hasAnnotations = ann.filter(a => !!a).length > 0; @@ -402,7 +396,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } this._logger.info('[ANNOTATIONS] To draw: ', annotationsToDraw); - this.pdf.deleteAnnotations(annotationsToDraw.map(a => a.annotationId)); + this.reusablePdf.deleteAnnotations(annotationsToDraw.map(a => a.annotationId)); return this._cleanupAndRedrawAnnotations(annotationsToDraw); } @@ -478,14 +472,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni #deactivateMultiSelect() { this.multiSelectService.deactivate(); - this.pdf.deselectAllAnnotations(); + this.reusablePdf.deselectAnnotations(); this.handleAnnotationSelected([]); } private _setExcludedPageStyles() { const file = this._filesMapService.get(this.dossierId, this.fileId); setTimeout(() => { - const iframeDoc = this.pdf.UI.iframeWindow.document; + const iframeDoc = this.reusablePdf.instance.UI.iframeWindow.document; const pageContainer = iframeDoc.getElementById(`pageWidgetContainer${this.pdf.currentPage}`); if (pageContainer) { if (file.excludedPages.includes(this.pdf.currentPage)) { @@ -519,6 +513,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni }), ) .subscribe(); + + this.addActiveScreenSubscription = this.reusablePdf.pageComplete$.subscribe(() => { + this._setExcludedPageStyles(); + }); } private _handleDeletedDossier(): void { diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index d17b230d7..03495689e 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -35,6 +35,7 @@ import { PdfViewer } from './pdf-viewer.service'; import { FilePreviewDialogService } from './file-preview-dialog.service'; import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service'; import { FileDataService } from './file-data.service'; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; import Quad = Core.Math.Quad; @Injectable() @@ -49,6 +50,7 @@ export class AnnotationActionsService { private readonly _dialogService: FilePreviewDialogService, private readonly _dialog: MatDialog, private readonly _pdf: PdfViewer, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _annotationDrawService: AnnotationDrawService, private readonly _activeDossiersService: ActiveDossiersService, private readonly _dictionariesMapService: DictionariesMapService, @@ -395,21 +397,21 @@ export class AnnotationActionsService { annotationWrapper.resizing = true; - const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(annotationWrapper.id); + const viewerAnnotation = this._reusablePdf.annotationManager.getAnnotationById(annotationWrapper.id); if (annotationWrapper.rectangle || annotationWrapper.imported || annotationWrapper.isImage) { - this._pdf.deleteAnnotations([annotationWrapper.id]); + this._reusablePdf.deleteAnnotations([annotationWrapper.id]); const rectangleAnnotation = this.#generateRectangle(annotationWrapper); - this._pdf.annotationManager.addAnnotation(rectangleAnnotation, { imported: true }); - await this._pdf.annotationManager.drawAnnotationsFromList([rectangleAnnotation]); - this._pdf.annotationManager.selectAnnotation(rectangleAnnotation); + this._reusablePdf.annotationManager.addAnnotation(rectangleAnnotation, { imported: true }); + await this._reusablePdf.annotationManager.drawAnnotationsFromList([rectangleAnnotation]); + this._reusablePdf.annotationManager.selectAnnotation(rectangleAnnotation); return; } viewerAnnotation.ReadOnly = false; viewerAnnotation.Hidden = false; viewerAnnotation.disableRotationControl(); - this._pdf.annotationManager.redrawAnnotation(viewerAnnotation); - this._pdf.annotationManager.selectAnnotation(viewerAnnotation); + this._reusablePdf.annotationManager.redrawAnnotation(viewerAnnotation); + this._reusablePdf.annotationManager.selectAnnotation(viewerAnnotation); } async acceptResize($event: MouseEvent, annotation: AnnotationWrapper): Promise { @@ -435,9 +437,9 @@ export class AnnotationActionsService { annotationWrapper.resizing = false; - this._pdf.deleteAnnotations([annotationWrapper.id]); + this._reusablePdf.deleteAnnotations([annotationWrapper.id]); await this._annotationDrawService.draw([annotationWrapper]); - this._pdf.annotationManager.deselectAllAnnotations(); + this._reusablePdf.deselectAnnotations(); await this._fileDataService.annotationsChanged(); } @@ -461,8 +463,8 @@ export class AnnotationActionsService { } #generateRectangle(annotationWrapper: AnnotationWrapper) { - const annotation = new this._pdf.Annotations.RectangleAnnotation(); - const pageHeight = this._pdf.documentViewer.getPageHeight(annotationWrapper.pageNumber); + const annotation = this._reusablePdf.rectangle(); + const pageHeight = this._reusablePdf.documentViewer.getPageHeight(annotationWrapper.pageNumber); const rectangle: IRectangle = annotationWrapper.positions[0]; annotation.PageNumber = annotationWrapper.pageNumber; annotation.X = rectangle.topLeft.x; @@ -527,17 +529,17 @@ export class AnnotationActionsService { } private async _extractTextAndPositions(annotationId: string) { - const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(annotationId); + const viewerAnnotation = this._reusablePdf.annotationManager.getAnnotationById(annotationId); - const document = await this._pdf.documentViewer.getDocument().getPDFDoc(); + const document = await this._reusablePdf.PDFDoc; const page = await document.getPage(viewerAnnotation.getPageNumber()); - if (viewerAnnotation instanceof this._pdf.Annotations.TextHighlightAnnotation) { + if (this._reusablePdf.isTextHighlight(viewerAnnotation)) { const words = []; const rectangles: IRectangle[] = []; for (const quad of viewerAnnotation.Quads) { const rect = toPosition( viewerAnnotation.getPageNumber(), - this._pdf.documentViewer.getPageHeight(viewerAnnotation.getPageNumber()), + this._reusablePdf.getPageHeight(viewerAnnotation.getPageNumber()), this._translateQuads(viewerAnnotation.getPageNumber(), quad), ); rectangles.push(rect); @@ -548,7 +550,7 @@ export class AnnotationActionsService { */ const percentHeightOffset = rect.height / 10; - const pdfNetRect = new this._pdf.instance.Core.PDFNet.Rect( + const pdfNetRect = new this._reusablePdf.PDFNet.Rect( rect.topLeft.x, rect.topLeft.y + percentHeightOffset, rect.topLeft.x + rect.width, @@ -565,7 +567,7 @@ export class AnnotationActionsService { } else { const rect = toPosition( viewerAnnotation.getPageNumber(), - this._pdf.documentViewer.getPageHeight(viewerAnnotation.getPageNumber()), + this._reusablePdf.documentViewer.getPageHeight(viewerAnnotation.getPageNumber()), this._annotationDrawService.annotationToQuads(viewerAnnotation), ); return { @@ -576,13 +578,13 @@ export class AnnotationActionsService { } private _translateQuads(page: number, quad: Quad): Quad { - const rotation = this._pdf.documentViewer.getCompleteRotation(page); + const rotation = this._reusablePdf.documentViewer.getCompleteRotation(page); return translateQuads(page, rotation, quad); } private async _extractTextFromRect(page: Core.PDFNet.Page, rect: Core.PDFNet.Rect) { - const txt = await this._pdf.PDFNet.TextExtractor.create(); - txt.begin(page, rect); // Read the page. + const txt = await this._reusablePdf.PDFNet.TextExtractor.create(); + await txt.begin(page, rect); // Read the page. const words: string[] = []; // Extract words one by one. diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts index 521a4b6d0..e0605da03 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts @@ -15,6 +15,7 @@ import { FilePreviewStateService } from './file-preview-state.service'; import { ViewModeService } from './view-mode.service'; import { FileDataService } from './file-data.service'; import { SuperTypes } from '@models/file/super-types'; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; import Annotation = Core.Annotations.Annotation; import Quad = Core.Math.Quad; @@ -29,6 +30,7 @@ export class AnnotationDrawService { private readonly _userPreferenceService: UserPreferenceService, private readonly _skippedService: SkippedService, private readonly _pdf: PdfViewer, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _state: FilePreviewStateService, private readonly _viewModeService: ViewModeService, private readonly _fileDataService: FileDataService, @@ -36,7 +38,7 @@ export class AnnotationDrawService { draw(annotations: readonly AnnotationWrapper[]) { const licenseKey = environment.licenseKey ? atob(environment.licenseKey) : null; - return this._pdf.PDFNet.runWithCleanup(() => this._draw(annotations), licenseKey); + return this._reusablePdf.PDFNet.runWithCleanup(() => this._draw(annotations), licenseKey); } getColor(superType: string, dictionary?: string) { @@ -64,8 +66,7 @@ export class AnnotationDrawService { } convertColor(hexColor: string) { - const rgbColor = hexToRgb(hexColor); - return new this._pdf.Annotations.Color(rgbColor.r, rgbColor.g, rgbColor.b); + return this._reusablePdf.color(hexToRgb(hexColor)); } annotationToQuads(annotation: Annotation) { @@ -81,13 +82,13 @@ export class AnnotationDrawService { const x4 = annotation.getRect().x1; const y4 = annotation.getRect().y1; - return new this._pdf.instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4); + return this._reusablePdf.quad(x1, y1, x2, y2, x3, y3, x4, y4); } private async _draw(annotationWrappers: readonly AnnotationWrapper[]) { const annotations = annotationWrappers.map(annotation => this._computeAnnotation(annotation)).filter(a => !!a); - this._pdf.annotationManager.addAnnotations(annotations, { imported: true }); - await this._pdf.annotationManager.drawAnnotationsFromList(annotations); + this._reusablePdf.annotationManager.addAnnotations(annotations, { imported: true }); + await this._reusablePdf.annotationManager.drawAnnotationsFromList(annotations); if (this._userPreferenceService.areDevFeaturesEnabled) { const { dossierId, fileId } = this._state; @@ -108,14 +109,13 @@ export class AnnotationDrawService { // }) }); } - const annotationManager = this._pdf.annotationManager; - annotationManager.addAnnotations(sections, { imported: true }); - await annotationManager.drawAnnotationsFromList(sections); + this._reusablePdf.annotationManager.addAnnotations(sections, { imported: true }); + await this._reusablePdf.annotationManager.drawAnnotationsFromList(sections); } private _computeSection(pageNumber: number, sectionRectangle: ISectionRectangle) { - const rectangleAnnot = new this._pdf.Annotations.RectangleAnnotation(); - const pageHeight = this._pdf.documentViewer.getPageHeight(pageNumber); + const rectangleAnnot = this._reusablePdf.rectangle(); + const pageHeight = this._reusablePdf.getPageHeight(pageNumber); const rectangle: IRectangle = { topLeft: sectionRectangle.topLeft, page: pageNumber, @@ -136,14 +136,14 @@ export class AnnotationDrawService { private _computeAnnotation(annotationWrapper: AnnotationWrapper) { const pageNumber = this._viewModeService.isCompare ? annotationWrapper.pageNumber * 2 - 1 : annotationWrapper.pageNumber; - if (pageNumber > this._pdf.pageCount) { + if (pageNumber > this._reusablePdf.pageCount) { // skip imported annotations from files that have more pages than the current one return; } if (annotationWrapper.superType === SuperTypes.TextHighlight) { - const rectangleAnnot = new this._pdf.Annotations.RectangleAnnotation(); - const pageHeight = this._pdf.documentViewer.getPageHeight(pageNumber); + const rectangleAnnot = this._reusablePdf.rectangle(); + const pageHeight = this._reusablePdf.getPageHeight(pageNumber); const rectangle: IRectangle = annotationWrapper.positions[0]; rectangleAnnot.PageNumber = pageNumber; rectangleAnnot.X = rectangle.topLeft.x; @@ -158,7 +158,7 @@ export class AnnotationDrawService { return rectangleAnnot; } - const annotation = new this._pdf.Annotations.TextHighlightAnnotation(); + const annotation = this._reusablePdf.textHighlight(); annotation.Quads = this._rectanglesToQuads(annotationWrapper.positions, pageNumber); annotation.Opacity = annotationWrapper.isChangeLogRemoved ? DEFAULT_REMOVED_ANNOTATION_OPACITY : DEFAULT_TEXT_ANNOTATION_OPACITY; annotation.setContents(annotationWrapper.content); @@ -185,7 +185,7 @@ export class AnnotationDrawService { } private _rectanglesToQuads(positions: IRectangle[], pageNumber: number): Quad[] { - const pageHeight = this._pdf.documentViewer.getPageHeight(pageNumber); + const pageHeight = this._reusablePdf.getPageHeight(pageNumber); return positions.map(p => this._rectangleToQuad(p, pageHeight)); } @@ -202,6 +202,6 @@ export class AnnotationDrawService { const x4 = rectangle.topLeft.x; const y4 = pageHeight - rectangle.topLeft.y; - return new this._pdf.instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4); + return this._reusablePdf.quad(x1, y1, x2, y2, x3, y3, x4, y4); } } diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts index c3bfcd7cd..2f0e883c9 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts @@ -71,14 +71,16 @@ export class FilePreviewStateService { } get #blob$() { - const reloadBlob$ = this.#reloadBlob$.pipe( - withLatestFrom(this.file$), - map(([, file]) => file), - ); - return merge(this.file$, reloadBlob$).pipe( + const file$ = this.file$.pipe( startWith(undefined), pairwise(), filter(([oldFile, newFile]) => oldFile?.cacheIdentifier !== newFile.cacheIdentifier), + ); + const reloadBlob$ = this.#reloadBlob$.pipe( + withLatestFrom(file$), + map(([, file]) => file), + ); + return merge(file$, reloadBlob$).pipe( switchMap(([oldFile, newFile]) => this.#downloadOriginalFile(newFile.cacheIdentifier, !!oldFile?.cacheIdentifier)), ); } diff --git a/apps/red-ui/src/app/modules/file-preview/services/page-rotation.service.ts b/apps/red-ui/src/app/modules/file-preview/services/page-rotation.service.ts index 06d9c0326..586a58a09 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/page-rotation.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/page-rotation.service.ts @@ -17,6 +17,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { MatDialog } from '@angular/material/dialog'; import { ViewerHeaderConfigService } from './viewer-header-config.service'; import { FilesService } from '@services/files/files.service'; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; const ACTION_BUTTONS = [HeaderElements.APPLY_ROTATION, HeaderElements.DISCARD_ROTATION]; const ONE_ROTATION_DEGREE = 90; @@ -27,6 +28,7 @@ export class PageRotationService { constructor( private readonly _pdf: PdfViewer, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _dialog: MatDialog, private readonly _loadingService: LoadingService, private readonly _screenState: FilePreviewStateService, @@ -68,7 +70,7 @@ export class PageRotationService { for (const page of Object.keys(rotations)) { const times = rotations[page] / ONE_ROTATION_DEGREE; for (let i = 1; i <= times; i++) { - this._pdf?.documentViewer?.rotateCounterClockwise(Number(page)); + this._reusablePdf.documentViewer.rotateCounterClockwise(Number(page)); } } @@ -83,9 +85,9 @@ export class PageRotationService { this.#rotations$.next({ ...this.#rotations$.value, [pageNumber]: rotationValue }); if (rotation === RotationTypes.LEFT) { - this._pdf.documentViewer.rotateCounterClockwise(pageNumber); + this._reusablePdf.documentViewer.rotateCounterClockwise(pageNumber); } else { - this._pdf.documentViewer.rotateClockwise(pageNumber); + this._reusablePdf.documentViewer.rotateClockwise(pageNumber); } if (this.hasRotations()) { diff --git a/apps/red-ui/src/app/modules/file-preview/services/pdf-viewer.service.ts b/apps/red-ui/src/app/modules/file-preview/services/pdf-viewer.service.ts index 7fad682ee..769624877 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/pdf-viewer.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/pdf-viewer.service.ts @@ -1,69 +1,21 @@ import { translateQuads } from '../../../utils'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; -import WebViewer, { Core, WebViewerInstance } from '@pdftron/webviewer'; +import { Core } from '@pdftron/webviewer'; import { ViewModeService } from './view-mode.service'; import { File } from '@red/domain'; -import { Inject, Injectable } from '@angular/core'; -import { BASE_HREF } from '../../../tokens'; -import { environment } from '@environments/environment'; -import { DISABLED_HOTKEYS } from '../utils/constants'; -import { Observable, Subject } from 'rxjs'; -import { NGXLogger } from 'ngx-logger'; -import { map, tap } from 'rxjs/operators'; -import { ListingService, shareDistinctLast, shareLast } from '@iqser/common-ui'; +import { Injectable } from '@angular/core'; +import { ListingService } from '@iqser/common-ui'; import { MultiSelectService } from './multi-select.service'; -import { ActivatedRoute } from '@angular/router'; -import Annotation = Core.Annotations.Annotation; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; @Injectable() export class PdfViewer { - instance?: WebViewerInstance; - - readonly documentLoaded$: Observable; - readonly currentPage$ = this._activatedRoute.queryParamMap.pipe( - map(params => Number(params.get('page') ?? '1')), - shareDistinctLast(), - ); - readonly #documentLoaded$ = new Subject(); - constructor( - @Inject(BASE_HREF) private readonly _baseHref: string, private readonly _viewModeService: ViewModeService, - private readonly _activatedRoute: ActivatedRoute, private readonly _multiSelectService: MultiSelectService, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _listingService: ListingService, - private readonly _logger: NGXLogger, - ) { - this.documentLoaded$ = this.#documentLoaded$.asObservable().pipe( - tap(() => this._logger.debug('[PDF] Loaded')), - tap(() => this.#setCurrentPage()), - shareLast(), - ); - } - - get documentViewer() { - return this.instance?.Core.documentViewer; - } - - get annotationManager() { - return this.instance?.Core.annotationManager; - } - - get UI() { - return this.instance.UI; - } - - get Annotations() { - return this.instance.Core.Annotations; - } - - get PDFNet(): typeof Core.PDFNet { - return this.instance.Core.PDFNet; - } - - get hasAnnotations() { - return this.annotationManager?.getAnnotationsList().length > 0 ?? false; - } + ) {} get paginationOffset() { return this._viewModeService.isCompare ? 2 : 1; @@ -74,72 +26,16 @@ export class PdfViewer { } get totalPages() { - return this._viewModeService.isCompare ? Math.ceil(this.pageCount / 2) : this.pageCount; - } - - get pageCount() { - try { - return this.instance?.Core.documentViewer?.getPageCount() ?? 1; - } catch { - // might throw Error: getPageCount was called before the 'documentLoaded' event - return 1; - } + const pageCount = this._reusablePdf.pageCount; + return this._viewModeService.isCompare ? Math.ceil(pageCount / 2) : pageCount; } private get _currentInternalPage() { - return this.instance?.Core.documentViewer?.getCurrentPage() ?? 1; - } - - async lockDocument() { - const document = await this.documentViewer.getDocument()?.getPDFDoc(); - if (!document) { - return false; - } - - await document.lock(); - this._logger.debug('[PDF] Locked'); - return true; - } - - hideAnnotations(annotations: Annotation[]): void { - this.annotationManager.hideAnnotations(annotations); - } - - showAnnotations(annotations: Annotation[]): void { - this.annotationManager.showAnnotations(annotations); - } - - getAnnotations(predicate?: (value) => boolean) { - const annotations = this.annotationManager?.getAnnotationsList() ?? []; - return predicate ? annotations.filter(predicate) : annotations; - } - - getAnnotationsById(ids: readonly string[]) { - if (this.annotationManager) { - return ids.map(id => this.annotationManager.getAnnotationById(id)).filter(a => !!a); - } - - return []; + return this._reusablePdf.documentViewer?.getCurrentPage() ?? 1; } emitDocumentLoaded() { - this.deleteAnnotations(); - this.#documentLoaded$.next(true); - } - - async loadViewer(htmlElement: HTMLElement) { - this.instance = await WebViewer( - { - licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null, - fullAPI: true, - path: this.#convertPath('/assets/wv-resources'), - css: this.#convertPath('/assets/pdftron/stylesheet.css'), - backendType: 'ems', - }, - htmlElement, - ); - - return this.instance; + this._reusablePdf.deleteAnnotations(); } isCurrentPageExcluded(file: File) { @@ -159,55 +55,26 @@ export class PdfViewer { } navigateNextPage() { - if (this._currentInternalPage < this.pageCount) { - this._navigateToPage(Math.min(this._currentInternalPage + this.paginationOffset, this.pageCount)); + const pageCount = this._reusablePdf.pageCount; + if (this._currentInternalPage < pageCount) { + this._navigateToPage(Math.min(this._currentInternalPage + this.paginationOffset, pageCount)); } } - disableHotkeys(): void { - DISABLED_HOTKEYS.forEach(key => this.instance.UI.hotkeys.off(key)); - } - translateQuad(page: number, quad: Core.Math.Quad) { - const rotation = this.documentViewer.getCompleteRotation(page); + const rotation = this._reusablePdf.documentViewer.getCompleteRotation(page); return translateQuads(page, rotation, quad); } - deselectAllAnnotations() { - this.annotationManager?.deselectAllAnnotations(); - } - selectAnnotations(annotations?: AnnotationWrapper[]) { if (!annotations) { - return this.deselectAllAnnotations(); + return this._reusablePdf.deselectAnnotations(); } const annotationsToSelect = this._multiSelectService.isActive ? [...this._listingService.selected, ...annotations] : annotations; this.#selectAnnotations(annotationsToSelect); } - deleteAnnotations(annotationsIds?: readonly string[]) { - let annotations: Annotation[]; - if (!annotationsIds) { - annotations = this.getAnnotations(); - } else { - annotations = this.getAnnotationsById(annotationsIds); - } - - try { - this.annotationManager.deleteAnnotations(annotations, { - force: true, - }); - } catch (error) { - console.log('Error while deleting annotations: ', error); - } - } - - deselectAnnotations(annotations: AnnotationWrapper[]) { - const ann = this.getAnnotationsById(annotations.map(a => a.id)); - this.annotationManager.deselectAnnotations(ann); - } - #selectAnnotations(annotations: AnnotationWrapper[] = []) { const filteredAnnotationsIds = annotations.filter(a => !!a).map(a => a.id); @@ -216,7 +83,7 @@ export class PdfViewer { } if (!this._multiSelectService.isActive) { - this.deselectAllAnnotations(); + this._reusablePdf.deselectAnnotations(); } const pageNumber = annotations[0].pageNumber; @@ -231,24 +98,15 @@ export class PdfViewer { } #jumpAndSelectAnnotations(annotationIds: readonly string[]) { - const annotationsFromViewer = this.getAnnotationsById(annotationIds); + const annotationsFromViewer = this._reusablePdf.getAnnotationsById(annotationIds); - this.annotationManager.jumpToAnnotation(annotationsFromViewer[0]); - this.annotationManager.selectAnnotations(annotationsFromViewer); + this._reusablePdf.annotationManager.jumpToAnnotation(annotationsFromViewer[0]); + this._reusablePdf.annotationManager.selectAnnotations(annotationsFromViewer); } private _navigateToPage(pageNumber: number) { if (this._currentInternalPage !== pageNumber) { - this.documentViewer.displayPageLocation(pageNumber, 0, 0); + this._reusablePdf.documentViewer.displayPageLocation(pageNumber, 0, 0); } } - - #convertPath(path: string) { - return `${this._baseHref}${path}`; - } - - #setCurrentPage() { - const currentDocPage = this._activatedRoute.snapshot.queryParamMap.get('page'); - this.documentViewer.setCurrentPage(Number(currentDocPage ?? '1')); - } } diff --git a/apps/red-ui/src/app/modules/file-preview/services/skipped.service.ts b/apps/red-ui/src/app/modules/file-preview/services/skipped.service.ts index a56db21ce..24b9fe89d 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/skipped.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/skipped.service.ts @@ -2,14 +2,14 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { skip, tap } from 'rxjs/operators'; import { shareDistinctLast } from '@iqser/common-ui'; -import { PdfViewer } from './pdf-viewer.service'; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; @Injectable() export class SkippedService { readonly hideSkipped$: Observable; readonly #hideSkipped$ = new BehaviorSubject(false); - constructor(private readonly _pdf: PdfViewer) { + constructor(private readonly _pdf: ReusablePdfViewer) { this.hideSkipped$ = this.#hideSkipped$.pipe( tap(hideSkipped => this._handleIgnoreAnnotationsDrawing(hideSkipped)), shareDistinctLast(), @@ -28,7 +28,7 @@ export class SkippedService { } private _handleIgnoreAnnotationsDrawing(hideSkipped: boolean): void { - const ignored = this._pdf.getAnnotations(a => a.getCustomData('skipped')); + const ignored = this._pdf.getAnnotations(a => Boolean(a.getCustomData('skipped'))); if (hideSkipped) { this._pdf.hideAnnotations(ignored); } else { diff --git a/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts b/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts index 8d637229a..e57fbb97d 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts @@ -8,12 +8,14 @@ import { TranslateService } from '@ngx-translate/core'; import { Core } from '@pdftron/webviewer'; import { firstValueFrom } from 'rxjs'; import { WatermarkService } from '@services/entity-services/watermark.service'; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; import PDFNet = Core.PDFNet; @Injectable() export class StampService { constructor( private readonly _pdf: PdfViewer, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _state: FilePreviewStateService, private readonly _logger: NGXLogger, private readonly _viewModeService: ViewModeService, @@ -22,7 +24,7 @@ export class StampService { ) {} async stampPDF(): Promise { - const pdfDoc = await this._pdf.documentViewer.getDocument()?.getPDFDoc(); + const pdfDoc = await this._reusablePdf.PDFDoc; if (!pdfDoc) { return; } @@ -31,9 +33,9 @@ export class StampService { const allPages = [...Array(file.numberOfPages).keys()].map(page => page + 1); try { - await clearStamps(pdfDoc, this._pdf.PDFNet, allPages); + await clearStamps(pdfDoc, this._reusablePdf.PDFNet, allPages); } catch (e) { - this._logger.error('Error clearing stamps: ', e); + console.error('Error clearing stamps: ', e); return; } @@ -46,15 +48,15 @@ export class StampService { await this._stampExcludedPages(pdfDoc, file.excludedPages); } - this._pdf.documentViewer.refreshAll(); - this._pdf.documentViewer.updateView([this._pdf.currentPage], this._pdf.currentPage); + this._reusablePdf.documentViewer.refreshAll(); + this._reusablePdf.documentViewer.updateView([this._pdf.currentPage], this._pdf.currentPage); } private async _stampExcludedPages(document: PDFNet.PDFDoc, excludedPages: number[]): Promise { if (excludedPages && excludedPages.length > 0) { await stampPDFPage( document, - this._pdf.PDFNet, + this._reusablePdf.PDFNet, this._translateService.instant('file-preview.excluded-from-redaction') as string, 17, 'courier', @@ -70,7 +72,7 @@ export class StampService { const watermark = await firstValueFrom(this._watermarkService.getWatermark(dossierTemplateId)); await stampPDFPage( document, - this._pdf.PDFNet, + this._reusablePdf.PDFNet, watermark.text, watermark.fontSize, watermark.fontType, diff --git a/apps/red-ui/src/app/modules/file-preview/services/tooltips.service.ts b/apps/red-ui/src/app/modules/file-preview/services/tooltips.service.ts index 5ce90ca03..d3481873a 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/tooltips.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/tooltips.service.ts @@ -1,16 +1,16 @@ import { Inject, Injectable } from '@angular/core'; -import { PdfViewer } from './pdf-viewer.service'; import { UserPreferenceService } from '@services/user-preference.service'; import { HeaderElements } from '../utils/constants'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { TranslateService } from '@ngx-translate/core'; import { BASE_HREF } from '../../../tokens'; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; @Injectable() export class TooltipsService { constructor( @Inject(BASE_HREF) private readonly _baseHref: string, - private readonly _pdfViewer: PdfViewer, + private readonly _pdfViewer: ReusablePdfViewer, private readonly _userPreferenceService: UserPreferenceService, private readonly _translateService: TranslateService, ) {} diff --git a/apps/red-ui/src/app/modules/file-preview/services/viewer-header-config.service.ts b/apps/red-ui/src/app/modules/file-preview/services/viewer-header-config.service.ts index 80928ced2..1dd454726 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/viewer-header-config.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/viewer-header-config.service.ts @@ -9,6 +9,7 @@ import { environment } from '@environments/environment'; import { ViewModeService } from './view-mode.service'; import { FilePreviewStateService } from './file-preview-state.service'; import { PageRotationService } from './page-rotation.service'; +import { ReusablePdfViewer } from '../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; @Injectable() export class ViewerHeaderConfigService { @@ -33,6 +34,7 @@ export class ViewerHeaderConfigService { private readonly _injector: Injector, private readonly _translateService: TranslateService, private readonly _pdfViewer: PdfViewer, + private readonly _reusablePdf: ReusablePdfViewer, private readonly _tooltipsService: TooltipsService, private readonly _viewModeService: ViewModeService, private readonly _stateService: FilePreviewStateService, @@ -163,13 +165,13 @@ export class ViewerHeaderConfigService { private async _closeCompareMode() { this._viewModeService.compareMode = false; - const pdfNet = this._pdfViewer.instance.Core.PDFNet; + const pdfNet = this._reusablePdf.PDFNet; await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); const blob = await this._stateService.blob; const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer()); const filename = this._stateService.file.filename ?? 'document.pdf'; - this._pdfViewer.instance.UI.loadDocument(currentDocument, { filename }); + this._reusablePdf.instance.UI.loadDocument(currentDocument, { filename }); this.disable([HeaderElements.CLOSE_COMPARE_BUTTON]); this.enable([HeaderElements.COMPARE_BUTTON]); @@ -197,7 +199,7 @@ export class ViewerHeaderConfigService { } private _updateElements(): void { - this._pdfViewer.instance.UI.setHeaderItems(header => { + this._reusablePdf.instance.UI.setHeaderItems(header => { const enabledItems: IHeaderElement[] = []; const groups: HeaderElementType[][] = [ [HeaderElements.COMPARE_BUTTON, HeaderElements.CLOSE_COMPARE_BUTTON], diff --git a/apps/red-ui/src/app/modules/file-preview/utils/constants.ts b/apps/red-ui/src/app/modules/file-preview/utils/constants.ts index a61d3f581..7e72ddf76 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/constants.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/constants.ts @@ -42,36 +42,3 @@ export const TextPopups = { ADD_RECTANGLE: 'add-rectangle', ADD_FALSE_POSITIVE: 'add-false-positive', } as const; - -export const DISABLED_HOTKEYS = [ - 'CTRL+SHIFT+EQUAL', - 'COMMAND+SHIFT+EQUAL', - 'CTRL+SHIFT+MINUS', - 'COMMAND+SHIFT+MINUS', - 'CTRL+V', - 'COMMAND+V', - 'CTRL+Y', - 'COMMAND+Y', - 'CTRL+O', - 'COMMAND+O', - 'CTRL+P', - 'COMMAND+P', - 'SPACE', - 'UP', - 'DOWN', - 'R', - 'P', - 'A', - 'C', - 'E', - 'I', - 'L', - 'N', - 'O', - 'T', - 'S', - 'G', - 'H', - 'K', - 'U', -] as const; diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts new file mode 100644 index 000000000..b51df7e60 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/constants.ts @@ -0,0 +1,60 @@ +import { CustomError } from '@iqser/common-ui'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; + +export const DOCUMENT_LOADING_ERROR = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh'); + +export const USELESS_ELEMENTS = [ + 'pageNavOverlay', + 'menuButton', + 'selectToolButton', + 'textHighlightToolButton', + 'textUnderlineToolButton', + 'textSquigglyToolButton', + 'textStrikeoutToolButton', + 'viewControlsButton', + 'contextMenuPopup', + 'linkButton', + 'toggleNotesButton', + 'notesPanel', + 'thumbnailControl', + 'documentControl', + 'ribbons', + 'toolsHeader', + 'rotateClockwiseButton', + 'rotateCounterClockwiseButton', + 'annotationStyleEditButton', + 'annotationGroupButton', +]; + +export const DISABLED_HOTKEYS = [ + 'CTRL+SHIFT+EQUAL', + 'COMMAND+SHIFT+EQUAL', + 'CTRL+SHIFT+MINUS', + 'COMMAND+SHIFT+MINUS', + 'CTRL+V', + 'COMMAND+V', + 'CTRL+Y', + 'COMMAND+Y', + 'CTRL+O', + 'COMMAND+O', + 'CTRL+P', + 'COMMAND+P', + 'SPACE', + 'UP', + 'DOWN', + 'R', + 'P', + 'A', + 'C', + 'E', + 'I', + 'L', + 'N', + 'O', + 'T', + 'S', + 'G', + 'H', + 'K', + 'U', +] as const; diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component.ts index bc49c8de7..4d75904e0 100644 --- a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; import { ReusablePdfViewer } from './reusable-pdf-viewer.service'; @Component({ @@ -16,12 +16,18 @@ import { ReusablePdfViewer } from './reusable-pdf-viewer.service'; `, ], }) -export class ReusablePdfViewerComponent implements OnInit { - @ViewChild('viewer', { static: true }) readonly viewer: ElementRef; +export class ReusablePdfViewerComponent { + #viewer: ElementRef; constructor(readonly reusablePdfViewer: ReusablePdfViewer) {} - ngOnInit() { - return this.reusablePdfViewer.init(this.viewer.nativeElement); + @ViewChild('viewer', { static: true }) + set viewer(value: ElementRef) { + if (this.#viewer) { + return; + } + + this.#viewer = value; + this.reusablePdfViewer.init(value.nativeElement as HTMLElement).then(); } } diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service.ts index 80223fffb..f17e9fd53 100644 --- a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service.ts +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service.ts @@ -1,47 +1,28 @@ import { Inject, Injectable, Injector } from '@angular/core'; import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer'; -import { environment } from '../../../../../environments/environment'; +import { environment } from '@environments/environment'; import { BASE_HREF_FN, BaseHrefFn } from '../../../../tokens'; import { File } from '@red/domain'; -import { CustomError, ErrorService, LoadingService, log, shareDistinctLast, shareLast } from '@iqser/common-ui'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker/'; -import { ActivatedRoute, ActivationStart, Router } from '@angular/router'; -import { filter, map } from 'rxjs/operators'; -import { fromEvent, merge, Observable } from 'rxjs'; -import { ConfigService } from '../../../../services/config.service'; +import { ErrorService, List, shareDistinctLast, shareLast } from '@iqser/common-ui'; +import { ActivatedRoute, Router } from '@angular/router'; +import { debounceTime, map, tap } from 'rxjs/operators'; +import { fromEvent, merge, Observable, Subject } from 'rxjs'; +import { ConfigService } from '@services/config.service'; +import { NGXLogger } from 'ngx-logger'; +import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from './constants'; +import { AnnotationWrapper } from '@models/file/annotation.wrapper'; +import { Rgb } from '@shared/components/reusable-pdf-viewer/types'; import DocumentViewer = Core.DocumentViewer; import AnnotationManager = Core.AnnotationManager; import TextTool = Core.Tools.TextTool; - -const DocLoadingError = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh'); -const uselessElements = [ - 'pageNavOverlay', - 'menuButton', - 'selectToolButton', - 'textHighlightToolButton', - 'textUnderlineToolButton', - 'textSquigglyToolButton', - 'textStrikeoutToolButton', - 'viewControlsButton', - 'contextMenuPopup', - 'linkButton', - 'toggleNotesButton', - 'notesPanel', - 'thumbnailControl', - 'documentControl', - 'ribbons', - 'toolsHeader', - 'rotateClockwiseButton', - 'rotateCounterClockwiseButton', - 'annotationStyleEditButton', - 'annotationGroupButton', -]; +import Annotation = Core.Annotations.Annotation; +import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation; @Injectable({ providedIn: 'root', }) export class ReusablePdfViewer { - readonly currentPage$ = this._injector.get(ActivatedRoute).queryParamMap.pipe( + readonly currentPage$ = this._activatedRoute.queryParamMap.pipe( map(params => Number(params.get('page') ?? '1')), shareDistinctLast(), ); @@ -50,51 +31,155 @@ export class ReusablePdfViewer { show$: Observable; documentLoaded$: Observable; + annotationSelected$: Observable; + pageComplete$: Observable; + #instance: WebViewerInstance; + #documentClosed$ = new Subject(); constructor( @Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn, - private readonly _loadingService: LoadingService, private readonly _errorService: ErrorService, private readonly _router: Router, + private readonly _logger: NGXLogger, private readonly _configService: ConfigService, + private readonly _activatedRoute: ActivatedRoute, private readonly _injector: Injector, ) {} - get #documentLoaded$() { - const docLoadedEvent = this.#instance.UI.Events.DOCUMENT_LOADED; - const event$ = fromEvent(this.documentViewer, docLoadedEvent).pipe(map(() => true)); - - return event$.pipe(log('[PDF] Document loaded'), shareLast()); + get instance() { + return this.#instance; } - get #show$() { - const routeChanged = this._router.events.pipe( - filter(event => event instanceof ActivationStart), - map(() => false), - ); + get PDFDoc() { + return this.document?.getPDFDoc(); + } - return merge(routeChanged, this.documentLoaded$); + get document() { + return this.documentViewer.getDocument(); + } + + get PDFNet(): typeof Core.PDFNet { + return this.#instance.Core.PDFNet; + } + + get pageCount() { + try { + return this.#instance.Core.documentViewer.getPageCount(); + } catch { + // might throw Error: getPageCount was called before the 'documentLoaded' event + return 1; + } + } + + get #pageComplete$() { + return fromEvent(this.documentViewer, 'pageComplete').pipe(debounceTime(300)); + } + + get #documentLoaded$() { + const event$ = fromEvent(this.documentViewer, this.#instance.UI.Events.DOCUMENT_LOADED); + const toBool$ = event$.pipe(map(() => true)); + const updateCurrentPage$ = toBool$.pipe(tap(() => this.#setCurrentPage())); + + const log = tap(() => this._logger.info('[PDF] Document loaded')); + return updateCurrentPage$.pipe(log, shareLast()); + } + + get #annotationSelected$() { + const onSelect$ = fromEvent<[Annotation[], string]>(this.annotationManager, 'annotationSelected'); + return onSelect$.pipe(tap(value => console.log('Annotation selected: ', value))); } async init(htmlElement: HTMLElement) { this.#instance = await this.#getInstance(htmlElement); - console.log('[PDF] Initialized'); + this._logger.info('[PDF] Initialized'); this.documentViewer = this.#instance.Core.documentViewer; this.annotationManager = this.#instance.Core.annotationManager; this.documentLoaded$ = this.#documentLoaded$; - this.show$ = this.#show$; + this.show$ = merge(this.#documentClosed$, this.documentLoaded$).pipe(shareLast()); + this.annotationSelected$ = this.#annotationSelected$; + this.pageComplete$ = this.#pageComplete$; this.#setSelectionMode(); this.#configureElements(); + this.#disableHotkeys(); } - loadDocument(blob: Blob, file: File) { - console.log('[PDF] Loading document', blob, file); + deleteAnnotations(annotationsIds?: List) { + let annotations: Annotation[]; + if (!annotationsIds) { + annotations = this.getAnnotations(); + } else { + annotations = this.getAnnotationsById(annotationsIds); + } + + try { + this.annotationManager.deleteAnnotations(annotations, { + force: true, + }); + } catch (error) { + console.log('Error while deleting annotations: ', error); + } + } + + getAnnotations(predicate?: (value: Annotation) => boolean) { + const annotations = this.annotationManager.getAnnotationsList(); + return predicate ? annotations.filter(predicate) : annotations; + } + + getAnnotationsById(ids: List) { + return ids.map(id => this.annotationManager.getAnnotationById(id)).filter(a => !!a); + } + + deselectAnnotations(annotations?: List) { + if (!annotations) { + return this.annotationManager.deselectAllAnnotations(); + } + + const ann = this.getAnnotationsById(annotations.map(a => a.id)); + this.annotationManager.deselectAnnotations(ann); + } + + hideAnnotations(annotations: Annotation[]): void { + this.annotationManager.hideAnnotations(annotations); + } + + showAnnotations(annotations: Annotation[]): void { + this.annotationManager.showAnnotations(annotations); + } + + getPageHeight(page: number) { + try { + return this.documentViewer.getPageHeight(page); + } catch { + // might throw Error: getPageHeight was called before the 'documentLoaded' event + return 0; + } + } + + closeDocument() { + this._logger.info('[PDF] Closing document'); + this.#documentClosed$.next(false); + this.#instance.Core.documentViewer.closeDocument(); + } + + async lockDocument() { + const document = await this.PDFDoc; + if (!document) { + return false; + } + + await document.lock(); + this._logger.info('[PDF] Locked'); + return true; + } + + async loadDocument(blob: Blob, file: File) { + this._logger.info('[PDF] Loading document', blob, file); const onError = () => { - this._loadingService.stop(); - this._errorService.set(DocLoadingError); + this._errorService.set(DOCUMENT_LOADING_ERROR); + this._logger.error('[PDF] Error while loading document'); // this.stateService.reloadBlob(); }; // const pdfNet = this._instance.Core.PDFNet; @@ -105,11 +190,40 @@ export class ReusablePdfViewer { // console.log('document initialized'); // await document.flattenAnnotations(false); // console.log(document); - this.#instance.UI.loadDocument(blob, { filename: file?.filename + '.pdf' ?? 'document.pdf', onError }); + const pdfNet = this.#instance.Core.PDFNet; + + await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); + const document = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer()); + await document.flattenAnnotations(false); + this.#instance.UI.loadDocument(document, { filename: file?.filename + '.pdf' ?? 'document.pdf', onError }); + } + + quad(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number) { + return new this.#instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4); + } + + color(rgb: Rgb) { + return new this.#instance.Core.Annotations.Color(rgb.r, rgb.g, rgb.b); + } + + rectangle() { + return new this.#instance.Core.Annotations.RectangleAnnotation(); + } + + textHighlight() { + return new this.#instance.Core.Annotations.TextHighlightAnnotation(); + } + + isTextHighlight(annotation: Annotation): annotation is TextHighlightAnnotation { + return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation; + } + + #disableHotkeys(): void { + DISABLED_HOTKEYS.forEach(key => this.#instance.UI.hotkeys.off(key)); } #configureElements() { - this.#instance.UI.disableElements(uselessElements); + this.#instance.UI.disableElements(USELESS_ELEMENTS); } #setSelectionMode(): void { @@ -128,4 +242,9 @@ export class ReusablePdfViewer { return WebViewer(options, htmlElement); } + + #setCurrentPage() { + const currentDocPage = this._activatedRoute.snapshot.queryParamMap.get('page'); + this.documentViewer.setCurrentPage(Number(currentDocPage ?? '1')); + } } diff --git a/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts new file mode 100644 index 000000000..9356aaff7 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/types.ts @@ -0,0 +1,5 @@ +export interface Rgb { + readonly r: number; + readonly g: number; + readonly b: number; +}