-
Notifications
You must be signed in to change notification settings - Fork 0
feat: async + sandboxed image loading via glycin with GDK fallback #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
49243c2
b8e81aa
3b16770
90806e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,6 @@ edition.workspace = true | |
| rust-version.workspace = true | ||
| license.workspace = true | ||
|
|
||
| [features] | ||
| # Optional safer image decoding path. | ||
| # Disabled by default in this scaffold to keep builds simple. | ||
| glycin = ["dep:glycin"] | ||
|
|
||
| [dependencies] | ||
| anyhow.workspace = true | ||
| tracing.workspace = true | ||
|
|
@@ -23,5 +18,7 @@ gtk4-layer-shell = "0.7" | |
| glib = "0.21" | ||
| async-channel = "2" | ||
|
|
||
| # Optional: sandboxed image decoding (requires system glycin libs) | ||
| glycin = { version = "3", optional = true } | ||
| # Sandboxed image decoding. Always compiled; whether it is used is decided at | ||
| # runtime by probing installed loaders. Links libseccomp, lcms2, and | ||
| # fontconfig (see docs/DEPENDENCIES.md). | ||
| glycin = { version = "3", features = ["gdk4"] } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 3b16770 — the three packages are now explicit in both install blocks. Worth noting the premise was partially off: CI passed on the reviewed commit because on Arch There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Fresh evidence beyond the earlier dependency thread is that Useful? React with 👍 / 👎. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| //! Async image decoding off the GTK main thread. | ||
| //! | ||
| //! Two backends, chosen once per session (ADR-0004): | ||
| //! - **glycin** — decodes in a sandboxed loader process. Used whenever the | ||
| //! system has glycin loaders installed. | ||
| //! - **GDK** — in-process `gdk::Texture` decoding on a worker thread. Used | ||
| //! only when the loader probe finds no glycin loaders for this session. | ||
| //! | ||
| //! The fallback is strictly session-wide, never per-file: a file glycin | ||
| //! rejects is a failed load. Re-feeding it to the unsandboxed GDK decoder | ||
| //! would let a crafted image reach the unsandboxed path simply by making the | ||
| //! sandboxed loader fail (NFR-002). | ||
|
|
||
| use std::{cell::Cell, path::Path}; | ||
|
|
||
| use gtk4 as gtk; | ||
|
|
||
| use gtk::{gdk, gio}; | ||
|
|
||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum Backend { | ||
| Glycin, | ||
| Gdk, | ||
| } | ||
|
|
||
| /// The session-wide decode backend, probed on first use. | ||
| /// | ||
| /// Main-thread only, like the rest of the UI crate. Concurrent first calls | ||
| /// may both run the probe; that is harmless (same result, cached once). | ||
| pub async fn backend() -> Backend { | ||
| thread_local! { | ||
| static BACKEND: Cell<Option<Backend>> = const { Cell::new(None) }; | ||
| } | ||
|
|
||
| if let Some(backend) = BACKEND.get() { | ||
| return backend; | ||
| } | ||
|
|
||
| let backend = if glycin::Loader::supported_mime_types().await.is_empty() { | ||
| tracing::warn!( | ||
| "image decoding: no glycin loaders installed; \ | ||
| falling back to in-process GDK decoding (unsandboxed)" | ||
| ); | ||
| Backend::Gdk | ||
| } else { | ||
| tracing::info!("image decoding: glycin (sandboxed loader process)"); | ||
| Backend::Glycin | ||
| }; | ||
| BACKEND.set(Some(backend)); | ||
| backend | ||
| } | ||
|
|
||
| /// Decode `path` into a texture without blocking the UI. | ||
| /// | ||
| /// Await this from `glib::MainContext::spawn_local`; the actual decode work | ||
| /// happens in a sandboxed glycin process or on a worker thread. | ||
| pub async fn decode_texture(path: &Path) -> anyhow::Result<gdk::Texture> { | ||
| match backend().await { | ||
| Backend::Glycin => decode_glycin(path).await, | ||
| Backend::Gdk => decode_gdk(path).await, | ||
| } | ||
| } | ||
|
|
||
| async fn decode_glycin(path: &Path) -> anyhow::Result<gdk::Texture> { | ||
| let file = gio::File::for_path(path); | ||
| let image = glycin::Loader::new(file) | ||
| .load() | ||
| .await | ||
| .map_err(|err| anyhow::anyhow!("glycin load failed: {err}"))?; | ||
| let frame = image | ||
| .next_frame() | ||
| .await | ||
| .map_err(|err| anyhow::anyhow!("glycin frame decode failed: {err}"))?; | ||
| Ok(frame.texture()) | ||
| } | ||
|
|
||
| async fn decode_gdk(path: &Path) -> anyhow::Result<gdk::Texture> { | ||
| let (sender, receiver) = async_channel::bounded::<Result<gdk::Texture, glib::Error>>(1); | ||
|
|
||
| let path = path.to_path_buf(); | ||
| // GdkTexture is immutable and thread-safe, so it can be created on a | ||
| // worker thread and sent back to the main thread. | ||
| std::thread::spawn(move || { | ||
| let result = gdk::Texture::from_filename(&path); | ||
| let _ = sender.send_blocking(result); | ||
| }); | ||
|
|
||
| let result = receiver | ||
| .recv() | ||
| .await | ||
| .map_err(|_| anyhow::anyhow!("GDK decode thread terminated unexpectedly"))?; | ||
| Ok(result?) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ use anyhow::Result; | |
|
|
||
| use adw::prelude::*; | ||
|
|
||
| mod decode; | ||
| pub mod widgets; | ||
| pub mod windows; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making
glycinunconditional means clean source/package builds now need glycin's native libraries available at build time, before the runtime GDK fallback can ever run. In environments that have the existing GTK/Tesseract setup but not packages such as libseccomp/lcms2/fontconfig development files,cargo buildcan fail even though the docs and setup paths still present glycin as optional; please either add those native dependencies to the setup/package recipes or keep this behind a feature.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verified and fixed in b8e81aa —
cargo treeconfirms the glycin client linkslibseccomp,lcms2, andfontconfig(vialibseccomp-sys,lcms2-sys,yeslogic-fontconfig-sys), needed at build/link time regardless of which backend runs. They are now declared in the PKGBUILDdepends, the README install line, and DEPENDENCIES.md (with the Debian/Ubuntu-devpackage names), and the inaccurate "pure Rust, no system deps" wording was corrected in Cargo.toml and ADR-0004. Keeping the crate unconditional rather than feature-gated: these libraries are present on effectively every GTK-capable system, and a single build recipe avoids per-distro variants.