Skip to content

Commit a0f2a08

Browse files
authored
Merge pull request #467 from embedpdf/feature/appearance-based-rendering
Appearance based rendering
2 parents f55cfad + 75f7ff9 commit a0f2a08

109 files changed

Lines changed: 4112 additions & 2625 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/brave-lions-roar.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@embedpdf/engines': minor
3+
---
4+
5+
Implemented `renderPageAnnotationsRaw` to batch render annotation appearance streams and updated `updatePageAnnotation` to support skipping appearance regeneration.

.changeset/calm-rivers-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@embedpdf/models': minor
3+
---
4+
5+
Added types and interfaces for annotation appearance streams (`AnnotationAppearanceMap`, `AnnotationAppearances`, `AnnotationAppearanceImage`) and updated `PdfEngine` interface with `renderPageAnnotationsRaw`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@embedpdf/plugin-ui': patch
3+
---
4+
5+
Fix Vue reactivity bugs when switching documents in the schema-driven viewer. `useRegisterAnchor` now accepts `MaybeRefOrGetter<string>` and re-registers anchors when `documentId` changes. `AutoMenuRenderer` now passes a reactive getter to `useUIState` so menu state tracks the active document.

.changeset/happy-birds-sing.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@embedpdf/plugin-annotation': minor
3+
---
4+
5+
- Added support for rendering annotation appearance streams (AP) for better visual fidelity with other PDF viewers.
6+
- Refactored annotation rendering to use a registry-based system, allowing for easier extensibility.
7+
- Introduced `moveAnnotation` API to update annotation positions without regenerating their appearance streams.
8+
- Added caching for rendered appearance streams.

.changeset/silent-winds-blow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@embedpdf/plugin-redaction': patch
3+
---
4+
5+
Updated redaction tools and renderers to explicitly disable appearance stream usage, ensuring dynamic rendering for redaction marks.

.changeset/wise-owls-hoot.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@embedpdf/snippet': patch
3+
---
4+
5+
Fixed color matching case insensitivity and rotation debounce logic in the annotation sidebar.

examples/vue-tailwind/src/components/CommandButton.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,15 @@ const props = withDefaults(defineProps<Props>(), {
7171
className: undefined,
7272
});
7373
74-
const command = useCommand(props.commandId, props.documentId);
74+
const command = useCommand(
75+
() => props.commandId,
76+
() => props.documentId,
77+
);
7578
7679
// Register this button with the anchor registry if itemId is provided
7780
// This allows menus to anchor to it when opened via UI state changes
7881
const finalItemId = computed(() => props.itemId || props.commandId);
79-
const anchorRef = useRegisterAnchor(props.documentId, finalItemId.value);
82+
const anchorRef = useRegisterAnchor(() => props.documentId, finalItemId);
8083
8184
// Get the icon component from the command's icon property
8285
const iconName = computed(() => (command.value?.icon ? `${command.value.icon}Icon` : null));

examples/vue-tailwind/src/components/CommandTabButton.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,14 @@ const props = withDefaults(defineProps<Props>(), {
5050
variant: 'text',
5151
});
5252
53-
const command = useCommand(props.commandId, props.documentId);
53+
const command = useCommand(
54+
() => props.commandId,
55+
() => props.documentId,
56+
);
5457
5558
// Register this button with the anchor registry if itemId is provided
5659
const finalItemId = computed(() => props.itemId || props.commandId);
57-
const anchorRef = useRegisterAnchor(props.documentId, finalItemId.value);
60+
const anchorRef = useRegisterAnchor(() => props.documentId, finalItemId);
5861
5962
// Get the icon component from the command's icon property
6063
const iconName = computed(() => (command.value?.icon ? `${command.value.icon}Icon` : null));

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

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
CompoundTask,
4747
ImageDataLike,
4848
IPdfiumExecutor,
49+
AnnotationAppearanceMap,
4950
} from '@embedpdf/models';
5051
import { WorkerTaskQueue, Priority } from './task-queue';
5152
import type { ImageDataConverter } from '../converters/types';
@@ -369,6 +370,59 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
369370
);
370371
}
371372

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+
412+
renderPageAnnotationsRaw(
413+
doc: PdfDocumentObject,
414+
page: PdfPageObject,
415+
options?: PdfRenderPageAnnotationOptions,
416+
): PdfTask<AnnotationAppearanceMap<ImageDataLike>> {
417+
return this.workerQueue.enqueue(
418+
{
419+
execute: () => this.executor.renderPageAnnotationsRaw(doc, page, options),
420+
meta: { docId: doc.id, pageIndex: page.index, operation: 'renderPageAnnotationsRaw' },
421+
},
422+
{ priority: Priority.MEDIUM },
423+
);
424+
}
425+
372426
/**
373427
* Helper to render and encode in two stages with priority queue
374428
*/
@@ -440,6 +494,62 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
440494
.catch((error) => resultTask.reject({ code: PdfErrorCode.Unknown, message: String(error) }));
441495
}
442496

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+
443553
// ========== Annotations ==========
444554

445555
getPageAnnotations(doc: PdfDocumentObject, page: PdfPageObject): PdfTask<PdfAnnotationObject[]> {
@@ -471,10 +581,11 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
471581
doc: PdfDocumentObject,
472582
page: PdfPageObject,
473583
annotation: PdfAnnotationObject,
584+
options?: { regenerateAppearance?: boolean },
474585
): PdfTask<boolean> {
475586
return this.workerQueue.enqueue(
476587
{
477-
execute: () => this.executor.updatePageAnnotation(doc, page, annotation),
588+
execute: () => this.executor.updatePageAnnotation(doc, page, annotation, options),
478589
meta: { docId: doc.id, pageIndex: page.index, operation: 'updatePageAnnotation' },
479590
},
480591
{ priority: Priority.MEDIUM },

packages/engines/src/lib/orchestrator/remote-executor.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
serializeLogger,
3939
IPdfiumExecutor,
4040
ImageDataLike,
41+
AnnotationAppearanceMap,
4142
} from '@embedpdf/models';
4243
import type { WorkerRequest, WorkerResponse } from './pdfium-native-runner';
4344
import type { FontFallbackConfig } from '../pdfium/font-fallback';
@@ -81,6 +82,7 @@ type MessageType =
8182
| 'renderPageRect'
8283
| 'renderThumbnailRaw'
8384
| 'renderPageAnnotationRaw'
85+
| 'renderPageAnnotationsRaw'
8486
| 'getPageAnnotations'
8587
| 'getPageAnnotationsRaw'
8688
| 'createPageAnnotation'
@@ -351,6 +353,14 @@ export class RemoteExecutor implements IPdfiumExecutor {
351353
return this.send<ImageDataLike>('renderPageAnnotationRaw', [doc, page, annotation, options]);
352354
}
353355

356+
renderPageAnnotationsRaw(
357+
doc: PdfDocumentObject,
358+
page: PdfPageObject,
359+
options?: PdfRenderPageAnnotationOptions,
360+
): PdfTask<AnnotationAppearanceMap> {
361+
return this.send<AnnotationAppearanceMap>('renderPageAnnotationsRaw', [doc, page, options]);
362+
}
363+
354364
getPageAnnotationsRaw(
355365
doc: PdfDocumentObject,
356366
page: PdfPageObject,
@@ -375,8 +385,9 @@ export class RemoteExecutor implements IPdfiumExecutor {
375385
doc: PdfDocumentObject,
376386
page: PdfPageObject,
377387
annotation: PdfAnnotationObject,
388+
options?: { regenerateAppearance?: boolean },
378389
): PdfTask<boolean> {
379-
return this.send<boolean>('updatePageAnnotation', [doc, page, annotation]);
390+
return this.send<boolean>('updatePageAnnotation', [doc, page, annotation, options]);
380391
}
381392

382393
removePageAnnotation(

0 commit comments

Comments
 (0)