Skip to content

Commit 2d4eaaa

Browse files
committed
Render appearance stream
1 parent df84618 commit 2d4eaaa

15 files changed

Lines changed: 580 additions & 4 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
CompoundTask,
4646
ImageDataLike,
4747
IPdfiumExecutor,
48+
AnnotationAppearanceMap,
4849
} from '@embedpdf/models';
4950
import { WorkerTaskQueue, Priority } from './task-queue';
5051
import type { ImageDataConverter } from '../converters/types';
@@ -337,6 +338,14 @@ export class PdfEngine<T = Blob> implements IPdfEngine<T> {
337338
);
338339
}
339340

341+
renderPageAnnotationsRaw(
342+
doc: PdfDocumentObject,
343+
page: PdfPageObject,
344+
options?: PdfRenderPageAnnotationOptions,
345+
): PdfTask<AnnotationAppearanceMap> {
346+
return this.executor.renderPageAnnotationsRaw(doc, page, options);
347+
}
348+
340349
/**
341350
* Helper to render and encode in two stages with priority queue
342351
*/

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
serializeLogger,
3838
IPdfiumExecutor,
3939
ImageDataLike,
40+
AnnotationAppearanceMap,
4041
} from '@embedpdf/models';
4142
import type { WorkerRequest, WorkerResponse } from './pdfium-native-runner';
4243
import type { FontFallbackConfig } from '../pdfium/font-fallback';
@@ -80,6 +81,7 @@ type MessageType =
8081
| 'renderPageRect'
8182
| 'renderThumbnailRaw'
8283
| 'renderPageAnnotationRaw'
84+
| 'renderPageAnnotationsRaw'
8385
| 'getPageAnnotations'
8486
| 'getPageAnnotationsRaw'
8587
| 'createPageAnnotation'
@@ -349,6 +351,14 @@ export class RemoteExecutor implements IPdfiumExecutor {
349351
return this.send<ImageDataLike>('renderPageAnnotationRaw', [doc, page, annotation, options]);
350352
}
351353

354+
renderPageAnnotationsRaw(
355+
doc: PdfDocumentObject,
356+
page: PdfPageObject,
357+
options?: PdfRenderPageAnnotationOptions,
358+
): PdfTask<AnnotationAppearanceMap> {
359+
return this.send<AnnotationAppearanceMap>('renderPageAnnotationsRaw', [doc, page, options]);
360+
}
361+
352362
getPageAnnotationsRaw(
353363
doc: PdfDocumentObject,
354364
page: PdfPageObject,

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

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ import {
113113
PdfTrappedStatus,
114114
PdfStampFit,
115115
PdfAddAttachmentParams,
116+
AnnotationAppearanceMap,
117+
AnnotationAppearances,
118+
AnnotationAppearanceImage,
119+
AP_MODE_NORMAL,
120+
AP_MODE_ROLLOVER,
121+
AP_MODE_DOWN,
116122
} from '@embedpdf/models';
117123
import { computeFormDrawParams, isValidCustomKey, readArrayBuffer, readString } from './helper';
118124
import { WrappedPdfiumModule } from '@embedpdf/pdfium';
@@ -4147,6 +4153,12 @@ export class PdfiumNative implements IPdfiumExecutor {
41474153
// Post-process: reverse-rotate vertices for vertex types that have rotation metadata
41484154
if (annotation) {
41494155
annotation = this.reverseRotateAnnotationOnLoad(annotation);
4156+
4157+
// Populate available appearance stream modes bitmask
4158+
const apModes = this.pdfiumModule.EPDFAnnot_GetAvailableAppearanceModes(annotationPtr);
4159+
if (apModes) {
4160+
annotation.appearanceModes = apModes;
4161+
}
41504162
}
41514163

41524164
return annotation;
@@ -7491,6 +7503,194 @@ export class PdfiumNative implements IPdfiumExecutor {
74917503
return task;
74927504
}
74937505

7506+
/**
7507+
* Batch-render all annotation appearance streams for a page in one call.
7508+
* Returns a map of annotation ID -> rendered appearances (Normal/Rollover/Down).
7509+
* Skips annotations that have rotation + unrotatedRect (EmbedPDF-rotated)
7510+
* and annotations without any appearance stream.
7511+
*
7512+
* @public
7513+
*/
7514+
renderPageAnnotationsRaw(
7515+
doc: PdfDocumentObject,
7516+
page: PdfPageObject,
7517+
options?: PdfRenderPageAnnotationOptions,
7518+
): PdfTask<AnnotationAppearanceMap> {
7519+
const { scaleFactor = 1, rotation = Rotation.Degree0, dpr = 1 } = options ?? {};
7520+
7521+
this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'renderPageAnnotationsRaw', doc, page, options);
7522+
this.logger.perf(
7523+
LOG_SOURCE,
7524+
LOG_CATEGORY,
7525+
'RenderPageAnnotationsRaw',
7526+
'Begin',
7527+
`${doc.id}-${page.index}`,
7528+
);
7529+
7530+
const ctx = this.cache.getContext(doc.id);
7531+
if (!ctx) {
7532+
this.logger.perf(
7533+
LOG_SOURCE,
7534+
LOG_CATEGORY,
7535+
'RenderPageAnnotationsRaw',
7536+
'End',
7537+
`${doc.id}-${page.index}`,
7538+
);
7539+
return PdfTaskHelper.reject({
7540+
code: PdfErrorCode.DocNotOpen,
7541+
message: 'document does not open',
7542+
});
7543+
}
7544+
7545+
const pageCtx = ctx.acquirePage(page.index);
7546+
const result: AnnotationAppearanceMap = {};
7547+
const finalScale = Math.max(0.01, scaleFactor * dpr);
7548+
const annotCount = this.pdfiumModule.FPDFPage_GetAnnotCount(pageCtx.pagePtr);
7549+
7550+
for (let i = 0; i < annotCount; i++) {
7551+
const annotPtr = this.pdfiumModule.FPDFPage_GetAnnot(pageCtx.pagePtr, i);
7552+
if (!annotPtr) continue;
7553+
7554+
try {
7555+
// Read annotation NM (id)
7556+
const nm = this.getAnnotString(annotPtr, 'NM');
7557+
if (!nm) continue;
7558+
7559+
// Skip EmbedPDF-rotated annotations (have rotation + unrotatedRect)
7560+
const extRotation = this.getAnnotExtendedRotation(annotPtr);
7561+
if (extRotation !== 0) {
7562+
const unrotatedRaw = this.readAnnotUnrotatedRect(annotPtr);
7563+
if (unrotatedRaw) continue;
7564+
}
7565+
7566+
// Detect available AP modes
7567+
const apModes = this.pdfiumModule.EPDFAnnot_GetAvailableAppearanceModes(annotPtr);
7568+
if (!apModes) continue;
7569+
7570+
const appearances: AnnotationAppearances = {};
7571+
7572+
// Render each available mode
7573+
const modesToRender: Array<{
7574+
bit: number;
7575+
mode: AppearanceMode;
7576+
key: keyof AnnotationAppearances;
7577+
}> = [
7578+
{ bit: AP_MODE_NORMAL, mode: AppearanceMode.Normal, key: 'normal' },
7579+
{ bit: AP_MODE_ROLLOVER, mode: AppearanceMode.Rollover, key: 'rollover' },
7580+
{ bit: AP_MODE_DOWN, mode: AppearanceMode.Down, key: 'down' },
7581+
];
7582+
7583+
for (const { bit, mode, key } of modesToRender) {
7584+
if (!(apModes & bit)) continue;
7585+
7586+
const rendered = this.renderSingleAnnotAppearance(
7587+
doc,
7588+
page,
7589+
pageCtx,
7590+
annotPtr,
7591+
mode,
7592+
rotation,
7593+
finalScale,
7594+
);
7595+
if (rendered) {
7596+
appearances[key] = rendered;
7597+
}
7598+
}
7599+
7600+
if (appearances.normal || appearances.rollover || appearances.down) {
7601+
result[nm] = appearances;
7602+
}
7603+
} finally {
7604+
this.pdfiumModule.FPDFPage_CloseAnnot(annotPtr);
7605+
}
7606+
}
7607+
7608+
pageCtx.release();
7609+
this.logger.perf(
7610+
LOG_SOURCE,
7611+
LOG_CATEGORY,
7612+
'RenderPageAnnotationsRaw',
7613+
'End',
7614+
`${doc.id}-${page.index}`,
7615+
);
7616+
7617+
const task = new Task<AnnotationAppearanceMap, PdfErrorReason>();
7618+
task.resolve(result);
7619+
return task;
7620+
}
7621+
7622+
/**
7623+
* Render a single annotation's appearance for a given mode.
7624+
* Returns the image data and rect, or null on failure.
7625+
* @private
7626+
*/
7627+
private renderSingleAnnotAppearance(
7628+
doc: PdfDocumentObject,
7629+
page: PdfPageObject,
7630+
pageCtx: PageContext,
7631+
annotPtr: number,
7632+
mode: AppearanceMode,
7633+
rotation: Rotation,
7634+
finalScale: number,
7635+
): AnnotationAppearanceImage | null {
7636+
// Read rect using EPDFAnnot_GetRect (normalized) and convert to device coords
7637+
const pageRect = this.readPageAnnoRect(annotPtr);
7638+
const annotRect = this.convertPageRectToDeviceRect(doc, page, pageRect);
7639+
7640+
const rect = toIntRect(annotRect);
7641+
const devRect = toIntRect(transformRect(page.size, rect, rotation, finalScale));
7642+
const wDev = Math.max(1, devRect.size.width);
7643+
const hDev = Math.max(1, devRect.size.height);
7644+
const stride = wDev * 4;
7645+
const bytes = stride * hDev;
7646+
7647+
const heapPtr = this.memoryManager.malloc(bytes);
7648+
const bitmapPtr = this.pdfiumModule.FPDFBitmap_CreateEx(
7649+
wDev,
7650+
hDev,
7651+
BitmapFormat.Bitmap_BGRA,
7652+
heapPtr,
7653+
stride,
7654+
);
7655+
this.pdfiumModule.FPDFBitmap_FillRect(bitmapPtr, 0, 0, wDev, hDev, 0x00000000);
7656+
7657+
const M = buildUserToDeviceMatrix(rect, rotation, wDev, hDev);
7658+
const mPtr = this.memoryManager.malloc(6 * 4);
7659+
const mView = new Float32Array(this.pdfiumModule.pdfium.HEAPF32.buffer, mPtr, 6);
7660+
mView.set([M.a, M.b, M.c, M.d, M.e, M.f]);
7661+
7662+
const FLAGS = RenderFlag.REVERSE_BYTE_ORDER;
7663+
let ok = false;
7664+
try {
7665+
ok = !!this.pdfiumModule.EPDF_RenderAnnotBitmap(
7666+
bitmapPtr,
7667+
pageCtx.pagePtr,
7668+
annotPtr,
7669+
mode,
7670+
mPtr,
7671+
FLAGS,
7672+
);
7673+
} finally {
7674+
this.memoryManager.free(mPtr);
7675+
this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
7676+
}
7677+
7678+
if (!ok) {
7679+
this.memoryManager.free(heapPtr);
7680+
return null;
7681+
}
7682+
7683+
const data = this.pdfiumModule.pdfium.HEAPU8.subarray(heapPtr, heapPtr + bytes);
7684+
const imageData: ImageDataLike = {
7685+
data: new Uint8ClampedArray(data),
7686+
width: wDev,
7687+
height: hDev,
7688+
};
7689+
this.memoryManager.free(heapPtr);
7690+
7691+
return { data: imageData, rect: annotRect };
7692+
}
7693+
74947694
private renderRectEncoded(
74957695
doc: PdfDocumentObject,
74967696
page: PdfPageObject,

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
PdfPrintOptions,
4040
PdfBookmarkObject,
4141
PdfAddAttachmentParams,
42+
AnnotationAppearanceMap,
4243
} from '@embedpdf/models';
4344
import { ExecuteRequest, Response, SpecificExecuteRequest } from './runner';
4445

@@ -451,6 +452,25 @@ export class WebWorkerEngine implements PdfEngine {
451452
return task;
452453
}
453454

455+
renderPageAnnotationsRaw(
456+
doc: PdfDocumentObject,
457+
page: PdfPageObject,
458+
options?: PdfRenderPageAnnotationOptions,
459+
) {
460+
this.logger.debug(LOG_SOURCE, LOG_CATEGORY, 'renderPageAnnotationsRaw', doc, page, options);
461+
const requestId = this.generateRequestId(doc.id);
462+
const task = new WorkerTask<AnnotationAppearanceMap>(this.worker, requestId);
463+
464+
const request: ExecuteRequest = createRequest(requestId, 'renderPageAnnotationsRaw', [
465+
doc,
466+
page,
467+
options,
468+
]);
469+
this.proxy(task, request);
470+
471+
return task;
472+
}
473+
454474
/**
455475
* {@inheritDoc @embedpdf/models!PdfEngine.getAllAnnotations}
456476
*

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ export class EngineRunner {
291291
case 'renderPageAnnotation':
292292
task = engine.renderPageAnnotation!(...args);
293293
break;
294+
case 'renderPageAnnotationsRaw':
295+
task = engine.renderPageAnnotationsRaw!(...args);
296+
break;
294297
case 'renderThumbnail':
295298
task = engine.renderThumbnail!(...args);
296299
break;

0 commit comments

Comments
 (0)