diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json index 264127e0b1..e1862c6511 100644 --- a/lib/core/src/lib/i18n/en.json +++ b/lib/core/src/lib/i18n/en.json @@ -479,7 +479,8 @@ "RESET": "Reset", "THUMBNAILS": "Document thumbnails", "THUMBNAILS_PANLEL_CLOSE": "Close thumbnails panel", - "LOADING": "Document is loading" + "LOADING": "Document is loading", + "NOTE_ANNOTATION_IMG": "Note Annotation" }, "PAGE_LABEL": { "SHOWING": "Showing", diff --git a/lib/core/src/lib/viewer/components/mock/pdfjs-lib.mock.ts b/lib/core/src/lib/viewer/components/mock/pdfjs-lib.mock.ts index 8f4c90cd2f..27bce86961 100644 --- a/lib/core/src/lib/viewer/components/mock/pdfjs-lib.mock.ts +++ b/lib/core/src/lib/viewer/components/mock/pdfjs-lib.mock.ts @@ -25,7 +25,24 @@ export default { promise: new Promise((resolve) => { resolve({ numPages: 6, - getPage: () => 'fakePage' + getPage: () => + Promise.resolve({ + getAnnotations: () => [ + { + subtype: 'Text', + name: 'NoIcon', + id: 'R13', + titleObj: { + str: 'Annotation title' + }, + contentsObj: { + str: 'Annotation contents' + }, + modificationDate: "D:20260202104106Z00'00", + popupRef: 'R1' + } + ] + }) }); }) }; diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer-host.component.scss b/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer-host.component.scss index 5072dff479..084304e5b7 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer-host.component.scss +++ b/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer-host.component.scss @@ -1,3 +1,4 @@ +/* stylelint-disable selector-class-pattern */ .adf-pdf-viewer { .textLayer { position: absolute; @@ -183,6 +184,30 @@ border: none; } } + + .adf-pdf-viewer-annotation-tooltip.popup { + outline-color: var(--adf-pdf-viewer-annotation-tooltip-color); + background-color: color-mix(in srgb, var(--adf-pdf-viewer-annotation-tooltip-color) 30%, var(--theme-primary-color-default-contrast)); + margin-left: 40px; + width: fit-content; + max-width: 300px; + opacity: 0; + pointer-events: none; + + .popupContent { + display: inherit; + } + } + + .textAnnotation { + &:hover, + &:focus-within { + .adf-pdf-viewer-annotation-tooltip.popup { + opacity: 1; + pointer-events: auto; + } + } + } } .adf-hidden, diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.spec.ts b/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.spec.ts index 5df1b2833a..c881979894 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.spec.ts +++ b/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.spec.ts @@ -28,6 +28,7 @@ import { RenderingQueueServices } from '../../services/rendering-queue.services' import { PdfThumbListComponent } from '../pdf-viewer-thumbnails/pdf-viewer-thumbnails.component'; import { PDFJS_MODULE, PDFJS_VIEWER_MODULE, PdfViewerComponent } from './pdf-viewer.component'; import pdfjsLibraryMock from '../mock/pdfjs-lib.mock'; +import { TranslateService } from '@ngx-translate/core'; declare const pdfjsLib: any; @@ -464,8 +465,8 @@ describe('Test PdfViewer - User interaction', () => { fixture.destroy(); }); - it('should init the viewer with annotation mode disabled', () => { - expect(pdfViewerSpy).toHaveBeenCalledWith(jasmine.objectContaining({ annotationMode: 0 })); + it('should init the viewer with annotation mode enabled', () => { + expect(pdfViewerSpy).toHaveBeenCalledWith(jasmine.objectContaining({ annotationMode: 1 })); }); it('should Total number of pages be loaded', () => { @@ -600,4 +601,62 @@ describe('Test PdfViewer - User interaction', () => { expect(component.isPanelDisabled).toBe(false); }); }); + + describe('Annotations', () => { + const annotationImageAlt = 'Note Annotation'; + const annotationAttribute = 'data-annotation-id'; + + let annotationElement: HTMLElement; + let annotationImageElement: HTMLImageElement; + let documentContainer: HTMLDivElement; + + const dispatchAnnotationLayerRenderedEvent = (): void => { + pdfViewerSpy.calls.mostRecent().args[0].eventBus.dispatch('annotationlayerrendered', { + pageNumber: 1, + source: { + div: documentContainer + } + }); + tick(); + }; + + const getAnnotationPopupElement = (): HTMLElement => annotationElement.querySelector('.adf-pdf-viewer-annotation-tooltip'); + + beforeEach(() => { + documentContainer = document.createElement('div'); + annotationImageElement = document.createElement('img'); + annotationElement = document.createElement('section'); + annotationElement.setAttribute(annotationAttribute, 'R13'); + annotationElement.append(annotationImageElement); + documentContainer.append(annotationElement); + spyOn(TestBed.inject(TranslateService), 'instant').withArgs('ADF_VIEWER.ARIA.NOTE_ANNOTATION_IMG').and.returnValue(annotationImageAlt); + }); + + it('should have corrected image in annotation popup', fakeAsync(() => { + dispatchAnnotationLayerRenderedEvent(); + expect(annotationImageElement.src).toBe( + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGV' + + 'pZ2h0PSIyNCI+PHBhdGggZD0iTTIgMmgxNHYxNEgyeiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0zIDNoMTJ2MTJIM3oiIGZpbGw9Ii' + + 'NmZmRiMDAiLz48cGF0aCBkPSJNNSA1aDh2OGgtOHoiIGZpbGw9IiNmZmJiMDAiLz48L3N2Zz4=' + ); + expect(annotationImageElement.alt).toBe(annotationImageAlt); + })); + + it('should have corrected content in annotation popup', fakeAsync(() => { + dispatchAnnotationLayerRenderedEvent(); + expect(annotationElement.querySelector('.title').textContent).toBe('Annotation title'); + expect(annotationElement.querySelector('.popupDate').textContent).toBe('2/2/2026, 10:41:06 AM'); + expect(annotationElement.querySelector('.popupContent').textContent).toBe('Annotation contents'); + expect(getAnnotationPopupElement()).toBeDefined(); + })); + + it('should not have corrected content', fakeAsync(() => { + const annotationPopupElement = document.createElement('section'); + annotationPopupElement.setAttribute(annotationAttribute, 'R1'); + documentContainer.append(annotationPopupElement); + + dispatchAnnotationLayerRenderedEvent(); + expect(getAnnotationPopupElement()).toBeNull(); + })); + }); }); diff --git a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.ts b/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.ts index ed86218124..b9b0cda6ac 100644 --- a/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.ts +++ b/lib/core/src/lib/viewer/components/pdf-viewer/pdf-viewer.component.ts @@ -37,7 +37,7 @@ import { import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { TranslatePipe } from '@ngx-translate/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { from, Subject, switchMap } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { AppConfigService } from '../../../app-config'; @@ -49,6 +49,7 @@ import * as pdfjsLib from 'pdfjs-dist/build/pdf.min.mjs'; import { EventBus, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'; import { OnProgressParameters, PDFDocumentLoadingTask, PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api'; import { IconModule } from '../../../icon/icon.module'; +import { PDFDateString } from 'pdfjs-dist'; export type PdfScaleMode = 'init' | 'page-actual' | 'page-width' | 'page-height' | 'page-fit' | 'auto'; @@ -155,6 +156,8 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { private renderingQueueServices = inject(RenderingQueueServices); private appConfigService = inject(AppConfigService); + private readonly translateService = inject(TranslateService); + constructor() { // needed to preserve "this" context this.onPageChange = this.onPageChange.bind(this); @@ -287,7 +290,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { viewer, renderingQueue: this.renderingQueueServices, eventBus: this.eventBus, - annotationMode: 0 + annotationMode: 1 }); // cspell: disable-next @@ -301,6 +304,9 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { this.eventBus.on('pagerendered', () => { this.onPageRendered(); }); + this.eventBus.on('annotationlayerrendered', (event) => + this.handleNotRecognizedAnnotations(pdfDocument, event.source.div, event.pageNumber) + ); this.renderingQueueServices.setViewer(this.pdfViewer); this.pdfViewer.setDocument(pdfDocument); @@ -639,4 +645,71 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { this.previousPage(); } } + + private async handleNotRecognizedAnnotations( + pdfDocument: PDFDocumentProxy, + documentContainer: HTMLDivElement, + pageNumber: number + ): Promise { + const page = await pdfDocument.getPage(pageNumber); + const annotations = await page.getAnnotations(); + annotations.forEach((annotation) => { + if (annotation.subtype !== 'Text' || annotation.name !== 'NoIcon') { + return; + } + const annotationElement = documentContainer.querySelector(`[data-annotation-id="${annotation.id}"]`); + if (!annotationElement) { + return; + } + this.correctAnnotationImage(annotationElement); + const text: string = annotation.contentsObj?.str?.trim(); + if (!text || (annotation.popupRef && documentContainer.querySelector(`[data-annotation-id="${annotation.popupRef}"]`))) { + return; + } + this.createAnnotationPopup(annotation, text, annotationElement); + }); + } + + private correctAnnotationImage(annotationElement: HTMLElement): void { + const annotationImageElement = annotationElement.querySelector('img'); + if (annotationImageElement) { + annotationImageElement.src = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGV' + + 'pZ2h0PSIyNCI+PHBhdGggZD0iTTIgMmgxNHYxNEgyeiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0zIDNoMTJ2MTJIM3oiIGZpbGw9Ii' + + 'NmZmRiMDAiLz48cGF0aCBkPSJNNSA1aDh2OGgtOHoiIGZpbGw9IiNmZmJiMDAiLz48L3N2Zz4='; + annotationImageElement.alt = this.translateService.instant('ADF_VIEWER.ARIA.NOTE_ANNOTATION_IMG'); + } + } + + private createAnnotationPopup(annotation: any, text: string, annotationElement: HTMLElement): void { + const popupElement = document.createElement('div'); + let headerElement: HTMLSpanElement; + if (annotation.titleObj?.str) { + headerElement = this.createAnnotationPopupHeader(annotation); + } + const contentElement = this.createAnnotationPopupContent(text); + popupElement.classList.add('popup', 'adf-pdf-viewer-annotation-tooltip'); + headerElement ? popupElement.append(headerElement, contentElement) : popupElement.append(contentElement); + annotationElement.appendChild(popupElement); + } + + private createAnnotationPopupHeader(annotation: any): HTMLSpanElement { + const headerElement = document.createElement('span'); + const titleElement = document.createElement('span'); + const dateElement = document.createElement('time'); + titleElement.innerText = annotation.titleObj.str; + titleElement.classList.add('title'); + dateElement.innerText = PDFDateString.toDateObject(annotation.modificationDate).toLocaleString(); + dateElement.classList.add('popupDate'); + headerElement.classList.add('header'); + headerElement.append(titleElement, dateElement); + return headerElement; + } + + private createAnnotationPopupContent(text: string): HTMLSpanElement { + const contentElement = document.createElement('span'); + contentElement.innerText = text; + contentElement.classList.add('popupContent'); + return contentElement; + } }