[MNT-25123] pdf annotations incorrect and or not visible (#11621)

* [MNT-25123] Fixed pdf annotations displaying

* [MNT-25123] Unit tests, used variables for colors

* [MNT-25123] Corrected sonar issues
This commit is contained in:
AleksanderSklorz
2026-02-09 09:18:09 +01:00
committed by GitHub
parent dcf133b37e
commit 5df193b255
5 changed files with 181 additions and 6 deletions
+2 -1
View File
@@ -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",
@@ -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'
}
]
})
});
})
};
@@ -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,
@@ -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();
}));
});
});
@@ -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<void> {
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<HTMLElement>(`[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;
}
}