From 7ad5fe09ac26ef722cb9421cfe50bb3aa37125f6 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 18 May 2022 20:58:21 +0300 Subject: [PATCH] RED-3988: make pdf viewer reusable --- apps/red-ui/src/app/app.component.html | 5 + apps/red-ui/src/app/app.component.ts | 2 + apps/red-ui/src/app/app.module.ts | 18 ++- .../pdf-viewer/pdf-viewer.component.ts | 3 + .../reusable-pdf-viewer.component.ts | 27 ++++ .../reusable-pdf-viewer.service.ts | 131 ++++++++++++++++++ apps/red-ui/src/app/tokens.ts | 6 +- 7 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component.ts create mode 100644 apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service.ts diff --git a/apps/red-ui/src/app/app.component.html b/apps/red-ui/src/app/app.component.html index 20a8fecb2..f7795dfd6 100644 --- a/apps/red-ui/src/app/app.component.html +++ b/apps/red-ui/src/app/app.component.html @@ -1,4 +1,9 @@ + + + diff --git a/apps/red-ui/src/app/app.component.ts b/apps/red-ui/src/app/app.component.ts index 05dec74fc..d63edc783 100644 --- a/apps/red-ui/src/app/app.component.ts +++ b/apps/red-ui/src/app/app.component.ts @@ -1,6 +1,7 @@ import { Component, ViewContainerRef } from '@angular/core'; import { RouterHistoryService } from '@services/router-history.service'; import { UserService } from '@services/user.service'; +import { ReusablePdfViewer } from './modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; @Component({ selector: 'redaction-root', @@ -14,5 +15,6 @@ export class AppComponent { public viewContainerRef: ViewContainerRef, private readonly _routerHistoryService: RouterHistoryService, private readonly _userService: UserService, + readonly reusablePdfViewer: ReusablePdfViewer, ) {} } diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 40bb58e12..21308feca 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -20,7 +20,7 @@ import { AppRoutingModule } from './app-routing.module'; import { SharedModule } from '@shared/shared.module'; import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module'; import { DatePipe as BaseDatePipe, PlatformLocation } from '@angular/common'; -import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE, BASE_HREF } from './tokens'; +import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE, BASE_HREF, BASE_HREF_FN } from './tokens'; import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; import { GlobalErrorHandler } from '@utils/global-error-handler.service'; import { REDMissingTranslationHandler } from '@utils/missing-translations-handler'; @@ -52,6 +52,7 @@ import { LoggerModule, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_S import { LoggerRulesService } from '@services/logger-rules.service'; import { ILoggerConfig } from '@red/domain'; import { SystemPreferencesService } from '@services/system-preferences.service'; +import { ReusablePdfViewerComponent } from './modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component'; export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader { return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`); @@ -69,7 +70,15 @@ function cleanupBaseUrl(baseUrl: string) { const screens = [BaseScreenComponent, DownloadsListScreenComponent]; -const components = [AppComponent, AuthErrorComponent, NotificationsComponent, SpotlightSearchComponent, BreadcrumbsComponent, ...screens]; +const components = [ + AppComponent, + ReusablePdfViewerComponent, + AuthErrorComponent, + NotificationsComponent, + SpotlightSearchComponent, + BreadcrumbsComponent, + ...screens, +]; @NgModule({ declarations: [...components], @@ -149,6 +158,11 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp useFactory: (s: PlatformLocation) => cleanupBaseUrl(s.getBaseHrefFromDOM()), deps: [PlatformLocation], }, + { + provide: BASE_HREF_FN, + useFactory: (baseHref: string) => (path: string) => baseHref + path, + deps: [BASE_HREF], + }, { provide: HTTP_INTERCEPTORS, multi: true, diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts index 3f70aabed..2e0dbbd49 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts @@ -43,6 +43,7 @@ import { FileDataService } from '../../services/file-data.service'; import { ViewerHeaderConfigService } from '../../services/viewer-header-config.service'; import { TooltipsService } from '../../services/tooltips.service'; import { ManualRedactionService } from '../../services/manual-redaction.service'; +import { ReusablePdfViewer } from '../../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service'; import Tools = Core.Tools; import TextTool = Tools.TextTool; import Annotation = Core.Annotations.Annotation; @@ -84,6 +85,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha private readonly _headerConfigService: ViewerHeaderConfigService, private readonly _tooltipsService: TooltipsService, private readonly _errorService: ErrorService, + private readonly _reusablePdfViewer: ReusablePdfViewer, readonly stateService: FilePreviewStateService, readonly viewModeService: ViewModeService, readonly multiSelectService: MultiSelectService, @@ -102,6 +104,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha withLatestFrom(this.stateService.file$), tap(() => this._errorService.clear()), tap(([blob, file]) => this._loadDocument(blob, file)), + tap(([blob, file]) => this._reusablePdfViewer.loadDocument(blob, file)), ) .subscribe(); } 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 new file mode 100644 index 000000000..bc49c8de7 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component.ts @@ -0,0 +1,27 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { ReusablePdfViewer } from './reusable-pdf-viewer.service'; + +@Component({ + selector: 'redaction-reusable-pdf-viewer', + template: '
', + styles: [ + ` + div { + width: calc(100% - 350px); + height: calc(100% - 111px); + bottom: 0; + left: 0; + position: absolute; + } + `, + ], +}) +export class ReusablePdfViewerComponent implements OnInit { + @ViewChild('viewer', { static: true }) readonly viewer: ElementRef; + + constructor(readonly reusablePdfViewer: ReusablePdfViewer) {} + + ngOnInit() { + return this.reusablePdfViewer.init(this.viewer.nativeElement); + } +} 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 new file mode 100644 index 000000000..80223fffb --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service.ts @@ -0,0 +1,131 @@ +import { Inject, Injectable, Injector } from '@angular/core'; +import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer'; +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 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', +]; + +@Injectable({ + providedIn: 'root', +}) +export class ReusablePdfViewer { + readonly currentPage$ = this._injector.get(ActivatedRoute).queryParamMap.pipe( + map(params => Number(params.get('page') ?? '1')), + shareDistinctLast(), + ); + documentViewer: DocumentViewer; + annotationManager: AnnotationManager; + + show$: Observable; + documentLoaded$: Observable; + #instance: WebViewerInstance; + + constructor( + @Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn, + private readonly _loadingService: LoadingService, + private readonly _errorService: ErrorService, + private readonly _router: Router, + private readonly _configService: ConfigService, + 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 #show$() { + const routeChanged = this._router.events.pipe( + filter(event => event instanceof ActivationStart), + map(() => false), + ); + + return merge(routeChanged, this.documentLoaded$); + } + + async init(htmlElement: HTMLElement) { + this.#instance = await this.#getInstance(htmlElement); + console.log('[PDF] Initialized'); + + this.documentViewer = this.#instance.Core.documentViewer; + this.annotationManager = this.#instance.Core.annotationManager; + + this.documentLoaded$ = this.#documentLoaded$; + this.show$ = this.#show$; + this.#setSelectionMode(); + this.#configureElements(); + } + + loadDocument(blob: Blob, file: File) { + console.log('[PDF] Loading document', blob, file); + const onError = () => { + this._loadingService.stop(); + this._errorService.set(DocLoadingError); + // this.stateService.reloadBlob(); + }; + // const pdfNet = this._instance.Core.PDFNet; + // console.log('pdfnet', pdfNet); + // await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); + // console.log('pdfnet initialized'); + // const document = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer()); + // console.log('document initialized'); + // await document.flattenAnnotations(false); + // console.log(document); + this.#instance.UI.loadDocument(blob, { filename: file?.filename + '.pdf' ?? 'document.pdf', onError }); + } + + #configureElements() { + this.#instance.UI.disableElements(uselessElements); + } + + #setSelectionMode(): void { + const textTool = this.#instance.Core.Tools.TextTool as unknown as TextTool; + textTool.SELECTION_MODE = this._configService.values.SELECTION_MODE; + } + + #getInstance(htmlElement: HTMLElement) { + const options: WebViewerOptions = { + licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null, + fullAPI: true, + path: this._convertPath('/assets/wv-resources'), + css: this._convertPath('/assets/pdftron/stylesheet.css'), + backendType: 'ems', + }; + + return WebViewer(options, htmlElement); + } +} diff --git a/apps/red-ui/src/app/tokens.ts b/apps/red-ui/src/app/tokens.ts index ce8f6a595..d19e040af 100644 --- a/apps/red-ui/src/app/tokens.ts +++ b/apps/red-ui/src/app/tokens.ts @@ -1,7 +1,9 @@ import { InjectionToken } from '@angular/core'; -export const BASE_HREF: InjectionToken = new InjectionToken('BASE_HREF'); -export const DOSSIER_ID: InjectionToken = new InjectionToken('DOSSIER_ID'); +export const BASE_HREF = new InjectionToken('BASE_HREF'); +export type BaseHrefFn = (path: string) => string; +export const BASE_HREF_FN = new InjectionToken('Convert path function'); +export const DOSSIER_ID = new InjectionToken('DOSSIER_ID'); export const ACTIVE_DOSSIERS_SERVICE = new InjectionToken('Active dossiers service'); export const ARCHIVED_DOSSIERS_SERVICE = new InjectionToken('Archived dossiers service');