Skip to main content

glycin_core/api/
editor.rs

1use std::collections::BTreeMap;
2use std::pin::Pin;
3use std::sync::Arc;
4
5#[cfg(feature = "builtin")]
6use futures_util::FutureExt;
7use gio::glib;
8use gio::prelude::{IsA, *};
9#[cfg(feature = "builtin")]
10use glycin_utils::EditorImplementation;
11use glycin_utils::safe_math::SafeConversion;
12use glycin_utils::{
13    ByteChanges, ByteData, CompleteEditorOutput, FungibleMemory, Operations, SparseEditorOutput,
14};
15#[cfg(feature = "external")]
16use zbus::zvariant::OwnedObjectPath;
17
18use crate::api::*;
19#[cfg(feature = "external")]
20use crate::dbus::EditorProxy;
21use crate::error::{ErrorKind, ResultExt};
22use crate::main_context::{MainContextSelector, ProvidesMainContext};
23#[cfg(feature = "external")]
24use crate::pool::PooledProcess;
25use crate::util::{self, CancellableFuture, ShortcutErrorFuture};
26use crate::{Error, MimeType, Pool, config};
27
28/// Builder pattern for editing images
29#[derive(Debug)]
30pub struct Editor {
31    source: Source,
32    pool: Arc<Pool>,
33    cancellable: gio::Cancellable,
34    pub(crate) sandbox_selector: SandboxSelector,
35    pub(crate) main_context_selector: MainContextSelector,
36}
37
38static_assertions::assert_impl_all!(Editor: Send, Sync);
39
40impl Editor {
41    /// Create an editor with a [`gio::File`] as source
42    pub fn new(file: gio::File) -> Self {
43        Self::new_source(Source::File(file))
44    }
45
46    /// Create an editor with a [`gio::InputStream`] as source
47    ///
48    /// # Safety
49    ///
50    /// The provided stream must no longer be used after being passed to glycin.
51    pub unsafe fn new_stream(stream: impl IsA<gio::InputStream>) -> Self {
52        unsafe { Self::new_source(Source::Stream(GInputStreamSend::new(stream.upcast()))) }
53    }
54
55    /// Create an editor with [`glib::Bytes`] as source
56    pub fn new_bytes(bytes: glib::Bytes) -> Self {
57        let stream = gio::MemoryInputStream::from_bytes(&bytes);
58        unsafe { Self::new_stream(stream) }
59    }
60
61    /// Create an editor with [`Vec<u8>`] as source
62    pub fn new_vec(buf: Vec<u8>) -> Self {
63        let bytes = glib::Bytes::from_owned(buf);
64        Self::new_bytes(bytes)
65    }
66
67    pub(crate) fn new_source(source: Source) -> Self {
68        Self {
69            source,
70            pool: Pool::global(),
71            cancellable: gio::Cancellable::new(),
72            sandbox_selector: SandboxSelector::default(),
73            main_context_selector: MainContextSelector::Auto,
74        }
75    }
76
77    pub fn main_context_selector(&mut self, selector: MainContextSelector) -> &mut Self {
78        self.main_context_selector = selector;
79        self
80    }
81
82    pub fn edit(self) -> Pin<Box<dyn Future<Output = Result<EditableImage, Error>> + Send>> {
83        self.edit_with_sync(false)
84    }
85
86    /// Same as [`Self::edit`] but with sync option
87    ///
88    /// See [`Loader::load_with_sync`] for details about the sync option.
89    fn edit_with_sync(
90        self,
91        sync: bool,
92    ) -> Pin<Box<dyn Future<Output = Result<EditableImage, Error>> + Send>> {
93        Box::pin(async move {
94            let main_context = self.main_context();
95            let cancellable = self.cancellable.clone();
96
97            let f =
98                move || async move { self.edit_internal(sync).await }.make_cancellable(cancellable);
99
100            main_context.spawn_from_within(f).await?
101        })
102    }
103
104    async fn edit_internal(mut self, sync: bool) -> Result<EditableImage, Error> {
105        let source: Source = self.source.send();
106
107        let editor_context =
108            ProcessorContext::new(source, false, &self.sandbox_selector, sync).await?;
109
110        let editor = editor_context
111            .editor(self.pool.clone(), &self.cancellable)
112            .await?;
113
114        match editor {
115            #[cfg(feature = "external")]
116            Processor::Binary(editor) => {
117                let process = editor.process.use_();
118
119                let (external_reader, load_image_future) =
120                    editor.source_transmission.spawn_external()?;
121
122                let editable_image_future = process.edit(external_reader, &editor.mime_type);
123
124                let editable_image = editable_image_future
125                    .join_abort_on_error(load_image_future)
126                    .await
127                    .err_context(&process)?;
128
129                self.cancellable.connect_cancelled(glib::clone!(
130                    #[strong(rename_to=process)]
131                    editor.process,
132                    #[strong(rename_to=path)]
133                    editable_image.edit_request,
134                    move |_| {
135                        tracing::debug!("Terminating loader");
136                        util::spawn_detached(process.use_().done(path))
137                    }
138                ));
139
140                Ok(EditableImage {
141                    editor: self,
142                    image_editor: ImageEditor::External(ImageEditorExternal {
143                        _active_sandbox_mechanism: editor.sandbox_mechanism,
144                        process: editor.process,
145                        editor_alive: Default::default(),
146                        edit_request: editable_image.edit_request,
147                    }),
148                    _mime_type: editor.mime_type,
149                })
150            }
151            #[cfg(feature = "builtin")]
152            Processor::Builtin(builtin) => {
153                let mime_type = builtin.mime_type.to_string();
154                let details = glycin_utils::InitializationDetails::default();
155                let edit_function: Box<dyn FnOnce() -> _ + Send>;
156
157                let (reader, read_data_future) = builtin.source_transmission.spawn_builtin();
158
159                match builtin.builtin {
160                    #[cfg(feature = "builtin-image-rs")]
161                    config::BuiltinProcessor::ImageRs(_) => {
162                        edit_function = Box::new(move || {
163                            glycin_image_rs::ImgEditor::edit(reader, mime_type, details)
164                                .map(|e| ImageEditorBuiltin::ImageRs(Arc::new(e)))
165                        });
166                    }
167                    #[cfg(feature = "builtin-test")]
168                    config::BuiltinProcessor::Test(_) => {
169                        edit_function = Box::new(move || {
170                            glycin_test::ImgEditor::edit(reader, mime_type, details)
171                                .map(|e| ImageEditorBuiltin::Test(Arc::new(e)))
172                        });
173                    }
174                }
175
176                let editor_future = gio::spawn_blocking(move || {
177                    edit_function().map_err(|err| Error::from(err.into_editor_error()))
178                })
179                .map(|x| x.map_err(|e| ErrorKind::panic(e).err()));
180
181                let editor = editor_future
182                    .join_abort_on_error(read_data_future)
183                    .await??;
184
185                Ok(EditableImage {
186                    editor: self,
187                    image_editor: ImageEditor::Builtin(editor),
188                    _mime_type: builtin.mime_type,
189                })
190            }
191        }
192    }
193
194    /// Sets the method by which the sandbox mechanism is selected.
195    ///
196    /// The default without calling this function is [`SandboxSelector::Auto`].
197    pub fn sandbox_selector(&mut self, sandbox_selector: SandboxSelector) -> &mut Self {
198        self.sandbox_selector = sandbox_selector;
199        self
200    }
201
202    /// Set [`Cancellable`](gio::Cancellable) to cancel any editing operations.
203    pub fn cancellable(&mut self, cancellable: impl IsA<gio::Cancellable>) -> &mut Self {
204        self.cancellable = cancellable.upcast();
205        self
206    }
207}
208
209/// Image handle on which editing operations can be applied
210///
211/// Obtained via [`Editor.edit()`](Editor::edit).
212#[derive(Debug)]
213pub struct EditableImage {
214    pub(crate) editor: Editor,
215    image_editor: ImageEditor,
216    // TODO: Use in error messages
217    _mime_type: MimeType,
218}
219
220impl Drop for EditableImage {
221    fn drop(&mut self) {
222        #[cfg(feature = "external")]
223        #[allow(irrefutable_let_patterns)]
224        if let ImageEditor::External(editor) = &self.image_editor {
225            editor.process.use_().done_background(self);
226            *editor.editor_alive.lock().unwrap() = Arc::new(());
227            util::spawn_detached(self.editor.pool.clone().clean_loaders());
228        }
229    }
230}
231
232impl EditableImage {
233    /// Apply operations to the image with a potentially sparse result.
234    ///
235    /// Some operations like rotation can be in some cases be conducted by only
236    /// changing one or a few bytes in a file. We call these cases *sparse* and
237    /// a [`SparseEdit::Sparse`] is returned.
238    pub fn apply_sparse(
239        self,
240        operations: &Operations,
241    ) -> Pin<Box<dyn Future<Output = Result<SparseEdit, Error>> + Send>> {
242        let operations = operations.to_owned();
243        Box::pin(self.apply_sparse_internal(operations))
244    }
245
246    async fn apply_sparse_internal(self, operations: Operations) -> Result<SparseEdit, Error> {
247        match &self.image_editor {
248            #[cfg(feature = "external")]
249            ImageEditor::External(editor) => {
250                let process = editor.process.use_();
251
252                let mut editor_output = process
253                    .editor_apply_sparse(&operations, &self)
254                    .await
255                    .err_context(&process)?;
256
257                editor_output.final_seal().await?;
258
259                SparseEdit::try_from(editor_output.into_fungible())
260            }
261            #[cfg(feature = "builtin")]
262            ImageEditor::Builtin(editor) => {
263                let editor_function: Box<dyn FnOnce() -> _ + Send>;
264
265                match editor {
266                    #[cfg(feature = "builtin-image-rs")]
267                    ImageEditorBuiltin::ImageRs(editor) => {
268                        let editor = editor.clone();
269                        editor_function = Box::new(move || editor.apply_sparse(operations));
270                    }
271                    #[cfg(feature = "builtin-test")]
272                    ImageEditorBuiltin::Test(editor) => {
273                        let editor = editor.clone();
274                        editor_function = Box::new(move || editor.apply_sparse(operations));
275                    }
276                }
277
278                let editor_output = gio::spawn_blocking(|| {
279                    editor_function().map_err(|e| Error::from(e.into_editor_error()))
280                })
281                .await
282                .map_err(|e| ErrorKind::panic(e))??;
283
284                SparseEdit::try_from(editor_output)
285            }
286        }
287    }
288
289    /// Apply operations to the image
290    pub fn apply_complete(
291        &self,
292        operations: &Operations,
293    ) -> Pin<Box<dyn Future<Output = Result<Edit, Error>> + Send + '_>> {
294        let operations = operations.to_owned();
295
296        Box::pin(self.apply_complete_internal(operations))
297    }
298
299    async fn apply_complete_internal(&self, operations: Operations) -> Result<Edit, Error> {
300        match &self.image_editor {
301            #[cfg(feature = "external")]
302            ImageEditor::External(editor) => {
303                let process = editor.process.use_();
304
305                let mut editor_output = process
306                    .editor_apply_complete(&operations, self)
307                    .await
308                    .err_context(&process)?
309                    .into_fungible();
310
311                editor_output.final_seal().await?;
312
313                Ok(Edit {
314                    inner: editor_output,
315                })
316            }
317            #[cfg(feature = "builtin")]
318            ImageEditor::Builtin(editor) => {
319                let apply_function: Box<dyn FnOnce() -> _ + Send + 'static>;
320
321                match editor {
322                    #[cfg(feature = "builtin-image-rs")]
323                    ImageEditorBuiltin::ImageRs(editor) => {
324                        let editor = editor.clone();
325                        apply_function = Box::new(move || editor.apply_complete(operations));
326                    }
327                    #[cfg(feature = "builtin-test")]
328                    ImageEditorBuiltin::Test(editor) => {
329                        let editor = editor.clone();
330                        apply_function = Box::new(move || editor.apply_complete(operations));
331                    }
332                }
333
334                let editor_output = gio::spawn_blocking(|| {
335                    apply_function().map_err(|e| Error::from(e.into_editor_error()))
336                })
337                .await
338                .map_err(|e| ErrorKind::panic(e))??;
339
340                Ok(Edit {
341                    inner: editor_output,
342                })
343            }
344        }
345    }
346
347    /// List all configured image editors
348    pub async fn supported_formats() -> BTreeMap<MimeType, config::EditorConfig> {
349        let config = config::Config::cached().await;
350        config.image_editor.clone()
351    }
352
353    #[cfg(feature = "external")]
354    pub(crate) fn edit_request_path(&self) -> OwnedObjectPath {
355        #[allow(irrefutable_let_patterns)]
356        if let ImageEditor::External(editor) = &self.image_editor {
357            editor.edit_request.clone()
358        } else {
359            todo!()
360        }
361    }
362}
363
364#[derive(Debug)]
365enum ImageEditor {
366    #[cfg(feature = "external")]
367    External(ImageEditorExternal),
368    #[cfg(feature = "builtin")]
369    Builtin(ImageEditorBuiltin),
370}
371
372#[cfg(feature = "external")]
373#[derive(Debug)]
374struct ImageEditorExternal {
375    pub(crate) process: Arc<PooledProcess<EditorProxy<'static>>>,
376    edit_request: OwnedObjectPath,
377    _active_sandbox_mechanism: SandboxMechanism,
378    editor_alive: std::sync::Mutex<Arc<()>>,
379}
380
381#[cfg(feature = "builtin")]
382#[derive(Clone)]
383enum ImageEditorBuiltin {
384    #[cfg(feature = "builtin-image-rs")]
385    ImageRs(Arc<glycin_image_rs::ImgEditor>),
386    #[cfg(feature = "builtin-test")]
387    Test(Arc<glycin_test::ImgEditor>),
388}
389
390#[cfg(feature = "builtin")]
391impl std::fmt::Debug for ImageEditorBuiltin {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        f.write_str("ImageEditorBuiltin")
394    }
395}
396
397#[derive(Debug)]
398/// Potentially sparse result of an [editor](Editor) operation
399///
400/// See also: [`EditableImage::apply_sparse()`]
401pub enum SparseEdit {
402    /// The operations can be applied to the image via only changing a few
403    /// bytes. The [`apply_to()`](Self::apply_to()) function can be used to
404    /// apply these changes.
405    Sparse(ByteChanges),
406    /// The operations require to completely rewrite the image.
407    Complete(FungibleMemory),
408}
409
410/// Result of an [editor](Editor) operation
411#[derive(Debug)]
412pub struct Edit {
413    inner: CompleteEditorOutput<FungibleMemory>,
414}
415
416impl Edit {
417    pub fn data(&self) -> &[u8] {
418        &self.inner.data
419    }
420
421    pub fn is_lossless(&self) -> bool {
422        self.inner.info.lossless
423    }
424}
425
426#[derive(Debug, PartialEq, Eq)]
427#[must_use]
428/// Whether an image could be changed via the chosen method.
429pub enum EditOutcome {
430    Changed,
431    Unchanged,
432}
433
434impl SparseEdit {
435    /// Apply sparse changes if applicable.
436    ///
437    /// If the type does not carry sparse changes, the function will return an
438    /// [`EditOutcome::Unchanged`] and the complete image needs to be rewritten.
439    pub async fn apply_to(&self, file: gio::File) -> Result<EditOutcome, Error> {
440        match self {
441            Self::Sparse(bit_changes) => {
442                let bit_changes = bit_changes.clone();
443                util::spawn_blocking(move || {
444                    let stream = file.open_readwrite(gio::Cancellable::NONE)?;
445                    let output_stream = stream.output_stream();
446                    for change in bit_changes.changes {
447                        stream.seek(
448                            change.offset.try_i64()?,
449                            glib::SeekType::Set,
450                            gio::Cancellable::NONE,
451                        )?;
452                        let (_, err) =
453                            output_stream.write_all(&[change.new_value], gio::Cancellable::NONE)?;
454
455                        if let Some(err) = err {
456                            return Err(err.into());
457                        }
458                    }
459                    Ok(EditOutcome::Changed)
460                })
461                .await?
462            }
463            Self::Complete(_) => Ok(EditOutcome::Unchanged),
464        }
465    }
466}
467
468impl TryFrom<SparseEditorOutput<FungibleMemory>> for SparseEdit {
469    type Error = Error;
470
471    fn try_from(
472        value: SparseEditorOutput<FungibleMemory>,
473    ) -> std::result::Result<Self, Self::Error> {
474        if value.byte_changes.is_some() && value.data.is_some() {
475            Err(
476                ErrorKind::RemoteError(glycin_utils::RemoteError::InternalLoaderError(
477                    "Sparse editor output with 'byte_changes' and 'data' returned.".into(),
478                ))
479                .into(),
480            )
481        } else if let Some(bit_changes) = value.byte_changes {
482            Ok(Self::Sparse(bit_changes))
483        } else if let Some(data) = value.data {
484            Ok(Self::Complete(data.into_fungible()))
485        } else {
486            Err(
487                ErrorKind::RemoteError(glycin_utils::RemoteError::InternalLoaderError(
488                    "Sparse editor output with neither 'bit_changes' nor 'data' returned.".into(),
489                ))
490                .into(),
491            )
492        }
493    }
494}