RED-3988: transform old pdf viewer component into pdf proxy service

This commit is contained in:
Dan Percic 2022-05-23 18:25:26 +03:00
parent 669ec2ba1d
commit 78e49d609e
20 changed files with 267 additions and 315 deletions

View File

@ -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<AnnotationWrapper>,
private readonly _listingService: AnnotationsListingService,
readonly multiSelectService: MultiSelectService,
) {
this.isSelected$ = this._annotationChanged$.pipe(switchMap(annotation => this._listingService.isSelected$(annotation)));

View File

@ -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<AnnotationWrapper>(undefined);
constructor(private readonly _listingService: ListingService<AnnotationWrapper>, private readonly _changeRef: ChangeDetectorRef) {
constructor(private readonly _listingService: AnnotationsListingService, private readonly _changeRef: ChangeDetectorRef) {
this._annotationChanged$
.pipe(
filter(annotation => !!annotation),

View File

@ -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<boolean>;
constructor(
private readonly _listingService: ListingService<AnnotationWrapper>,
private readonly _listingService: AnnotationsListingService,
readonly annotationReferencesService: AnnotationReferencesService,
) {
this.isSelected$ = this.annotationReferencesService.annotation$.pipe(

View File

@ -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<AnnotationWrapper>,
readonly listingService: AnnotationsListingService,
readonly multiSelectService: MultiSelectService,
) {
this.isSelected$ = this._annotationChanged$.pipe(

View File

@ -1,11 +0,0 @@
.searching {
position: absolute;
top: 9px;
right: 60px;
display: flex;
align-items: center;
mat-spinner {
margin-left: 15px;
}
}

View File

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

View File

@ -62,15 +62,7 @@
<div class="overlay-shadow"></div>
<div class="content-inner">
<div class="content-container">
<redaction-pdf-viewer
(annotationSelected)="handleAnnotationSelected($event)"
(manualAnnotationRequested)="openManualAnnotationDialog($event)"
(pageChanged)="viewerPageChanged($event)"
[canPerformActions]="canPerformAnnotationActions$ | async"
[dossier]="dossier"
></redaction-pdf-viewer>
</div>
<div class="content-container"></div>
<div class="right-container">
<iqser-empty-state
@ -98,7 +90,7 @@
<ng-template #annotationActionsTemplate let-annotation="annotation">
<redaction-annotation-actions
[annotations]="[annotation]"
[canPerformAnnotationActions]="canPerformAnnotationActions$ | async"
[canPerformAnnotationActions]="pdfProxyService.canPerformAnnotationActions$ | async"
[iqserHelpMode]="getActionsHelpModeKey(annotation)"
[scrollableParentView]="scrollableParentView"
></redaction-annotation-actions>

View File

@ -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<unknown>;
fullScreen = 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)));
@ -82,7 +85,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
readonly pdf: PdfViewer,
readonly documentInfoService: DocumentInfoService,
readonly state: FilePreviewStateService,
readonly listingService: ListingService<AnnotationWrapper>,
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<void> {
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 {

View File

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

View File

@ -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<AnnotationWrapper>,
private readonly _listingService: AnnotationsListingService,
) {}
private get _dossier(): Dossier {
@ -197,11 +198,10 @@ export class AnnotationActionsService {
});
}
getViewerAvailableActions(): Record<string, unknown>[] {
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.

View File

@ -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<ManualRedactionEntryWrapper>();
@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<ManualRedactionEntryWrapper>();
readonly pageChanged$ = this._pdf.pageChanged$.pipe(tap(() => this._handleCustomActions()));
canPerformActions = true;
instance = this._pdf.instance;
canPerformAnnotationActions$: Observable<boolean>;
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<Record<string, Core.Math.Quad[]>> = this.pdf.documentViewer.getSelectedTextQuads();
const text = this.pdf.documentViewer.getSelectedText();
const selectedQuads: Record<string, Quad[]> = 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<Record<string, Core.Math.Quad[]>>,
text?: string,
convertQuads = false,
): IManualRedactionEntry {
private _getManualRedaction(quads: Record<string, Quad[]>, 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);
}
}

View File

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

View File

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

View File

@ -47,9 +47,7 @@ export class REDAnnotationManager {
get(annotation: AnnotationWrapper | string): Annotation;
get(annotations: List | List<AnnotationWrapper>): Annotation[];
get(predicate?: (value: Annotation) => boolean): Annotation[];
get(argument?: AnnotationPredicate | List<AnnotationWrapper> | List | AnnotationWrapper | string): Annotation | Annotation[] {
if (isStringOrWrapper(argument)) {
return this.#getById(argument);

View File

@ -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<boolean>;
pageComplete$: Observable<boolean>;
keyUp$: Observable<KeyboardEvent>;
textSelected$: Observable<string>;
selectedText = '';
#document: DocumentViewer;
@ -67,6 +71,24 @@ export class REDDocumentViewer {
return fromEvent(this.#document, 'pageComplete').pipe(debounceTime(300));
}
get #textSelected$(): Observable<string> {
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() {

View File

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

View File

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

View File

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

View File

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