@@ -89,9 +89,68 @@ def has(needles: tuple[str, ...]) -> bool:
8989 "tesseract-ocr-nor maim" )
9090
9191
92+ def _in_flatpak () -> bool :
93+ return os .path .exists ("/.flatpak-info" )
94+
95+
96+ def _portal_screenshot (out_path : Path ) -> bool :
97+ """Capture a region via the XDG Screenshot portal (interactive). On KDE
98+ this opens Spectacle's region selector; the portal hands back the saved
99+ image URI, which we copy to out_path. This is how OCR captures inside the
100+ Flatpak sandbox, where no screenshot binary is on PATH."""
101+ try :
102+ import urllib .parse
103+ import dbus
104+ from dbus .mainloop .glib import DBusGMainLoop
105+ from gi .repository import GLib
106+ except Exception as exc : # noqa: BLE001
107+ log .warning ("[ocr] screenshot portal unavailable: %s" , exc )
108+ return False
109+ got : dict [str , str ] = {}
110+ try :
111+ DBusGMainLoop (set_as_default = True )
112+ bus = dbus .SessionBus ()
113+ portal = bus .get_object ("org.freedesktop.portal.Desktop" ,
114+ "/org/freedesktop/portal/desktop" )
115+ iface = dbus .Interface (portal , "org.freedesktop.portal.Screenshot" )
116+ loop = GLib .MainLoop ()
117+
118+ def _on_response (response , results ):
119+ if int (response ) == 0 and "uri" in results :
120+ got ["uri" ] = str (results ["uri" ])
121+ loop .quit ()
122+
123+ req_path = iface .Screenshot (
124+ "" , {"interactive" : dbus .Boolean (True ),
125+ "handle_token" : "linuxpop_ocr_%d" % os .getpid ()})
126+ bus .add_signal_receiver (
127+ _on_response , signal_name = "Response" ,
128+ dbus_interface = "org.freedesktop.portal.Request" , path = str (req_path ))
129+ # Generous timeout: the user is drawing a box by hand.
130+ GLib .timeout_add (180000 , lambda : (loop .quit (), False )[1 ])
131+ loop .run ()
132+ except Exception as exc : # noqa: BLE001
133+ log .warning ("[ocr] screenshot portal call failed: %s" , exc )
134+ return False
135+ uri = got .get ("uri" )
136+ if not uri :
137+ return False # user cancelled, or no result
138+ src = uri [7 :] if uri .startswith ("file://" ) else uri
139+ src = urllib .parse .unquote (src )
140+ try :
141+ shutil .copyfile (src , str (out_path ))
142+ except OSError as exc :
143+ log .warning ("[ocr] could not read portal screenshot %s: %s" , src , exc )
144+ return False
145+ return out_path .is_file () and out_path .stat ().st_size > 0
146+
147+
92148def _has_capture_tool () -> bool :
93- """True if any supported region-capture tool is on PATH. spectacle and
94- grim work natively on Wayland; maim/gnome-screenshot are the X11 path."""
149+ """True if we can capture a region. Inside Flatpak we go through the XDG
150+ Screenshot portal (no binary needed). Otherwise we need spectacle/grim
151+ (Wayland) or maim/gnome-screenshot (X11) on PATH."""
152+ if _in_flatpak ():
153+ return True
95154 return bool (shutil .which ("spectacle" ) or shutil .which ("grim" )
96155 or shutil .which ("maim" ) or shutil .which ("gnome-screenshot" ))
97156
@@ -114,6 +173,8 @@ def install_argv() -> "list[str] | None":
114173 we don't recognise the package manager. A capture tool is only added
115174 when none is present - KDE already ships spectacle, so on most Wayland
116175 desktops only tesseract is missing."""
176+ if _in_flatpak ():
177+ return None # can't install host packages from the sandbox; OCR is bundled
117178 ids = _distro_id ()
118179
119180 def has (needles : tuple ) -> bool :
@@ -148,6 +209,8 @@ def _capture_region(out_path: Path) -> bool:
148209 """Use whichever region-capture tool is installed to grab a user-
149210 drawn rectangle and write it as a PNG. Returns False if the user
150211 cancelled or the tool errored out."""
212+ if _in_flatpak ():
213+ return _portal_screenshot (out_path )
151214 if shutil .which ("spectacle" ):
152215 # KDE's capture tool. Its rectangular-region selector works
153216 # natively on Wayland (maim is X11-only and grim needs wlroots),
@@ -290,16 +353,6 @@ def run_ocr_to_clipboard() -> None:
290353 tray menu. Captures a region, OCRs it, puts the result on the
291354 clipboard, and shows the result text in the popup (so it lands as
292355 a selection the rest of LinuxPop's actions can pick up)."""
293- if os .path .exists ("/.flatpak-info" ):
294- # Screen OCR needs a screenshot tool + tesseract; neither is in the
295- # Flatpak (and Wayland capture would need the Screenshot portal). Don't
296- # show the distro "install ..." hint to a Flatpak user - just say so.
297- subprocess .run (
298- ["notify-send" , "--hint=byte:transient:1" , "-t" , "4000" ,
299- "-i" , "dialog-information" , "LinuxPop OCR" ,
300- "Screen OCR isn't available in the Flatpak build of LinuxPop." ],
301- check = False )
302- return
303356 ok_sup , reason = is_supported ()
304357 if not ok_sup :
305358 subprocess .run (
0 commit comments