Skip to content

Feature image occlusion - #2367

Merged
dae merged 39 commits into
ankitects:mainfrom
krmanik:feature-image-occlusion
Mar 29, 2023
Merged

dae merged 39 commits into
ankitects:mainfrom
krmanik:feature-image-occlusion

Conversation

@krmanik

@krmanik krmanik commented Feb 6, 2023 •

Copy link
Copy Markdown
Contributor

This is most requested feature for AnkiDroid and AnkiMobile users. This feature is implemented like import-csv features.

Only image is stored in collection. Media, the question & answer masks are stored in notes itself, using data-* attributes and drawn using canvas api in reviewer.

Mask Editor

  • It is implemented using fabricjs and panzoom.
  • Currently following shape tools are supported
    • Rectangle
    • Ellipse
    • Polygon (draw using mouse pointer)
  • Following tools are available to customize shapes
    • Generate group and individual cloze
    • Group and ungroup shapes
    • Delete shapes
    • Copy and paste selected shape
    • Undo and redo

Note editor

Very simple text editor is implemented (still needs improvement)

  • Types of notes generated
    • hide all, guess one
    • hide one, guess one

Image

@glutanimate

Copy link
Copy Markdown
Contributor

Nice work, Mani! Will take a closer look at this later today/tomorrow, but just wanted to say for now: Glad to see you back!

For continuity's sake, here are the discussions/iterations up to this point: krmanik#1

@krmanik

krmanik commented Feb 6, 2023

Copy link
Copy Markdown
Contributor Author

Trying to fix these errors but I am not able to figure out the issues.
The backend have input: pb::image_occlusion::ImageClozeMetadataRequest, and input: pb::image_occlusion::AddImageOcclusionNotesRequest, but still it showing errors. What should be possible fix?

~/anki$ ./ninja pylib/anki qt/aqt check
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
[50/54; 5 active; 99.734s] check:mypy
FAILED: /home/mani/anki/out/tests/python_typecheck.10916796379318679380 
/home/mani/anki/out/rust/debug/runner run --stamp=/home/mani/anki/out/tests/python_typecheck.10916796379318679380 /home/mani/anki/out/pyenv/bin/mypy pylib ts/lib qt/aqt qt/tools out/pylib/anki out/qt/_aqt ftl python tools
Command failed: 
pylib/anki/collection.py:464: error: Argument 1 to "get_image_cloze_metadata" of "RustBackendGenerated" has incompatible type "ImageClozeMetadataRequest"; expected "str"  [arg-type]
pylib/anki/collection.py:483: error: Too many positional arguments for "add_image_occlusion_notes" of "RustBackendGenerated"  [misc]
pylib/anki/collection.py:483: error: Missing named argument "deck_id" for "add_image_occlusion_notes" of "RustBackendGenerated"  [call-arg]
pylib/anki/collection.py:483: error: Missing named argument "occlusions" for "add_image_occlusion_notes" of "RustBackendGenerated"  [call-arg]
pylib/anki/collection.py:483: error: Missing named argument "header" for "add_image_occlusion_notes" of "RustBackendGenerated"  [call-arg]
pylib/anki/collection.py:483: error: Missing named argument "notes" for "add_image_occlusion_notes" of "RustBackendGenerated"  [call-arg]
pylib/anki/collection.py:483: error: Missing named argument "tags" for "add_image_occlusion_notes" of "RustBackendGenerated"  [call-arg]
pylib/anki/collection.py:483: error: Argument 1 to "add_image_occlusion_notes" of "RustBackendGenerated" has incompatible type "AddImageOcclusionNotesRequest"; expected "str"  [arg-type]
Found 8 errors in 1 file (checked 345 source files)

@RumovZ

RumovZ commented Feb 6, 2023

Copy link
Copy Markdown
Collaborator

Wow, great job!

You usually don't need to construct those request types when calling backend routines. So instead of:

request = image_occlusion_pb2.ImageClozeMetadataRequest(path=path)
return self._backend.get_image_cloze_metadata(request)

Try:

return self._backend.get_image_cloze_metadata(path=path)

I assume you modeled your code after get_csv_metadata(), but that seems to be a special case, presumably because of the optionals. You can check what methods are generated exactly in out/pylib/anki/_backend_generated.py.

@dae

dae commented Feb 7, 2023

Copy link
Copy Markdown
Member

Thank you so much for working on this Mani!

I've only had a brief play with it so far, but some initial thoughts:

  • There's a lot of great stuff here - undo, object selection/transformation, a variety of tools, the fact that occlusions are in code instead of generated images, etc.
  • I wonder if we really need all those tools? The triangle, square and circle tools feel a bit redundant given there's a path tool, rectangle tool and oval tool as well. I suspect the rectangle tool alone is going to be the most-used one by far. But @glutanimate would have a better idea here.
  • For the Notes tab, I'd be tempted to leave the formatting buttons out for now, or limit them to just B/I/U. Eventually we'll want to be able to create occlusions directly inside our normal editor instead of having a separate window (point 4 on Feature: Image occlusion krmanik/anki#1 (comment))
  • Is there a way to edit previously added cards at the moment?
  • It took me a while to find the zoom tools. Would it perhaps make more sense to always show the zoom icons, and get rid of the separate zoom mode?
  • I wasn't able to figure out how the fill tool works.

Some thoughts on design decisions:

Storing the occlusion data in the fields instead of images is great. Storing them as HTML, I'm a little less sure about. Because users can't see the paths unless they enable the HTML editor, I fear some users may accidentally damage or delete their occlusions when they edit the text (point 1 on krmanik#1 (comment)). We could partly avoid that by changing the editor so that users aren't able to edit the field, though that won't solve the problem for older clients/AnkiWeb/etc.

Would it make sense to make the paths visible instead? Eg instead of something like

<div data-type="rect" data-left="399.01" data-top="99.52" data-width="167.09" data-height="33.78" data-fill="#0a2cee" data-stroke="null" data-strokewidth="1" data-angle="0" data-rx="0" data-ry="0">{{c5::hidden::prompted}}</div>

What if we used something like this instead?

{{c5::image-occlusion:rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1}}

(omitting values if they're the default)

We could then adjust cloze.rs to look for image-occlusion as a prefix, and transform the text into the desired HTML for both the question and answer side.

What do people think? Would that be an improvement, or make things worse?

One other thing - the script in the notetype. It's impressive how simple it is, but I think storing it in the notetype will make it hard for us to make changes in the future if we have old notetypes to worry about, and it increases the chances the user will make accidental changes. Maybe our reviewer TypeScript could export the required function(s) instead, so we could keep the code in the notetype to the minimum?

@glutanimate

glutanimate commented Feb 7, 2023 •

Copy link
Copy Markdown
Contributor

@dae I started jotting down some notes yesterday, and your points echo a lot of my thoughts 👍

The triangle, square and circle tools feel a bit redundant given there's a path tool, rectangle tool and oval tool as well

Agreed, yes. I appreciate the effort you went through in adding all of these, Mani, but I do think we should keep the UI complexity down. Even the rectangle tool by its own will likely cover 90% of all IO use cases. With the ellipse tool and path tool by its side, I think we have all bases covered. Being lean here is particularly important on mobile I would argue, where selecting the right tool via touches is likely more of a challenge than on desktop (though even on desktop, in playing around with the PR over the past couple of days I've already had multiple instances where I ended up confusing square vs rectangle and circle vs ellipse, leading to momentary confusion).

So from a UX and maintenance standpoint, I would vote to not include triangle, square, and circle. Though again, thanks a lot for your work on these, Mani!

For the rest, focusing on note type design decisions for now as I don't want to make this comment overlong:

  • @dae user modifiability across the card template, field content, and even field set-up also remains my main concern here, in particular in the context of future iterations and Anki version compatibility. We definitely want to avoid a situation where users have to deal with similar compatibility woes for note types as for add-ons.

  • For the editor, I agree and still think it would be crucial to make the Occlusions field immutable, but I would even go one step beyond that and do the same for the Image field to e.g. avoid accidental mishaps by attempting to resize the image. Basically, any programmatically managed field should not be user-editable outside of the IO editor.

  • Regarding additional protections for field content: I do find the idea interesting to be more transparent about the actual data contained in the field. I could see this leading to less edits made out of confusion where users do not understand that keeping {{c1::hidden::prompted}} verbatim is actually essential for IO to work. It would also kill two birds with one stone, as I noticed that we currently trigger Anki's dupe detection on the Occlusions field, likely because the logic is comparing the plain text values of the fields (or at least ignoring data attributes).

    At the same time, storing the data as plain text clozes might encourage users to attempt to manually adjust the values. It also seems more fickle to a degree as we would have to consider HTML escaping, etc. So I'm a bit undecided here still.

    I would actually also like to revisit whether we still need the shown::prompted approach to control mask visibility. With the improved cloze metadata on 2.1.57+, the cloze::hint system primarily only serves to distinguish between different mask reveal modes (Hide All vs Hide One). However, as we have to split these into separate note types anyways in order to provide different sibling burying behavior, there really isn't any need for this system anymore as the behavior would be managed by the template, which might allow us to simplify the field data format further.

    I'll have to play around with this a bit more.

  • Regarding the card template: I do like the idea of moving the core logic into reviewer.ts and thus out of reach of accidental user edits. I would even vote to compress this down to a single entry point function called in the template. To override Anki default behavior, user templates could then simply forfeit calling the entry point and implementing their own mask composition logic. Or, in a second iteration, we could introduce a JS hook API that would allow template-residing code or add-ons to customize Anki's behavior in a more fine-grained way.

  • Regarding the field set-up: I would also strongly vote to treat the IO note type specially here and add some protections that prevent users from deleting or otherwise modifying the Image and Occlusions fields. While less common than accidental edits to field content and templates, this is yet another issue I've seen users run into many times over the years with IOE. Basically, I would see us adding a field-specific flag that would mark them as protected both in the Fields dialog and Editor.

Last, some considerations towards editing existing notes and handling multiple IO note types:

  • I do think it's essential that we provide users with a way to edit existing notes.
  • This is not the only place where we have to take this into consideration, but it's probably where the need for this is the most clear: Given that users are likely to end up with multiple copies of the IO note type as they traverse shared decks, etc., the IO editor has to be capable of handling more than one note type. This is of course also essential to enable users to create their own custom IO note types, which they will expect to be able to do similar to any other note type.
  • However, decoupling the native IO implementation from one hard-coded note type has a number of implications:
    • On the UX side, users would have to be able to select the IO note type to use when creating image occlusions. While we're still following the separate IO editor window paradigm, the most reasonable place to perform this selection would likely be in the IO editor window (next to the deck selector, probably)
    • On the backend side, we would need to have a concept of what constitutes a valid IO note type (so that users don't e.g. attempt to select Basic). We would probably want to handle this in the same way as we currently do for Cloze note types.

@krmanik

krmanik commented Feb 7, 2023

Copy link
Copy Markdown
Contributor Author

I assume you modeled your code after get_csv_metadata(), but that seems to be a special case, presumably because of the optionals. You can check what methods are generated exactly in out/pylib/anki/_backend_generated.py.

It works. Thanks.


I wonder if we really need all those tools? The triangle, square and circle tools feel a bit redundant given there's a path tool, rectangle tool and oval tool as well. I suspect the rectangle tool alone is going to be the most-used one by far. But @glutanimate would have a better idea here.

Some of the tools will be removed and only selected tools be available.

For the Notes tab, I'd be tempted to leave the formatting buttons out for now, or limit them to just B/I/U. Eventually we'll want to be able to create occlusions directly inside our normal editor instead of having a separate window (point 4 on krmanik#1 (comment))

I have implementing considering mobile version, but notes editor will be also ported similar to other web pages. So, I think I should look into integrating into the existing note editor(but currently separate note editor for mobile version should be used).

Is there a way to edit previously added cards at the moment?

The editing of card is not available, the edit button should open mask editor and generate fabric.Canvas objects and place on mask editor. After finish of edit, the data in cloze re-adjusted. (design idea)

It took me a while to find the zoom tools. Would it perhaps make more sense to always show the zoom icons, and get rid of the separate zoom mode?

I will change the zoom tools to always show.

I wasn't able to figure out how the fill tool works.

It is reverse of what fill tools works. In my (incorrect) implementation first select shapes and then select fill tools to change the color of shapes. But it should work like select fill tools and select any shapes and change their fill-color. It will be fixed in next commits.

What if we used something like this instead?

{{c5::image-occlusion:rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1}}

I think it will reduce the html in the notes. This is information visible to users. I will look into cloze.rs for implementations.

As pointed by glutanimate,

keeping {{c1::hidden::prompted}} verbatim is actually essential for IO to work.

Because it checks for hidden and cloze (css) class to show and hide the shapes.

So the following cloze should be

{{c5::image-occlusion:rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1}}

replaced with

{{c5::hidden::prompted::image-occlusion:rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1}}

And generated html using later part image-occlusion:rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1.

Or may be other cloze generation should be used.

Maybe our reviewer TypeScript could export the required function(s) instead, so we could keep the code in the notetype to the minimum?

It will be best to export this to reviewer TypeScript, because minimum changes in image occlusion note type make it easier to updates.

However, decoupling the native IO implementation from one hard-coded note type has a number of implications:
On the backend side, we would need to have a concept of what constitutes a valid IO note type (so that users don't e.g. attempt to select Basic). We would probably want to handle this in the same way as we currently do for Cloze note types.

This can be implemented similar to cloze, when cloze note types selected then other cloze button are shown. So, here the image selection to generate image occlusion should show/hide the button depending on note types. If note types are image occlusion then show the button to select image and show mask editor otherwise hide it.

Also three icon button will be shown when note types Image Occlusion selected.
| Select Image | Mask Editor | Notes Editor |

| Select Image |

  • Open file manager, select image and load mask editor in native note editor
  • If already image selected then prompt for removing image generating again

| Mask Editor |

  • The mask editor will be shown only shown after image selected.
  • The mask editor will be shown when mask editor button clicked

| Notes Editor |

  • The note editor will be shown by default but when user select image, it will be replaced with mask editor
  • The fields in note editor will be shown when mask editor button clicked

Edit scenario

  • When user click edits then it load the canvas objects and place mask editor. After edit update notes.
  • It will handle the immutability of Occlusions and Image fields.

@krmanik
krmanik force-pushed the feature-image-occlusion branch from 9c4fbd0 to 6581d04 Compare February 7, 2023 16:26
@krmanik

krmanik commented Feb 7, 2023

Copy link
Copy Markdown
Contributor Author

The latest commit is not complete. I have tried to filter out the data-cloze, but need help to finish it.

I want the data-cloze should only contains image-occlusion, but filtering is not done.

thread 'cloze::test::image_cloze' panicked at 'assertion failed: `(left == right)`
  left: `"<span class=\"cloze\" data-cloze=\"image&#x2D;occlusion&#x3A;rect&#x3A;left&#x3D;10&#x2E;0&#x3A;top&#x3D;20&#x3A;width&#x3D;30&#x3A;height&#x3D;10&#x3A;fill&#x3D;&#x23;ffe34d&#x3A;stroke&#x3D;5\" data-shape=\"rect\" data-left=\"10.0\" data-top=\"20\" data-width=\"30\" data-height=\"10\" data-fill=\"#ffe34d\" data-stroke=\"5\"  data-ordinal=\"1\">[...]</span>"`,
 right: `"<span class=\"cloze\" data-cloze=\"image-occlusion\" data-shape=\"rect\" data-left=\"10.0\" data-top=\"20\" data-width=\"30\" data-height=\"10\" data-fill=\"#ffe34d\" data-stroke=\"5\" data-ordinal=\"1\">[...]</span>"`', rslib/src/cloze.rs:544:9

@dae

dae commented Feb 8, 2023

Copy link
Copy Markdown
Member

We can bypass all the usual cloze rendering and have complete control over what is output for image occlusion. Eg:

diff --git a/rslib/src/cloze.rs b/rslib/src/cloze.rs
index 720f89e74..158cd2f2d 100644
--- a/rslib/src/cloze.rs
+++ b/rslib/src/cloze.rs
@@ -140,9 +140,12 @@ impl ExtractedCloze<'_> {
         buf.into()
     }
 
-    fn image_occlusion(&self) -> Cow<str> {
-        get_image_cloze_data(&self.clozed_text()).into()
-    } 
+    /// If cloze starts with image-occlusion:, return the text following that.
+    fn image_occlusion(&self) -> Option<&str> {
+        let Some(first_node) = self.nodes.get(0) else { return None };
+        let TextOrCloze::Text(text) = first_node else { return None };
+        text.strip_prefix("image-occlusion:")
+    }
 }
 
 fn parse_text_with_clozes(text: &str) -> Vec<TextOrCloze<'_>> {
@@ -217,6 +220,10 @@ fn reveal_cloze(
 ) {
     let active = cloze.ordinal == cloze_ord;
     *active_cloze_found_in_text |= active;
+    if let Some(image_occlusion_text) = cloze.image_occlusion() {
+        buf.push_str(&render_image_occlusion(image_occlusion_text, question, active));
+        return;
+    }
     match (question, active) {
         (true, true) => {
             // question side with active cloze; all inner content is elided
@@ -235,9 +242,8 @@ fn reveal_cloze(
             }
             write!(
                 buf,
-                r#"<span class="cloze" data-cloze="{}" {} data-ordinal="{}">[{}]</span>"#,
+                r#"<span class="cloze" data-cloze="{}" data-ordinal="{}">[{}]</span>"#,
                 encode_attribute(&content_buf),
-                cloze.image_occlusion(),
                 cloze.ordinal,
                 cloze.hint()
             )
@@ -246,8 +252,7 @@ fn reveal_cloze(
         (false, true) => {
             write!(
                 buf,
-                r#"<span class="cloze" {} data-ordinal="{}">"#,
-                cloze.image_occlusion(),
+                r#"<span class="cloze" data-ordinal="{}">"#,
                 cloze.ordinal
             )
             .unwrap();
@@ -265,8 +270,7 @@ fn reveal_cloze(
             // question or answer side inactive cloze; text shown, children may be active
             write!(
                 buf,
-                r#"<span class="cloze-inactive" {} data-ordinal="{}">"#,
-                cloze.image_occlusion(),
+                r#"<span class="cloze-inactive" data-ordinal="{}">"#,
                 cloze.ordinal
             )
             .unwrap();
@@ -283,6 +287,11 @@ fn reveal_cloze(
     }
 }
 
+fn render_image_occlusion(text: &str, question_side: bool, active: bool) -> String {
+    // todo: alter HTML depending on question/answer side, active/non-active cloze
+    get_image_cloze_data(text)
+}
+
 pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str> {
     let mut buf = String::new();
     let mut active_cloze_found_in_text = false;

Does that make things easier?

@dae

dae commented Feb 8, 2023 •

Copy link
Copy Markdown
Member

(we don't even need to use <span class=cloze ...> - you could output something completely different if that was easier)

@krmanik

krmanik commented Feb 8, 2023 •

Copy link
Copy Markdown
Contributor Author

Thanks I have used the patch and it is easier then earlier implementation.

I have added new notes type for image cloze in stock.rs, also moved the image shape generation to canvas to reviewer ts.

Now adding the following to Occlusion fields and image in Image tag with id="img" generate the image occlusion for now.

{{c1::image-occlusion:rect:left=19.54:top=46.96:width=126.13:height=53.29:fill=#55aaff:quesmaskcolor=#f06}}
{{c2::image-occlusion:ellipse:left=46.83:top=188.56:width=166.65:height=66.51:fill=#55aa6f:rx=88.82324548896866:ry=33.65283560259413:quesmaskcolor=#f06}}
{{c3::image-occlusion:polygon:left=3.05:top=353.02:width=94.15:height=36.31:fill=#ffaa00:points=3.55,353.51 96.60,355.29 95.92,390.82 3.55,389.04:quesmaskcolor=#f06}}

@glutanimate

Copy link
Copy Markdown
Contributor

The new card rendering approach and note type format look clean, nice work!

A couple of thoughts regarding setupImageCloze():

  • How were you thinking of encoding the "Hide all" vs "Hide one" behavior? I was thinking that this is something that we could specify as an argument to setupImageCloze(), e.g. a hideAll bool flag. To make the interface extensible and expressive, we could group all customizable properties in a single-argument object, i.e.:

    interface SetupImageClozeOptions {
      hideAll?: bool;
    }
    
    export function setupImageCloze(options: SetupImageClozeOptions) {
        const hideAll = options.hideAll || true;
        ...
    }
  • Given that the API and behavior of setupImageCloze() might change in the future, perhaps it would then also make sense to also equip SetupImageClozeOptions with an apiVersion parameter or something similar?

Comment thread ts/reviewer/image_occlusion.ts Resolved
@krmanik
krmanik force-pushed the feature-image-occlusion branch from 7bf0e0b to d56ea36 Compare February 9, 2023 10:59
@krmanik

krmanik commented Feb 9, 2023 •

Copy link
Copy Markdown
Contributor Author

Included the mask editor in note editor. The editor have toggle button to switch between note editor and mask editor.

Next implementation todo, add a button in mask editor's top toolbar [generate]. When the generate button clicked then it generate cloze using generate.ts and set the data to the fields in note editor. When Add at button clicked it add the notes.

Comment thread ts/editor/NoteEditor.svelte Resolved
@dae

dae commented Feb 9, 2023

Copy link
Copy Markdown
Member

I'll have more time to return to this in a day or so, but just a quick note for now: it might be nicer to keep I/O in a separate screen/page for the initial implementation, as that will make it easier to integrate into the mobile clients. There's a bunch of work that needs to be done before we can integrate the editor into the mobile clients, so combining the two would mean there'd be a longer wait for mobile users to get access to this.

@krmanik

krmanik commented Feb 9, 2023

Copy link
Copy Markdown
Contributor Author

The implementation for mobile will be done using ImageOcclusionPage.svelete. The page will be built for mobile device from web.rs. On mobile device we have to just show the show the hidden buttons for mask edit, note edit and note add. I will push the change how it can be used in anki note editor as well as on mobile client. Because the getting image and adding notes is already implemented in backend in previous commits.

@dae

dae commented Feb 10, 2023

Copy link
Copy Markdown
Member

Just trying to keep things simple to start with :-) If we have two different ways of invoking/using the I/O editor depending on platform, that's more variables we need to test. Integration inside the editor is the best way in the long run, but I just thought it might make more sense to focus on the external-case for now, and then when the mobile clients have adopted the new editor, we can then switch the implementation over. But I don't have strong feelings here if you're keen to work on both implementations.

Comment thread ftl/core/editing.ftl Resolved
Comment thread ftl/core/editing.ftl Resolved
Comment thread