@@ -366,12 +366,75 @@ class FlutterPdfDocument(
366366 attachment : Any? ,
367367 callback : (Result <Boolean ?>) -> Unit
368368 ) {
369+ // WORKAROUND for PSPDFKit Native SDK limitation:
370+ //
371+ // Problem: When creating image annotations, PSPDFKit fires `onAnnotationCreated`
372+ // synchronously during `createAnnotationFromInstantJsonAsync()`, BEFORE we can attach
373+ // the binary image data. When event listeners call `toInstantJson()` on the annotation,
374+ // it fails with: "Can't create Instant JSON for stamp annotation that has no content -
375+ // title, stamp icon or an image!"
376+ //
377+ // Root cause: PSPDFKit stores image annotations as StampAnnotations internally.
378+ // The `toInstantJson()` method validates that stamps have "content" (title, stampType,
379+ // or attached image). Since the image isn't attached yet when the event fires, and
380+ // image annotations don't have title/stampType by spec, validation fails.
381+ //
382+ // Workaround: Inject a temporary title into the JSON before creating the annotation.
383+ // This satisfies the validation check. Once the image is attached (after creation),
384+ // subsequent serialization includes the image data.
385+ //
386+ // Note: The Instant JSON spec does NOT define `title` for image annotations
387+ // (only for stamps). This is purely an internal workaround for PSPDFKit's validation.
388+ //
389+ // Ideal fix: PSPDFKit should either:
390+ // 1. Allow attaching binary data before/during annotation creation, or
391+ // 2. Skip validation in `toInstantJson()` for annotations being created, or
392+ // 3. Delay firing `onAnnotationCreated` until after attachments are set
393+ val processedJson = if (attachment != null && attachment is String ) {
394+ try {
395+ val json = JSONObject (jsonAnnotation)
396+ val annotationType = json.optString(" type" , " " )
397+ // Inject placeholder title for image/stamp annotations with binary attachments.
398+ // "Image" for pspdfkit/image, "Stamp" for pspdfkit/stamp (per spec naming).
399+ when (annotationType) {
400+ " pspdfkit/image" -> {
401+ if (! json.has(" title" ) || json.optString(" title" ).isNullOrEmpty()) {
402+ json.put(" title" , " Image" )
403+ }
404+ }
405+ " pspdfkit/stamp" -> {
406+ if (! json.has(" title" ) || json.optString(" title" ).isNullOrEmpty()) {
407+ json.put(" title" , " Stamp" )
408+ }
409+ }
410+ }
411+ json.toString()
412+ } catch (e: Exception ) {
413+ jsonAnnotation // Use original if parsing fails
414+ }
415+ } else {
416+ jsonAnnotation
417+ }
418+
369419 disposable =
370- pdfDocument.annotationProvider.createAnnotationFromInstantJsonAsync(jsonAnnotation )
420+ pdfDocument.annotationProvider.createAnnotationFromInstantJsonAsync(processedJson )
371421 .subscribeOn(Schedulers .computation())
372422 .observeOn(AndroidSchedulers .mainThread())
373423 .subscribe(
374424 { annotation ->
425+ // For stamp/image annotations, set title IMMEDIATELY after creation.
426+ // This is needed because PSPDFKit fires onAnnotationCreated synchronously
427+ // during createAnnotationFromInstantJsonAsync(), and the title from JSON
428+ // may not be applied for image annotations (title is not in the image spec).
429+ if (annotation is com.pspdfkit.annotations.StampAnnotation ) {
430+ if (annotation.title.isNullOrEmpty()) {
431+ val type = try {
432+ JSONObject (processedJson).optString(" type" , " " )
433+ } catch (e: Exception ) { " " }
434+ annotation.title = if (type == " pspdfkit/image" ) " Image" else " Stamp"
435+ }
436+ }
437+
375438 // Handle attachment if provided
376439 if (attachment != null && attachment is String ) {
377440 try {
@@ -383,6 +446,23 @@ class FlutterPdfDocument(
383446 val binaryData = Base64 .decode(binary, Base64 .DEFAULT )
384447 val dataProvider = BinaryDataProvider (binaryData)
385448 annotation.attachBinaryInstantJsonAttachment(dataProvider, contentType)
449+
450+ // Generate the appearance stream to render the attached image.
451+ annotation.generateAppearanceStreamAsync()
452+ .subscribeOn(Schedulers .computation())
453+ .observeOn(AndroidSchedulers .mainThread())
454+ .subscribe(
455+ {
456+ callback(Result .success(true ))
457+ },
458+ { appearanceError ->
459+ // Log but don't fail - the annotation was created successfully,
460+ // just the appearance generation failed
461+ android.util.Log .w(" FlutterPdfDocument" , " Failed to generate appearance stream: ${appearanceError.message} " )
462+ callback(Result .success(true ))
463+ }
464+ )
465+ return @subscribe
386466 }
387467 } catch (e: Exception ) {
388468 // Log but don't fail - annotation was created successfully
@@ -479,48 +559,72 @@ class FlutterPdfDocument(
479559 }
480560
481561 override fun getAnnotationsJson (pageIndex : Long , type : String , callback : (Result <String >) -> Unit ) {
482- val jsonArray = JSONArray ()
562+ try {
563+ val jsonArray = JSONArray ()
483564
484- // Get annotations directly from the annotation provider
485- val annotations = pdfDocument.annotationProvider.getAnnotations(pageIndex.toInt())
486- val annotationTypeSet = AnnotationTypeAdapter .fromString(type)
487- val filterAll = annotationTypeSet.size == com.pspdfkit.annotations.AnnotationType .values().size
565+ // Get annotations directly from the annotation provider
566+ val annotations = pdfDocument.annotationProvider.getAnnotations(pageIndex.toInt())
567+ val annotationTypeSet = AnnotationTypeAdapter .fromString(type)
568+ val filterAll = annotationTypeSet.size == com.pspdfkit.annotations.AnnotationType .values().size
488569
489- for (annotation in annotations) {
490- // Filter by type if not "all"
491- if (! filterAll && ! annotationTypeSet.contains(annotation.type)) {
492- continue
493- }
570+ for (annotation in annotations) {
571+ // Filter by type if not "all"
572+ if (! filterAll && ! annotationTypeSet.contains(annotation.type)) {
573+ continue
574+ }
494575
495- // Skip floating/unattached annotations that can't be serialized to JSON.
496- // These are typically internal annotations (like some LINK annotations) that
497- // haven't been fully connected to the document structure.
498- if (! annotation.isAttached) {
499- continue
500- }
576+ // Skip floating/unattached annotations that can't be serialized to JSON.
577+ // These are typically internal annotations (like some LINK annotations) that
578+ // haven't been fully connected to the document structure.
579+ if (! annotation.isAttached) {
580+ continue
581+ }
501582
502- var jsonString = annotation.toInstantJson()
583+ // For stamp/image annotations that don't have content yet,
584+ // set a placeholder title so toInstantJson() can succeed.
585+ // Don't check hasBinaryInstantJsonAttachment() because it may return true
586+ // before actual data is attached (it checks imageAttachmentId, not binary data).
587+ if (annotation is com.pspdfkit.annotations.StampAnnotation ) {
588+ if (annotation.title.isNullOrEmpty() && annotation.stampType == null ) {
589+ annotation.title = " Image"
590+ }
591+ }
503592
504- // Skip annotations that can't be serialized to JSON
505- if (jsonString.isNullOrEmpty()) {
506- continue
507- }
593+ // Try to convert annotation to JSON, skip if it fails
594+ // Some annotations (like stamp/image annotations) may throw IllegalStateException
595+ // if they don't have their content fully set yet
596+ var jsonString: String
597+ try {
598+ jsonString = annotation.toInstantJson() ? : continue
599+ } catch (e: IllegalStateException ) {
600+ // Skip annotations that can't be serialized
601+ continue
602+ }
508603
509- // For annotations with binary attachments, include the attachment data
510- if (annotation.hasBinaryInstantJsonAttachment ()) {
511- jsonString = addAttachmentToJson(annotation, jsonString)
512- }
604+ // Skip annotations that can't be serialized to JSON
605+ if (jsonString.isEmpty ()) {
606+ continue
607+ }
513608
514- // Try to parse the JSON, skip if it fails
515- try {
516- jsonArray.put(JSONObject (jsonString))
517- } catch (e: Exception ) {
518- android.util.Log .w(" FlutterPdfDocument" , " Failed to parse annotation JSON: ${e.message} " )
519- continue
609+ // For annotations with binary attachments, include the attachment data
610+ if (annotation.hasBinaryInstantJsonAttachment()) {
611+ jsonString = addAttachmentToJson(annotation, jsonString)
612+ }
613+
614+ // Try to parse the JSON, skip if it fails
615+ try {
616+ jsonArray.put(JSONObject (jsonString))
617+ } catch (e: Exception ) {
618+ android.util.Log .w(" FlutterPdfDocument" , " Failed to parse annotation JSON: ${e.message} " )
619+ continue
620+ }
520621 }
521- }
522622
523- callback(Result .success(jsonArray.toString()))
623+ callback(Result .success(jsonArray.toString()))
624+ } catch (e: Exception ) {
625+ android.util.Log .e(" FlutterPdfDocument" , " Error getting annotations JSON: ${e.message} " , e)
626+ callback(Result .failure(e))
627+ }
524628 }
525629
526630 /* *
0 commit comments