Top bar randomly disappearing in image occlusion : can’t save progress

Hello ankers,


See the bar highlighted by the blue arrow ? It disappears in this image occlusion UI, preventing saving progress on mask creation.

It happens :slight_smile:

  • usually after 10-15 minutes of scribbling masks on a note
  • using gestures in more than half of the cases
  • or just using keyboard shortcuts! And I do not know why ! These shortcuts actually prompted me to report this behavior because even taking a screenshot (CMD+SHIT+3) somehow removed the top bar.

Some things worth mentioning:

  • images are ≈5 MB
  • happens whether the hide Top bar feature is (in)active.
  • iPadOS26
  • the touchscreen gets heavily janky on IO : since there is no « pan » tool, I default to the « select » one to zoom in or out. Despite this, IO does NOT like the use of two fingers (see the attached video on Streamable).

And the problem ?

I can’t get that bar back in any known way! Shortcuts ? Nonexistent for such a niche action; CMD+S or CMD+Enter ? Not working in this « realm » ; Swiping from the top of the screen ? Hopeless ; Swapping apps back and forth ? Locking and unlocked the iPad ? Both unyielding
At the end of the day, I reluctantly force closing Anki, losing anywhere between 30 minutes and 3 hours of work.

Solutions on my end ?

Alternatively, I am considering trying to export my highlights done on another app to SVG and try getting ChatGPT to « translate » the shapes into the mask syntax readable by Anki…but that is another can of worms that lies outside of the scope of this report.

Solutions on your end ?

Perhaps an auto-saving of the masks in IO ? Add a setting to set the time interval : 5 minutes, 10 minutes… Otherwise, I sit clueless.

Thank you for reading this bare-bones wall of text fueled by despair. I do acknowledge its tone, or perhaps mine as this is the 2nd time I had to type this report because the draft didn’t save while swapping apps. Apparently I’m having issues with saving inputs both on the website and Anki x).

May you rejoice the end of 2025 among your loved ones,
Vlad.

Some things that will help us track the issue –

Was this happening before you upgraded your OS?

I see you’re using AnkiMobile 25.09 – was this happening before you upgraded to that?

Which upgrade came first?

Hey Danika,
I can’t know for sure since the timeframe is thin. iPadOS26 released on September 12th and Anki 25.09 on the 6th. I believe I noticed it for the first time mid-october but it was also the first time I added new notes since July. ^^“

Also, new update! Before typing this answer, I purposefully swapped apps from Anki to Safari, leaving it running in the background. It has been 10 minutes and I haven’t touched anything on the IO interface, yet the top bar disappeared again!

Regarding some settings that could (?) help : I use fullscreen mode full-time.

Hello again,

I found a work-around using :

  • Affinity Designer
  • a-Shell
  • the following python file (created and refined with ChatGPT)
  1. Install Affinity Designer, a-Shell, paste the python file at On my iPad/a-Shell.

Now we can actually start:

  1. Launch Affinity Designer
  2. Using the image, I highlight the texts using a pen tool on Affinity designer. I can group them as in Anki.
  3. Add a big rectangle with the exact dimensions on top of the whole thing, it will serve as a base for the relative coordinates of the highlights.
  4. Export the selection (rectangle + highlights but not the image) by SVG, enable « Relative coordinates » in the export UI.
  5. Copy the exported SVG file.
  6. Launch a-Shell
  7. type « pbpaste | python3 script_v13.py | pbcopy » and hit Enter
    (There is no feedback message because the result got « injected » into your clipboard)
  8. paste that somewhere to not lose it (I paste it in Notes)
  9. Launch Anki
  10. Create new note using IO, select the image, make sure it has the same size as the « big rectangle » I talked about at step 3, else you will lose the scaling of the masks.
  11. Create a mask using IO interface, any! Then hit save. That way you can access the input fields to manually paste the result of the python script.
  12. Go fetch the pasted results from the python script and copy them. Paste them in the Masks field and hit save.

There you go!

↓ The said python script :


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys, io, re
import xml.etree.ElementTree as ET
from math import fabs

svg_text = sys.stdin.read()
if not svg_text or not svg_text.strip():
    raise RuntimeError("Aucune entrée SVG reçue (stdin vide).")

tree = ET.parse(io.StringIO(svg_text))
root = tree.getroot()

def tagname(node):
    return node.tag.split("}")[-1]

# -------------------------
# Matrices SVG
# -------------------------
def parse_matrix(transform: str):
    if not transform:
        return (1.0,0.0,0.0,1.0,0.0,0.0)
    m = re.search(r"matrix\(([^)]+)\)", transform)
    if not m:
        return (1.0,0.0,0.0,1.0,0.0,0.0)
    parts = m.group(1).replace(" ", "").split(",")
    if len(parts) != 6:
        return (1.0,0.0,0.0,1.0,0.0,0.0)
    return tuple(map(float, parts))

def mul(m1, m2):
    a1,b1,c1,d1,e1,f1 = m1
    a2,b2,c2,d2,e2,f2 = m2
    return (
        a1*a2 + c1*b2,
        b1*a2 + d1*b2,
        a1*c2 + c1*d2,
        b1*c2 + d1*d2,
        a1*e2 + c1*f2 + e1,
        b1*e2 + d1*f2 + f1
    )

def apply(m, x, y):
    a,b,c,d,e,f = m
    return (a*x + c*y + e, b*x + d*y + f)

# -------------------------
# Utilitaires shapes
# -------------------------
def parse_stroke_width(style: str):
    if not style:
        return None
    m = re.search(r"stroke-width\s*:\s*([0-9.]+)", style)
    return float(m.group(1)) if m else None

def extract_segment_from_d(d: str):
    if not d:
        return None
    d = d.strip()
    m = re.search(
        r"[Mm]\s*([-0-9.]+)[,\s]+([-0-9.]+)\s*([lL])\s*([-0-9.]+)[,\s]+([-0-9.]+)",
        d
    )
    if not m:
        return None
    x0 = float(m.group(1)); y0 = float(m.group(2))
    cmd = m.group(3)
    a_ = float(m.group(4)); b_ = float(m.group(5))
    if cmd == "l":
        x1 = x0 + a_
        y1 = y0 + b_
    else:
        x1 = a_
        y1 = b_
    return (x0, y0, x1, y1)

def subtree_contains_reference_rect(node):
    for n in node.iter():
        if tagname(n) == "rect":
            style = n.attrib.get("style","")
            if ("fill:" in style) and ("stroke" not in style):
                return True
    return False

# -------------------------
# 1) Construire la matrice globale de chaque node
# -------------------------
node_global = {}

def build_globals(node, parent_matrix):
    m = mul(parent_matrix, parse_matrix(node.attrib.get("transform")))
    node_global[id(node)] = m
    for ch in node:
        build_globals(ch, m)

build_globals(root, (1.0,0.0,0.0,1.0,0.0,0.0))

# -------------------------
# 2) Référence: rect gris (dans le bon repère global)
# -------------------------
reference = None
for n in root.iter():
    if tagname(n) != "rect":
        continue
    style = n.attrib.get("style","")
    if ("fill:" in style) and ("stroke" not in style):
        m = node_global[id(n)]
        x = float(n.attrib["x"]); y = float(n.attrib["y"])
        w = float(n.attrib["width"]); h = float(n.attrib["height"])
        X0,Y0 = apply(m, x, y)
        X1,Y1 = apply(m, x+w, y+h)
        reference = (min(X0,X1), min(Y0,Y1), max(X0,X1), max(Y0,Y1))
        break

if not reference:
    raise RuntimeError("Rectangle de référence (rect gris) non trouvé.")

rx0, ry0, rx1, ry1 = reference
rw = rx1 - rx0
rh = ry1 - ry0
if abs(rw) < 1e-9 or abs(rh) < 1e-9:
    raise RuntimeError("Rectangle de référence invalide (rw/rh ~ 0).")

# -------------------------
# Extraction des masques depuis un sous-arbre
# -------------------------
def extract_masks_from_subtree(subroot):
    rects = []

    def walk(node):
        m = node_global[id(node)]

        if tagname(node) == "path":
            style = node.attrib.get("style","")
            sw = parse_stroke_width(style)
            if sw is None:
                return
            seg = extract_segment_from_d(node.attrib.get("d",""))
            if seg is None:
                return

            x0,y0,x1,y1 = seg
            X0,Y0 = apply(m, x0, y0)
            X1,Y1 = apply(m, x1, y1)

            a,b,c_,d,e,f = m
            scale_x = fabs(a)
            scale_y = fabs(d)
            sw_world = sw * (scale_x + scale_y) / 2.0

            x = min(X0, X1)
            y = min(Y0, Y1) - sw_world/2.0
            w = fabs(X1 - X0)
            h = sw_world
            rects.append((x,y,w,h))

        for ch in node:
            walk(ch)

    walk(subroot)
    return rects

# -------------------------
# Grouping "Affinity-like"
# -------------------------
drawing_root = None
for c in list(root):
    if tagname(c) == "g":
        drawing_root = c
        break
if drawing_root is None:
    raise RuntimeError("Aucun <g> racine trouvé sous <svg>.")

candidate_groups = [c for c in list(drawing_root) if tagname(c) == "g"]
candidate_groups = [g for g in candidate_groups if not subtree_contains_reference_rect(g)]

final_groups = []
for g in candidate_groups:
    id_children = [x for x in list(g) if tagname(x) == "g" and x.attrib.get("id")]
    final_groups.extend(id_children if id_children else [g])

# -------------------------
# Sortie Anki normalisée (doit retomber ~0..1)
# -------------------------
cid = 0
for grp in final_groups:
    cid += 1
    rects = extract_masks_from_subtree(grp)
    for (x,y,w,h) in rects:
        left   = (x - rx0) / rw
        top    = (y - ry0) / rh
        width  = w / rw
        height = h / rh
        print(
            f"{{{{c{cid}::image-occlusion:rect:"
            f"left={left:.4f}:top={top:.4f}:"
            f"width={width:.4f}:height={height:.4f}}}}}"
        )