@@ -46,6 +46,7 @@ import {
4646 CompoundTask ,
4747 ImageDataLike ,
4848 IPdfiumExecutor ,
49+ AnnotationAppearanceMap ,
4950} from '@embedpdf/models' ;
5051import { WorkerTaskQueue , Priority } from './task-queue' ;
5152import 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 } ,
0 commit comments