use std::sync::{Arc, Mutex};
use gio::glib;
use gio::prelude::*;
pub use glycin_common::MemoryFormat;
use glycin_common::{BinaryData, MemoryFormatSelection};
#[cfg(feature = "gdk4")]
use glycin_utils::safe_math::*;
use gufo_common::orientation::{Orientation, Rotation};
use zbus::zvariant::OwnedObjectPath;
use crate::api_common::*;
pub use crate::config::MimeType;
use crate::dbus::*;
use crate::error::ResultExt;
use crate::pool::{Pool, PooledProcess, UsageTracker};
use crate::util::spawn_detached;
use crate::{ErrorCtx, config};
#[derive(Debug)]
pub struct Loader {
source: Source,
pool: Arc<Pool>,
cancellable: gio::Cancellable,
use_expose_base_dir: bool,
pub(crate) apply_transformations: bool,
pub(crate) sandbox_selector: SandboxSelector,
pub(crate) memory_format_selection: MemoryFormatSelection,
}
static_assertions::assert_impl_all!(Loader: Send, Sync);
impl Loader {
pub fn new(file: gio::File) -> Self {
Self::new_source(Source::File(file))
}
pub unsafe fn new_stream(stream: impl IsA<gio::InputStream>) -> Self {
unsafe { Self::new_source(Source::Stream(GInputStreamSend::new(stream.upcast()))) }
}
pub fn new_bytes(bytes: glib::Bytes) -> Self {
let stream = gio::MemoryInputStream::from_bytes(&bytes);
unsafe { Self::new_stream(stream) }
}
pub fn new_vec(buf: Vec<u8>) -> Self {
let bytes = glib::Bytes::from_owned(buf);
Self::new_bytes(bytes)
}
pub(crate) fn new_source(source: Source) -> Self {
Self {
source,
pool: Pool::global(),
cancellable: gio::Cancellable::new(),
apply_transformations: true,
use_expose_base_dir: false,
sandbox_selector: SandboxSelector::default(),
memory_format_selection: MemoryFormatSelection::all(),
}
}
pub fn sandbox_selector(&mut self, sandbox_selector: SandboxSelector) -> &mut Self {
self.sandbox_selector = sandbox_selector;
self
}
pub fn cancellable(&mut self, cancellable: impl IsA<gio::Cancellable>) -> &mut Self {
self.cancellable = cancellable.upcast();
self
}
pub fn apply_transformations(&mut self, apply_transformations: bool) -> &mut Self {
self.apply_transformations = apply_transformations;
self
}
pub fn accepted_memory_formats(
&mut self,
memory_format_selection: MemoryFormatSelection,
) -> &mut Self {
self.memory_format_selection = memory_format_selection;
self
}
pub fn use_expose_base_dir(&mut self, use_epose_base_dir: bool) -> &mut Self {
self.use_expose_base_dir = use_epose_base_dir;
self
}
pub fn pool(&mut self, pool: Arc<Pool>) -> &mut Self {
self.pool = pool;
self
}
pub async fn load(mut self) -> Result<Image, ErrorCtx> {
let source = self.source.send();
let process_basics = spin_up_loader(
source,
self.use_expose_base_dir,
self.pool.clone(),
&self.cancellable,
&self.sandbox_selector,
)
.await
.err_no_context(&self.cancellable)?;
let process = process_basics.process.use_();
let mut remote_image = process
.init(
process_basics.g_file_worker.unwrap(),
&process_basics.mime_type,
)
.await
.err_context(&process, &self.cancellable)?;
if self.apply_transformations {
match Image::transformation_orientation_internal(&remote_image.details).rotate() {
Rotation::_90 | Rotation::_270 => {
std::mem::swap(
&mut remote_image.details.width,
&mut remote_image.details.height,
);
}
_ => {}
}
}
let path = remote_image.frame_request.clone();
self.cancellable.connect_cancelled(glib::clone!(
#[strong(rename_to=process)]
process_basics.process,
move |_| {
tracing::debug!("Terminating loader");
crate::util::spawn_detached(process.use_().done(path))
}
));
Ok(Image {
process: process_basics.process,
frame_request: remote_image.frame_request,
details: Arc::new(remote_image.details),
loader: self,
mime_type: process_basics.mime_type,
active_sandbox_mechanism: process_basics.sandbox_mechanism,
usage_tracker: Mutex::new(Some(process_basics.usage_tracker)),
})
}
pub async fn supported_mime_types() -> Vec<MimeType> {
config::Config::cached()
.await
.image_loader
.keys()
.cloned()
.collect()
}
pub const DEFAULT_MIME_TYPES: &'static [&'static str] = &[
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/tiff",
"image/x-tga",
"image/x-dds",
"image/bmp",
"image/x-win-bitmap",
"image/vnd.microsoft.icon",
"image/vnd.radiance",
"image/x-exr",
"image/x-portable-bitmap",
"image/x-portable-graymap",
"image/x-portable-pixmap",
"image/x-portable-anymap",
"image/x-qoi",
"image/qoi",
"image/avif",
"image/heif",
"image/jxl",
"image/svg+xml",
"image/svg+xml-compressed",
];
}
#[derive(Debug)]
pub struct Image {
pub(crate) loader: Loader,
pub(crate) process: Arc<PooledProcess<LoaderProxy<'static>>>,
frame_request: OwnedObjectPath,
details: Arc<glycin_utils::ImageDetails>,
mime_type: MimeType,
active_sandbox_mechanism: SandboxMechanism,
usage_tracker: Mutex<Option<Arc<UsageTracker>>>,
}
static_assertions::assert_impl_all!(Image: Send, Sync);
impl Drop for Image {
fn drop(&mut self) {
let process = self.process.clone();
let path = self.frame_request_path();
let loader_alive = std::mem::take(&mut *self.usage_tracker.lock().unwrap());
spawn_detached(async move {
if let Err(err) = process.use_().done(path).await {
tracing::warn!("Failed to tear down loader: {err}")
}
drop(loader_alive);
});
}
}
impl Image {
pub async fn next_frame(&self) -> Result<Frame, ErrorCtx> {
let process = self.process.use_();
let mut frame_request = glycin_utils::FrameRequest::default();
frame_request.loop_animation = true;
process
.request_frame(frame_request, self)
.await
.err_context(&process, &self.cancellable())
}
pub async fn specific_frame(&self, frame_request: FrameRequest) -> Result<Frame, ErrorCtx> {
let process = self.process.use_();
process
.request_frame(frame_request.request, self)
.await
.err_context(&process, &self.cancellable())
}
pub fn details(&self) -> ImageDetails {
ImageDetails::new(self.details.clone())
}
pub(crate) fn frame_request_path(&self) -> OwnedObjectPath {
self.frame_request.clone()
}
pub fn mime_type(&self) -> MimeType {
self.mime_type.clone()
}
pub fn file(&self) -> Option<gio::File> {
self.loader.source.file()
}
pub fn cancellable(&self) -> gio::Cancellable {
self.loader.cancellable.clone()
}
pub fn active_sandbox_mechanism(&self) -> SandboxMechanism {
self.active_sandbox_mechanism
}
pub fn transformation_orientation(&self) -> Orientation {
Self::transformation_orientation_internal(&self.details)
}
fn transformation_orientation_internal(details: &glycin_utils::ImageDetails) -> Orientation {
if let Some(orientation) = details.transformation_orientation {
orientation
} else if !details.transformation_ignore_exif {
details
.metadata_exif
.as_ref()
.and_then(|x| x.get_full().ok())
.and_then(|x| match gufo_exif::Exif::new(x) {
Err(err) => {
tracing::warn!("exif: Failed to parse data: {err:?}");
None
}
Ok(x) => x.orientation(),
})
.unwrap_or(Orientation::Id)
} else {
Orientation::Id
}
}
}
#[derive(Debug, Clone)]
pub struct ImageDetails {
inner: Arc<glycin_utils::ImageDetails>,
}
impl ImageDetails {
fn new(inner: Arc<glycin_utils::ImageDetails>) -> Self {
Self { inner }
}
pub fn width(&self) -> u32 {
self.inner.width
}
pub fn height(&self) -> u32 {
self.inner.height
}
pub fn dimensions_inch(&self) -> Option<(f64, f64)> {
self.inner.dimensions_inch
}
pub fn info_format_name(&self) -> Option<&str> {
self.inner.info_format_name.as_deref()
}
pub fn info_dimensions_text(&self) -> Option<&str> {
self.inner.info_dimensions_text.as_deref()
}
pub fn metadata_exif(&self) -> Option<BinaryData> {
self.inner.metadata_exif.clone()
}
pub fn transformation_orientation(&self) -> Option<Orientation> {
self.inner.transformation_orientation
}
pub fn metadata_xmp(&self) -> Option<BinaryData> {
self.inner.metadata_xmp.clone()
}
pub fn metadata_key_value(&self) -> Option<&std::collections::BTreeMap<String, String>> {
self.inner.metadata_key_value.as_ref()
}
pub fn transformation_ignore_exif(&self) -> bool {
self.inner.transformation_ignore_exif
}
}
#[derive(Debug, Clone)]
pub struct Frame {
pub(crate) buffer: glib::Bytes,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) stride: u32,
pub(crate) memory_format: MemoryFormat,
pub(crate) delay: Option<std::time::Duration>,
pub(crate) details: Arc<glycin_utils::FrameDetails>,
pub(crate) color_state: ColorState,
}
impl Frame {
pub fn buf_bytes(&self) -> glib::Bytes {
self.buffer.clone()
}
pub fn buf_slice(&self) -> &[u8] {
self.buffer.as_ref()
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn stride(&self) -> u32 {
self.stride
}
pub fn memory_format(&self) -> MemoryFormat {
self.memory_format
}
pub fn color_state(&self) -> &ColorState {
&self.color_state
}
pub fn delay(&self) -> Option<std::time::Duration> {
self.delay
}
pub fn details(&self) -> FrameDetails {
FrameDetails::new(self.details.clone())
}
#[cfg(feature = "gdk4")]
pub fn texture(&self) -> gdk::Texture {
let color_state = crate::util::gdk_color_state(&self.color_state).unwrap_or_else(|_| {
tracing::warn!("Unsupported color state: {:?}", self.color_state);
gdk::ColorState::srgb()
});
gdk::MemoryTextureBuilder::new()
.set_bytes(Some(&self.buffer))
.set_width(self.width().try_i32().unwrap())
.set_height(self.height().try_i32().unwrap())
.set_stride(self.stride().try_usize().unwrap())
.set_format(crate::util::gdk_memory_format(self.memory_format()))
.set_color_state(&color_state)
.build()
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct FrameRequest {
pub(crate) request: glycin_utils::FrameRequest,
}
impl Default for FrameRequest {
fn default() -> Self {
Self::new()
}
}
impl FrameRequest {
pub fn new() -> Self {
let mut request = glycin_utils::FrameRequest::default();
request.loop_animation = true;
Self { request }
}
pub fn scale(mut self, width: u32, height: u32) -> Self {
self.request.scale = Some((width, height));
self
}
pub fn clip(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
self.request.clip = Some((x, y, width, height));
self
}
pub fn loop_animation(mut self, loop_animation: bool) -> Self {
self.request.loop_animation = loop_animation;
self
}
}
#[derive(Debug, Clone)]
pub struct FrameDetails {
inner: Arc<glycin_utils::FrameDetails>,
}
impl FrameDetails {
fn new(inner: Arc<glycin_utils::FrameDetails>) -> Self {
Self { inner }
}
pub fn color_cicp(&self) -> Option<crate::Cicp> {
self.inner
.color_cicp
.and_then(|x| crate::Cicp::from_bytes(&x).ok())
}
pub fn color_icc_profile(&self) -> Option<BinaryData> {
self.inner.color_icc_profile.clone()
}
pub fn info_alpha_channel(&self) -> Option<bool> {
self.inner.info_alpha_channel
}
pub fn info_bit_depth(&self) -> Option<u8> {
self.inner.info_bit_depth
}
pub fn info_grayscale(&self) -> Option<bool> {
self.inner.info_grayscale
}
pub fn n_frame(&self) -> Option<u64> {
self.inner.n_frame
}
}
#[cfg(test)]
mod test {
use super::*;
#[allow(dead_code)]
fn ensure_futures_are_send() {
gio::glib::spawn_future(async {
let loader = Loader::new(gio::File::for_uri("invalid"));
let image = loader.load().await.unwrap();
image.next_frame().await.unwrap();
});
}
}