Skip to main content

glycin_core/api/
loader.rs

1use std::pin::Pin;
2use std::sync::{Arc, Mutex, OnceLock};
3
4#[cfg(feature = "builtin")]
5use futures_util::FutureExt;
6use gio::glib;
7use gio::prelude::*;
8pub use glycin_common::MemoryFormat;
9use glycin_common::{ColorProfilePreference, MemoryFormatInfo, MemoryFormatSelection};
10#[cfg(feature = "builtin")]
11use glycin_utils::LoaderImplementation;
12use glycin_utils::safe_math::*;
13use glycin_utils::{ByteData, FungibleMemory};
14use gufo_common::cicp::Cicp;
15use gufo_common::orientation::{Orientation, Rotation};
16use gufo_common::physical_dimension;
17use util::{CancellableFuture, ShortcutErrorFuture, TimeoutFuture};
18#[cfg(feature = "external")]
19use zbus::zvariant::OwnedObjectPath;
20
21use crate::api::*;
22pub use crate::config::MimeType;
23#[cfg(feature = "external")]
24use crate::dbus::*;
25use crate::error::{ErrorKind, ResultExt};
26use crate::main_context::{MainContextSelector, ProvidesMainContext};
27#[cfg(feature = "external")]
28use crate::pool::{PooledProcess, UsageTracker};
29use crate::source::SourceTransmission;
30use crate::util::spawn_blocking;
31use crate::{Error, MAX_TEXTURE_SIZE, Pool, config, icc, orientation, util};
32
33/// Builder pattern for loading images
34#[derive(Debug)]
35pub struct Loader {
36    pub(crate) source: Source,
37    pool: Arc<Pool>,
38    pub(crate) cancellable: gio::Cancellable,
39    use_expose_base_dir: bool,
40    pub(crate) apply_transformations: bool,
41    pub(crate) sandbox_selector: SandboxSelector,
42    pub(crate) memory_format_selection: MemoryFormatSelection,
43    pub(crate) limits: Limits,
44    pub(crate) main_context_selector: MainContextSelector,
45    pub(crate) color_convert_icc_srgb: bool,
46}
47
48static_assertions::assert_impl_all!(Loader: Send, Sync);
49
50impl Loader {
51    /// Create a loader with a [`gio::File`] as source
52    pub fn new(file: gio::File) -> Self {
53        Self::new_source(Source::File(file))
54    }
55
56    /// Create a loader with a [`gio::InputStream`] as source
57    ///
58    /// # Safety
59    ///
60    /// The provided stream must no longer be used after being passed to glycin.
61    pub unsafe fn new_stream(stream: impl IsA<gio::InputStream>) -> Self {
62        unsafe { Self::new_source(Source::Stream(GInputStreamSend::new(stream.upcast()))) }
63    }
64
65    /// Create a loader with [`glib::Bytes`] as source
66    pub fn new_bytes(bytes: glib::Bytes) -> Self {
67        let stream = gio::MemoryInputStream::from_bytes(&bytes);
68        unsafe { Self::new_stream(stream) }
69    }
70
71    /// Create a loader with [`Vec<u8>`] as source
72    pub fn new_vec(buf: Vec<u8>) -> Self {
73        let bytes = glib::Bytes::from_owned(buf);
74        Self::new_bytes(bytes)
75    }
76
77    pub(crate) fn new_source(source: Source) -> Self {
78        Self {
79            source,
80            pool: Pool::global(),
81            cancellable: gio::Cancellable::new(),
82            apply_transformations: true,
83            use_expose_base_dir: false,
84            sandbox_selector: SandboxSelector::default(),
85            memory_format_selection: MemoryFormatSelection::all(),
86            limits: Limits::default(),
87            main_context_selector: MainContextSelector::Auto,
88            color_convert_icc_srgb: true,
89        }
90    }
91
92    /// Sets the method by which the sandbox mechanism is selected.
93    ///
94    /// The default without calling this function is [`SandboxSelector::Auto`].
95    pub fn sandbox_selector(&mut self, sandbox_selector: SandboxSelector) -> &mut Self {
96        self.sandbox_selector = sandbox_selector;
97        self
98    }
99
100    /// Set [`Cancellable`](gio::Cancellable) to cancel any loader operations
101    pub fn cancellable(&mut self, cancellable: impl IsA<gio::Cancellable>) -> &mut Self {
102        self.cancellable = cancellable.upcast();
103        self
104    }
105
106    /// Set whether to apply transformations to texture
107    ///
108    /// When enabled, transformations like image orientation are applied to the
109    /// texture data.
110    ///
111    /// This option is enabled by default.
112    pub fn apply_transformations(&mut self, apply_transformations: bool) -> &mut Self {
113        self.apply_transformations = apply_transformations;
114        self
115    }
116
117    /// Sets which memory formats can be returned by the loader
118    ///
119    /// If the memory format doesn't match one of the selected formats, the
120    /// format will be transformed into the best suitable format selected.
121    pub fn accepted_memory_formats(
122        &mut self,
123        memory_format_selection: MemoryFormatSelection,
124    ) -> &mut Self {
125        self.memory_format_selection = memory_format_selection;
126        self
127    }
128
129    /// Sets whether to convert textures to sRGB if ICC profile is present
130    ///
131    /// The default value if not changed is `true`.
132    pub fn color_convert_icc_srgb(&mut self, convert: bool) -> &mut Self {
133        self.color_convert_icc_srgb = convert;
134        self
135    }
136
137    /// Sets if the file's directory can be exposed to loaders
138    ///
139    /// Some loaders have the `use_base_dir` option enabled to load external
140    /// files. One example is SVGs which can display external images inside the
141    /// picture. By default, `use_expose_base_dir` is set to `false`. You need
142    /// to enable it for the `use_base_dir` option to have any effect. The
143    /// downside of enabling it is that separate sandboxes are needed for
144    /// different base directories, which has a noticeable performance impact
145    /// when loading many small SVGs from many different directories.
146    pub fn use_expose_base_dir(&mut self, use_epose_base_dir: bool) -> &mut Self {
147        self.use_expose_base_dir = use_epose_base_dir;
148        self
149    }
150
151    pub fn pool(&mut self, pool: Arc<Pool>) -> &mut Self {
152        self.pool = pool;
153        self
154    }
155
156    pub fn limits(&mut self, limits: Limits) -> &mut Self {
157        self.limits = limits;
158        self
159    }
160
161    pub fn main_context_selector(&mut self, selector: MainContextSelector) -> &mut Self {
162        self.main_context_selector = selector;
163        self
164    }
165
166    /// Load basic image information and enable further operations
167    pub fn load(self) -> Pin<Box<dyn Future<Output = Result<Image, Error>> + Send>> {
168        self.load_with_sync(false)
169    }
170
171    /// Same as [`load`](Self::load) but with sync option
172    ///
173    /// Setting `sync` to true will use sync variants of the Gio.File API.
174    /// Otherwise, the async Gio.File function might make no progress since some
175    /// libglycin consumers block all GTasks with sync operations, also
176    /// blocking the internal GIO thread pools for IO.
177    ///
178    /// See <https://gitlab.gnome.org/GNOME/glib/-/work_items/4034>.
179    pub(crate) fn load_with_sync(
180        mut self,
181        sync: bool,
182    ) -> Pin<Box<dyn Future<Output = Result<Image, Error>> + Send>> {
183        Box::pin(async move {
184            tracing::debug!(image = self.source.display(), "Loading image");
185
186            let source = self.source.send();
187            let main_context = self.main_context();
188            let cancellable = self.cancellable.clone();
189            let timeout = self.limits.inner.timeout;
190
191            let f = move || {
192                async move { self.load_internal(source, sync).await }
193                    .make_cancellable(cancellable)
194                    .enforce_timeout(timeout)
195            };
196
197            main_context.spawn_from_within(f).await?
198        })
199    }
200
201    async fn load_internal(self, source: Source, sync: bool) -> Result<Image, Error> {
202        let loader_context = ProcessorContext::new(
203            source,
204            self.use_expose_base_dir,
205            &self.sandbox_selector,
206            sync,
207        )
208        .await?;
209
210        let loader = loader_context
211            .loader(self.pool.clone(), &self.cancellable)
212            .await?;
213
214        match loader {
215            #[cfg(feature = "external")]
216            Processor::Binary(binary_loader) => self.load_internal_external(binary_loader).await,
217            #[cfg(feature = "builtin")]
218            Processor::Builtin(builtin) => self.load_internal_builtin(builtin).await,
219        }
220    }
221
222    #[cfg(feature = "external")]
223    async fn load_internal_external(
224        self,
225        binary_loader: ExternalProcessor<LoaderProxy<'static>, SourceTransmission>,
226    ) -> Result<Image, Error> {
227        tracing::debug!("Using external loader");
228
229        let process = binary_loader.use_process();
230        let (remote_reader, file_read_future) =
231            binary_loader.source_transmission.spawn_external()?;
232
233        let remote_image_future = process.init(&binary_loader.mime_type, remote_reader);
234
235        // Drive reading the image source in parallel and shortcut if it errors
236        let mut remote_image = remote_image_future
237            .join_abort_on_error(file_read_future)
238            .await
239            .err_context(&process)?;
240
241        remote_image.final_seal().await?;
242
243        let mut details = remote_image.details.into_fungible();
244
245        if self.apply_transformations {
246            match Image::transformation_orientation_internal(&details).rotate() {
247                Rotation::_90 | Rotation::_270 => {
248                    std::mem::swap(&mut details.width, &mut details.height);
249                }
250                _ => {}
251            }
252        }
253
254        let path = remote_image.frame_request.clone();
255        self.cancellable.connect_cancelled(glib::clone!(
256            #[strong(rename_to=process)]
257            binary_loader.process,
258            move |_| {
259                tracing::debug!("Terminating loader");
260                util::spawn_detached(process.use_().done(path))
261            }
262        ));
263
264        let mime_type = binary_loader.mime_type.clone();
265
266        let image_loader = ImageLoader::Binary(ImageExternalLoader {
267            process: binary_loader.process,
268            active_sandbox_mechanism: binary_loader.sandbox_mechanism,
269            usage_tracker: Mutex::new(Some(binary_loader.usage_tracker)),
270            frame_request: remote_image.frame_request,
271        });
272
273        Ok(Image {
274            image_loader,
275            details: Arc::new(details),
276            loader: self,
277            mime_type,
278        })
279    }
280
281    #[cfg(feature = "builtin")]
282    async fn load_internal_builtin<P: DBusProxy>(
283        self,
284        builtin: BuiltinProcessor<P, SourceTransmission>,
285    ) -> Result<Image, Error> {
286        tracing::debug!("Using builtin loader '{}'", builtin.builtin.common().name());
287
288        let init_function: Box<dyn Fn(_, _, _) -> _ + Send>;
289
290        match builtin.builtin {
291            #[cfg(feature = "builtin-image-rs")]
292            config::BuiltinProcessor::ImageRs(_) => {
293                init_function = Box::new(|stream, mime_type, details| {
294                    glycin_image_rs::ImgLoader::load(stream, mime_type, details).map(
295                        |(decoder, details)| {
296                            (
297                                ImageBuiltinLoader::ImageRs(Arc::new(Mutex::new(decoder))),
298                                details,
299                            )
300                        },
301                    )
302                });
303            }
304            #[cfg(feature = "builtin-test")]
305            config::BuiltinProcessor::Test(_) => {
306                init_function = Box::new(|stream, mime_type, details| {
307                    glycin_test::ImgDecoder::load(stream, mime_type, details).map(
308                        |(decoder, details)| {
309                            (
310                                ImageBuiltinLoader::Test(Arc::new(Mutex::new(decoder))),
311                                details,
312                            )
313                        },
314                    )
315                });
316            }
317        }
318
319        let mime_type = builtin.mime_type.clone();
320
321        let (source_reader, file_read_future) = builtin.source_transmission.spawn_builtin();
322
323        let remote_image_future = gio::spawn_blocking(move || {
324            init_function(
325                source_reader,
326                builtin.mime_type.to_string(),
327                // TODO: That should be something different?
328                glycin_utils::InitializationDetails::default(),
329            )
330            .map_err(|e| Error::from(e.into_loader_error()))
331        })
332        .map(|x| x.map_err(|e| ErrorKind::panic(e).err()));
333
334        let (image_loader, image_details) = remote_image_future
335            .join_abort_on_error(file_read_future)
336            .await??;
337
338        Ok(Image {
339            image_loader: ImageLoader::Builtin(image_loader),
340            details: Arc::new(image_details),
341            loader: self,
342            mime_type,
343        })
344    }
345
346    /// Returns a list of mime types for which loaders are configured
347    pub async fn supported_mime_types() -> Vec<MimeType> {
348        config::Config::cached()
349            .await
350            .image_loader
351            .keys()
352            .cloned()
353            .collect()
354    }
355
356    /// Formats that the default glycin loaders support
357    pub const DEFAULT_MIME_TYPES: &'static [&'static str] = &[
358        // image-rs
359        "image/jpeg",
360        "image/png",
361        "image/gif",
362        "image/webp",
363        "image/tiff",
364        "image/x-tga",
365        "image/x-dds",
366        "image/bmp",
367        "image/x-win-bitmap",
368        "image/vnd.microsoft.icon",
369        "image/vnd.radiance",
370        "image/x-exr",
371        "image/x-portable-bitmap",
372        "image/x-portable-graymap",
373        "image/x-portable-pixmap",
374        "image/x-portable-anymap",
375        "image/x-qoi",
376        "image/qoi",
377        // HEIF
378        "image/avif",
379        "image/heif",
380        // JXL
381        "image/jxl",
382        // SVG
383        "image/svg+xml",
384        "image/svg+xml-compressed",
385    ];
386}
387
388/// Image handle containing metadata and allowing frame requests
389#[derive(Debug)]
390pub struct Image {
391    pub(crate) loader: Loader,
392    image_loader: ImageLoader,
393    details: Arc<glycin_utils::ImageDetails<FungibleMemory>>,
394    mime_type: MimeType,
395}
396
397static_assertions::assert_impl_all!(Image: Send, Sync);
398
399impl Drop for Image {
400    fn drop(&mut self) {
401        #[cfg(feature = "external")]
402        #[allow(irrefutable_let_patterns)]
403        if let ImageLoader::Binary(image_loader) = &self.image_loader {
404            let process = image_loader.process.clone();
405            let path = self.frame_request_path();
406            let loader_alive = std::mem::take(&mut *image_loader.usage_tracker.lock().unwrap());
407            util::spawn_detached(async move {
408                if let Err(err) = process.use_().done(path).await {
409                    tracing::warn!("Failed to tear down loader: {err}")
410                }
411
412                drop(loader_alive);
413            });
414        }
415    }
416}
417
418impl Image {
419    /// Loads next frame
420    ///
421    /// Loads texture and information of the next frame. For single still
422    /// images, this can only be called once. For animated images, this
423    /// function will loop to the first frame, when the last frame is reached.
424    pub fn next_frame<'a>(
425        &'a mut self,
426    ) -> Pin<Box<dyn Future<Output = Result<Frame, Error>> + 'a + Send>> {
427        self.specific_frame(FrameRequest::default())
428    }
429
430    /// Loads a specific frame
431    ///
432    /// Loads a specific frame from the file. Loaders can ignore parts of the
433    /// instructions in the `FrameRequest`.
434    pub fn specific_frame<'a>(
435        &'a mut self,
436        frame_request: FrameRequest,
437    ) -> Pin<Box<dyn Future<Output = Result<Frame, Error>> + 'a + Send>> {
438        Box::pin(async move {
439            let cancellable = self.loader.cancellable.clone();
440
441            self.specific_frame_internal(frame_request)
442                .make_cancellable(cancellable)
443                .enforce_timeout(self.loader.limits.inner.timeout)
444                .await
445        })
446    }
447
448    async fn specific_frame_internal(&self, frame_request: FrameRequest) -> Result<Frame, Error> {
449        let frame_request = frame_request.request;
450
451        match &self.image_loader {
452            #[cfg(feature = "external")]
453            ImageLoader::Binary(image_loader) => {
454                let process = image_loader.process.use_();
455
456                let frame = process
457                    .request_frame(frame_request, self)
458                    .await
459                    .err_context(&process)?;
460
461                Frame::from_loader(frame, self).await
462            }
463            #[cfg(feature = "builtin")]
464            ImageLoader::Builtin(builtin) => {
465                use glycin_utils::LocalMemory;
466
467                let editor_function: Box<dyn FnOnce() -> _ + Send>;
468
469                match builtin {
470                    #[cfg(feature = "builtin-image-rs")]
471                    ImageBuiltinLoader::ImageRs(loader) => {
472                        let loader: Arc<Mutex<glycin_image_rs::ImgLoader>> = loader.to_owned();
473                        editor_function = Box::new(move || {
474                            loader
475                                .lock()
476                                .unwrap()
477                                .specific_frame::<LocalMemory>(frame_request)
478                        });
479                    }
480                    #[cfg(feature = "builtin-test")]
481                    ImageBuiltinLoader::Test(editor) => {
482                        let editor = editor.to_owned();
483                        editor_function = Box::new(move || {
484                            editor
485                                .lock()
486                                .unwrap()
487                                .specific_frame::<LocalMemory>(frame_request)
488                        });
489                    }
490                }
491
492                let frame = gio::spawn_blocking(|| {
493                    editor_function().map_err(|e| Error::from(e.into_loader_error()))
494                })
495                .await
496                .map_err(|e| ErrorKind::panic(e))??;
497
498                Frame::from_loader(frame, self).await
499            }
500        }
501    }
502
503    /// Returns already obtained info
504    pub fn details(&self) -> ImageDetails {
505        ImageDetails::new(self.details.clone())
506    }
507
508    /// Returns already obtained info
509    #[cfg(feature = "external")]
510    pub(crate) fn frame_request_path(&self) -> OwnedObjectPath {
511        #[allow(irrefutable_let_patterns)]
512        if let ImageLoader::Binary(image_loader) = &self.image_loader {
513            image_loader.frame_request.clone()
514        } else {
515            todo!()
516        }
517    }
518
519    /// Returns detected MIME type of the file
520    pub fn mime_type(&self) -> MimeType {
521        self.mime_type.clone()
522    }
523
524    /// File the image was loaded from
525    ///
526    /// Is `None` if the file was loaded from a stream or binary data.
527    pub fn file(&self) -> Option<gio::File> {
528        self.loader.source.file()
529    }
530
531    /// [`Cancellable`](gio::Cancellable) to cancel operations within this image
532    pub fn cancellable(&self) -> gio::Cancellable {
533        self.loader.cancellable.clone()
534    }
535
536    /// Active sandbox mechanism
537    pub fn active_sandbox_mechanism(&self) -> SandboxMechanism {
538        match &self.image_loader {
539            #[cfg(feature = "external")]
540            ImageLoader::Binary(image_loader) => image_loader.active_sandbox_mechanism,
541            #[cfg(feature = "builtin")]
542            ImageLoader::Builtin(_) => SandboxMechanism::NotSandboxed,
543        }
544    }
545
546    /// Tramsformations to be applied to orient image correctly
547    ///
548    /// If the [`Loader::apply_transformations`] has ben set to `false`, these
549    /// transformations have to be applied to display the image correctly.
550    /// Otherwise, they are applied automatically to the image after loading it.
551    pub fn transformation_orientation(&self) -> Orientation {
552        Self::transformation_orientation_internal(&self.details)
553    }
554
555    fn transformation_orientation_internal(
556        details: &glycin_utils::ImageDetails<FungibleMemory>,
557    ) -> Orientation {
558        if let Some(orientation) = details.transformation_orientation {
559            orientation
560        } else if !details.transformation_ignore_exif {
561            details
562                .metadata_exif
563                .as_ref()
564                .map(|x| x.to_vec())
565                .and_then(|x| match gufo_exif::Exif::for_vec(x) {
566                    Err(err) => {
567                        tracing::warn!("exif: Failed to parse data: {err:?}");
568                        None
569                    }
570                    Ok(x) => x.orientation(),
571                })
572                .unwrap_or(Orientation::Id)
573        } else {
574            Orientation::Id
575        }
576    }
577}
578
579#[derive(Debug)]
580enum ImageLoader {
581    #[cfg(feature = "external")]
582    Binary(ImageExternalLoader),
583    #[cfg(feature = "builtin")]
584    Builtin(ImageBuiltinLoader),
585}
586
587#[cfg(feature = "external")]
588#[derive(Debug)]
589struct ImageExternalLoader {
590    process: Arc<PooledProcess<LoaderProxy<'static>>>,
591    active_sandbox_mechanism: SandboxMechanism,
592    usage_tracker: Mutex<Option<Arc<UsageTracker>>>,
593    frame_request: OwnedObjectPath,
594}
595
596#[cfg(feature = "builtin")]
597#[derive(Clone)]
598enum ImageBuiltinLoader {
599    #[cfg(feature = "builtin-image-rs")]
600    ImageRs(Arc<Mutex<glycin_image_rs::ImgLoader>>),
601    #[cfg(feature = "builtin-test")]
602    Test(Arc<Mutex<glycin_test::ImgDecoder>>),
603}
604
605#[cfg(feature = "builtin")]
606impl std::fmt::Debug for ImageBuiltinLoader {
607    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608        f.write_str("ImageBuiltinLoader")
609    }
610}
611
612/// More information about an [image](Image)
613#[derive(Debug, Clone)]
614pub struct ImageDetails {
615    inner: Arc<glycin_utils::ImageDetails<FungibleMemory>>,
616    metadata: Arc<OnceLock<gufo::Metadata>>,
617}
618
619static_assertions::assert_impl_all!(ImageDetails: Send, Sync);
620
621impl ImageDetails {
622    fn new(inner: Arc<glycin_utils::ImageDetails<FungibleMemory>>) -> Self {
623        Self {
624            inner,
625            metadata: Default::default(),
626        }
627    }
628
629    pub fn width(&self) -> u32 {
630        self.inner.width
631    }
632
633    pub fn height(&self) -> u32 {
634        self.inner.height
635    }
636
637    /// A textual representation of the image format
638    pub fn info_format_name(&self) -> Option<&str> {
639        self.inner.info_format_name.as_deref()
640    }
641
642    pub fn info_dimensions_text(&self) -> Option<&str> {
643        self.inner.info_dimensions_text.as_deref()
644    }
645
646    pub fn metadata_exif(&self) -> Option<&[u8]> {
647        self.inner.metadata_exif.as_deref()
648    }
649
650    pub fn transformation_orientation(&self) -> Option<Orientation> {
651        self.inner.transformation_orientation
652    }
653
654    pub fn metadata_xmp(&self) -> Option<&[u8]> {
655        self.inner.metadata_xmp.as_deref()
656    }
657
658    pub fn metadata_key_value(&self) -> Option<&std::collections::BTreeMap<String, String>> {
659        self.inner.metadata_key_value.as_ref()
660    }
661
662    pub fn transformation_ignore_exif(&self) -> bool {
663        self.inner.transformation_ignore_exif
664    }
665
666    fn metadata(&self) -> &gufo::Metadata {
667        self.metadata.get_or_init(|| {
668            let mut metadata = gufo::Metadata::new();
669
670            if let Some(exif) = &self.inner.metadata_exif
671                && let Err(err) = metadata.add_raw_exif(exif.to_vec())
672            {
673                tracing::info!("Could parse Exif data: {err}");
674            }
675
676            if let Some(xmp) = &self.inner.metadata_xmp
677                && let Err(err) = metadata.add_raw_xmp(xmp.to_vec())
678            {
679                tracing::info!("Could parse XMP data: {err}");
680            }
681
682            if let Some(key_value) = &self.inner.metadata_key_value
683                && let Err(err) = metadata.add_key_value(key_value.to_owned())
684            {
685                tracing::info!("Could parse key-value data: {err}");
686            }
687
688            metadata
689        })
690    }
691}
692
693/// A frame of an image often being the complete image
694#[derive(Debug, Clone)]
695pub struct Frame {
696    pub(crate) buffer: glib::Bytes,
697    pub(crate) width: u32,
698    pub(crate) height: u32,
699    /// Line stride
700    pub(crate) stride: u32,
701    pub(crate) memory_format: MemoryFormat,
702    pub(crate) delay: Option<std::time::Duration>,
703    pub(crate) details: Arc<glycin_utils::FrameDetails<FungibleMemory>>,
704    pub(crate) image_details: ImageDetails,
705    pub(crate) color_state: ColorState,
706}
707
708static_assertions::assert_impl_all!(Frame: Send, Sync);
709
710impl Frame {
711    pub fn buf_bytes(&self) -> glib::Bytes {
712        self.buffer.clone()
713    }
714
715    pub fn buf_slice(&self) -> &[u8] {
716        self.buffer.as_ref()
717    }
718
719    /// Width in pixels
720    pub fn width(&self) -> u32 {
721        self.width
722    }
723
724    /// Height in pixels
725    pub fn height(&self) -> u32 {
726        self.height
727    }
728
729    /// Line stride in bytes
730    pub fn stride(&self) -> u32 {
731        self.stride
732    }
733
734    pub fn memory_format(&self) -> MemoryFormat {
735        self.memory_format
736    }
737
738    pub fn color_state(&self) -> &ColorState {
739        &self.color_state
740    }
741
742    /// Duration to show frame for animations.
743    ///
744    /// If the value is not set, the image is not animated.
745    pub fn delay(&self) -> Option<std::time::Duration> {
746        self.delay
747    }
748
749    pub fn details(&self) -> FrameDetails {
750        FrameDetails::new(self.details.clone(), self.image_details.clone())
751    }
752
753    #[cfg(feature = "gdk4")]
754    pub fn texture(&self) -> gdk::Texture {
755        let color_state = crate::util::gdk_color_state(&self.color_state).unwrap_or_else(|_| {
756            tracing::warn!("Unsupported color state: {:?}", self.color_state);
757            gdk::ColorState::srgb()
758        });
759
760        gdk::MemoryTextureBuilder::new()
761            .set_bytes(Some(&self.buffer))
762            // Use unwraps here since the compatibility was checked before
763            .set_width(self.width().try_i32().unwrap())
764            .set_height(self.height().try_i32().unwrap())
765            .set_stride(self.stride().try_usize().unwrap())
766            .set_format(crate::util::gdk_memory_format(self.memory_format()))
767            .set_color_state(&color_state)
768            .build()
769    }
770
771    pub(crate) async fn from_loader<B: ByteData>(
772        mut frame: glycin_utils::Frame<B>,
773        image: &Image,
774    ) -> Result<Self, Error> {
775        frame.initial_seal().await?;
776
777        validate_frame(&frame, &image.loader.limits)?;
778
779        let frame = if image.loader.apply_transformations {
780            orientation::apply_exif_orientation(frame.into_fungible(), image)
781        } else {
782            frame.into_fungible()
783        };
784
785        let mut color_state = ColorState::Srgb;
786
787        let cicp = frame
788            .details
789            .color_cicp
790            .and_then(|x| Cicp::from_bytes(&x).ok());
791        let icc_profile = frame.details.color_icc_profile.as_ref().map(|x| x.to_vec());
792        let color_profile_preference = frame.details.color_profile_preference.unwrap_or_default();
793
794        // Use CICP if preferred or no ICC profile is available
795        let use_cicp = matches!(color_profile_preference, ColorProfilePreference::Cicp)
796            || icc_profile.is_none();
797
798        let frame = if let Some(cicp) = cicp
799            && use_cicp
800        {
801            color_state = ColorState::Cicp(cicp);
802            frame
803        } else if let Some(icc_profile) = icc_profile {
804            if image.loader.color_convert_icc_srgb {
805                let (frame, icc_result) =
806                    spawn_blocking(move || icc::apply_transformation(&icc_profile, frame)).await?;
807
808                match icc_result {
809                    Err(err) => {
810                        tracing::warn!("Failed to apply ICC profile: {err}");
811                    }
812                    Ok(new_color_state) => {
813                        color_state = new_color_state;
814                    }
815                }
816
817                frame
818            } else {
819                color_state = ColorState::IccProfile(icc_profile);
820                frame
821            }
822        } else {
823            frame
824        };
825
826        let mut frame = frame.into_fungible();
827
828        if let Some(target_format) = image
829            .loader
830            .memory_format_selection
831            .best_format_for(frame.memory_format)
832            && frame.memory_format != target_format
833        {
834            frame = util::spawn_blocking(move || {
835                glycin_utils::editing::change_memory_format(&mut frame, target_format)?;
836                Ok::<_, Error>(frame)
837            })
838            .await??;
839        }
840
841        frame.final_seal().await?;
842
843        Ok(Self {
844            buffer: frame.texture.into_gbytes()?,
845            width: frame.width,
846            height: frame.height,
847            stride: frame.stride,
848            memory_format: frame.memory_format,
849            delay: frame.delay.into(),
850            details: Arc::new(frame.details.into_other()?),
851            image_details: image.details(),
852            color_state,
853        })
854    }
855}
856
857#[derive(Debug, Clone)]
858#[must_use]
859/// Request information to get a specific frame
860pub struct FrameRequest {
861    pub(crate) request: glycin_utils::FrameRequest,
862}
863
864impl Default for FrameRequest {
865    fn default() -> Self {
866        Self::new()
867    }
868}
869
870fn validate_frame<B: ByteData>(
871    frame: &glycin_utils::Frame<B>,
872    limits: &Limits,
873) -> Result<(), Error> {
874    let img_buf = &frame.texture;
875
876    if img_buf.len() < frame.n_bytes()? {
877        return Err(ErrorKind::TextureWrongSize {
878            texture_size: img_buf.len(),
879            frame: format!("{:?}", frame.desc()),
880        }
881        .err());
882    }
883
884    if frame.stride < frame.width.smul(frame.memory_format.n_bytes().u32())? {
885        return Err(ErrorKind::StrideTooSmall(format!("{:?}", frame.desc())).err());
886    }
887
888    if frame.width < 1 || frame.height < 1 {
889        return Err(ErrorKind::WidgthOrHeightZero(format!("{:?}", frame.desc())).err());
890    }
891
892    if (frame.stride as u64).smul(frame.height as u64)? > MAX_TEXTURE_SIZE {
893        return Err(ErrorKind::TextureTooLarge.err());
894    }
895
896    if frame.width > limits.inner.max_dimensions.0 {
897        return Err(ErrorKind::TextureTooLarge.err());
898    }
899
900    if frame.height > limits.inner.max_dimensions.1 {
901        return Err(ErrorKind::TextureTooLarge.err());
902    }
903
904    // Ensure
905    frame.width.try_i32()?;
906    frame.height.try_i32()?;
907    frame.stride.try_usize()?;
908
909    Ok(())
910}
911
912impl FrameRequest {
913    pub fn new() -> Self {
914        let mut request = glycin_utils::FrameRequest::default();
915        request.loop_animation = true;
916
917        Self { request }
918    }
919
920    pub fn scale(mut self, width: u32, height: u32) -> Self {
921        self.request.scale = Some((width, height));
922        self
923    }
924
925    pub fn clip(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
926        self.request.clip = Some((x, y, width, height));
927        self
928    }
929
930    /// Controls if first frame is returned after last frame
931    ///
932    /// By default, this option is set to `true`, returning the first frame, if
933    /// the previously requested frame was the last frame.
934    pub fn loop_animation(mut self, loop_animation: bool) -> Self {
935        self.request.loop_animation = loop_animation;
936        self
937    }
938}
939
940/// Additional information about a [frame](Frame)
941#[derive(Debug, Clone)]
942pub struct FrameDetails {
943    inner: Arc<glycin_utils::FrameDetails<FungibleMemory>>,
944    image_details: ImageDetails,
945}
946
947impl FrameDetails {
948    fn new(
949        inner: Arc<glycin_utils::FrameDetails<FungibleMemory>>,
950        image_details: ImageDetails,
951    ) -> Self {
952        Self {
953            inner,
954            image_details,
955        }
956    }
957
958    pub fn color_cicp(&self) -> Option<crate::Cicp> {
959        self.inner
960            .color_cicp
961            .and_then(|x| crate::Cicp::from_bytes(&x).ok())
962    }
963
964    pub fn color_icc_profile(&self) -> Option<&[u8]> {
965        self.inner.color_icc_profile.as_deref()
966    }
967
968    pub fn color_profile_preference(&self) -> ColorProfilePreference {
969        self.inner.color_profile_preference.unwrap_or_default()
970    }
971
972    pub fn info_alpha_channel(&self) -> Option<bool> {
973        self.inner.info_alpha_channel
974    }
975
976    pub fn info_bit_depth(&self) -> Option<u8> {
977        self.inner.info_bit_depth
978    }
979
980    pub fn info_grayscale(&self) -> Option<bool> {
981        self.inner.info_grayscale
982    }
983
984    pub fn n_frame(&self) -> Option<u64> {
985        self.inner.n_frame
986    }
987
988    pub fn pixel_density(&self) -> Option<physical_dimension::PixelDensity> {
989        self.inner
990            .pixel_density
991            .clone()
992            .or_else(|| self.image_details.metadata().resolution())
993    }
994
995    pub fn physical_size(&self) -> Option<physical_dimension::PhysicalSize> {
996        self.inner.physical_size.clone()
997    }
998}
999
1000#[cfg(test)]
1001mod test {
1002    use super::*;
1003    #[allow(dead_code)]
1004    fn ensure_futures_are_send() {
1005        gio::glib::spawn_future(async {
1006            let loader = Loader::new(gio::File::for_uri("invalid"));
1007            let mut image = loader.load().await.unwrap();
1008            image.next_frame().await.unwrap();
1009        });
1010    }
1011}