@embedpdf/engines 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pdfium.cjs CHANGED
@@ -912,11 +912,17 @@ class PdfiumEngine {
912
912
  let isSucceed = false;
913
913
  switch (annotation.type) {
914
914
  case models.PdfAnnotationSubtype.INK:
915
- isSucceed = this.addInkStroke(page, pageCtx.pagePtr, annotationPtr, annotation.inkList);
915
+ isSucceed = this.addInkStroke(page, pageCtx.pagePtr, annotationPtr, annotation);
916
916
  break;
917
917
  case models.PdfAnnotationSubtype.STAMP:
918
918
  isSucceed = this.addStampContent(ctx.docPtr, page, pageCtx.pagePtr, annotationPtr, annotation.rect, annotation.contents);
919
919
  break;
920
+ case models.PdfAnnotationSubtype.UNDERLINE:
921
+ case models.PdfAnnotationSubtype.STRIKEOUT:
922
+ case models.PdfAnnotationSubtype.SQUIGGLY:
923
+ case models.PdfAnnotationSubtype.HIGHLIGHT:
924
+ isSucceed = this.addTextMarkupContent(page, pageCtx.pagePtr, annotationPtr, annotation);
925
+ break;
920
926
  }
921
927
  if (!isSucceed) {
922
928
  this.pdfiumModule.FPDFPage_RemoveAnnot(pageCtx.pagePtr, annotationPtr);
@@ -927,90 +933,104 @@ class PdfiumEngine {
927
933
  message: 'can not add content of the annotation',
928
934
  });
929
935
  }
936
+ this.pdfiumModule.EPDFAnnot_GenerateAppearance(annotationPtr);
930
937
  this.pdfiumModule.FPDFPage_GenerateContent(pageCtx.pagePtr);
938
+ const annotId = this.pdfiumModule.FPDFPage_GetAnnotIndex(pageCtx.pagePtr, annotationPtr);
931
939
  this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
932
940
  pageCtx.release();
933
941
  this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `CreatePageAnnotation`, 'End', `${doc.id}-${page.index}`);
934
- return models.PdfTaskHelper.resolve(true);
942
+ return annotId >= 0
943
+ ? models.PdfTaskHelper.resolve(annotId)
944
+ : models.PdfTaskHelper.reject({
945
+ code: models.PdfErrorCode.CantCreateAnnot,
946
+ message: 'annotation created but index could not be determined',
947
+ });
935
948
  }
936
949
  /**
937
- * {@inheritDoc @embedpdf/models!PdfEngine.transformPageAnnotation}
950
+ * Update an existing page annotation in-place
938
951
  *
939
- * @public
952
+ * Locates the annot by page-local index (`annotation.id`)
953
+ * • Re-writes its /Rect and type-specific payload
954
+ * • Calls FPDFPage_GenerateContent so the new appearance is rendered
955
+ *
956
+ * @returns PdfTask<boolean> – true on success
940
957
  */
941
- transformPageAnnotation(doc, page, annotation, transformation) {
942
- this.logger.debug(LOG_SOURCE$1, LOG_CATEGORY$1, 'transformPageAnnotation', doc, page, annotation, transformation);
943
- this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `TransformPageAnnotation`, 'Begin', `${doc.id}-${page.index}`);
958
+ updatePageAnnotation(doc, page, annotation) {
959
+ this.logger.debug(LOG_SOURCE$1, LOG_CATEGORY$1, 'updatePageAnnotation', doc, page, annotation);
960
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'UpdatePageAnnotation', 'Begin', `${doc.id}-${page.index}`);
944
961
  const ctx = this.cache.getContext(doc.id);
945
962
  if (!ctx) {
946
- this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
963
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'UpdatePageAnnotation', 'End', `${doc.id}-${page.index}`);
947
964
  return models.PdfTaskHelper.reject({
948
965
  code: models.PdfErrorCode.DocNotOpen,
949
966
  message: 'document does not open',
950
967
  });
951
968
  }
952
969
  const pageCtx = ctx.acquirePage(page.index);
953
- const annotationPtr = this.pdfiumModule.FPDFPage_GetAnnot(pageCtx.pagePtr, annotation.id);
954
- const rect = {
955
- origin: {
956
- x: annotation.rect.origin.x + transformation.offset.x,
957
- y: annotation.rect.origin.y + transformation.offset.y,
958
- },
959
- size: {
960
- width: annotation.rect.size.width * transformation.scale.width,
961
- height: annotation.rect.size.height * transformation.scale.height,
962
- },
963
- };
964
- if (!this.setPageAnnoRect(page, pageCtx.pagePtr, annotationPtr, rect)) {
965
- this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
970
+ const annotPtr = this.pdfiumModule.FPDFPage_GetAnnot(pageCtx.pagePtr, annotation.id);
971
+ if (!annotPtr) {
972
+ pageCtx.release();
973
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'UpdatePageAnnotation', 'End', `${doc.id}-${page.index}`);
974
+ return models.PdfTaskHelper.reject({ code: models.PdfErrorCode.NotFound, message: 'annotation not found' });
975
+ }
976
+ /* 1 ── (re)set bounding-box ────────────────────────────────────────────── */
977
+ if (!this.setPageAnnoRect(page, pageCtx.pagePtr, annotPtr, annotation.rect)) {
978
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotPtr);
966
979
  pageCtx.release();
967
- this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
980
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'UpdatePageAnnotation', 'End', `${doc.id}-${page.index}`);
968
981
  return models.PdfTaskHelper.reject({
969
982
  code: models.PdfErrorCode.CantSetAnnotRect,
970
- message: 'can not set the rect of the annotation',
983
+ message: 'failed to move annotation',
971
984
  });
972
985
  }
986
+ /* 2 ── wipe previous payload and rebuild fresh one ─────────────────────── */
987
+ let ok = false;
973
988
  switch (annotation.type) {
974
- case models.PdfAnnotationSubtype.INK:
975
- {
976
- if (!this.pdfiumModule.FPDFAnnot_RemoveInkList(annotationPtr)) {
977
- this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
978
- pageCtx.release();
979
- this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
980
- return models.PdfTaskHelper.reject({
981
- code: models.PdfErrorCode.CantRemoveInkList,
982
- message: 'can not set the rect of the annotation',
983
- });
984
- }
985
- const inkList = annotation.inkList.map((inkStroke) => {
986
- return {
987
- points: inkStroke.points.map((point) => {
988
- return {
989
- x: rect.origin.x +
990
- (point.x - annotation.rect.origin.x) * transformation.scale.width,
991
- y: rect.origin.y +
992
- (point.y - annotation.rect.origin.y) * transformation.scale.height,
993
- };
994
- }),
995
- };
996
- });
997
- if (!this.addInkStroke(page, pageCtx.pagePtr, annotationPtr, inkList)) {
998
- this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
999
- pageCtx.release();
1000
- this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
1001
- return models.PdfTaskHelper.reject({
1002
- code: models.PdfErrorCode.CantAddInkStoke,
1003
- message: 'can not add stroke to the ink list of annotation',
1004
- });
1005
- }
989
+ /* ── Ink ─────────────────────────────────────────────────────────────── */
990
+ case models.PdfAnnotationSubtype.INK: {
991
+ /* clear every existing stroke first */
992
+ if (!this.pdfiumModule.FPDFAnnot_RemoveInkList(annotPtr))
993
+ break;
994
+ ok = this.addInkStroke(page, pageCtx.pagePtr, annotPtr, annotation);
995
+ break;
996
+ }
997
+ /* ── Stamp ───────────────────────────────────────────────────────────── */
998
+ case models.PdfAnnotationSubtype.STAMP: {
999
+ /* drop every page-object inside the annot */
1000
+ for (let i = this.pdfiumModule.FPDFAnnot_GetObjectCount(annotPtr) - 1; i >= 0; i--) {
1001
+ this.pdfiumModule.FPDFAnnot_RemoveObject(annotPtr, i);
1006
1002
  }
1003
+ ok = this.addStampContent(ctx.docPtr, page, pageCtx.pagePtr, annotPtr, annotation.rect, annotation.contents);
1004
+ break;
1005
+ }
1006
+ /* ── Text-markup family ──────────────────────────────────────────────── */
1007
+ case models.PdfAnnotationSubtype.HIGHLIGHT:
1008
+ case models.PdfAnnotationSubtype.UNDERLINE:
1009
+ case models.PdfAnnotationSubtype.STRIKEOUT:
1010
+ case models.PdfAnnotationSubtype.SQUIGGLY: {
1011
+ /* replace quad-points / colour / strings in one go */
1012
+ ok = this.addTextMarkupContent(page, pageCtx.pagePtr, annotPtr, annotation);
1007
1013
  break;
1014
+ }
1015
+ /* ── Unsupported edits – fall through to error ───────────────────────── */
1016
+ default:
1017
+ ok = false;
1008
1018
  }
1009
- this.pdfiumModule.FPDFPage_GenerateContent(pageCtx.pagePtr);
1010
- this.pdfiumModule.FPDFPage_CloseAnnot(annotationPtr);
1019
+ /* 3 ── regenerate appearance if payload was changed ───────────────────── */
1020
+ if (ok) {
1021
+ this.pdfiumModule.EPDFAnnot_GenerateAppearance(annotPtr);
1022
+ this.pdfiumModule.FPDFPage_GenerateContent(pageCtx.pagePtr);
1023
+ }
1024
+ /* 4 ── tidy-up native handles ──────────────────────────────────────────── */
1025
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotPtr);
1011
1026
  pageCtx.release();
1012
- this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `TransformPageAnnotation`, 'End', `${doc.id}-${page.index}`);
1013
- return models.PdfTaskHelper.resolve(true);
1027
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'UpdatePageAnnotation', 'End', `${doc.id}-${page.index}`);
1028
+ return ok
1029
+ ? models.PdfTaskHelper.resolve(true)
1030
+ : models.PdfTaskHelper.reject({
1031
+ code: models.PdfErrorCode.CantSetAnnotContent,
1032
+ message: 'failed to update annotation',
1033
+ });
1014
1034
  }
1015
1035
  /**
1016
1036
  * {@inheritDoc @embedpdf/models!PdfEngine.removePageAnnotation}
@@ -1632,21 +1652,60 @@ class PdfiumEngine {
1632
1652
  *
1633
1653
  * @private
1634
1654
  */
1635
- addInkStroke(page, pagePtr, annotationPtr, inkList) {
1636
- for (const inkStroke of inkList) {
1637
- const inkPointsCount = inkStroke.points.length;
1638
- const inkPointsPtr = this.malloc(inkPointsCount * 8);
1639
- for (let i = 0; i < inkPointsCount; i++) {
1640
- const point = inkStroke.points[i];
1641
- const { x, y } = this.convertDevicePointToPagePoint(page, point);
1642
- this.pdfiumModule.pdfium.setValue(inkPointsPtr + i * 8, x, 'float');
1643
- this.pdfiumModule.pdfium.setValue(inkPointsPtr + i * 8 + 4, y, 'float');
1644
- }
1645
- if (this.pdfiumModule.FPDFAnnot_AddInkStroke(annotationPtr, inkPointsPtr, inkPointsCount) === -1) {
1646
- this.free(inkPointsPtr);
1647
- return false;
1648
- }
1649
- this.free(inkPointsPtr);
1655
+ addInkStroke(page, pagePtr, annotationPtr, annotation) {
1656
+ if (!this.setBorderStyle(annotationPtr, models.PdfAnnotationBorderStyle.SOLID, annotation.strokeWidth)) {
1657
+ return false;
1658
+ }
1659
+ if (!this.setPageAnnoRect(page, pagePtr, annotationPtr, annotation.rect)) {
1660
+ return false;
1661
+ }
1662
+ if (!this.setInkList(page, annotationPtr, annotation.inkList)) {
1663
+ return false;
1664
+ }
1665
+ if (!this.setAnnotString(annotationPtr, 'T', annotation.author || '')) {
1666
+ return false;
1667
+ }
1668
+ if (!this.setAnnotString(annotationPtr, 'M', models.dateToPdfDate(annotation.modified))) {
1669
+ return false;
1670
+ }
1671
+ if (!this.setAnnotationColor(annotationPtr, {
1672
+ color: annotation.color ?? '#FFFF00',
1673
+ opacity: annotation.opacity ?? 1,
1674
+ }, models.PdfAnnotationColorType.Color)) {
1675
+ return false;
1676
+ }
1677
+ return true;
1678
+ }
1679
+ /**
1680
+ * Add highlight content to annotation
1681
+ * @param page - page info
1682
+ * @param annotationPtr - pointer to highlight annotation
1683
+ * @param annotation - highlight annotation
1684
+ * @returns whether highlight content is added to annotation
1685
+ *
1686
+ * @private
1687
+ */
1688
+ addTextMarkupContent(page, pagePtr, annotationPtr, annotation) {
1689
+ if (!this.setPageAnnoRect(page, pagePtr, annotationPtr, annotation.rect)) {
1690
+ return false;
1691
+ }
1692
+ if (!this.syncQuadPointsAnno(page, annotationPtr, annotation.segmentRects)) {
1693
+ return false;
1694
+ }
1695
+ if (!this.setAnnotString(annotationPtr, 'Contents', annotation.contents ?? '')) {
1696
+ return false;
1697
+ }
1698
+ if (!this.setAnnotString(annotationPtr, 'T', annotation.author || '')) {
1699
+ return false;
1700
+ }
1701
+ if (!this.setAnnotString(annotationPtr, 'M', models.dateToPdfDate(annotation.modified))) {
1702
+ return false;
1703
+ }
1704
+ if (!this.setAnnotationColor(annotationPtr, {
1705
+ color: annotation.color ?? '#FFFF00',
1706
+ opacity: annotation.opacity ?? 1,
1707
+ }, models.PdfAnnotationColorType.Color)) {
1708
+ return false;
1650
1709
  }
1651
1710
  return true;
1652
1711
  }
@@ -2272,8 +2331,6 @@ class PdfiumEngine {
2272
2331
  annotation = this.readPdfCaretAnno(page, pageCtx.pagePtr, annotationPtr, index);
2273
2332
  }
2274
2333
  break;
2275
- case models.PdfAnnotationSubtype.POPUP:
2276
- break;
2277
2334
  default:
2278
2335
  {
2279
2336
  annotation = this.readPdfAnno(page, pageCtx.pagePtr, subType, annotationPtr, index);
@@ -2294,14 +2351,13 @@ class PdfiumEngine {
2294
2351
  *
2295
2352
  * @private
2296
2353
  */
2297
- readAnnotationColor(annotationPtr) {
2354
+ readAnnotationColor(annotationPtr, colorType = models.PdfAnnotationColorType.Color) {
2298
2355
  const rPtr = this.malloc(4);
2299
2356
  const gPtr = this.malloc(4);
2300
2357
  const bPtr = this.malloc(4);
2301
2358
  const aPtr = this.malloc(4);
2302
2359
  // colourType 0 = "colour" (stroke/fill); other types are interior/border
2303
- const ok = this.pdfiumModule.FPDFAnnot_GetColor(annotationPtr,
2304
- /* colorType = */ 0, rPtr, gPtr, bPtr, aPtr);
2360
+ const ok = this.pdfiumModule.EPDFAnnot_GetColor(annotationPtr, colorType, rPtr, gPtr, bPtr, aPtr);
2305
2361
  let colour;
2306
2362
  if (ok) {
2307
2363
  colour = {
@@ -2319,116 +2375,144 @@ class PdfiumEngine {
2319
2375
  }
2320
2376
  /* --------------------------------------------------------------------------- */
2321
2377
  /**
2322
- * Extract the fill (or, if absent, the stroke) colour from a **path object**
2323
- * inside an appearance stream.
2378
+ * Resolve the visible fill colour for **Highlight / Underline / StrikeOut /
2379
+ * Squiggly** markup annotations.
2324
2380
  *
2325
- * Works for simple highlights produced by Chrome, Preview, etc. that paint a
2326
- * single filled rectangle with the desired tint.
2381
+ * Resolution order (first non-`undefined` wins):
2382
+ * 1. `/C` dictionary entry fast, present in Acrobat / Office PDFs
2383
+ * 2. Appearance-stream objects – drills into paths & nested forms
2384
+ * 3. Hard-coded fallback (Acrobat-style opaque yellow)
2327
2385
  *
2328
- * @param pathPtr - pointer to a `FPDF_PAGEOBJECT` of type **PATH**
2329
- * @returns RGBA tuple or `undefined` when no colour is set on the path
2386
+ * @param annotationPtr - pointer to an `FPDF_ANNOTATION`
2387
+ * @param fallback - colour to use when the PDF stores no tint at all
2388
+ * @returns WebAlphaColor with hex color and opacity (0-1)
2330
2389
  *
2331
2390
  * @private
2332
2391
  */
2333
- getColorFromPath(pathPtr) {
2334
- const r = this.malloc(4), g = this.malloc(4), b = this.malloc(4), a = this.malloc(4);
2335
- const fillOk = this.pdfiumModule.FPDFPageObj_GetFillColor(pathPtr, r, g, b, a);
2336
- const strokeOk = !fillOk && // try stroke only if fill failed
2337
- this.pdfiumModule.FPDFPageObj_GetStrokeColor(pathPtr, r, g, b, a);
2338
- const ok = fillOk || strokeOk;
2339
- let c;
2340
- if (ok) {
2341
- c = {
2342
- red: this.pdfiumModule.pdfium.getValue(r, 'i32') & 0xff,
2343
- green: this.pdfiumModule.pdfium.getValue(g, 'i32') & 0xff,
2344
- blue: this.pdfiumModule.pdfium.getValue(b, 'i32') & 0xff,
2345
- alpha: this.pdfiumModule.pdfium.getValue(a, 'i32') & 0xff,
2346
- };
2347
- }
2348
- this.free(r);
2349
- this.free(g);
2350
- this.free(b);
2351
- this.free(a);
2352
- return c;
2392
+ resolveAnnotationColor(annotationPtr, colorType = models.PdfAnnotationColorType.Color, fallback = { red: 255, green: 245, blue: 155, alpha: 255 }) {
2393
+ const pdfColor = this.readAnnotationColor(annotationPtr, colorType) ?? fallback;
2394
+ return models.pdfAlphaColorToWebAlphaColor(pdfColor);
2353
2395
  }
2354
- /* --------------------------------------------------------------------------- */
2355
2396
  /**
2356
- * Recursively walk a page-object tree (PATHs and nested FORM XObjects) until
2357
- * a colour can be extracted.
2358
- *
2359
- * Acrobat often wraps its highlight rectangle in a Form XObject referenced by
2360
- * the "Do" operator, so this function drills down unlimited depth.
2397
+ * Set the fill/stroke colour for a **Highlight / Underline / StrikeOut / Squiggly** markup annotation.
2361
2398
  *
2362
- * @param objPtr - pointer to a `FPDF_PAGEOBJECT`
2363
- * @returns First RGBA tint found, or `undefined` if none of the descendants
2364
- * carry an explicit fill/stroke colour
2399
+ * @param annotationPtr - pointer to the annotation whose colour is being set
2400
+ * @param webAlphaColor - WebAlphaColor with hex color and opacity (0-1)
2401
+ * @param shouldClearAP - whether to clear the /AP entry
2402
+ * @param which - which colour to set (0 = fill, 1 = stroke)
2403
+ * @returns `true` if the operation was successful
2365
2404
  *
2366
2405
  * @private
2367
2406
  */
2368
- walkPageObjTree(objPtr) {
2369
- const type = this.pdfiumModule.FPDFPageObj_GetType(objPtr);
2370
- if (type === models.PdfPageObjectType.PATH)
2371
- return this.getColorFromPath(objPtr);
2372
- if (type !== models.PdfPageObjectType.FORM)
2373
- return undefined;
2374
- const cnt = this.pdfiumModule.FPDFFormObj_CountObjects(objPtr);
2375
- for (let i = 0; i < cnt; i++) {
2376
- const child = this.pdfiumModule.FPDFFormObj_GetObject(objPtr, i);
2377
- if (!child)
2378
- continue;
2379
- const c = this.walkPageObjTree(child);
2380
- if (c)
2381
- return c;
2382
- }
2383
- return undefined;
2407
+ setAnnotationColor(annotationPtr, webAlphaColor, colorType = models.PdfAnnotationColorType.Color) {
2408
+ const pdfAlphaColor = models.webAlphaColorToPdfAlphaColor(webAlphaColor);
2409
+ return this.pdfiumModule.EPDFAnnot_SetColor(annotationPtr, colorType, pdfAlphaColor.red & 0xff, pdfAlphaColor.green & 0xff, pdfAlphaColor.blue & 0xff, (pdfAlphaColor.alpha ?? 255) & 0xff);
2384
2410
  }
2385
- /* --------------------------------------------------------------------------- */
2386
2411
  /**
2387
- * Iterate over every top-level object in the annotation's **appearance stream**
2388
- * and invoke {@link walkPageObjTree} to locate a usable tint.
2412
+ * Border‐style + width helper
2389
2413
  *
2390
- * Catches:
2391
- * Simple filled path (Preview, Chrome)
2392
- * • Form XObject containing the path (Acrobat)
2414
+ * Tries the new PDFium helper `EPDFAnnot_GetBorderStyle()` (patch series
2415
+ * 9 July 2025).
2393
2416
  *
2394
- * @param annotPtr - pointer to an `FPDF_ANNOTATION`
2395
- * @returns RGBA tuple or `undefined` when no colour can be resolved from AP
2417
+ * @param annotationPtr pointer to an `FPDF_ANNOTATION`
2418
+ * @returns `{ ok, style, width }`
2419
+ * • `ok` – `true` when the call succeeded
2420
+ * • `style` – `PdfAnnotationBorderStyle` enum
2421
+ * • `width` – stroke-width in points (defaults to 0 pt)
2422
+ */
2423
+ getBorderStyle(annotationPtr) {
2424
+ /* 1 ── allocate tmp storage for the returned width ─────────────── */
2425
+ const widthPtr = this.malloc(4);
2426
+ let width = 0;
2427
+ let style = models.PdfAnnotationBorderStyle.UNKNOWN;
2428
+ let ok = false;
2429
+ style = this.pdfiumModule.EPDFAnnot_GetBorderStyle(annotationPtr, widthPtr);
2430
+ width = this.pdfiumModule.pdfium.getValue(widthPtr, 'float');
2431
+ ok = style !== models.PdfAnnotationBorderStyle.UNKNOWN;
2432
+ this.free(widthPtr);
2433
+ return { ok, style, width };
2434
+ }
2435
+ setBorderStyle(annotationPtr, style, width) {
2436
+ return this.pdfiumModule.EPDFAnnot_SetBorderStyle(annotationPtr, style, width);
2437
+ }
2438
+ /**
2439
+ * Border-effect (“cloudy”) helper
2396
2440
  *
2397
- * @private
2441
+ * Calls the new PDFium function `EPDFAnnot_GetBorderEffect()` (July 2025).
2442
+ *
2443
+ * @param annotationPtr pointer to an `FPDF_ANNOTATION`
2444
+ * @returns `{ ok, intensity }`
2445
+ * • `ok` – `true` when the annotation *does* have a
2446
+ * valid cloudy-border effect
2447
+ * • `intensity` – radius/intensity value (0 when `ok` is false)
2398
2448
  */
2399
- colorFromAppearance(annotPtr) {
2400
- const n = this.pdfiumModule.FPDFAnnot_GetObjectCount(annotPtr);
2401
- for (let i = 0; i < n; i++) {
2402
- const obj = this.pdfiumModule.FPDFAnnot_GetObject(annotPtr, i);
2403
- if (!obj)
2404
- continue;
2405
- const c = this.walkPageObjTree(obj);
2406
- if (c)
2407
- return c;
2408
- }
2409
- return undefined;
2449
+ getBorderEffect(annotationPtr) {
2450
+ const intensityPtr = this.malloc(4);
2451
+ const ok = !!this.pdfiumModule.EPDFAnnot_GetBorderEffect(annotationPtr, intensityPtr);
2452
+ const intensity = ok ? this.pdfiumModule.pdfium.getValue(intensityPtr, 'float') : 0;
2453
+ this.free(intensityPtr);
2454
+ return { ok, intensity };
2410
2455
  }
2411
- /* --------------------------------------------------------------------------- */
2412
2456
  /**
2413
- * Resolve the visible fill colour for **Highlight / Underline / StrikeOut /
2414
- * Squiggly** markup annotations.
2457
+ * Rectangle-differences helper ( /RD array on Square / Circle annots )
2415
2458
  *
2416
- * Resolution order (first non-`undefined` wins):
2417
- * 1. `/C` dictionary entry – fast, present in Acrobat / Office PDFs
2418
- * 2. Appearance-stream objects – drills into paths & nested forms
2419
- * 3. Hard-coded fallback (Acrobat-style opaque yellow)
2459
+ * Calls `EPDFAnnot_GetRectangleDifferences()` introduced in July 2025.
2420
2460
  *
2421
- * @param annotationPtr - pointer to an `FPDF_ANNOTATION`
2422
- * @param fallback - colour to use when the PDF stores no tint at all
2423
- * @returns Guaranteed RGBA tuple (never `undefined`)
2461
+ * @param annotationPtr pointer to an `FPDF_ANNOTATION`
2462
+ * @returns `{ ok, left, top, right, bottom }`
2463
+ * `ok` – `true` when the annotation *has* an /RD entry
2464
+ * • the four floats are 0 when `ok` is false
2465
+ */
2466
+ getRectangleDifferences(annotationPtr) {
2467
+ /* tmp storage ─────────────────────────────────────────── */
2468
+ const lPtr = this.malloc(4);
2469
+ const tPtr = this.malloc(4);
2470
+ const rPtr = this.malloc(4);
2471
+ const bPtr = this.malloc(4);
2472
+ const ok = !!this.pdfiumModule.EPDFAnnot_GetRectangleDifferences(annotationPtr, lPtr, tPtr, rPtr, bPtr);
2473
+ const pdf = this.pdfiumModule.pdfium;
2474
+ const left = pdf.getValue(lPtr, 'float');
2475
+ const top = pdf.getValue(tPtr, 'float');
2476
+ const right = pdf.getValue(rPtr, 'float');
2477
+ const bottom = pdf.getValue(bPtr, 'float');
2478
+ /* cleanup ─────────────────────────────────────────────── */
2479
+ this.free(lPtr);
2480
+ this.free(tPtr);
2481
+ this.free(rPtr);
2482
+ this.free(bPtr);
2483
+ return { ok, left, top, right, bottom };
2484
+ }
2485
+ /**
2486
+ * Dash-pattern helper ( /BS → /D array, dashed borders only )
2424
2487
  *
2425
- * @private
2488
+ * Uses the two new PDFium helpers:
2489
+ * • `EPDFAnnot_GetBorderDashPatternCount`
2490
+ * • `EPDFAnnot_GetBorderDashPattern`
2491
+ *
2492
+ * @param annotationPtr pointer to an `FPDF_ANNOTATION`
2493
+ * @returns `{ ok, pattern }`
2494
+ * • `ok` – `true` when the annot is dashed *and* the array
2495
+ * was retrieved successfully
2496
+ * • `pattern` – numeric array of dash/space lengths (empty when `ok` is false)
2426
2497
  */
2427
- resolveAnnotationColor(annotationPtr, fallback = { red: 255, green: 245, blue: 155, alpha: 255 }) {
2428
- return (this.readAnnotationColor(annotationPtr) ?? // 1 – /C entry
2429
- this.colorFromAppearance(annotationPtr) ?? // 2 – AP stream walk
2430
- fallback // 3 default
2431
- );
2498
+ getBorderDashPattern(annotationPtr) {
2499
+ const count = this.pdfiumModule.EPDFAnnot_GetBorderDashPatternCount(annotationPtr);
2500
+ if (count === 0) {
2501
+ return { ok: false, pattern: [] };
2502
+ }
2503
+ /* allocate `count` floats on the WASM heap */
2504
+ const arrPtr = this.malloc(4 * count);
2505
+ const okNative = !!this.pdfiumModule.EPDFAnnot_GetBorderDashPattern(annotationPtr, arrPtr, count);
2506
+ /* copy out */
2507
+ const pattern = [];
2508
+ if (okNative) {
2509
+ const pdf = this.pdfiumModule.pdfium;
2510
+ for (let i = 0; i < count; i++) {
2511
+ pattern.push(pdf.getValue(arrPtr + 4 * i, 'float'));
2512
+ }
2513
+ }
2514
+ this.free(arrPtr);
2515
+ return { ok: okNative, pattern };
2432
2516
  }
2433
2517
  /**
2434
2518
  * Read `/QuadPoints` from any annotation and convert each quadrilateral to
@@ -2441,11 +2525,11 @@ class PdfiumEngine {
2441
2525
  *
2442
2526
  * @param page - logical page info object (`PdfPageObject`)
2443
2527
  * @param annotationPtr - pointer to the annotation whose quads are needed
2444
- * @returns Array of `Quad` objects (`[]` if the annotation has no quads)
2528
+ * @returns Array of `Rect` objects (`[]` if the annotation has no quads)
2445
2529
  *
2446
2530
  * @private
2447
2531
  */
2448
- readAnnotationQuads(page, annotationPtr) {
2532
+ getQuadPointsAnno(page, annotationPtr) {
2449
2533
  const quadCount = this.pdfiumModule.FPDFAnnot_CountAttachmentPoints(annotationPtr);
2450
2534
  if (quadCount === 0)
2451
2535
  return [];
@@ -2472,7 +2556,116 @@ class PdfiumEngine {
2472
2556
  }
2473
2557
  this.free(quadPtr);
2474
2558
  }
2475
- return quads;
2559
+ return quads.map(models.quadToRect);
2560
+ }
2561
+ /**
2562
+ * Set the quadrilaterals for a **Highlight / Underline / StrikeOut / Squiggly** markup annotation.
2563
+ *
2564
+ * @param page - logical page info object (`PdfPageObject`)
2565
+ * @param annotationPtr - pointer to the annotation whose quads are needed
2566
+ * @param rects - array of `Rect` objects (`[]` if the annotation has no quads)
2567
+ * @returns `true` if the operation was successful
2568
+ *
2569
+ * @private
2570
+ */
2571
+ syncQuadPointsAnno(page, annotPtr, rects) {
2572
+ const FS_QUADPOINTSF_SIZE = 8 * 4; // eight floats, 32 bytes
2573
+ const pdf = this.pdfiumModule.pdfium;
2574
+ const count = this.pdfiumModule.FPDFAnnot_CountAttachmentPoints(annotPtr);
2575
+ const buf = this.malloc(FS_QUADPOINTSF_SIZE);
2576
+ /** write one quad into `buf` in annotation space */
2577
+ const writeQuad = (r) => {
2578
+ const q = models.rectToQuad(r); // TL, TR, BR, BL
2579
+ const p1 = this.convertDevicePointToPagePoint(page, q.p1);
2580
+ const p2 = this.convertDevicePointToPagePoint(page, q.p2);
2581
+ const p3 = this.convertDevicePointToPagePoint(page, q.p3); // BR
2582
+ const p4 = this.convertDevicePointToPagePoint(page, q.p4); // BL
2583
+ // PDF QuadPoints order: BL, BR, TL, TR (bottom-left, bottom-right, top-left, top-right)
2584
+ pdf.setValue(buf + 0, p1.x, 'float'); // BL (bottom-left)
2585
+ pdf.setValue(buf + 4, p1.y, 'float');
2586
+ pdf.setValue(buf + 8, p2.x, 'float'); // BR (bottom-right)
2587
+ pdf.setValue(buf + 12, p2.y, 'float');
2588
+ pdf.setValue(buf + 16, p4.x, 'float'); // TL (top-left)
2589
+ pdf.setValue(buf + 20, p4.y, 'float');
2590
+ pdf.setValue(buf + 24, p3.x, 'float'); // TR (top-right)
2591
+ pdf.setValue(buf + 28, p3.y, 'float');
2592
+ };
2593
+ /* ----------------------------------------------------------------------- */
2594
+ /* 1. overwrite the quads that already exist */
2595
+ const min = Math.min(count, rects.length);
2596
+ for (let i = 0; i < min; i++) {
2597
+ writeQuad(rects[i]);
2598
+ if (!this.pdfiumModule.FPDFAnnot_SetAttachmentPoints(annotPtr, i, buf)) {
2599
+ this.free(buf);
2600
+ return false;
2601
+ }
2602
+ }
2603
+ /* 2. append new quads if rects.length > count */
2604
+ for (let i = count; i < rects.length; i++) {
2605
+ writeQuad(rects[i]);
2606
+ if (!this.pdfiumModule.FPDFAnnot_AppendAttachmentPoints(annotPtr, buf)) {
2607
+ this.free(buf);
2608
+ return false;
2609
+ }
2610
+ }
2611
+ this.free(buf);
2612
+ return true;
2613
+ }
2614
+ /**
2615
+ * Read ink list from annotation
2616
+ * @param page - logical page info object (`PdfPageObject`)
2617
+ * @param annotationPtr - pointer to the annotation whose ink list is needed
2618
+ * @returns ink list
2619
+ */
2620
+ getInkList(page, annotationPtr) {
2621
+ const inkList = [];
2622
+ const count = this.pdfiumModule.FPDFAnnot_GetInkListCount(annotationPtr);
2623
+ for (let i = 0; i < count; i++) {
2624
+ const points = [];
2625
+ const pointsCount = this.pdfiumModule.FPDFAnnot_GetInkListPath(annotationPtr, i, 0, 0);
2626
+ if (pointsCount > 0) {
2627
+ const pointMemorySize = 8;
2628
+ const pointsPtr = this.malloc(pointsCount * pointMemorySize);
2629
+ this.pdfiumModule.FPDFAnnot_GetInkListPath(annotationPtr, i, pointsPtr, pointsCount);
2630
+ for (let j = 0; j < pointsCount; j++) {
2631
+ const pointX = this.pdfiumModule.pdfium.getValue(pointsPtr + j * 8, 'float');
2632
+ const pointY = this.pdfiumModule.pdfium.getValue(pointsPtr + j * 8 + 4, 'float');
2633
+ const { x, y } = this.convertPagePointToDevicePoint(page, {
2634
+ x: pointX,
2635
+ y: pointY,
2636
+ });
2637
+ points.push({ x, y });
2638
+ }
2639
+ this.free(pointsPtr);
2640
+ }
2641
+ inkList.push({ points });
2642
+ }
2643
+ return inkList;
2644
+ }
2645
+ /**
2646
+ * Add ink list to annotation
2647
+ * @param page - logical page info object (`PdfPageObject`)
2648
+ * @param annotationPtr - pointer to the annotation whose ink list is needed
2649
+ * @param annotation - annotation object (`PdfInkAnnoObject`)
2650
+ * @returns `true` if the operation was successful
2651
+ */
2652
+ setInkList(page, annotationPtr, inkList) {
2653
+ for (const inkStroke of inkList) {
2654
+ const inkPointsCount = inkStroke.points.length;
2655
+ const inkPointsPtr = this.malloc(inkPointsCount * 8);
2656
+ for (let i = 0; i < inkPointsCount; i++) {
2657
+ const point = inkStroke.points[i];
2658
+ const { x, y } = this.convertDevicePointToPagePoint(page, point);
2659
+ this.pdfiumModule.pdfium.setValue(inkPointsPtr + i * 8, x, 'float');
2660
+ this.pdfiumModule.pdfium.setValue(inkPointsPtr + i * 8 + 4, y, 'float');
2661
+ }
2662
+ if (this.pdfiumModule.FPDFAnnot_AddInkStroke(annotationPtr, inkPointsPtr, inkPointsCount) === -1) {
2663
+ this.free(inkPointsPtr);
2664
+ return false;
2665
+ }
2666
+ this.free(inkPointsPtr);
2667
+ }
2668
+ return true;
2476
2669
  }
2477
2670
  /**
2478
2671
  * Read pdf text annotation
@@ -2485,30 +2678,23 @@ class PdfiumEngine {
2485
2678
  * @private
2486
2679
  */
2487
2680
  readPdfTextAnno(page, pagePtr, annotationPtr, index) {
2488
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2489
2681
  const annoRect = this.readPageAnnoRect(annotationPtr);
2490
2682
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, annoRect);
2491
2683
  const author = this.getAnnotString(annotationPtr, 'T');
2492
2684
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2493
- const modified = this.toIsoDate(modifiedRaw);
2685
+ const modified = models.pdfDateToDate(modifiedRaw);
2494
2686
  const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
2495
2687
  const state = this.getAnnotString(annotationPtr, 'State');
2496
2688
  const stateModel = this.getAnnotString(annotationPtr, 'StateModel');
2497
- const color = this.resolveAnnotationColor(annotationPtr);
2689
+ const webAlphaColor = this.resolveAnnotationColor(annotationPtr);
2498
2690
  const inReplyToId = this.getInReplyToId(pagePtr, annotationPtr);
2499
- const popup = !inReplyToId
2500
- ? this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index)
2501
- : undefined;
2502
2691
  return {
2503
- status: models.PdfAnnotationObjectStatus.Committed,
2504
2692
  pageIndex: page.index,
2505
2693
  id: index,
2506
2694
  type: models.PdfAnnotationSubtype.TEXT,
2507
2695
  contents,
2508
- color,
2696
+ ...webAlphaColor,
2509
2697
  rect,
2510
- popup,
2511
- appearances,
2512
2698
  inReplyToId,
2513
2699
  author,
2514
2700
  modified,
@@ -2527,16 +2713,13 @@ class PdfiumEngine {
2527
2713
  * @private
2528
2714
  */
2529
2715
  readPdfFreeTextAnno(page, pagePtr, annotationPtr, index) {
2530
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2531
2716
  const annoRect = this.readPageAnnoRect(annotationPtr);
2532
2717
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, annoRect);
2533
2718
  const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
2534
2719
  const author = this.getAnnotString(annotationPtr, 'T');
2535
2720
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2536
- const modified = this.toIsoDate(modifiedRaw);
2537
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2721
+ const modified = models.pdfDateToDate(modifiedRaw);
2538
2722
  return {
2539
- status: models.PdfAnnotationObjectStatus.Committed,
2540
2723
  pageIndex: page.index,
2541
2724
  id: index,
2542
2725
  type: models.PdfAnnotationSubtype.FREETEXT,
@@ -2544,8 +2727,6 @@ class PdfiumEngine {
2544
2727
  author,
2545
2728
  modified,
2546
2729
  rect,
2547
- popup,
2548
- appearances,
2549
2730
  };
2550
2731
  }
2551
2732
  /**
@@ -2565,13 +2746,12 @@ class PdfiumEngine {
2565
2746
  if (!linkPtr) {
2566
2747
  return;
2567
2748
  }
2568
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2569
2749
  const annoRect = this.readPageAnnoRect(annotationPtr);
2570
2750
  const { left, top, right, bottom } = annoRect;
2571
2751
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, annoRect);
2572
2752
  const author = this.getAnnotString(annotationPtr, 'T');
2573
2753
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2574
- const modified = this.toIsoDate(modifiedRaw);
2754
+ const modified = models.pdfDateToDate(modifiedRaw);
2575
2755
  const utf16Length = this.pdfiumModule.FPDFText_GetBoundedText(textPagePtr, left, top, right, bottom, 0, 0);
2576
2756
  const bytesCount = (utf16Length + 1) * 2; // include NIL
2577
2757
  const textBufferPtr = this.malloc(bytesCount);
@@ -2583,17 +2763,13 @@ class PdfiumEngine {
2583
2763
  }, () => {
2584
2764
  return this.pdfiumModule.FPDFLink_GetDest(docPtr, linkPtr);
2585
2765
  });
2586
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2587
2766
  return {
2588
- status: models.PdfAnnotationObjectStatus.Committed,
2589
2767
  pageIndex: page.index,
2590
2768
  id: index,
2591
2769
  type: models.PdfAnnotationSubtype.LINK,
2592
2770
  text,
2593
2771
  target,
2594
2772
  rect,
2595
- popup,
2596
- appearances,
2597
2773
  author,
2598
2774
  modified,
2599
2775
  };
@@ -2610,23 +2786,18 @@ class PdfiumEngine {
2610
2786
  * @private
2611
2787
  */
2612
2788
  readPdfWidgetAnno(page, pagePtr, annotationPtr, formHandle, index) {
2613
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2614
2789
  const pageRect = this.readPageAnnoRect(annotationPtr);
2615
2790
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2616
2791
  const author = this.getAnnotString(annotationPtr, 'T');
2617
2792
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2618
- const modified = this.toIsoDate(modifiedRaw);
2619
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2793
+ const modified = models.pdfDateToDate(modifiedRaw);
2620
2794
  const field = this.readPdfWidgetAnnoField(formHandle, annotationPtr);
2621
2795
  return {
2622
- status: models.PdfAnnotationObjectStatus.Committed,
2623
2796
  pageIndex: page.index,
2624
2797
  id: index,
2625
2798
  type: models.PdfAnnotationSubtype.WIDGET,
2626
2799
  rect,
2627
2800
  field,
2628
- popup,
2629
- appearances,
2630
2801
  author,
2631
2802
  modified,
2632
2803
  };
@@ -2642,21 +2813,16 @@ class PdfiumEngine {
2642
2813
  * @private
2643
2814
  */
2644
2815
  readPdfFileAttachmentAnno(page, pagePtr, annotationPtr, index) {
2645
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2646
2816
  const pageRect = this.readPageAnnoRect(annotationPtr);
2647
2817
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2648
2818
  const author = this.getAnnotString(annotationPtr, 'T');
2649
2819
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2650
- const modified = this.toIsoDate(modifiedRaw);
2651
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2820
+ const modified = models.pdfDateToDate(modifiedRaw);
2652
2821
  return {
2653
- status: models.PdfAnnotationObjectStatus.Committed,
2654
2822
  pageIndex: page.index,
2655
2823
  id: index,
2656
2824
  type: models.PdfAnnotationSubtype.FILEATTACHMENT,
2657
2825
  rect,
2658
- popup,
2659
- appearances,
2660
2826
  author,
2661
2827
  modified,
2662
2828
  };
@@ -2672,44 +2838,22 @@ class PdfiumEngine {
2672
2838
  * @private
2673
2839
  */
2674
2840
  readPdfInkAnno(page, pagePtr, annotationPtr, index) {
2675
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2676
2841
  const pageRect = this.readPageAnnoRect(annotationPtr);
2677
2842
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2678
2843
  const author = this.getAnnotString(annotationPtr, 'T');
2679
2844
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2680
- const modified = this.toIsoDate(modifiedRaw);
2681
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2682
- const inkList = [];
2683
- const count = this.pdfiumModule.FPDFAnnot_GetInkListCount(annotationPtr);
2684
- for (let i = 0; i < count; i++) {
2685
- const points = [];
2686
- const pointsCount = this.pdfiumModule.FPDFAnnot_GetInkListPath(annotationPtr, i, 0, 0);
2687
- if (pointsCount > 0) {
2688
- const pointMemorySize = 8;
2689
- const pointsPtr = this.malloc(pointsCount * pointMemorySize);
2690
- this.pdfiumModule.FPDFAnnot_GetInkListPath(annotationPtr, i, pointsPtr, pointsCount);
2691
- for (let j = 0; j < pointsCount; j++) {
2692
- const pointX = this.pdfiumModule.pdfium.getValue(pointsPtr + j * 8, 'float');
2693
- const pointY = this.pdfiumModule.pdfium.getValue(pointsPtr + j * 8 + 4, 'float');
2694
- const { x, y } = this.convertPagePointToDevicePoint(page, {
2695
- x: pointX,
2696
- y: pointY,
2697
- });
2698
- points.push({ x, y });
2699
- }
2700
- this.free(pointsPtr);
2701
- }
2702
- inkList.push({ points });
2703
- }
2845
+ const modified = models.pdfDateToDate(modifiedRaw);
2846
+ const webAlphaColor = this.resolveAnnotationColor(annotationPtr);
2847
+ const { width: strokeWidth } = this.getBorderStyle(annotationPtr);
2848
+ const inkList = this.getInkList(page, annotationPtr);
2704
2849
  return {
2705
- status: models.PdfAnnotationObjectStatus.Committed,
2706
2850
  pageIndex: page.index,
2707
2851
  id: index,
2708
2852
  type: models.PdfAnnotationSubtype.INK,
2853
+ ...webAlphaColor,
2854
+ strokeWidth,
2709
2855
  rect,
2710
- popup,
2711
2856
  inkList,
2712
- appearances,
2713
2857
  author,
2714
2858
  modified,
2715
2859
  };
@@ -2725,23 +2869,18 @@ class PdfiumEngine {
2725
2869
  * @private
2726
2870
  */
2727
2871
  readPdfPolygonAnno(page, pagePtr, annotationPtr, index) {
2728
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2729
2872
  const pageRect = this.readPageAnnoRect(annotationPtr);
2730
2873
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2731
2874
  const author = this.getAnnotString(annotationPtr, 'T');
2732
2875
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2733
- const modified = this.toIsoDate(modifiedRaw);
2734
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2876
+ const modified = models.pdfDateToDate(modifiedRaw);
2735
2877
  const vertices = this.readPdfAnnoVertices(page, pagePtr, annotationPtr);
2736
2878
  return {
2737
- status: models.PdfAnnotationObjectStatus.Committed,
2738
2879
  pageIndex: page.index,
2739
2880
  id: index,
2740
2881
  type: models.PdfAnnotationSubtype.POLYGON,
2741
2882
  rect,
2742
- popup,
2743
2883
  vertices,
2744
- appearances,
2745
2884
  author,
2746
2885
  modified,
2747
2886
  };
@@ -2757,23 +2896,18 @@ class PdfiumEngine {
2757
2896
  * @private
2758
2897
  */
2759
2898
  readPdfPolylineAnno(page, pagePtr, annotationPtr, index) {
2760
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2761
2899
  const pageRect = this.readPageAnnoRect(annotationPtr);
2762
2900
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2763
2901
  const author = this.getAnnotString(annotationPtr, 'T');
2764
2902
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2765
- const modified = this.toIsoDate(modifiedRaw);
2766
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2903
+ const modified = models.pdfDateToDate(modifiedRaw);
2767
2904
  const vertices = this.readPdfAnnoVertices(page, pagePtr, annotationPtr);
2768
2905
  return {
2769
- status: models.PdfAnnotationObjectStatus.Committed,
2770
2906
  pageIndex: page.index,
2771
2907
  id: index,
2772
2908
  type: models.PdfAnnotationSubtype.POLYLINE,
2773
2909
  rect,
2774
- popup,
2775
2910
  vertices,
2776
- appearances,
2777
2911
  author,
2778
2912
  modified,
2779
2913
  };
@@ -2789,13 +2923,11 @@ class PdfiumEngine {
2789
2923
  * @private
2790
2924
  */
2791
2925
  readPdfLineAnno(page, pagePtr, annotationPtr, index) {
2792
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2793
2926
  const pageRect = this.readPageAnnoRect(annotationPtr);
2794
2927
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2795
2928
  const author = this.getAnnotString(annotationPtr, 'T');
2796
2929
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2797
- const modified = this.toIsoDate(modifiedRaw);
2798
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2930
+ const modified = models.pdfDateToDate(modifiedRaw);
2799
2931
  const startPointPtr = this.malloc(8);
2800
2932
  const endPointPtr = this.malloc(8);
2801
2933
  this.pdfiumModule.FPDFAnnot_GetLine(annotationPtr, startPointPtr, endPointPtr);
@@ -2814,15 +2946,12 @@ class PdfiumEngine {
2814
2946
  this.free(startPointPtr);
2815
2947
  this.free(endPointPtr);
2816
2948
  return {
2817
- status: models.PdfAnnotationObjectStatus.Committed,
2818
2949
  pageIndex: page.index,
2819
2950
  id: index,
2820
2951
  type: models.PdfAnnotationSubtype.LINE,
2821
2952
  rect,
2822
- popup,
2823
2953
  startPoint,
2824
2954
  endPoint,
2825
- appearances,
2826
2955
  author,
2827
2956
  modified,
2828
2957
  };
@@ -2838,25 +2967,22 @@ class PdfiumEngine {
2838
2967
  * @private
2839
2968
  */
2840
2969
  readPdfHighlightAnno(page, pagePtr, annotationPtr, index) {
2841
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2842
2970
  const pageRect = this.readPageAnnoRect(annotationPtr);
2843
2971
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2844
- const quads = this.readAnnotationQuads(page, annotationPtr);
2845
- const color = this.resolveAnnotationColor(annotationPtr);
2846
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
2972
+ const segmentRects = this.getQuadPointsAnno(page, annotationPtr);
2973
+ const webAlphaColor = this.resolveAnnotationColor(annotationPtr);
2847
2974
  const author = this.getAnnotString(annotationPtr, 'T');
2848
2975
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2849
- const modified = this.toIsoDate(modifiedRaw);
2976
+ const modified = models.pdfDateToDate(modifiedRaw);
2977
+ const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
2850
2978
  return {
2851
- status: models.PdfAnnotationObjectStatus.Committed,
2852
2979
  pageIndex: page.index,
2853
2980
  id: index,
2854
2981
  type: models.PdfAnnotationSubtype.HIGHLIGHT,
2855
2982
  rect,
2856
- popup,
2857
- appearances,
2858
- segmentRects: quads.map(models.quadToRect),
2859
- color,
2983
+ contents,
2984
+ segmentRects,
2985
+ ...webAlphaColor,
2860
2986
  author,
2861
2987
  modified,
2862
2988
  };
@@ -2872,21 +2998,22 @@ class PdfiumEngine {
2872
2998
  * @private
2873
2999
  */
2874
3000
  readPdfUnderlineAnno(page, pagePtr, annotationPtr, index) {
2875
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2876
3001
  const pageRect = this.readPageAnnoRect(annotationPtr);
2877
3002
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2878
3003
  const author = this.getAnnotString(annotationPtr, 'T');
2879
3004
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2880
- const modified = this.toIsoDate(modifiedRaw);
2881
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3005
+ const modified = models.pdfDateToDate(modifiedRaw);
3006
+ const segmentRects = this.getQuadPointsAnno(page, annotationPtr);
3007
+ const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
3008
+ const webAlphaColor = this.resolveAnnotationColor(annotationPtr);
2882
3009
  return {
2883
- status: models.PdfAnnotationObjectStatus.Committed,
2884
3010
  pageIndex: page.index,
2885
3011
  id: index,
2886
3012
  type: models.PdfAnnotationSubtype.UNDERLINE,
2887
3013
  rect,
2888
- popup,
2889
- appearances,
3014
+ contents,
3015
+ segmentRects,
3016
+ ...webAlphaColor,
2890
3017
  author,
2891
3018
  modified,
2892
3019
  };
@@ -2902,21 +3029,22 @@ class PdfiumEngine {
2902
3029
  * @private
2903
3030
  */
2904
3031
  readPdfStrikeOutAnno(page, pagePtr, annotationPtr, index) {
2905
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2906
3032
  const pageRect = this.readPageAnnoRect(annotationPtr);
2907
3033
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2908
3034
  const author = this.getAnnotString(annotationPtr, 'T');
2909
3035
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2910
- const modified = this.toIsoDate(modifiedRaw);
2911
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3036
+ const modified = models.pdfDateToDate(modifiedRaw);
3037
+ const segmentRects = this.getQuadPointsAnno(page, annotationPtr);
3038
+ const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
3039
+ const webAlphaColor = this.resolveAnnotationColor(annotationPtr);
2912
3040
  return {
2913
- status: models.PdfAnnotationObjectStatus.Committed,
2914
3041
  pageIndex: page.index,
2915
3042
  id: index,
2916
3043
  type: models.PdfAnnotationSubtype.STRIKEOUT,
2917
3044
  rect,
2918
- popup,
2919
- appearances,
3045
+ contents,
3046
+ segmentRects,
3047
+ ...webAlphaColor,
2920
3048
  author,
2921
3049
  modified,
2922
3050
  };
@@ -2932,21 +3060,22 @@ class PdfiumEngine {
2932
3060
  * @private
2933
3061
  */
2934
3062
  readPdfSquigglyAnno(page, pagePtr, annotationPtr, index) {
2935
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2936
3063
  const pageRect = this.readPageAnnoRect(annotationPtr);
2937
3064
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2938
3065
  const author = this.getAnnotString(annotationPtr, 'T');
2939
3066
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2940
- const modified = this.toIsoDate(modifiedRaw);
2941
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3067
+ const modified = models.pdfDateToDate(modifiedRaw);
3068
+ const segmentRects = this.getQuadPointsAnno(page, annotationPtr);
3069
+ const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
3070
+ const webAlphaColor = this.resolveAnnotationColor(annotationPtr);
2942
3071
  return {
2943
- status: models.PdfAnnotationObjectStatus.Committed,
2944
3072
  pageIndex: page.index,
2945
3073
  id: index,
2946
3074
  type: models.PdfAnnotationSubtype.SQUIGGLY,
2947
3075
  rect,
2948
- popup,
2949
- appearances,
3076
+ contents,
3077
+ segmentRects,
3078
+ ...webAlphaColor,
2950
3079
  author,
2951
3080
  modified,
2952
3081
  };
@@ -2962,21 +3091,16 @@ class PdfiumEngine {
2962
3091
  * @private
2963
3092
  */
2964
3093
  readPdfCaretAnno(page, pagePtr, annotationPtr, index) {
2965
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2966
3094
  const pageRect = this.readPageAnnoRect(annotationPtr);
2967
3095
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2968
3096
  const author = this.getAnnotString(annotationPtr, 'T');
2969
3097
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
2970
- const modified = this.toIsoDate(modifiedRaw);
2971
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3098
+ const modified = models.pdfDateToDate(modifiedRaw);
2972
3099
  return {
2973
- status: models.PdfAnnotationObjectStatus.Committed,
2974
3100
  pageIndex: page.index,
2975
3101
  id: index,
2976
3102
  type: models.PdfAnnotationSubtype.CARET,
2977
3103
  rect,
2978
- popup,
2979
- appearances,
2980
3104
  author,
2981
3105
  modified,
2982
3106
  };
@@ -2993,13 +3117,11 @@ class PdfiumEngine {
2993
3117
  * @private
2994
3118
  */
2995
3119
  readPdfStampAnno(docPtr, page, pagePtr, annotationPtr, index) {
2996
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
2997
3120
  const pageRect = this.readPageAnnoRect(annotationPtr);
2998
3121
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
2999
3122
  const author = this.getAnnotString(annotationPtr, 'T');
3000
3123
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3001
- const modified = this.toIsoDate(modifiedRaw);
3002
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3124
+ const modified = models.pdfDateToDate(modifiedRaw);
3003
3125
  const contents = [];
3004
3126
  const objectCount = this.pdfiumModule.FPDFAnnot_GetObjectCount(annotationPtr);
3005
3127
  for (let i = 0; i < objectCount; i++) {
@@ -3010,14 +3132,11 @@ class PdfiumEngine {
3010
3132
  }
3011
3133
  }
3012
3134
  return {
3013
- status: models.PdfAnnotationObjectStatus.Committed,
3014
3135
  pageIndex: page.index,
3015
3136
  id: index,
3016
3137
  type: models.PdfAnnotationSubtype.STAMP,
3017
3138
  rect,
3018
- popup,
3019
3139
  contents,
3020
- appearances,
3021
3140
  author,
3022
3141
  modified,
3023
3142
  };
@@ -3186,6 +3305,39 @@ class PdfiumEngine {
3186
3305
  this.free(matrixPtr);
3187
3306
  return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
3188
3307
  }
3308
+ /**
3309
+ * Return the stroke-width declared in the annotation’s /Border or /BS entry.
3310
+ * Falls back to 1 pt when nothing is defined.
3311
+ *
3312
+ * @param annotationPtr - pointer to pdf annotation
3313
+ * @returns stroke-width
3314
+ *
3315
+ * @private
3316
+ */
3317
+ getStrokeWidth(annotationPtr) {
3318
+ // FPDFAnnot_GetBorder(annot, &hRadius, &vRadius, &borderWidth)
3319
+ const hPtr = this.malloc(4);
3320
+ const vPtr = this.malloc(4);
3321
+ const wPtr = this.malloc(4);
3322
+ const ok = this.pdfiumModule.FPDFAnnot_GetBorder(annotationPtr, hPtr, vPtr, wPtr);
3323
+ const width = ok ? this.pdfiumModule.pdfium.getValue(wPtr, 'float') : 1; // default 1 pt
3324
+ this.free(hPtr);
3325
+ this.free(vPtr);
3326
+ this.free(wPtr);
3327
+ return width;
3328
+ }
3329
+ /**
3330
+ * Fetches the `/F` flag bit-field from an annotation.
3331
+ *
3332
+ * @param annotationPtr pointer to an `FPDF_ANNOTATION`
3333
+ * @returns `{ raw, flags }`
3334
+ * • `raw` – the 32-bit integer returned by PDFium
3335
+ * • `flags` – object with individual booleans
3336
+ */
3337
+ getAnnotationFlags(annotationPtr) {
3338
+ const rawFlags = this.pdfiumModule.FPDFAnnot_GetFlags(annotationPtr); // number
3339
+ return models.flagsToNames(rawFlags);
3340
+ }
3189
3341
  /**
3190
3342
  * Read circle annotation
3191
3343
  * @param page - pdf page infor
@@ -3197,23 +3349,51 @@ class PdfiumEngine {
3197
3349
  * @private
3198
3350
  */
3199
3351
  readPdfCircleAnno(page, pagePtr, annotationPtr, index) {
3200
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3352
+ const flags = this.getAnnotationFlags(annotationPtr);
3201
3353
  const pageRect = this.readPageAnnoRect(annotationPtr);
3202
3354
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3203
3355
  const author = this.getAnnotString(annotationPtr, 'T');
3204
3356
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3205
- const modified = this.toIsoDate(modifiedRaw);
3206
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3357
+ const modified = models.pdfDateToDate(modifiedRaw);
3358
+ const { color, opacity } = this.resolveAnnotationColor(annotationPtr, models.PdfAnnotationColorType.InteriorColor);
3359
+ const { color: strokeColor } = this.resolveAnnotationColor(annotationPtr);
3360
+ let { style: strokeStyle, width: strokeWidth } = this.getBorderStyle(annotationPtr);
3361
+ let cloudyBorderIntensity;
3362
+ let cloudyBorderInset;
3363
+ if (strokeStyle === models.PdfAnnotationBorderStyle.CLOUDY ||
3364
+ strokeStyle === models.PdfAnnotationBorderStyle.UNKNOWN) {
3365
+ const { ok: hasEffect, intensity } = this.getBorderEffect(annotationPtr);
3366
+ if (hasEffect) {
3367
+ cloudyBorderIntensity = intensity;
3368
+ strokeStyle = models.PdfAnnotationBorderStyle.CLOUDY;
3369
+ const { ok: hasInset, left, top, right, bottom, } = this.getRectangleDifferences(annotationPtr);
3370
+ if (hasInset)
3371
+ cloudyBorderInset = [left, top, right, bottom];
3372
+ }
3373
+ }
3374
+ let strokeDashArray;
3375
+ if (strokeStyle === models.PdfAnnotationBorderStyle.DASHED) {
3376
+ const { ok, pattern } = this.getBorderDashPattern(annotationPtr);
3377
+ if (ok) {
3378
+ strokeDashArray = pattern;
3379
+ }
3380
+ }
3207
3381
  return {
3208
- status: models.PdfAnnotationObjectStatus.Committed,
3209
3382
  pageIndex: page.index,
3210
3383
  id: index,
3211
3384
  type: models.PdfAnnotationSubtype.CIRCLE,
3385
+ flags,
3386
+ color,
3387
+ opacity,
3388
+ strokeWidth,
3389
+ strokeColor,
3390
+ strokeStyle,
3212
3391
  rect,
3213
- popup,
3214
- appearances,
3215
3392
  author,
3216
3393
  modified,
3394
+ ...(cloudyBorderIntensity !== undefined && { cloudyBorderIntensity }),
3395
+ ...(cloudyBorderInset !== undefined && { cloudyBorderInset }),
3396
+ ...(strokeDashArray !== undefined && { strokeDashArray }),
3217
3397
  };
3218
3398
  }
3219
3399
  /**
@@ -3227,23 +3407,51 @@ class PdfiumEngine {
3227
3407
  * @private
3228
3408
  */
3229
3409
  readPdfSquareAnno(page, pagePtr, annotationPtr, index) {
3230
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3410
+ const flags = this.getAnnotationFlags(annotationPtr);
3231
3411
  const pageRect = this.readPageAnnoRect(annotationPtr);
3232
3412
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3233
3413
  const author = this.getAnnotString(annotationPtr, 'T');
3234
3414
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3235
- const modified = this.toIsoDate(modifiedRaw);
3236
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3415
+ const modified = models.pdfDateToDate(modifiedRaw);
3416
+ const { color, opacity } = this.resolveAnnotationColor(annotationPtr, models.PdfAnnotationColorType.InteriorColor);
3417
+ const { color: strokeColor } = this.resolveAnnotationColor(annotationPtr);
3418
+ let { style: strokeStyle, width: strokeWidth } = this.getBorderStyle(annotationPtr);
3419
+ let cloudyBorderIntensity;
3420
+ let cloudyBorderInset;
3421
+ if (strokeStyle === models.PdfAnnotationBorderStyle.CLOUDY ||
3422
+ strokeStyle === models.PdfAnnotationBorderStyle.UNKNOWN) {
3423
+ const { ok: hasEffect, intensity } = this.getBorderEffect(annotationPtr);
3424
+ if (hasEffect) {
3425
+ cloudyBorderIntensity = intensity;
3426
+ strokeStyle = models.PdfAnnotationBorderStyle.CLOUDY;
3427
+ const { ok: hasInset, left, top, right, bottom, } = this.getRectangleDifferences(annotationPtr);
3428
+ if (hasInset)
3429
+ cloudyBorderInset = [left, top, right, bottom];
3430
+ }
3431
+ }
3432
+ let strokeDashArray;
3433
+ if (strokeStyle === models.PdfAnnotationBorderStyle.DASHED) {
3434
+ const { ok, pattern } = this.getBorderDashPattern(annotationPtr);
3435
+ if (ok) {
3436
+ strokeDashArray = pattern;
3437
+ }
3438
+ }
3237
3439
  return {
3238
- status: models.PdfAnnotationObjectStatus.Committed,
3239
3440
  pageIndex: page.index,
3240
3441
  id: index,
3241
3442
  type: models.PdfAnnotationSubtype.SQUARE,
3443
+ flags,
3444
+ color,
3445
+ opacity,
3446
+ strokeColor,
3447
+ strokeWidth,
3448
+ strokeStyle,
3242
3449
  rect,
3243
- popup,
3244
- appearances,
3245
3450
  author,
3246
3451
  modified,
3452
+ ...(cloudyBorderIntensity !== undefined && { cloudyBorderIntensity }),
3453
+ ...(cloudyBorderInset !== undefined && { cloudyBorderInset }),
3454
+ ...(strokeDashArray !== undefined && { strokeDashArray }),
3247
3455
  };
3248
3456
  }
3249
3457
  /**
@@ -3258,21 +3466,16 @@ class PdfiumEngine {
3258
3466
  * @private
3259
3467
  */
3260
3468
  readPdfAnno(page, pagePtr, type, annotationPtr, index) {
3261
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3262
3469
  const pageRect = this.readPageAnnoRect(annotationPtr);
3263
3470
  const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3264
3471
  const author = this.getAnnotString(annotationPtr, 'T');
3265
3472
  const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3266
- const modified = this.toIsoDate(modifiedRaw);
3267
- const popup = this.readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index);
3473
+ const modified = models.pdfDateToDate(modifiedRaw);
3268
3474
  return {
3269
- status: models.PdfAnnotationObjectStatus.Committed,
3270
3475
  pageIndex: page.index,
3271
3476
  id: index,
3272
3477
  type,
3273
3478
  rect,
3274
- popup,
3275
- appearances,
3276
3479
  author,
3277
3480
  modified,
3278
3481
  };
@@ -3294,25 +3497,6 @@ class PdfiumEngine {
3294
3497
  const idx = this.pdfiumModule.FPDFPage_GetAnnotIndex(pagePtr, parentPtr);
3295
3498
  return idx >= 0 ? idx : undefined;
3296
3499
  }
3297
- /**
3298
- * Parse a PDF date string **D:YYYYMMDDHHmmSSOHH'mm'** to ISO-8601.
3299
- *
3300
- * Returns `undefined` if the input is malformed.
3301
- *
3302
- * @private
3303
- */
3304
- toIsoDate(pdfDate) {
3305
- if (!pdfDate?.startsWith('D:'))
3306
- return;
3307
- // Minimal parse – ignore timezone for brevity
3308
- const y = pdfDate.substring(2, 6);
3309
- const m = pdfDate.substring(6, 8) || '01';
3310
- const d = pdfDate.substring(8, 10) || '01';
3311
- const H = pdfDate.substring(10, 12) || '00';
3312
- const M = pdfDate.substring(12, 14) || '00';
3313
- const S = pdfDate.substring(14, 16) || '00';
3314
- return `${y}-${m}-${d}T${H}:${M}:${S}`;
3315
- }
3316
3500
  /**
3317
3501
  * Fetch a string value (`/T`, `/M`, `/State`, …) from an annotation.
3318
3502
  *
@@ -3332,41 +3516,19 @@ class PdfiumEngine {
3332
3516
  return value || undefined;
3333
3517
  }
3334
3518
  /**
3335
- * Read linked popup of pdf annotation
3336
- * @param page - pdf page infor
3337
- * @param pagePtr - pointer to pdf page object
3338
- * @param annotationPtr - pointer to pdf annotation
3339
- * @param index - index of annotation in the pdf page
3340
- * @returns pdf popup linked to annotation
3519
+ * Set a string value (`/T`, `/M`, `/State`, …) to an annotation.
3520
+ *
3521
+ * @returns `true` if the operation was successful
3341
3522
  *
3342
3523
  * @private
3343
3524
  */
3344
- readPdfAnnoLinkedPopup(page, pagePtr, annotationPtr, index) {
3345
- const appearances = this.readPageAnnoAppearanceStreams(annotationPtr);
3346
- const popupAnnotationPtr = this.pdfiumModule.FPDFAnnot_GetLinkedAnnot(annotationPtr, 'Popup');
3347
- if (!popupAnnotationPtr) {
3348
- return;
3349
- }
3350
- const pageRect = this.readPageAnnoRect(popupAnnotationPtr);
3351
- const rect = this.convertPageRectToDeviceRect(page, pagePtr, pageRect);
3352
- const author = this.getAnnotString(annotationPtr, 'T');
3353
- const modifiedRaw = this.getAnnotString(annotationPtr, 'M');
3354
- const modified = this.toIsoDate(modifiedRaw);
3355
- const contents = this.getAnnotString(annotationPtr, 'Contents') || '';
3356
- const open = this.getAnnotString(annotationPtr, 'Open') || 'false';
3357
- this.pdfiumModule.FPDFPage_CloseAnnot(popupAnnotationPtr);
3358
- return {
3359
- status: models.PdfAnnotationObjectStatus.Committed,
3360
- pageIndex: page.index,
3361
- id: index,
3362
- type: models.PdfAnnotationSubtype.POPUP,
3363
- rect,
3364
- contents,
3365
- open: open === 'true',
3366
- appearances,
3367
- author,
3368
- modified,
3369
- };
3525
+ setAnnotString(annotationPtr, key, value) {
3526
+ const bytes = 2 * (value.length + 1);
3527
+ const ptr = this.malloc(bytes);
3528
+ this.pdfiumModule.pdfium.stringToUTF16(value, ptr, bytes);
3529
+ const ok = this.pdfiumModule.FPDFAnnot_SetStringValue(annotationPtr, key, ptr);
3530
+ this.free(ptr);
3531
+ return ok;
3370
3532
  }
3371
3533
  /**
3372
3534
  * Read vertices of pdf annotation
@@ -3475,6 +3637,81 @@ class PdfiumEngine {
3475
3637
  options,
3476
3638
  };
3477
3639
  }
3640
+ /**
3641
+ * {@inheritDoc @embedpdf/models!PdfEngine.renderAnnotation}
3642
+ *
3643
+ * @public
3644
+ */
3645
+ renderAnnotation(doc, page, annotation, scaleFactor, rotation, dpr = 1, // device-pixel-ratio (canvas)
3646
+ mode = models.AppearanceMode.Normal, imageType = 'image/webp') {
3647
+ this.logger.debug(LOG_SOURCE$1, LOG_CATEGORY$1, 'renderAnnotation', doc, page, annotation, scaleFactor, rotation, dpr, mode, imageType);
3648
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `RenderAnnotation`, 'Begin', `${doc.id}-${page.index}-${annotation.id}`);
3649
+ const task = new models.Task();
3650
+ const ctx = this.cache.getContext(doc.id);
3651
+ if (!ctx) {
3652
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `RenderAnnotation`, 'End', `${doc.id}-${page.index}-${annotation.id}`);
3653
+ return models.PdfTaskHelper.reject({
3654
+ code: models.PdfErrorCode.DocNotOpen,
3655
+ message: 'document does not open',
3656
+ });
3657
+ }
3658
+ /* ── 1. grab native handles ───────────────────────────────────────── */
3659
+ const pageCtx = ctx.acquirePage(page.index);
3660
+ const annotPtr = this.pdfiumModule.FPDFPage_GetAnnot(pageCtx.pagePtr, annotation.id);
3661
+ if (!annotPtr) {
3662
+ pageCtx.release();
3663
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `RenderAnnotation`, 'End', `${doc.id}-${page.index}-${annotation.id}`);
3664
+ return models.PdfTaskHelper.reject({
3665
+ code: models.PdfErrorCode.NotFound,
3666
+ message: 'annotation not found',
3667
+ });
3668
+ }
3669
+ const finalScale = scaleFactor * dpr;
3670
+ /* ── 2. decide bitmap size (integer pixels) ──────────────────────── */
3671
+ const annotRect = annotation.rect;
3672
+ const bitmapRect = models.toIntRect(models.transformRect(page.size, annotRect, rotation, finalScale));
3673
+ const format = exports.BitmapFormat.Bitmap_BGRA;
3674
+ const bytesPerPixel = 4;
3675
+ const bitmapHeapLength = bitmapRect.size.width * bitmapRect.size.height * bytesPerPixel;
3676
+ const bitmapHeapPtr = this.malloc(bitmapHeapLength);
3677
+ const bitmapPtr = this.pdfiumModule.FPDFBitmap_CreateEx(bitmapRect.size.width, bitmapRect.size.height, format, bitmapHeapPtr, bitmapRect.size.width * bytesPerPixel);
3678
+ this.pdfiumModule.FPDFBitmap_FillRect(bitmapPtr, 0, 0, bitmapRect.size.width, bitmapRect.size.height, 0x00000000);
3679
+ const matrix = models.makeMatrix(annotation.rect, rotation, finalScale);
3680
+ // Allocate memory for the matrix on the wasm heap and write to it
3681
+ const matrixSize = 6 * 4;
3682
+ const matrixPtr = this.malloc(matrixSize);
3683
+ const matrixView = new Float32Array(this.pdfiumModule.pdfium.HEAPF32.buffer, matrixPtr, 6);
3684
+ matrixView.set([matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f]);
3685
+ /* ── 5. call the native helper with the new matrix ───────────────── */
3686
+ const FLAGS = exports.RenderFlag.REVERSE_BYTE_ORDER;
3687
+ const ok = !!this.pdfiumModule.EPDF_RenderAnnotBitmap(bitmapPtr, pageCtx.pagePtr, annotPtr, mode, matrixPtr, FLAGS);
3688
+ /* ── 6. tear down native resources ───────────────────────────────── */
3689
+ this.free(matrixPtr); // Free the matrix memory
3690
+ this.pdfiumModule.FPDFBitmap_Destroy(bitmapPtr);
3691
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotPtr);
3692
+ pageCtx.release();
3693
+ if (!ok) {
3694
+ this.free(bitmapHeapPtr);
3695
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `RenderAnnotation`, 'End', `${doc.id}-${page.index}-${annotation.id}`);
3696
+ return models.PdfTaskHelper.reject({
3697
+ code: models.PdfErrorCode.Unknown,
3698
+ message: 'EPDF_RenderAnnotBitmap failed',
3699
+ });
3700
+ }
3701
+ /* ── 6. copy out + convert to Blob (reuse existing converter) ─────── */
3702
+ const data = this.pdfiumModule.pdfium.HEAPU8.subarray(bitmapHeapPtr, bitmapHeapPtr + bitmapHeapLength);
3703
+ const imageData = {
3704
+ data: new Uint8ClampedArray(data),
3705
+ width: bitmapRect.size.width,
3706
+ height: bitmapRect.size.height,
3707
+ };
3708
+ this.free(bitmapHeapPtr);
3709
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, `RenderAnnotation`, 'End', `${doc.id}-${page.index}-${annotation.id}`);
3710
+ this.imageDataConverter(imageData, imageType)
3711
+ .then((blob) => task.resolve(blob))
3712
+ .catch((err) => task.reject({ code: models.PdfErrorCode.Unknown, message: String(err) }));
3713
+ return task;
3714
+ }
3478
3715
  /**
3479
3716
  * render rectangle of pdf page to image
3480
3717
  * @param docPtr - pointer to pdf document object
@@ -3807,6 +4044,64 @@ class PdfiumEngine {
3807
4044
  this.free(bufferPtr);
3808
4045
  return ap;
3809
4046
  }
4047
+ /**
4048
+ * Change the visible colour (and opacity) of an existing annotation.
4049
+ *
4050
+ * For markup annotations (highlight / underline / strikeout / squiggly) we
4051
+ * first clear the AP dictionary entry, otherwise the stored appearance stream
4052
+ * will override the new tint. For all other sub-types we keep the existing
4053
+ * AP so custom artwork isn't lost.
4054
+ *
4055
+ * @param doc logical document object
4056
+ * @param page logical page object
4057
+ * @param annotation the annotation we want to recolour
4058
+ * @param colour RGBA tuple (0-255 per channel)
4059
+ * @param which 0 = stroke/fill colour (PDFium's "colourType" param)
4060
+ *
4061
+ * @returns `true` when the operation succeeded
4062
+ */
4063
+ updateAnnotationColor(doc, page, annotation, color, which = 0) {
4064
+ this.logger.debug(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor', doc, page, annotation, color, which);
4065
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor', 'Begin', doc.id);
4066
+ const task = models.PdfTaskHelper.create();
4067
+ try {
4068
+ /* 1 ── sanity & native handles ────────────────────────────────────────── */
4069
+ const ctx = this.cache.getContext(doc.id);
4070
+ if (!ctx) {
4071
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor', 'End', doc.id);
4072
+ this.logger.warn(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor: doc closed');
4073
+ task.resolve(false);
4074
+ return task;
4075
+ }
4076
+ const pageCtx = ctx.acquirePage(page.index);
4077
+ const annotPtr = this.pdfiumModule.FPDFPage_GetAnnot(pageCtx.pagePtr, annotation.id);
4078
+ if (!annotPtr) {
4079
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor', 'End', doc.id);
4080
+ this.logger.warn(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor: annot not found');
4081
+ pageCtx.release();
4082
+ task.resolve(false);
4083
+ return task;
4084
+ }
4085
+ const ok = this.setAnnotationColor(annotPtr, color, which);
4086
+ /* 4 ── regenerate appearance & clean-up ───────────────────────────────── */
4087
+ if (ok) {
4088
+ this.pdfiumModule.FPDFPage_GenerateContent(pageCtx.pagePtr);
4089
+ }
4090
+ this.pdfiumModule.FPDFPage_CloseAnnot(annotPtr);
4091
+ pageCtx.release();
4092
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor', 'End', doc.id);
4093
+ task.resolve(!!ok);
4094
+ }
4095
+ catch (error) {
4096
+ this.logger.perf(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor', 'End', doc.id);
4097
+ this.logger.error(LOG_SOURCE$1, LOG_CATEGORY$1, 'setAnnotationColor: error', error);
4098
+ task.reject({
4099
+ code: models.PdfErrorCode.Unknown,
4100
+ message: `Failed to set annotation color: ${error instanceof Error ? error.message : String(error)}`,
4101
+ });
4102
+ }
4103
+ return task;
4104
+ }
3810
4105
  /**
3811
4106
  * Set the rect of specified annotation
3812
4107
  * @param page - page info that the annotation is belonged to
@@ -4142,7 +4437,7 @@ class EngineRunner {
4142
4437
  type: 'reject',
4143
4438
  reason: {
4144
4439
  code: models.PdfErrorCode.NotSupport,
4145
- message: 'engine method has not supported yet',
4440
+ message: `engine method ${name} is not supported yet`,
4146
4441
  },
4147
4442
  };
4148
4443
  const response = {
@@ -4197,6 +4492,9 @@ class EngineRunner {
4197
4492
  case 'renderPageRect':
4198
4493
  task = this.engine[name](...args);
4199
4494
  break;
4495
+ case 'renderAnnotation':
4496
+ task = this.engine[name](...args);
4497
+ break;
4200
4498
  case 'renderThumbnail':
4201
4499
  task = this.engine[name](...args);
4202
4500
  break;
@@ -4209,12 +4507,15 @@ class EngineRunner {
4209
4507
  case 'createPageAnnotation':
4210
4508
  task = this.engine[name](...args);
4211
4509
  break;
4212
- case 'transformPageAnnotation':
4510
+ case 'updatePageAnnotation':
4213
4511
  task = this.engine[name](...args);
4214
4512
  break;
4215
4513
  case 'removePageAnnotation':
4216
4514
  task = this.engine[name](...args);
4217
4515
  break;
4516
+ case 'updateAnnotationColor':
4517
+ task = this.engine[name](...args);
4518
+ break;
4218
4519
  case 'getPageTextRects':
4219
4520
  task = this.engine[name](...args);
4220
4521
  break;