RED-3988: replace old pdf viewer

This commit is contained in:
Dan Percic 2022-05-19 16:35:50 +03:00
parent 7ad5fe09ac
commit edc53c7971
23 changed files with 431 additions and 454 deletions

View File

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

View File

@ -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<AnnotationWrapper>,
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) {

View File

@ -48,7 +48,7 @@
<div *ngIf="multiSelectService.active$ | async" class="multi-select">
<div class="selected-wrapper">
<iqser-round-checkbox
(click)="pdf.deselectAllAnnotations()"
(click)="reusablePdf.deselectAnnotations()"
[indeterminate]="listingService.areSomeSelected$ | async"
type="with-bg"
></iqser-round-checkbox>

View File

@ -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<AnnotationWrapper>,
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'])

View File

@ -1,29 +1,31 @@
<div class="page">
<div #viewer [id]="(stateService.file$ | async).fileId" class="viewer"></div>
</div>
<ng-container *ngIf="reusablePdf.show$ | async">
<input #compareFileInput (change)="uploadFile($event.target['files'])" accept="application/pdf" class="file-upload-input" type="file" />
<input #compareFileInput (change)="uploadFile($event.target['files'])" accept="application/pdf" class="file-upload-input" type="file" />
<div class="pagination noselect">
<div (click)="pdfViewer.navigatePreviousPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
</div>
<div *ngIf="pdfViewer?.totalPages && pdfViewer?.currentPage" class="pagination noselect">
<div (click)="pdfViewer.navigatePreviousPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
<div>
<input
#pageInput
(change)="pdfViewer.navigateToPage(pageInput.value)"
[max]="pdfViewer.totalPages"
[value]="pdfViewer.currentPage"
class="page-number-input"
min="1"
type="number"
/>
</div>
<div class="separator">/</div>
<div>
{{ pdfViewer.totalPages }}
</div>
<div (click)="pdfViewer.navigateNextPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-next"></mat-icon>
</div>
</div>
<div>
<input
#pageInput
(change)="pdfViewer.navigateToPage(pageInput.value)"
[max]="pdfViewer.totalPages"
[value]="pdfViewer.currentPage"
class="page-number-input"
min="1"
type="number"
/>
</div>
<div class="separator">/</div>
<div>
{{ pdfViewer.totalPages }}
</div>
<div (click)="pdfViewer.navigateNextPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-next"></mat-icon>
</div>
</div>
</ng-container>

View File

@ -1,14 +1,3 @@
.page {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.viewer {
height: 100%;
}
.searching {
position: absolute;
top: 9px;
@ -22,6 +11,7 @@
}
.pagination {
z-index: 1;
position: absolute;
bottom: 20px;
left: 50%;

View File

@ -26,7 +26,7 @@ import { AnnotationActionsService } from '../../services/annotation-actions.serv
import { UserPreferenceService } from '@services/user-preference.service';
import { BASE_HREF } from '../../../../tokens';
import { ConfigService } from '@services/config.service';
import { AutoUnsubscribe, ConfirmationDialogInput, CustomError, ErrorService, LoadingService } from '@iqser/common-ui';
import { AutoUnsubscribe, ConfirmationDialogInput, CustomError, ErrorService, LoadingService, log } from '@iqser/common-ui';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { toPosition } from '../../utils/pdf-calculation.utils';
@ -44,8 +44,6 @@ import { ViewerHeaderConfigService } from '../../services/viewer-header-config.s
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;
const DocLoadingError = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh');
@ -62,7 +60,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
@Output() readonly pageChanged = new EventEmitter<number>();
@Output() readonly keyUp = new EventEmitter<KeyboardEvent>();
@ViewChild('viewer', { static: true }) viewer: ElementRef;
@ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef;
instance: WebViewerInstance;
documentViewer: Core.DocumentViewer;
@ -85,7 +82,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 reusablePdf: ReusablePdfViewer,
readonly stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
@ -94,27 +91,25 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
super();
}
async ngOnInit() {
ngOnInit() {
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
await this._loadViewer();
this._loadViewer();
console.log('pdf-viewer.component.ts: ngOnInit');
this.addActiveScreenSubscription = this.stateService.blob$
.pipe(
switchMap(blob => from(this.pdfViewer.lockDocument()).pipe(map(() => blob))),
log('Reload blob'),
switchMap(blob => from(this.reusablePdf.lockDocument()).pipe(map(() => blob))),
withLatestFrom(this.stateService.file$),
tap(() => this._errorService.clear()),
tap(([blob, file]) => this._loadDocument(blob, file)),
tap(([blob, file]) => this._reusablePdfViewer.loadDocument(blob, file)),
tap(([blob, file]) => this.reusablePdf.loadDocument(blob, file)),
tap(() => this._pageRotationService.clearRotationsHideActions()),
)
.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
if (!this.instance) {
return;
}
if (changes.canPerformActions) {
if (changes.canPerformActions && !!this.instance) {
this._handleCustomActions();
}
}
@ -187,14 +182,12 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
fileReader.readAsArrayBuffer(fileToCompare);
}
private async _loadViewer() {
this.instance = await this.pdfViewer.loadViewer(this.viewer.nativeElement as HTMLElement);
this.documentViewer = this.pdfViewer.documentViewer;
this.annotationManager = this.pdfViewer.annotationManager;
private _loadViewer() {
this.instance = this.reusablePdf.instance;
this.documentViewer = this.reusablePdf.documentViewer;
this.annotationManager = this.reusablePdf.annotationManager;
this._setSelectionMode();
this._configureElements();
this.pdfViewer.disableHotkeys();
this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
@ -202,7 +195,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
if (action === 'deselected') {
// Remove deselected annotations from selected list
nextAnnotations = this.annotationManager.getSelectedAnnotations().filter(ann => !annotations.find(a => a.Id === ann.Id));
nextAnnotations = this.annotationManager.getSelectedAnnotations().filter(ann => !annotations.some(a => a.Id === ann.Id));
} else if (!this.multiSelectService.isEnabled) {
// Only choose the last selected annotation, to bypass viewer multi select
nextAnnotations = annotations;
@ -217,8 +210,8 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
}
if (!this.multiSelectService.isEnabled) {
this.pdfViewer.deselectAnnotations(
this._fileDataService.all.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)),
this.reusablePdf.deselectAnnotations(
this._fileDataService.all.filter(wrapper => !nextAnnotations.some(ann => ann.Id === wrapper.id)),
);
}
this.#configureAnnotationSpecificActions(annotations);
@ -236,7 +229,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
});
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
this.pdfViewer.deselectAllAnnotations();
this.reusablePdf.deselectAnnotations();
this._ngZone.run(() => this.pageChanged.emit(pageNumber));
return this._handleCustomActions();
});
@ -302,11 +295,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
return this._baseHref + path;
}
private _setSelectionMode(): void {
const textTool = this.instance.Core.Tools.TextTool as unknown as TextTool;
textTool.SELECTION_MODE = this._configService.values.SELECTION_MODE;
}
private _toggleRectangleAnnotationAction(readonly = false) {
if (!readonly) {
this.instance.UI.enableElements([TextPopups.ADD_RECTANGLE]);
@ -316,29 +304,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
}
private _configureElements() {
this.instance.UI.disableElements([
'pageNavOverlay',
'menuButton',
'selectToolButton',
'textHighlightToolButton',
'textUnderlineToolButton',
'textSquigglyToolButton',
'textStrikeoutToolButton',
'viewControlsButton',
'contextMenuPopup',
'linkButton',
'toggleNotesButton',
'notesPanel',
'thumbnailControl',
'documentControl',
'ribbons',
'toolsHeader',
'rotateClockwiseButton',
'rotateCounterClockwiseButton',
'annotationStyleEditButton',
'annotationGroupButton',
]);
this._headerConfigService.initialize(this.compareFileInput);
const dossierTemplateId = this.dossier.dossierTemplateId;
@ -568,7 +533,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
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 });
this._pageRotationService.clearRotationsHideActions();
}
private _setReadyAndInitialState() {

View File

@ -68,9 +68,7 @@
(keyUp)="handleKeyEvent($event); workloadComponent.handleKeyEvent($event)"
(manualAnnotationRequested)="openManualAnnotationDialog($event)"
(pageChanged)="viewerPageChanged($event)"
*ngIf="displayPdfViewer"
[canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready"
[dossier]="dossier"
></redaction-pdf-viewer>
</div>
@ -88,7 +86,7 @@
<redaction-file-workload
(selectPage)="selectPage($event)"
*ngIf="!file.excluded"
[activeViewerPage]="pdf.currentPage$ | async"
[activeViewerPage]="reusablePdf.currentPage$ | async"
[annotationActionsTemplate]="annotationActionsTemplate"
[dialogRef]="dialogRef"
[file]="file"

View File

@ -31,7 +31,7 @@ import { download, handleFilterDelta } from '../../utils';
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
import { FilesService } from '@services/files/files.service';
import { FileManagementService } from '@services/files/file-management.service';
import { catchError, map, startWith, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators';
import { FilesMapService } from '@services/files/files-map.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
import { ViewModeService } from './services/view-mode.service';
@ -51,6 +51,7 @@ import { FileDataService } from './services/file-data.service';
import { ActionsHelpModeKeys, ALL_HOTKEYS } from './utils/constants';
import { NGXLogger } from 'ngx-logger';
import { StampService } from './services/stamp.service';
import { ReusablePdfViewer } from '../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service';
import Annotation = Core.Annotations.Annotation;
@Component({
@ -63,12 +64,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
dialogRef: MatDialogRef<unknown>;
fullScreen = false;
displayPdfViewer = false;
readonly canPerformAnnotationActions$: Observable<boolean>;
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<AnnotationWrapper>,
@ -143,7 +143,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
async updateViewMode(): Promise<void> {
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 {

View File

@ -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<void> {
@ -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.

View File

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

View File

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

View File

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

View File

@ -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<boolean>;
readonly currentPage$ = this._activatedRoute.queryParamMap.pipe(
map(params => Number(params.get('page') ?? '1')),
shareDistinctLast(),
);
readonly #documentLoaded$ = new Subject<boolean>();
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<AnnotationWrapper>,
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'));
}
}

View File

@ -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<boolean>;
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 {

View File

@ -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<void> {
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<void> {
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,

View File

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

View File

@ -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],

View File

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

View File

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

View File

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

View File

@ -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<boolean>;
documentLoaded$: Observable<boolean>;
annotationSelected$: Observable<unknown>;
pageComplete$: Observable<unknown>;
#instance: WebViewerInstance;
#documentClosed$ = new Subject<boolean>();
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<boolean>(() => 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<AnnotationWrapper>) {
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'));
}
}

View File

@ -0,0 +1,5 @@
export interface Rgb {
readonly r: number;
readonly g: number;
readonly b: number;
}