From 78e49d609edfebc845d34a4c9f35958b44676106 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 23 May 2022 18:25:26 +0300 Subject: [PATCH] RED-3988: transform old pdf viewer component into pdf proxy service --- .../annotation-details.component.ts | 5 +- .../annotation-reference.component.ts | 4 +- .../annotation-references-list.component.ts | 4 +- .../annotation-wrapper.component.ts | 4 +- .../pdf-paginator.component.html | 0 .../pdf-paginator.component.scss | 11 - .../file-preview/file-preview-providers.ts | 2 + .../file-preview-screen.component.html | 12 +- .../file-preview-screen.component.ts | 93 ++++--- .../file-preview/file-preview.module.ts | 2 - .../services/annotation-actions.service.ts | 21 +- .../pdf-proxy.service.ts} | 254 +++++++----------- .../file-preview/services/skipped.service.ts | 4 +- .../utils/pdf-calculation.utils.ts | 20 +- .../services/annotation-manager.service.ts | 2 - .../services/document-viewer.service.ts | 25 +- .../pdf-viewer/services/pdf-viewer.service.ts | 54 +++- .../app/modules/pdf-viewer/utils/constants.ts | 9 + apps/red-ui/src/app/utils/index.ts | 1 - apps/red-ui/src/app/utils/pdf-coordinates.ts | 55 ---- 20 files changed, 267 insertions(+), 315 deletions(-) delete mode 100644 apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.html delete mode 100644 apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.scss rename apps/red-ui/src/app/modules/file-preview/{components/pdf-paginator/pdf-paginator.component.ts => services/pdf-proxy.service.ts} (55%) delete mode 100644 apps/red-ui/src/app/utils/pdf-coordinates.ts diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts index cb873d3a2..290a60570 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts @@ -4,9 +4,10 @@ import { TranslateService } from '@ngx-translate/core'; import { annotationChangesTranslations } from '@translations/annotation-changes-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { MultiSelectService } from '../../services/multi-select.service'; -import { KeysOf, ListingService } from '@iqser/common-ui'; +import { KeysOf } from '@iqser/common-ui'; import { BehaviorSubject, Observable } from 'rxjs'; import { filter, map, switchMap } from 'rxjs/operators'; +import { AnnotationsListingService } from '../../services/annotations-listing.service'; interface Engine { readonly icon: string; @@ -41,7 +42,7 @@ export class AnnotationDetailsComponent implements OnChanges { constructor( private readonly _translateService: TranslateService, - private readonly _listingService: ListingService, + private readonly _listingService: AnnotationsListingService, readonly multiSelectService: MultiSelectService, ) { this.isSelected$ = this._annotationChanged$.pipe(switchMap(annotation => this._listingService.isSelected$(annotation))); diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-reference/annotation-reference.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-reference/annotation-reference.component.ts index 22fcaf817..d53922c6d 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-reference/annotation-reference.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-reference/annotation-reference.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Inp import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { BehaviorSubject, filter } from 'rxjs'; import { switchMap, tap } from 'rxjs/operators'; -import { ListingService } from '@iqser/common-ui'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { AnnotationsListingService } from '../../services/annotations-listing.service'; @UntilDestroy() @Component({ @@ -17,7 +17,7 @@ export class AnnotationReferenceComponent implements OnChanges { @HostBinding('class.active') isSelected = false; private readonly _annotationChanged$ = new BehaviorSubject(undefined); - constructor(private readonly _listingService: ListingService, private readonly _changeRef: ChangeDetectorRef) { + constructor(private readonly _listingService: AnnotationsListingService, private readonly _changeRef: ChangeDetectorRef) { this._annotationChanged$ .pipe( filter(annotation => !!annotation), diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts index 2093bc674..0b4048496 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationReferencesService } from '../../services/annotation-references.service'; -import { ListingService } from '@iqser/common-ui'; import { Observable, switchMap } from 'rxjs'; +import { AnnotationsListingService } from '../../services/annotations-listing.service'; @Component({ selector: 'redaction-annotation-references-list', @@ -15,7 +15,7 @@ export class AnnotationReferencesListComponent { readonly isSelected$: Observable; constructor( - private readonly _listingService: ListingService, + private readonly _listingService: AnnotationsListingService, readonly annotationReferencesService: AnnotationReferencesService, ) { this.isSelected$ = this.annotationReferencesService.annotation$.pipe( diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts index 90242a747..78092c2dc 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnChanges, TemplateRef } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; -import { ListingService } from '@iqser/common-ui'; import { switchMap, tap } from 'rxjs/operators'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { MultiSelectService } from '../../services/multi-select.service'; +import { AnnotationsListingService } from '../../services/annotations-listing.service'; @Component({ selector: 'redaction-annotation-wrapper [annotation] [annotationActionsTemplate]', @@ -23,7 +23,7 @@ export class AnnotationWrapperComponent implements OnChanges { constructor( private readonly _changeRef: ChangeDetectorRef, - readonly listingService: ListingService, + readonly listingService: AnnotationsListingService, readonly multiSelectService: MultiSelectService, ) { this.isSelected$ = this._annotationChanged$.pipe( diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.html b/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.scss b/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.scss deleted file mode 100644 index 2d79be4aa..000000000 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.scss +++ /dev/null @@ -1,11 +0,0 @@ -.searching { - position: absolute; - top: 9px; - right: 60px; - display: flex; - align-items: center; - - mat-spinner { - margin-left: 15px; - } -} diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts index 2cd235d1a..3222fc1af 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts @@ -13,6 +13,7 @@ import { dossiersServiceProvider } from '@services/entity-services/dossiers.serv import { FileDataService } from './services/file-data.service'; import { AnnotationsListingService } from './services/annotations-listing.service'; import { StampService } from './services/stamp.service'; +import { PdfProxyService } from './services/pdf-proxy.service'; export const filePreviewScreenProviders = [ FilterService, @@ -33,4 +34,5 @@ export const filePreviewScreenProviders = [ { provide: ListingService, useExisting: AnnotationsListingService }, SearchService, StampService, + PdfProxyService, ]; diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html index cd1e04a5a..d4a6ac6af 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html @@ -62,15 +62,7 @@
-
- -
+
diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index cfe5cbb13..aa375310d 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -3,20 +3,20 @@ import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from import { Core } from '@pdftron/webviewer'; import { AutoUnsubscribe, + bool, CircleButtonTypes, CustomError, Debounce, ErrorService, FilterService, - ListingService, LoadingService, + log, NestedFilter, OnAttach, OnDetach, processFilters, ScrollableParentView, ScrollableParentViews, - shareDistinctLast, } from '@iqser/common-ui'; import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; @@ -25,7 +25,7 @@ import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.se import { AnnotationProcessingService } from './services/annotation-processing.service'; import { File, ViewMode, ViewModes } from '@red/domain'; import { PermissionsService } from '@services/permissions.service'; -import { combineLatest, firstValueFrom, Observable, of, pairwise } from 'rxjs'; +import { combineLatest, firstValueFrom, from, of, pairwise } from 'rxjs'; import { UserPreferenceService } from '@services/user-preference.service'; import { download, handleFilterDelta } from '../../utils'; import { FilesService } from '@services/files/files.service'; @@ -46,7 +46,7 @@ import { PageRotationService } from '../pdf-viewer/services/page-rotation.servic import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { FilePreviewDialogService } from './services/file-preview-dialog.service'; import { FileDataService } from './services/file-data.service'; -import { ActionsHelpModeKeys, ALL_HOTKEYS } from './utils/constants'; +import { ActionsHelpModeKeys, ALL_HOTKEYS, TextPopups } from './utils/constants'; import { NGXLogger } from 'ngx-logger'; import { StampService } from './services/stamp.service'; import { PdfViewer } from '../pdf-viewer/services/pdf-viewer.service'; @@ -55,8 +55,12 @@ import { ViewerHeaderService } from '../pdf-viewer/services/viewer-header.servic import { ROTATION_ACTION_BUTTONS } from '../pdf-viewer/utils/constants'; import { SkippedService } from './services/skipped.service'; import { REDDocumentViewer } from '../pdf-viewer/services/document-viewer.service'; +import { AnnotationsListingService } from './services/annotations-listing.service'; +import { PdfProxyService } from './services/pdf-proxy.service'; import Annotation = Core.Annotations.Annotation; +const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]; + @Component({ templateUrl: './file-preview-screen.component.html', styleUrls: ['./file-preview-screen.component.scss'], @@ -67,7 +71,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni dialogRef: MatDialogRef; fullScreen = 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))); @@ -82,7 +85,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni readonly pdf: PdfViewer, readonly documentInfoService: DocumentInfoService, readonly state: FilePreviewStateService, - readonly listingService: ListingService, + readonly listingService: AnnotationsListingService, readonly permissionsService: PermissionsService, readonly multiSelectService: MultiSelectService, readonly excludedPagesService: ExcludedPagesService, @@ -109,11 +112,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private readonly _annotationDrawService: AnnotationDrawService, private readonly _annotationProcessingService: AnnotationProcessingService, private readonly _stampService: StampService, + readonly pdfProxyService: PdfProxyService, private readonly _injector: Injector, ) { super(); - this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$; - document.documentElement.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement) { this.fullScreen = false; @@ -129,15 +131,23 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return ScrollableParentViews.ANNOTATIONS_LIST; } - private get _canPerformAnnotationActions$() { - const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect())); + get #textSelected$() { + const textSelected$ = combineLatest([ + this._documentViewer.textSelected$, + this.pdfProxyService.canPerformAnnotationActions$, + this.state.file$, + ]); - return combineLatest([this.state.file$, this.state.dossier$, viewMode$, this.pdf.compareMode$]).pipe( - map( - ([file, dossier, viewMode]) => - this.permissionsService.canPerformAnnotationActions(file, dossier) && viewMode === 'STANDARD', - ), - shareDistinctLast(), + return textSelected$.pipe( + tap(([selectedText, canPerformActions, file]) => { + const isCurrentPageExcluded = file.isPageExcluded(this.pdf.currentPage); + + if (selectedText.length > 2 && canPerformActions && !isCurrentPageExcluded) { + this.pdf.enable(textActions); + } else { + this.pdf.disable(textActions); + } + }), ); } @@ -149,8 +159,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni async updateViewMode(): Promise { this._logger.info(`[PDF] Update ${this._viewModeService.viewMode} view mode`); - const annotations = this._annotationManager.get(a => Boolean(a.getCustomData('redact-manager'))); - const redactions = annotations.filter(a => a.getCustomData('redaction')); + const annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager'))); + const redactions = annotations.filter(a => bool(a.getCustomData('redaction'))); switch (this._viewModeService.viewMode) { case 'STANDARD': { @@ -158,17 +168,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni const wrappers = await this._fileDataService.annotations; const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id); const standardEntries = annotations - .filter(a => a.getCustomData('changeLogRemoved') === 'false') + .filter(a => !bool(a.getCustomData('changeLogRemoved'))) .filter(a => !ocrAnnotationIds.includes(a.Id)); - const nonStandardEntries = annotations.filter(a => a.getCustomData('changeLogRemoved') === 'true'); + const nonStandardEntries = annotations.filter(a => bool(a.getCustomData('changeLogRemoved'))); this._setAnnotationsOpacity(standardEntries, true); this._annotationManager.show(standardEntries); this._annotationManager.hide(nonStandardEntries); break; } case 'DELTA': { - const changeLogEntries = annotations.filter(a => a.getCustomData('changeLog') === 'true'); - const nonChangeLogEntries = annotations.filter(a => a.getCustomData('changeLog') === 'false'); + const changeLogEntries = annotations.filter(a => bool(a.getCustomData('changeLog'))); + const nonChangeLogEntries = annotations.filter(a => !bool(a.getCustomData('changeLog'))); this._setAnnotationsColor(redactions, 'annotationColor'); this._setAnnotationsOpacity(changeLogEntries, true); this._annotationManager.show(changeLogEntries); @@ -176,7 +186,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni break; } case 'REDACTED': { - const nonRedactionEntries = annotations.filter(a => a.getCustomData('redaction') === 'false'); + const nonRedactionEntries = annotations.filter(a => !bool(a.getCustomData('redaction'))); this._setAnnotationsOpacity(redactions); this._setAnnotationsColor(redactions, 'redactionColor'); this._annotationManager.show(redactions); @@ -197,7 +207,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } ngOnDetach(): void { - this._pageRotationService.clearRotations(); this._documentViewer.close(); super.ngOnDetach(); this._changeDetectorRef.markForCheck(); @@ -209,7 +218,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } this._viewModeService.switchToStandard(); - this.state.reloadBlob(); await this.ngOnInit(); await this._fileDataService.loadRedactionLog(); @@ -234,12 +242,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni const reanalyzeFiles = reanalysisService.reanalyzeFilesForDossier([file], this.dossierId, { force: true }); await firstValueFrom(reanalyzeFiles); } - } - - handleAnnotationSelected(annotationIds: string[]) { - console.log(annotationIds); - this.listingService.setSelected(annotationIds.map(id => this._fileDataService.find(id)).filter(ann => ann !== undefined)); - this._changeDetectorRef.markForCheck(); + this.pdfProxyService.loadViewer(); } selectPage(pageNumber: number) { @@ -355,6 +358,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni loadAnnotations() { const documentLoaded$ = this._documentViewer.loaded$.pipe( + tap(() => { + this._pageRotationService.clearRotations(); + this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS); + }), filter(s => s), tap(() => this.viewerReady()), ); @@ -479,12 +486,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni }); } - #deactivateMultiSelect() { - this.multiSelectService.deactivate(); - this._annotationManager.deselect(); - this.handleAnnotationSelected([]); - } - private _setExcludedPageStyles() { const file = this._filesMapService.get(this.dossierId, this.fileId); setTimeout(() => { @@ -532,6 +533,24 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this.addActiveScreenSubscription = this._documentViewer.keyUp$.subscribe($event => { this.handleKeyEvent($event); }); + + this.addActiveScreenSubscription = this.#textSelected$.subscribe(); + + this.addActiveScreenSubscription = this.state.blob$ + .pipe( + log('Reload blob'), + switchMap(blob => from(this._documentViewer.lock()).pipe(map(() => blob))), + tap(() => this._errorService.clear()), + tap(blob => this.pdf.loadDocument(blob, this.state.file)), + ) + .subscribe(); + + this.addActiveScreenSubscription = this.pdfProxyService.manualAnnotationRequested$.subscribe($event => { + this.openManualAnnotationDialog($event); + }); + + this.addActiveScreenSubscription = this.pdfProxyService.pageChanged$.subscribe(page => this.viewerPageChanged(page)); + this.addActiveScreenSubscription = this.pdfProxyService.annotationSelected$.subscribe(); } private _handleDeletedDossier(): void { diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts index be146b0bc..0098d12b8 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts @@ -11,7 +11,6 @@ import { AnnotationDetailsComponent } from './components/annotation-details/anno import { AnnotationsListComponent } from './components/annotations-list/annotations-list.component'; import { PageIndicatorComponent } from './components/page-indicator/page-indicator.component'; import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component'; -import { PdfPaginatorComponent } from './components/pdf-paginator/pdf-paginator.component'; import { AnnotationActionsComponent } from './components/annotation-actions/annotation-actions.component'; import { CommentsComponent } from './components/comments/comments.component'; import { DocumentInfoComponent } from './components/document-info/document-info.component'; @@ -70,7 +69,6 @@ const components = [ AnnotationsListComponent, PageIndicatorComponent, PageExclusionComponent, - PdfPaginatorComponent, AnnotationActionsComponent, CommentsComponent, DocumentInfoComponent, 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 933bd9438..3116e084d 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 @@ -4,7 +4,7 @@ import { ManualRedactionService } from './manual-redaction.service'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { Observable } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; -import { getFirstRelevantTextPart, translateQuads } from '../../../utils'; +import { getFirstRelevantTextPart } from '../../../utils'; import { AnnotationPermissions } from '@models/file/annotation.permissions'; import { BASE_HREF } from '../../../tokens'; import { UserService } from '@services/user.service'; @@ -13,6 +13,7 @@ import { DictionaryEntryTypes, Dossier, IAddRedactionRequest, + IHeaderElement, ILegalBasisChangeRequest, IRecategorizationRequest, IRectangle, @@ -27,7 +28,7 @@ import { AcceptRecommendationDialogComponent, AcceptRecommendationReturnType, } from '../dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component'; -import { defaultDialogConfig, List, ListingService } from '@iqser/common-ui'; +import { defaultDialogConfig, List } from '@iqser/common-ui'; import { filter } from 'rxjs/operators'; import { MatDialog } from '@angular/material/dialog'; import { FilePreviewStateService } from './file-preview-state.service'; @@ -38,7 +39,7 @@ import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service'; import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service'; import { SkippedService } from './skipped.service'; import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service'; -import Quad = Core.Math.Quad; +import { AnnotationsListingService } from './annotations-listing.service'; @Injectable() export class AnnotationActionsService { @@ -60,7 +61,7 @@ export class AnnotationActionsService { private readonly _state: FilePreviewStateService, private readonly _fileDataService: FileDataService, private readonly _skippedService: SkippedService, - private readonly _listingService: ListingService, + private readonly _listingService: AnnotationsListingService, ) {} private get _dossier(): Dossier { @@ -197,11 +198,10 @@ export class AnnotationActionsService { }); } - getViewerAvailableActions(): Record[] { + getViewerAvailableActions(annotations: AnnotationWrapper[]): IHeaderElement[] { const dossier = this._state.dossier; - const annotations = this._listingService.selected; - const availableActions = []; + const availableActions: IHeaderElement[] = []; const annotationPermissions = annotations.map(annotation => ({ annotation, permissions: AnnotationPermissions.forUser( @@ -549,7 +549,7 @@ export class AnnotationActionsService { const rect = toPosition( viewerAnnotation.getPageNumber(), this._documentViewer.getHeight(viewerAnnotation.getPageNumber()), - this._translateQuads(viewerAnnotation.getPageNumber(), quad), + this._pdf.translateQuad(viewerAnnotation.getPageNumber(), quad), ); rectangles.push(rect); @@ -586,11 +586,6 @@ export class AnnotationActionsService { } } - private _translateQuads(page: number, quad: Quad): Quad { - const rotation = this._pdf.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(); await txt.begin(page, rect); // Read the page. diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.ts b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts similarity index 55% rename from apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.ts rename to apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts index bd6990a67..94e8eba33 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-paginator/pdf-paginator.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts @@ -1,56 +1,48 @@ -import { Component, EventEmitter, Inject, Input, NgZone, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { Dossier, IHeaderElement, IManualRedactionEntry } from '@red/domain'; -import { Core, WebViewerInstance } from '@pdftron/webviewer'; +import { ChangeDetectorRef, Inject, Injectable, NgZone } from '@angular/core'; +import { IHeaderElement, IManualRedactionEntry } from '@red/domain'; +import { Core } from '@pdftron/webviewer'; import { TranslateService } from '@ngx-translate/core'; import { ManualRedactionEntryType, ManualRedactionEntryTypes, ManualRedactionEntryWrapper, -} from '@models/file/manual-redaction-entry.wrapper'; -import { AnnotationWrapper } from '@models/file/annotation.wrapper'; -import { AnnotationDrawService } from '../../../pdf-viewer/services/annotation-draw.service'; -import { AnnotationActionsService } from '../../services/annotation-actions.service'; -import { UserPreferenceService } from '@services/user-preference.service'; -import { BASE_HREF_FN, BaseHrefFn } from '../../../../tokens'; -import { AutoUnsubscribe, ErrorService, log } from '@iqser/common-ui'; -import { toPosition } from '../../utils/pdf-calculation.utils'; -import { MultiSelectService } from '../../services/multi-select.service'; -import { FilePreviewStateService } from '../../services/file-preview-state.service'; -import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; -import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service'; -import { HeaderElements, TextPopups } from '../../utils/constants'; -import { from } from 'rxjs'; -import { FileDataService } from '../../services/file-data.service'; -import { ViewerHeaderService } from '../../../pdf-viewer/services/viewer-header.service'; -import { ManualRedactionService } from '../../services/manual-redaction.service'; -import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service'; -import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service'; -import { translateQuads } from '../../../../utils'; -import { - ALLOWED_ACTIONS_WHEN_PAGE_EXCLUDED, - HEADER_ITEMS_TO_TOGGLE, - ROTATION_ACTION_BUTTONS, - TEXT_POPUPS_TO_TOGGLE, -} from '../../../pdf-viewer/utils/constants'; -import { REDDocumentViewer } from '../../../pdf-viewer/services/document-viewer.service'; +} from '../../../models/file/manual-redaction-entry.wrapper'; +import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; +import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service'; +import { AnnotationActionsService } from './annotation-actions.service'; +import { UserPreferenceService } from '../../../services/user-preference.service'; +import { BASE_HREF_FN, BaseHrefFn } from '../../../tokens'; +import { shareDistinctLast } from '@iqser/common-ui'; +import { toPosition } from '../utils/pdf-calculation.utils'; +import { MultiSelectService } from './multi-select.service'; +import { FilePreviewStateService } from './file-preview-state.service'; +import { map, tap } from 'rxjs/operators'; +import { HeaderElements, TextPopups } from '../utils/constants'; +import { FileDataService } from './file-data.service'; +import { ViewerHeaderService } from '../../pdf-viewer/services/viewer-header.service'; +import { ManualRedactionService } from './manual-redaction.service'; +import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service'; +import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service'; +import { ALLOWED_ACTIONS_WHEN_PAGE_EXCLUDED, HEADER_ITEMS_TO_TOGGLE, TEXT_POPUPS_TO_TOGGLE } from '../../pdf-viewer/utils/constants'; +import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service'; +import { combineLatest, Observable, Subject } from 'rxjs'; +import { ViewModeService } from './view-mode.service'; +import { PermissionsService } from '../../../services/permissions.service'; +import { AnnotationsListingService } from './annotations-listing.service'; import Annotation = Core.Annotations.Annotation; +import Quad = Core.Math.Quad; -@Component({ - selector: 'redaction-pdf-viewer', - templateUrl: './pdf-paginator.component.html', - styleUrls: ['./pdf-paginator.component.scss'], -}) -export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, OnChanges { - @Input() dossier: Dossier; - @Input() canPerformActions = false; - @Output() readonly annotationSelected = this.#annotationSelected$; - @Output() readonly manualAnnotationRequested = new EventEmitter(); - @Output() readonly pageChanged = this.pdf.pageChanged$.pipe(tap(() => this._handleCustomActions())); - instance: WebViewerInstance; - private _selectedText = ''; +@Injectable() +export class PdfProxyService { + readonly annotationSelected$ = this.#annotationSelected$; + readonly manualAnnotationRequested$ = new Subject(); + readonly pageChanged$ = this._pdf.pageChanged$.pipe(tap(() => this._handleCustomActions())); + canPerformActions = true; + + instance = this._pdf.instance; + canPerformAnnotationActions$: Observable; readonly #visibilityOffIcon = this._convertPath('/assets/icons/general/visibility-off.svg'); readonly #visibilityIcon = this._convertPath('/assets/icons/general/visibility.svg'); - readonly #searchIcon = this._convertPath('/assets/icons/general/pdftron-action-search.svg'); readonly #falsePositiveIcon = this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'); readonly #addRedactionIcon = this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'); readonly #addDictIcon = this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'); @@ -63,45 +55,56 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On private readonly _userPreferenceService: UserPreferenceService, private readonly _annotationDrawService: AnnotationDrawService, private readonly _annotationActionsService: AnnotationActionsService, - private readonly _pageRotationService: PageRotationService, private readonly _fileDataService: FileDataService, private readonly _viewerHeaderService: ViewerHeaderService, - private readonly _errorService: ErrorService, + private readonly _viewModeService: ViewModeService, + private readonly _permissionsService: PermissionsService, private readonly _documentViewer: REDDocumentViewer, private readonly _annotationManager: REDAnnotationManager, - readonly pdf: PdfViewer, + private readonly _pdf: PdfViewer, private readonly _state: FilePreviewStateService, private readonly _multiSelectService: MultiSelectService, + private readonly _listingService: AnnotationsListingService, + private readonly _changeDetectorRef: ChangeDetectorRef, ) { - super(); + this.canPerformAnnotationActions$ = this.#canPerformAnnotationActions$; + } + + get #canPerformAnnotationActions$() { + const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect())); + + return combineLatest([this._state.file$, this._state.dossier$, viewMode$, this._pdf.compareMode$]).pipe( + map( + ([file, dossier, viewMode]) => + this._permissionsService.canPerformAnnotationActions(file, dossier) && viewMode === 'STANDARD', + ), + tap(canPerformActions => (this.canPerformActions = canPerformActions)), + tap(() => this._handleCustomActions()), + shareDistinctLast(), + ); } get #annotationSelected$() { - return this._annotationManager.annotationSelected$.pipe(map(value => this.#processSelectedAnnotations(...value))); + return this._annotationManager.annotationSelected$.pipe( + map(value => this.#processSelectedAnnotations(...value)), + tap(annotations => this.handleAnnotationSelected(annotations)), + ); } - ngOnInit() { - this._loadViewer(); - - this.addActiveScreenSubscription = this._state.blob$ - .pipe( - log('Reload blob'), - switchMap(blob => from(this._documentViewer.lock()).pipe(map(() => blob))), - withLatestFrom(this._state.file$), - tap(() => this._errorService.clear()), - tap(([blob, file]) => this.pdf.loadDocument(blob, file)), - tap(() => { - this._pageRotationService.clearRotations(); - this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS); - }), - ) - .subscribe(); + handleAnnotationSelected(annotationIds: string[]) { + this._listingService.setSelected(annotationIds.map(id => this._fileDataService.find(id)).filter(ann => ann !== undefined)); + this._changeDetectorRef.markForCheck(); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.canPerformActions && !!this.instance) { - this._handleCustomActions(); - } + loadViewer() { + this._configureElements(); + this._configureTextPopup(); + } + + #deactivateMultiSelect() { + this._multiSelectService.deactivate(); + this._annotationManager.deselect(); + this.handleAnnotationSelected([]); } #processSelectedAnnotations(annotations: Annotation[], action) { @@ -110,7 +113,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On if (action === 'deselected') { // Remove deselected annotations from selected list nextAnnotations = this._annotationManager.selected.filter(ann => !annotations.some(a => a.Id === ann.Id)); - this.pdf.disable(TextPopups.ADD_RECTANGLE); + this._pdf.disable(TextPopups.ADD_RECTANGLE); return nextAnnotations.map(ann => ann.Id); } else if (!this._multiSelectService.isEnabled) { // Only choose the last selected annotation, to bypass viewer multi select @@ -123,46 +126,18 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On } this.#configureAnnotationSpecificActions(annotations); - this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly); + + if (!(annotations.length === 1 && annotations[0].ReadOnly)) { + this._pdf.enable(TextPopups.ADD_RECTANGLE); + } else { + this._pdf.disable(TextPopups.ADD_RECTANGLE); + } + return nextAnnotations.map(ann => ann.Id); } - private _loadViewer() { - this.instance = this.pdf.instance; - - this._configureElements(); - this._configureTextPopup(); - - this.pdf.documentViewer.addEventListener('textSelected', (_, selectedText, pageNumber: number) => { - this._selectedText = selectedText; - - if (this.pdf.isCompare && pageNumber % 2 === 0) { - this.pdf.disable('textPopup'); - } else { - this.pdf.enable('textPopup'); - } - - const isCurrentPageExcluded = this._state.file.isPageExcluded(this.pdf.currentPage); - const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]; - - if (selectedText.length > 2 && this.canPerformActions && !isCurrentPageExcluded) { - this.pdf.enable(textActions); - } else { - this.pdf.disable(textActions); - } - }); - } - - private _toggleRectangleAnnotationAction(disable = false) { - if (!disable) { - this.pdf.enable(TextPopups.ADD_RECTANGLE); - } else { - this.pdf.disable(TextPopups.ADD_RECTANGLE); - } - } - private _configureElements() { - const dossierTemplateId = this.dossier.dossierTemplateId; + const dossierTemplateId = this._state.dossierTemplateId; const color = this._annotationDrawService.getAndConvertColor(dossierTemplateId, dossierTemplateId, 'manual'); this._documentViewer.setRectangleToolStyles(color); } @@ -209,12 +184,12 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On ]); } - const actions = this._annotationActionsService.getViewerAvailableActions(); + const actions = this._annotationActionsService.getViewerAvailableActions(annotationWrappers); this.instance.UI.annotationPopup.add(actions); } private _configureRectangleAnnotationPopup(annotation: Annotation) { - if (!this.pdf.isCompare || annotation.getPageNumber() % 2 === 1) { + if (!this._pdf.isCompare || annotation.getPageNumber() % 2 === 1) { this.instance.UI.annotationPopup.add([ { type: 'actionButton', @@ -234,7 +209,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads }); this._cleanUpSelectionAndButtonState(); - this.manualAnnotationRequested.emit({ manualRedactionEntry, type: 'REDACTION' }); + this.manualAnnotationRequested$.next({ manualRedactionEntry, type: 'REDACTION' }); } private _cleanUpSelectionAndButtonState() { @@ -243,25 +218,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On } private _configureTextPopup() { - const searchButton = { - type: 'actionButton', - img: this.#searchIcon, - title: this._translateService.instant('pdf-viewer.text-popup.actions.search'), - onClick: () => { - const text = this.pdf.documentViewer.getSelectedText(); - const searchOptions = { - caseSensitive: true, // match case - wholeWord: true, // match whole words only - wildcard: false, // allow using '*' as a wildcard value - regex: false, // string is treated as a regular expression - searchUp: false, // search from the end of the document upwards - ambientString: true, // return ambient string as part of the result - }; - this.instance.UI.openElements(['searchPanel']); - setTimeout(() => this.instance.UI.searchTextFull(text, searchOptions), 250); - }, - }; - const popups: IHeaderElement[] = [searchButton]; + const popups: IHeaderElement[] = []; // Adding directly to the false-positive dict is only available in dev-mode if (this._userPreferenceService.areDevFeaturesEnabled) { @@ -288,38 +245,38 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY), onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY), }); - console.log(popups); - this.instance.UI.textPopup.add(popups); + console.log('configure popup'); + this._pdf.configureTextPopups(popups); return this._handleCustomActions(); } #getTitle(type: ManualRedactionEntryType) { - return this._translateService.instant(this._manualRedactionService.getTitle(type, this.dossier)); + return this._translateService.instant(this._manualRedactionService.getTitle(type, this._state.dossier)); } private _addManualRedactionOfType(type: ManualRedactionEntryType) { - const selectedQuads: Readonly> = this.pdf.documentViewer.getSelectedTextQuads(); - const text = this.pdf.documentViewer.getSelectedText(); + const selectedQuads: Record = this._pdf.documentViewer.getSelectedTextQuads(); + const text = this._pdf.documentViewer.getSelectedText(); const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true); - this.manualAnnotationRequested.emit({ manualRedactionEntry, type }); + this.manualAnnotationRequested$.next({ manualRedactionEntry, type }); } private _handleCustomActions() { - const isCurrentPageExcluded = this._state.file.isPageExcluded(this.pdf.currentPage); + const isCurrentPageExcluded = this._state.file.isPageExcluded(this._pdf.currentPage); if (this.canPerformActions && !isCurrentPageExcluded) { try { - this.pdf.instance.UI.enableTools(['AnnotationCreateRectangle']); + this._pdf.instance.UI.enableTools(['AnnotationCreateRectangle']); } catch (e) { // happens } - this.pdf.enable(TEXT_POPUPS_TO_TOGGLE); + this._pdf.enable(TEXT_POPUPS_TO_TOGGLE); this._viewerHeaderService.enable(HEADER_ITEMS_TO_TOGGLE); - if (this._selectedText.length > 2) { - this.pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]); + if (this._documentViewer.selectedText.length > 2) { + this._pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]); } return; @@ -334,25 +291,21 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On ); headerElementsToDisable = headerElementsToDisable.filter(element => !ALLOWED_ACTIONS_WHEN_PAGE_EXCLUDED.includes(element)); } else { - this.pdf.instance.UI.disableTools(['AnnotationCreateRectangle']); + this._pdf.instance.UI.disableTools(['AnnotationCreateRectangle']); } - this.pdf.disable(textPopupElementsToDisable); + this._pdf.disable(textPopupElementsToDisable); this._viewerHeaderService.disable(headerElementsToDisable); } - private _getManualRedaction( - quads: Readonly>, - text?: string, - convertQuads = false, - ): IManualRedactionEntry { + private _getManualRedaction(quads: Record, text?: string, convertQuads = false): IManualRedactionEntry { const entry: IManualRedactionEntry = { positions: [] }; for (const key of Object.keys(quads)) { for (const quad of quads[key]) { const page = parseInt(key, 10); - const pageHeight = this.pdf.documentViewer.getPageHeight(page); - entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.#translateQuad(page, quad) : quad)); + const pageHeight = this._documentViewer.getHeight(page); + entry.positions.push(toPosition(page, pageHeight, convertQuads ? this._pdf.translateQuad(page, quad) : quad)); } } @@ -360,9 +313,4 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On entry.rectangle = !text; return entry; } - - #translateQuad(page: number, quad: Core.Math.Quad) { - const rotation = this.pdf.documentViewer.getCompleteRotation(page); - return translateQuads(page, rotation, quad); - } } 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 306f11953..fa6e81fde 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 @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { skip, tap } from 'rxjs/operators'; -import { shareDistinctLast } from '@iqser/common-ui'; +import { bool, shareDistinctLast } from '@iqser/common-ui'; import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service'; @Injectable() @@ -28,7 +28,7 @@ export class SkippedService { } private _handleIgnoreAnnotationsDrawing(hideSkipped: boolean): void { - const ignored = this._annotationManager.get(a => Boolean(a.getCustomData('skipped'))); + const ignored = this._annotationManager.get(a => bool(a.getCustomData('skipped'))); if (hideSkipped) { this._annotationManager.hide(ignored); } else { diff --git a/apps/red-ui/src/app/modules/file-preview/utils/pdf-calculation.utils.ts b/apps/red-ui/src/app/modules/file-preview/utils/pdf-calculation.utils.ts index 7d53d0a12..ac5a63395 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/pdf-calculation.utils.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/pdf-calculation.utils.ts @@ -1,18 +1,16 @@ import { IRectangle } from '@red/domain'; +import { Core } from '@pdftron/webviewer'; +import Quad = Core.Math.Quad; -export const toPosition = ( - page: number, - pageHeight: number, - selectedQuad: { x1: number; x2: number; x3: number; x4: number; y4: number; y2: number }, -): IRectangle => { - const height = selectedQuad.y2 - selectedQuad.y4; +export const toPosition = (page: number, pageHeight: number, { x1, x2, x3, x4, y2, y4 }: Quad): IRectangle => { + const height = y2 - y4; return { - page: page, + page, topLeft: { - x: Math.min(selectedQuad.x3, selectedQuad.x4, selectedQuad.x2, selectedQuad.x1), - y: pageHeight - (selectedQuad.y4 + height), + x: Math.min(x3, x4, x2, x1), + y: pageHeight - (y4 + height), }, - height: height, - width: Math.max(4, Math.abs(selectedQuad.x3 - selectedQuad.x4), Math.abs(selectedQuad.x3 - selectedQuad.x1)), + height, + width: Math.max(4, Math.abs(x3 - x4), Math.abs(x3 - x1)), }; }; diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts index 8b2cf370e..ed326bd39 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts @@ -47,9 +47,7 @@ export class REDAnnotationManager { get(annotation: AnnotationWrapper | string): Annotation; get(annotations: List | List): Annotation[]; - get(predicate?: (value: Annotation) => boolean): Annotation[]; - get(argument?: AnnotationPredicate | List | List | AnnotationWrapper | string): Annotation | Annotation[] { if (isStringOrWrapper(argument)) { return this.#getById(argument); diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts index 960080ccf..590def5d0 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts @@ -10,12 +10,16 @@ import { log, shareLast } from '@iqser/common-ui'; import { stopAndPrevent, stopAndPreventIfNotAllowed } from '../utils/functions'; import DocumentViewer = Core.DocumentViewer; import Color = Core.Annotations.Color; +import Quad = Core.Math.Quad; @Injectable() export class REDDocumentViewer { loaded$: Observable; pageComplete$: Observable; keyUp$: Observable; + textSelected$: Observable; + + selectedText = ''; #document: DocumentViewer; @@ -67,6 +71,24 @@ export class REDDocumentViewer { return fromEvent(this.#document, 'pageComplete').pipe(debounceTime(300)); } + get #textSelected$(): Observable { + return fromEvent<[Quad, string, number]>(this.#document, 'textSelected').pipe( + tap(([, selectedText]) => (this.selectedText = selectedText)), + tap(([, , pageNumber]) => { + if (this._pdf.isCompare && pageNumber % 2 === 0) { + this._pdf.disable('textPopup'); + } else { + this._pdf.enable('textPopup'); + } + }), + map(([, selectedText]) => selectedText), + ); + } + + get #loaded$() { + return merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast()); + } + close() { this._logger.info('[PDF] Closing document'); this.#document.closeDocument(); @@ -80,9 +102,10 @@ export class REDDocumentViewer { init(document: DocumentViewer) { this.#document = document; - this.loaded$ = merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast()); + this.loaded$ = this.#loaded$; this.pageComplete$ = this.#pageComplete$.pipe(shareLast()); this.keyUp$ = this.#keyUp$; + this.textSelected$ = this.#textSelected$; } async lock() { diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts index abac8ee04..dc6f7d7fd 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts @@ -1,23 +1,25 @@ -import { Injectable, Injector } from '@angular/core'; +import { Inject, Injectable, Injector } from '@angular/core'; import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer'; import { environment } from '../../../../environments/environment'; -import { BASE_HREF_FN } from '../../../tokens'; -import { File } from '@red/domain'; +import { BASE_HREF_FN, BaseHrefFn } from '../../../tokens'; +import { File, IHeaderElement } from '@red/domain'; import { ErrorService, shareDistinctLast, shareLast } from '@iqser/common-ui'; import { ActivatedRoute } from '@angular/router'; import { distinctUntilChanged, map, startWith, tap } from 'rxjs/operators'; import { BehaviorSubject, combineLatest, fromEvent, Observable } from 'rxjs'; import { ConfigService } from '../../../services/config.service'; import { NGXLogger } from 'ngx-logger'; -import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from '../utils/constants'; +import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, SEARCH_OPTIONS, USELESS_ELEMENTS } from '../utils/constants'; import { Rgb } from '../utils/types'; import { asList } from '../utils/functions'; import { REDAnnotationManager } from './annotation-manager.service'; +import { TranslateService } from '@ngx-translate/core'; import AnnotationManager = Core.AnnotationManager; import TextTool = Core.Tools.TextTool; import Annotation = Core.Annotations.Annotation; import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation; import DocumentViewer = Core.DocumentViewer; +import Quad = Core.Math.Quad; @Injectable() export class PdfViewer { @@ -42,12 +44,23 @@ export class PdfViewer { #instance: WebViewerInstance; #currentBlob: Blob; readonly #compareMode$ = new BehaviorSubject(false); + readonly #searchButton: IHeaderElement = { + type: 'actionButton', + img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'), + title: this._translateService.instant('pdf-viewer.text-popup.actions.search'), + onClick: () => { + this.#instance.UI.openElements(['searchPanel']); + setTimeout(() => this.#searchForSelectedText(), 250); + }, + }; constructor( private readonly _logger: NGXLogger, - private readonly _activatedRoute: ActivatedRoute, - private readonly _annotationManager: REDAnnotationManager, private readonly _injector: Injector, + private readonly _activatedRoute: ActivatedRoute, + private readonly _translateService: TranslateService, + private readonly _annotationManager: REDAnnotationManager, + @Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn, ) {} get instance() { @@ -178,6 +191,21 @@ export class PdfViewer { return new this.#instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4); } + translateQuad(page: number, quad: Quad): Quad { + const rotationsEnum = this.#instance.Core.PageRotation; + switch (this.documentViewer.getCompleteRotation(page)) { + case rotationsEnum.E_90: + return this.quad(quad.x2, quad.y2, quad.x3, quad.y3, quad.x4, quad.y4, quad.x1, quad.y1); + case rotationsEnum.E_180: + return this.quad(quad.x3, quad.y3, quad.x4, quad.y4, quad.x1, quad.y1, quad.x2, quad.y2); + case rotationsEnum.E_270: + return this.quad(quad.x4, quad.y4, quad.x1, quad.y1, quad.x2, quad.y2, quad.x3, quad.y3); + case rotationsEnum.E_0: + default: + return quad; + } + } + color(rgb: Rgb) { return new this.#instance.Core.Annotations.Color(rgb.r, rgb.g, rgb.b); } @@ -194,6 +222,15 @@ export class PdfViewer { return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation; } + configureTextPopups(popups: IHeaderElement[]) { + this.#instance.UI.textPopup.update([]); + this.#instance.UI.textPopup.add([...popups, this.#searchButton]); + } + + #searchForSelectedText() { + this.#instance.UI.searchTextFull(this.documentViewer.getSelectedText(), SEARCH_OPTIONS); + } + #clearSearchResultsWhenVisibilityChanged() { const iframeWindow = this.#instance.UI.iframeWindow; iframeWindow.addEventListener('visibilityChanged', (event: any) => { @@ -232,12 +269,11 @@ export class PdfViewer { } #getInstance(htmlElement: HTMLElement) { - const convertPath = this._injector.get(BASE_HREF_FN); const options: WebViewerOptions = { licenseKey: environment.licenseKey ? window.atob(environment.licenseKey) : null, fullAPI: true, - path: convertPath('/assets/wv-resources'), - css: convertPath('/assets/pdftron/stylesheet.css'), + path: this._convertPath('/assets/wv-resources'), + css: this._convertPath('/assets/pdftron/stylesheet.css'), backendType: 'ems', }; diff --git a/apps/red-ui/src/app/modules/pdf-viewer/utils/constants.ts b/apps/red-ui/src/app/modules/pdf-viewer/utils/constants.ts index 02f3dcef9..8753b2f53 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/utils/constants.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/utils/constants.ts @@ -22,6 +22,15 @@ export const ALLOWED_KEYBOARD_SHORTCUTS: List = ['+', '-', 'p', 'r', 'Escape'] a export const DOCUMENT_LOADING_ERROR = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh'); +export const SEARCH_OPTIONS = { + caseSensitive: true, // match case + wholeWord: true, // match whole words only + wildcard: false, // allow using '*' as a wildcard value + regex: false, // string is treated as a regular expression + searchUp: false, // search from the end of the document upwards + ambientString: true, // return ambient string as part of the result +}; + export const USELESS_ELEMENTS = [ 'pageNavOverlay', 'menuButton', diff --git a/apps/red-ui/src/app/utils/index.ts b/apps/red-ui/src/app/utils/index.ts index bff85cbba..6c671d076 100644 --- a/apps/red-ui/src/app/utils/index.ts +++ b/apps/red-ui/src/app/utils/index.ts @@ -11,5 +11,4 @@ export * from './functions'; export * from './global-error-handler.service'; export * from './missing-translations-handler'; export * from './page-stamper'; -export * from './pdf-coordinates'; export * from './pruning-translation-loader'; diff --git a/apps/red-ui/src/app/utils/pdf-coordinates.ts b/apps/red-ui/src/app/utils/pdf-coordinates.ts deleted file mode 100644 index 96234551d..000000000 --- a/apps/red-ui/src/app/utils/pdf-coordinates.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Core } from '@pdftron/webviewer'; -import Quad = Core.Math.Quad; - -enum PageRotation { - E_0 = 0, - E_90 = 1, - E_180 = 2, - E_270 = 3, -} - -export function translateQuads(page: number, rotation: number, quad: Quad): Quad { - let result; - switch (rotation) { - case PageRotation.E_90: - result = { - x1: quad.x2, - x2: quad.x3, - x3: quad.x4, - x4: quad.x1, - y1: quad.y2, - y2: quad.y3, - y3: quad.y4, - y4: quad.y1, - }; - break; - case PageRotation.E_180: - result = { - x1: quad.x3, - x2: quad.x4, - x3: quad.x1, - x4: quad.x2, - y1: quad.y3, - y2: quad.y4, - y3: quad.y1, - y4: quad.y2, - }; - break; - case PageRotation.E_270: - result = { - x1: quad.x4, - x2: quad.x1, - x3: quad.x2, - x4: quad.x3, - y1: quad.y4, - y2: quad.y1, - y3: quad.y2, - y4: quad.y3, - }; - break; - case PageRotation.E_0: - default: - result = quad; - } - return result; -}