Skip to content

Commit 75f7ff9

Browse files
committed
Change annotation renderer to blob instead of canvas
1 parent 84e0e8d commit 75f7ff9

36 files changed

Lines changed: 470 additions & 193 deletions

packages/engines/src/lib/orchestrator/pdf-engine.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,11 +370,50 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
370370
);
371371
}
372372

373+
renderPageAnnotations(
374+
doc: PdfDocumentObject,
375+
page: PdfPageObject,
376+
options?: PdfRenderPageAnnotationOptions,
377+
): PdfTask<AnnotationAppearanceMap<T>> {
378+
const resultTask = new Task<AnnotationAppearanceMap<T>, PdfErrorReason>();
379+
380+
const renderHandle = this.workerQueue.enqueue(
381+
{
382+
execute: () => this.executor.renderPageAnnotationsRaw(doc, page, options),
383+
meta: { docId: doc.id, pageIndex: page.index, operation: 'renderPageAnnotationsRaw' },
384+
},
385+
{ priority: Priority.MEDIUM },
386+
);
387+
388+
// Wire up abort: when resultTask is aborted, also abort the queue task
389+
const originalAbort = resultTask.abort.bind(resultTask);
390+
resultTask.abort = (reason) => {
391+
renderHandle.abort(reason);
392+
originalAbort(reason);
393+
};
394+
395+
renderHandle.wait(
396+
(rawMap) => {
397+
if (resultTask.state.stage !== 0 /* Pending */) {
398+
return;
399+
}
400+
this.encodeAppearanceMap(rawMap, options, resultTask);
401+
},
402+
(error) => {
403+
if (resultTask.state.stage === 0 /* Pending */) {
404+
resultTask.fail(error);
405+
}
406+
},
407+
);
408+
409+
return resultTask;
410+
}
411+
373412
renderPageAnnotationsRaw(
374413
doc: PdfDocumentObject,
375414
page: PdfPageObject,
376415
options?: PdfRenderPageAnnotationOptions,
377-
): PdfTask<AnnotationAppearanceMap> {
416+
): PdfTask<AnnotationAppearanceMap<ImageDataLike>> {
378417
return this.workerQueue.enqueue(
379418
{
380419
execute: () => this.executor.renderPageAnnotationsRaw(doc, page, options),
@@ -455,6 +494,62 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
455494
.catch((error) => resultTask.reject({ code: PdfErrorCode.Unknown, message: String(error) }));
456495
}
457496

497+
/**
498+
* Encode a full annotation appearance map to the output type T.
499+
*/
500+
private encodeAppearanceMap(
501+
rawMap: AnnotationAppearanceMap<ImageDataLike>,
502+
options: PdfRenderPageAnnotationOptions | undefined,
503+
resultTask: Task<AnnotationAppearanceMap<T>, PdfErrorReason>,
504+
): void {
505+
const imageType = options?.imageType ?? 'image/webp';
506+
const quality = options?.imageQuality;
507+
508+
const convertImage = (rawImageData: ImageDataLike): Promise<T> => {
509+
const plainImageData = {
510+
data: new Uint8ClampedArray(rawImageData.data),
511+
width: rawImageData.width,
512+
height: rawImageData.height,
513+
};
514+
return this.options.imageConverter(() => plainImageData, imageType, quality);
515+
};
516+
517+
const jobs: Promise<void>[] = [];
518+
const encodedMap: AnnotationAppearanceMap<T> = {};
519+
const modes: Array<'normal' | 'rollover' | 'down'> = ['normal', 'rollover', 'down'];
520+
521+
for (const [annotationId, appearances] of Object.entries(rawMap)) {
522+
const encodedAppearances: NonNullable<AnnotationAppearanceMap<T>[string]> = {};
523+
encodedMap[annotationId] = encodedAppearances;
524+
525+
for (const mode of modes) {
526+
const appearance = appearances[mode];
527+
if (!appearance) continue;
528+
529+
jobs.push(
530+
convertImage(appearance.data).then((encodedData) => {
531+
encodedAppearances[mode] = {
532+
data: encodedData,
533+
rect: appearance.rect,
534+
};
535+
}),
536+
);
537+
}
538+
}
539+
540+
Promise.all(jobs)
541+
.then(() => {
542+
if (resultTask.state.stage === 0 /* Pending */) {
543+
resultTask.resolve(encodedMap);
544+
}
545+
})
546+
.catch((error) => {
547+
if (resultTask.state.stage === 0 /* Pending */) {
548+
resultTask.reject({ code: PdfErrorCode.Unknown, message: String(error) });
549+
}
550+
});
551+
}
552+
458553
// ========== Annotations ==========
459554

460555
getPageAnnotations(doc: PdfDocumentObject, page: PdfPageObject): PdfTask<PdfAnnotationObject[]> {

packages/engines/src/lib/webworker/engine.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,14 +496,33 @@ export class WebWorkerEngine implements PdfEngine {
496496
return task;
497497
}
498498

499+
renderPageAnnotations(
500+
doc: PdfDocumentObject,
501+
page: PdfPageObject,
502+
options?: PdfRenderPageAnnotationOptions,
503+
) {
504+
this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'renderPageAnnotations', doc, page, options);
505+
const requestId = this.generateRequestId(doc.id);
506+
const task = new WorkerTask<AnnotationAppearanceMap<Blob>>(this.worker, requestId);
507+
508+
const request: ExecuteRequest = createRequest(requestId, 'renderPageAnnotations', [
509+
doc,
510+
page,
511+
options,
512+
]);
513+
this.proxy(task, request);
514+
515+
return task;
516+
}
517+
499518
renderPageAnnotationsRaw(
500519
doc: PdfDocumentObject,
501520
page: PdfPageObject,
502521
options?: PdfRenderPageAnnotationOptions,
503522
) {
504523
this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'renderPageAnnotationsRaw', doc, page, options);
505524
const requestId = this.generateRequestId(doc.id);
506-
const task = new WorkerTask<AnnotationAppearanceMap>(this.worker, requestId);
525+
const task = new WorkerTask<AnnotationAppearanceMap<ImageDataLike>>(this.worker, requestId);
507526

508527
const request: ExecuteRequest = createRequest(requestId, 'renderPageAnnotationsRaw', [
509528
doc,

packages/engines/src/lib/webworker/runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ export class EngineRunner {
297297
case 'renderPageAnnotation':
298298
task = engine.renderPageAnnotation!(...args);
299299
break;
300+
case 'renderPageAnnotations':
301+
task = engine.renderPageAnnotations!(...args);
302+
break;
300303
case 'renderPageAnnotationsRaw':
301304
task = engine.renderPageAnnotationsRaw!(...args);
302305
break;

packages/models/src/pdf.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,8 @@ export const AP_MODE_DOWN = 4; // bit 2
439439
* A rendered appearance stream image for a single mode of an annotation.
440440
* @public
441441
*/
442-
export interface AnnotationAppearanceImage {
443-
data: ImageDataLike;
442+
export interface AnnotationAppearanceImage<TImage = ImageDataLike> {
443+
data: TImage;
444444
rect: Rect;
445445
}
446446

@@ -449,17 +449,20 @@ export interface AnnotationAppearanceImage {
449449
* keyed by mode (normal, rollover, down).
450450
* @public
451451
*/
452-
export interface AnnotationAppearances {
453-
normal?: AnnotationAppearanceImage;
454-
rollover?: AnnotationAppearanceImage;
455-
down?: AnnotationAppearanceImage;
452+
export interface AnnotationAppearances<TImage = ImageDataLike> {
453+
normal?: AnnotationAppearanceImage<TImage>;
454+
rollover?: AnnotationAppearanceImage<TImage>;
455+
down?: AnnotationAppearanceImage<TImage>;
456456
}
457457

458458
/**
459459
* Map of annotation ID to its rendered appearance stream images.
460460
* @public
461461
*/
462-
export type AnnotationAppearanceMap = Record<string, AnnotationAppearances>;
462+
export type AnnotationAppearanceMap<TImage = ImageDataLike> = Record<
463+
string,
464+
AnnotationAppearances<TImage>
465+
>;
463466

464467
/**
465468
* Representation of pdf action
@@ -3217,6 +3220,19 @@ export interface PdfEngine<T = Blob> {
32173220
annotation: PdfAnnotationObject,
32183221
options?: PdfRenderPageAnnotationOptions,
32193222
): PdfTask<T>;
3223+
/**
3224+
* Batch-render all annotation appearance streams for a page and encode
3225+
* each rendered image to the output type of this engine.
3226+
* Returns a map of annotation ID to rendered appearances (Normal/Rollover/Down).
3227+
* @param doc - pdf document
3228+
* @param page - pdf page
3229+
* @param options - render options
3230+
*/
3231+
renderPageAnnotations(
3232+
doc: PdfDocumentObject,
3233+
page: PdfPageObject,
3234+
options?: PdfRenderPageAnnotationOptions,
3235+
): PdfTask<AnnotationAppearanceMap<T>>;
32203236
/**
32213237
* Batch-render all annotation appearance streams for a page.
32223238
* Returns a map of annotation ID to rendered appearances (Normal/Rollover/Down).
@@ -3229,7 +3245,7 @@ export interface PdfEngine<T = Blob> {
32293245
doc: PdfDocumentObject,
32303246
page: PdfPageObject,
32313247
options?: PdfRenderPageAnnotationOptions,
3232-
): PdfTask<AnnotationAppearanceMap>;
3248+
): PdfTask<AnnotationAppearanceMap<ImageDataLike>>;
32333249
/**
32343250
* Get annotations of pdf page
32353251
* @param doc - pdf document
@@ -3604,7 +3620,7 @@ export interface IPdfiumExecutor {
36043620
doc: PdfDocumentObject,
36053621
page: PdfPageObject,
36063622
options?: PdfRenderPageAnnotationOptions,
3607-
): PdfTask<AnnotationAppearanceMap>;
3623+
): PdfTask<AnnotationAppearanceMap<ImageDataLike>>;
36083624

36093625
// Single page operations
36103626
getPageAnnotationsRaw(

packages/plugin-annotation/src/lib/annotation-plugin.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ export class AnnotationPlugin extends BasePlugin<
148148
private readonly toolsChange$ = createBehaviorEmitter<AnnotationToolsChangeEvent>();
149149
private readonly patchRegistry = new PatchRegistry();
150150

151-
// Appearance stream cache: documentId -> pageIndex -> AnnotationAppearanceMap
152-
private readonly appearanceCache = new Map<string, Map<number, AnnotationAppearanceMap>>();
151+
// Appearance stream cache: documentId -> pageIndex -> AnnotationAppearanceMap<Blob>
152+
private readonly appearanceCache = new Map<string, Map<number, AnnotationAppearanceMap<Blob>>>();
153153

154154
// Unified drag coordination (per-document)
155155
private readonly unifiedDragStates = new Map<string, UnifiedDragState>();
@@ -721,7 +721,7 @@ export class AnnotationPlugin extends BasePlugin<
721721
pageIndex: number,
722722
options?: PdfRenderPageAnnotationOptions,
723723
documentId?: string,
724-
): Task<AnnotationAppearanceMap, PdfErrorReason> {
724+
): Task<AnnotationAppearanceMap<Blob>, PdfErrorReason> {
725725
const id = documentId ?? this.getActiveDocumentId();
726726
const docState = this.getCoreDocument(id);
727727
const doc = docState?.document;
@@ -744,13 +744,13 @@ export class AnnotationPlugin extends BasePlugin<
744744

745745
const cached = docCache.get(pageIndex);
746746
if (cached && !options) {
747-
const task = new Task<AnnotationAppearanceMap, PdfErrorReason>();
747+
const task = new Task<AnnotationAppearanceMap<Blob>, PdfErrorReason>();
748748
task.resolve(cached);
749749
return task;
750750
}
751751

752-
const engineTask = this.engine.renderPageAnnotationsRaw(doc, page, options);
753-
const resultTask = new Task<AnnotationAppearanceMap, PdfErrorReason>();
752+
const engineTask = this.engine.renderPageAnnotations(doc, page, options);
753+
const resultTask = new Task<AnnotationAppearanceMap<Blob>, PdfErrorReason>();
754754

755755
engineTask.wait(
756756
(result) => {

packages/plugin-annotation/src/lib/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ export interface AnnotationScope {
268268
getPageAppearances(
269269
pageIndex: number,
270270
options?: PdfRenderPageAnnotationOptions,
271-
): Task<AnnotationAppearanceMap, PdfErrorReason>;
271+
): Task<AnnotationAppearanceMap<Blob>, PdfErrorReason>;
272272
/** Clear cached appearance images for a page (e.g. on zoom change) */
273273
invalidatePageAppearances(pageIndex: number): void;
274274
commit(): Task<boolean, PdfErrorReason>;
@@ -360,7 +360,7 @@ export interface AnnotationCapability {
360360
pageIndex: number,
361361
options?: PdfRenderPageAnnotationOptions,
362362
documentId?: string,
363-
) => Task<AnnotationAppearanceMap, PdfErrorReason>;
363+
) => Task<AnnotationAppearanceMap<Blob>, PdfErrorReason>;
364364
/** Clear cached appearance images for a page (e.g. on zoom change) */
365365
invalidatePageAppearances: (pageIndex: number, documentId?: string) => void;
366366
commit: () => Task<boolean, PdfErrorReason>;

packages/plugin-annotation/src/shared/components/annotation-container.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ interface AnnotationContainerProps<T extends PdfAnnotationObject> {
5959
onDoubleClick?: (event: any) => void;
6060
onSelect: (event: AnnotationInteractionEvent) => void;
6161
/** Pre-rendered appearance stream images for AP mode rendering */
62-
appearance?: AnnotationAppearances | null;
62+
appearance?: AnnotationAppearances<Blob> | null;
6363
zIndex?: number;
6464
resizeUI?: ResizeHandleUI;
6565
vertexUI?: VertexHandleUI;

packages/plugin-annotation/src/shared/components/annotations.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function Annotations(annotationsProps: AnnotationsProps) {
6868
const { register } = usePointerHandlers({ documentId, pageIndex });
6969
const [allSelectedIds, setAllSelectedIds] = useState<string[]>([]);
7070
const [editingId, setEditingId] = useState<string | null>(null);
71-
const [appearanceMap, setAppearanceMap] = useState<AnnotationAppearanceMap>({});
71+
const [appearanceMap, setAppearanceMap] = useState<AnnotationAppearanceMap<Blob>>({});
7272
const prevScaleRef = useRef<number>(scale);
7373

7474
const annotationProvides = useMemo(
@@ -236,7 +236,7 @@ export function Annotations(annotationsProps: AnnotationsProps) {
236236
}, [annotationProvides, pageIndex, allSelectedIds]);
237237

238238
const getAppearanceForAnnotation = useCallback(
239-
(ta: TrackedAnnotation): AnnotationAppearances | null => {
239+
(ta: TrackedAnnotation): AnnotationAppearances<Blob> | null => {
240240
if (ta.dictMode) return null;
241241
if (ta.object.rotation && ta.object.unrotatedRect) return null;
242242
const appearances = appearanceMap[ta.object.id];
Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,52 @@
1-
import { useEffect, useRef, CSSProperties } from '@framework';
2-
import { AnnotationAppearanceImage } from '@embedpdf/models';
1+
import type { AnnotationAppearanceImage } from '@embedpdf/models';
2+
import { useEffect, useRef, useState, CSSProperties } from '@framework';
33

44
interface AppearanceImageProps {
5-
appearance: AnnotationAppearanceImage;
5+
appearance: AnnotationAppearanceImage<Blob>;
66
style?: CSSProperties;
77
}
88

99
/**
10-
* Renders a pre-rendered annotation appearance stream image using a canvas.
11-
* The ImageDataLike data is drawn once and the canvas is sized to fill its container.
10+
* Renders a pre-rendered annotation appearance stream image as an img URL.
1211
* Purely visual -- pointer events are always disabled; hit-area SVG handles interaction.
1312
*/
1413
export function AppearanceImage({ appearance, style }: AppearanceImageProps) {
15-
const canvasRef = useRef<HTMLCanvasElement>(null);
14+
const [imageUrl, setImageUrl] = useState<string | null>(null);
15+
const urlRef = useRef<string | null>(null);
1616

1717
useEffect(() => {
18-
const canvas = canvasRef.current;
19-
if (!canvas) return;
18+
const url = URL.createObjectURL(appearance.data);
19+
setImageUrl(url);
20+
urlRef.current = url;
2021

21-
const { data } = appearance;
22-
canvas.width = data.width;
23-
canvas.height = data.height;
22+
return () => {
23+
if (urlRef.current) {
24+
URL.revokeObjectURL(urlRef.current);
25+
urlRef.current = null;
26+
}
27+
};
28+
}, [appearance.data]);
2429

25-
const ctx = canvas.getContext('2d');
26-
if (!ctx) return;
30+
const handleImageLoad = () => {
31+
if (urlRef.current) {
32+
URL.revokeObjectURL(urlRef.current);
33+
urlRef.current = null;
34+
}
35+
};
2736

28-
const imageData = new ImageData(data.data, data.width, data.height);
29-
ctx.putImageData(imageData, 0, 0);
30-
}, [appearance]);
31-
32-
return (
33-
<canvas
34-
ref={canvasRef}
37+
return imageUrl ? (
38+
<img
39+
src={imageUrl}
40+
onLoad={handleImageLoad}
3541
style={{
3642
position: 'absolute',
3743
width: '100%',
3844
height: '100%',
3945
display: 'block',
4046
pointerEvents: 'none',
47+
userSelect: 'none',
4148
...style,
4249
}}
4350
/>
44-
);
51+
) : null;
4552
}

0 commit comments

Comments
 (0)