Anki-Android icon indicating copy to clipboard operation
Anki-Android copied to clipboard

The message "An error occurred when generating an image occlusion note" is sometimes shown when clicking the check button in the Image Occlusion editor

Open rodrigo-morales-1 opened this issue 3 weeks ago • 7 comments

Checked for duplicates?

  • [x] This issue is not a duplicate

Does it also happen in the desktop version?

  • [ ] This bug does not occur in the latest version of Anki Desktop

What are the steps to reproduce this bug?

In the first video below, I added an occlusion to an existing Image Occlusion note. When pressing the "Save" button, the message "2 cards updated" was shown. This is expected behavior.

In the second video below, I opened the Image Occlusion editor and didn't make any changes. I repeated this multiple times to show that sometimes the message "2 cards updated" was shown and sometimes the message "An error occurred when generating an image occlusion note".

  • Time number 1: "2 cards updated"
  • Time number 2: "2 cards updated"
  • Time number 3: "An error occurred when generating an image occlusion note"
  • Time number 4: "2 cards updated"
  • Time number 5: "2 cards updated"
  • Time number 6: "An error occurred when generating an image occlusion note"
  • Time number 7: "An error occurred when generating an image occlusion note"
  • Time number 8: "An error occurred when generating an image occlusion note"
  • Time number 9: "An error occurred when generating an image occlusion note"

https://github.com/user-attachments/assets/08c3e219-e0f4-496f-974f-9733004896c0

https://github.com/user-attachments/assets/df3182a3-4a01-4705-ae3b-3a47d3ebc756

I only got the message "An error occurred when generating an image occlusion note" when I didn't make changes in the Image Occlusion editor. That is, I didn't get that message after having made some changes in the Image Occlusion editor, so I can't confirm that it actually means that the changes that were done in the Image Occlusion editor were not saved.

Even if the changes are saved when that message is shown, I believe that it shouldn't be shown in the first place because it might lead to some users think that the changes that they did in the Image Occlusion editor were not saved, which could ultimately cause her/him to waste some time going through their notes to confirm that the changes were actually saved.

Expected behaviour

The message "An error occurred when generating an image occlusion note" shouldn't be shown when no changes were done in the Image Occlusion editor.

Debug info

AnkiDroid Version = 2.23.0beta4 (367917ada802088c982a8ce11312fbb35d4290c3)  
Backend Version = 0.1.62-anki25.09.2 (25.09.2 3890e12c9e48c028c3f12aa58cb64bd9f8895e30)  
Android Version = 15 (SDK 35)  
ProductFlavor = full  
Device Info = samsung | samsung | gta4xlswifi | gta4xlswifixx | SM-P620 | s5e8825  
Webview User Agent = Mozilla/5.0 (Linux; Android 15; SM-P620 Build/AP3A.240905.015.A2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.102 Safari/537.36  
ACRA UUID = a32527bb-1a56-4c2f-92a8-65ce53ea03ff  
FSRS = 5.1.0 (Enabled: false)  
Crash Reports Enabled = true

(Optional) Anything else you want to share?

No response

Research

  • [x] I have checked the manual and the FAQ and could not find a solution to my issue
  • [ ] (Optional) I have confirmed the issue is not resolved in the latest alpha release (instructions)

rodrigo-morales-1 avatar Dec 03 '25 20:12 rodrigo-morales-1

this is most likely related to backend

sanjaysargam avatar Dec 06 '25 18:12 sanjaysargam

Relevant code (permalinks - may not be up to date):

Frontend logic

https://github.com/ankitects/anki/blob/2d4de33cf3160342c4c704c294e643c3e11071b1/ts/routes/image-occlusion/add-or-update-note.svelte.ts#L19-L52

proto

https://github.com/ankitects/anki/blob/2d4de33cf3160342c4c704c294e643c3e11071b1/proto/anki/image_occlusion.proto#L26-L27

service

https://github.com/ankitects/anki/blob/2d4de33cf3160342c4c704c294e643c3e11071b1/rslib/src/image_occlusion/service.rs#L38-L50

impl

https://github.com/ankitects/anki/blob/2d4de33cf3160342c4c704c294e643c3e11071b1/rslib/src/image_occlusion/imagedata.rs#L134-L155

david-allison avatar Dec 06 '25 19:12 david-allison

There are two bugs:

  1. Add an occlusion, then edit the note, making no changes
    • The mask may be moved
add    = "{{c1::image-occlusion:rect:left=.3734:top=.2508:width=.3858:height=.2429:oi=1}}<br>"
update = "{{c1::image-occlusion:rect:left=.3736:top=.2504:width=.3861:height=.2425:oi=1}}<br>"
  1. BackendInvalidInputException is thrown from updateImageOcclusionNote if the input is identical to the current note data
Test
Subject: [PATCH] 
---
Index: AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt	(revision 98a5a5a899b3a62bb136de8af9271d40c5241f5f)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt	(date 1765052484554)
@@ -148,6 +148,8 @@
     ): ByteArray {
         val methodName = uri.backendMethodName ?: throw IllegalArgumentException("unhandled request: $uri")
 
+        Timber.wtf("POST REQUEST: %s", methodName)
+
         val resolvedUiMethod =
             when (val uiResponse = activity.handleUiPostRequest(methodName, bytes)) {
                 is UiPostRequestResponse.Handled -> return uiResponse.data
Index: AnkiDroid/src/main/java/com/ichi2/anki/pages/PostRequestHandler.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PostRequestHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PostRequestHandler.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PostRequestHandler.kt	(revision 98a5a5a899b3a62bb136de8af9271d40c5241f5f)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PostRequestHandler.kt	(date 1765052760738)
@@ -21,7 +21,10 @@
 import androidx.annotation.VisibleForTesting
 import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.lifecycleScope
+import anki.backend.BackendError
 import anki.collection.OpChanges
+import anki.image_occlusion.AddImageOcclusionNoteRequest
+import anki.image_occlusion.UpdateImageOcclusionNoteRequest
 import com.ichi2.anki.CollectionManager
 import com.ichi2.anki.CollectionManager.withCol
 import com.ichi2.anki.NoteEditorFragment
@@ -50,6 +53,7 @@
 import kotlinx.coroutines.async
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.withContext
+import net.ankiweb.rsdroid.BackendException
 import timber.log.Timber
 
 interface PostRequestHandler {
@@ -154,12 +158,23 @@
         "importAnkiPackage" to { bytes -> lifecycleScope.async { importAnkiPackageUndoable(bytes) } },
         "addImageOcclusionNote" to { bytes ->
             lifecycleScope.async {
+                val data = AddImageOcclusionNoteRequest.parseFrom(bytes)
+                Timber.wtf("%s", data.toString())
                 withCol { addImageOcclusionNoteRaw(bytes) }
             }
         },
         "updateImageOcclusionNote" to { bytes ->
             lifecycleScope.async {
-                withCol { updateImageOcclusionNoteRaw(bytes) }
+                val request = UpdateImageOcclusionNoteRequest.parseFrom(bytes)
+                Timber.wtf("%s", request.toString())
+                withCol { updateImageOcclusionNoteRaw(bytes) }.also {
+                    try {
+                        val ex = BackendException.fromError(BackendError.parseFrom(it))
+                        Timber.wtf(ex)
+                    } catch (e: Exception) {
+
+                    }
+                }
             }
         },
         "deckOptionsReady" to { bytes -> lifecycleScope.async { deckOptionsReady(bytes) } },
Index: AnkiDroid/src/test/java/com/ichi2/AA.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/test/java/com/ichi2/AA.kt b/AnkiDroid/src/test/java/com/ichi2/AA.kt
new file mode 100644
--- /dev/null	(date 1765053409563)
+++ b/AnkiDroid/src/test/java/com/ichi2/AA.kt	(date 1765053409563)
@@ -0,0 +1,80 @@
+/*
+ *  Copyright (c) 2025 David Allison <[email protected]>
+ *
+ *  This program is free software; you can redistribute it and/or modify it under
+ *  the terms of the GNU General Public License as published by the Free Software
+ *  Foundation; either version 3 of the License, or (at your option) any later
+ *  version.
+ *
+ *  This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ *  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ *  PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along with
+ *  this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.ichi2
+
+import android.annotation.SuppressLint
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import anki.backend.BackendError
+import anki.image_occlusion.addImageOcclusionNoteRequest
+import anki.image_occlusion.updateImageOcclusionNoteRequest
+import com.ichi2.anki.RobolectricTest
+import net.ankiweb.rsdroid.BackendException
+import net.ankiweb.rsdroid.exceptions.BackendInvalidInputException
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AA : RobolectricTest() {
+    override fun getCollectionStorageMode(): CollectionStorageMode {
+        return CollectionStorageMode.ON_DISK
+    }
+
+    @SuppressLint("CheckResult")
+    @Test
+    fun `image occlusion update`() {
+        val ntid = col.notetypes.imageOcclusion.id
+
+        val addInput = "{{c1::image-occlusion:rect:left=.3734:top=.2508:width=.3858:height=.2429:oi=1}}<br>"
+        val updateInput = "{{c1::image-occlusion:rect:left=.3736:top=.2504:width=.3861:height=.2425:oi=1}}<br>"
+
+        val addRequest = addImageOcclusionNoteRequest {
+            this.notetypeId = ntid
+            this.imagePath = "/Users/davidallison/Documents/Screenshots/Screenshot 2025-12-06 at 20.37.46.png"
+            this.occlusions = addInput
+        }.toByteArray()
+
+        col.addImageOcclusionNoteRaw(addRequest)
+
+        assertThat(col.cardCount(), equalTo(1))
+
+        val cid = col.findCards("").single()
+        val nid = col.getCard(cid).nid
+
+        val updateRequest = updateImageOcclusionNoteRequest {
+            this.noteId = nid
+            this.occlusions = updateInput
+        }.toByteArray()
+
+        col.updateImageOcclusionNoteRaw(updateRequest).let { res ->
+            val ex = BackendException.fromError(BackendError.parseFrom(res))
+            if (ex is BackendInvalidInputException) {
+                Assert.fail("1")
+            }
+        }
+
+        col.updateImageOcclusionNoteRaw(updateRequest).let { res ->
+            val ex = BackendException.fromError(BackendError.parseFrom(res))
+            if (ex is BackendInvalidInputException) {
+                Assert.fail("2")
+            }
+        }
+
+    }
+}
\ No newline at end of file
Index: libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt b/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt
--- a/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt	(revision 98a5a5a899b3a62bb136de8af9271d40c5241f5f)
+++ b/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt	(date 1765049986304)
@@ -344,4 +344,7 @@
 
     val Notetypes.cloze
         get() = byName("Cloze")!!
+
+    val Notetypes.imageOcclusion
+        get() = byName("Image Occlusion")!!
 }

david-allison avatar Dec 06 '25 20:12 david-allison

@iamllama FYI are these known bugs? Both seem unintentional from reading the code

david-allison avatar Dec 06 '25 20:12 david-allison

We can mark this as 'upstream issue' after a quick discussion, and the 2 image occlusion bugs are reported & linked

david-allison avatar Dec 06 '25 20:12 david-allison

I'd need to look into (1) a bit more, but was able to reproduce (2) (the error toast, not the invalid input exception 👀) and will open a pr shortly for it. I don't think either have already been reported as issues

iamllama avatar Dec 07 '25 09:12 iamllama

Just to note: there's a .patch in my comment (Test) which reproduces the issue on AnkiDroid. Assert.fail("2") is triggered

david-allison avatar Dec 07 '25 14:12 david-allison