use std::collections::BTreeMap;
use std::pin::Pin;
use std::sync::Arc;
#[cfg(feature = "builtin")]
use futures_util::FutureExt;
use gio::glib;
use gio::prelude::{IsA, *};
#[cfg(feature = "builtin")]
use glycin_utils::EditorImplementation;
use glycin_utils::safe_math::SafeConversion;
use glycin_utils::{
ByteChanges, ByteData, CompleteEditorOutput, FungibleMemory, Operations, SparseEditorOutput,
};
#[cfg(feature = "external")]
use zbus::zvariant::OwnedObjectPath;
use crate::api::*;
#[cfg(feature = "external")]
use crate::dbus::EditorProxy;
use crate::error::ResultExt;
use crate::main_context::{MainContextSelector, ProvidesMainContext};
#[cfg(feature = "external")]
use crate::pool::PooledProcess;
use crate::util::{self, CancellableFuture, ShortcutErrorFuture};
use crate::{Error, ErrorKind, MimeType, Pool, config};
#[derive(Debug)]
pub struct Editor {
source: Source,
pool: Arc<Pool>,
cancellable: gio::Cancellable,
pub(crate) sandbox_selector: SandboxSelector,
pub(crate) main_context_selector: MainContextSelector,
}
static_assertions::assert_impl_all!(Editor: Send, Sync);
impl Editor {
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(),
sandbox_selector: SandboxSelector::default(),
main_context_selector: MainContextSelector::Auto,
}
}
pub fn main_context_selector(&mut self, selector: MainContextSelector) -> &mut Self {
self.main_context_selector = selector;
self
}
pub fn edit(self) -> Pin<Box<dyn Future<Output = Result<EditableImage, Error>> + Send>> {
Box::pin(async move {
let main_context = self.main_context();
let cancellable = self.cancellable.clone();
let f = || async move { self.edit_internal().await }.make_cancellable(cancellable);
main_context.spawn_from_within(f).await?
})
}
async fn edit_internal(mut self) -> Result<EditableImage, Error> {
let source: Source = self.source.send();
let editor_context = ProcessorContext::new(source, false, &self.sandbox_selector).await?;
let editor = editor_context
.editor(self.pool.clone(), &self.cancellable)
.await?;
match editor {
#[cfg(feature = "external")]
Processor::Binary(editor) => {
let process = editor.process.use_();
let (external_reader, load_image_future) =
editor.source_transmission.spawn_external()?;
let editable_image_future = process.edit(external_reader, &editor.mime_type);
let editable_image = editable_image_future
.join_abort_on_error(load_image_future)
.await
.err_context(&process)?;
self.cancellable.connect_cancelled(glib::clone!(
#[strong(rename_to=process)]
editor.process,
#[strong(rename_to=path)]
editable_image.edit_request,
move |_| {
tracing::debug!("Terminating loader");
util::spawn_detached(process.use_().done(path))
}
));
Ok(EditableImage {
editor: self,
image_editor: ImageEditor::External(ImageEditorExternal {
_active_sandbox_mechanism: editor.sandbox_mechanism,
process: editor.process,
editor_alive: Default::default(),
edit_request: editable_image.edit_request,
}),
_mime_type: editor.mime_type,
})
}
#[cfg(feature = "builtin")]
Processor::Builtin(builtin) => {
let mime_type = builtin.mime_type.to_string();
let details = glycin_utils::InitializationDetails::default();
let edit_function: Box<dyn FnOnce() -> _ + Send>;
let (reader, read_data_future) = builtin.source_transmission.spawn_builtin();
match builtin.builtin {
#[cfg(feature = "builtin-image-rs")]
config::BuiltinProcessor::ImageRs(_) => {
edit_function = Box::new(move || {
glycin_image_rs::ImgEditor::edit(reader, mime_type, details)
.map(|e| ImageEditorBuiltin::ImageRs(Arc::new(e)))
});
}
#[cfg(feature = "builtin-test")]
config::BuiltinProcessor::Test(_) => {
edit_function = Box::new(move || {
glycin_test::ImgEditor::edit(reader, mime_type, details)
.map(|e| ImageEditorBuiltin::Test(Arc::new(e)))
});
}
}
let editor_future = gio::spawn_blocking(move || {
edit_function().map_err(|err| Error::from(err.into_editor_error()))
})
.map(|x| x.map_err(|e| ErrorKind::panic(e).err()));
let editor = editor_future
.join_abort_on_error(read_data_future)
.await??;
Ok(EditableImage {
editor: self,
image_editor: ImageEditor::Builtin(editor),
_mime_type: builtin.mime_type,
})
}
}
}
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
}
}
#[derive(Debug)]
pub struct EditableImage {
pub(crate) editor: Editor,
image_editor: ImageEditor,
_mime_type: MimeType,
}
impl Drop for EditableImage {
fn drop(&mut self) {
#[cfg(feature = "external")]
#[allow(irrefutable_let_patterns)]
if let ImageEditor::External(editor) = &self.image_editor {
editor.process.use_().done_background(self);
*editor.editor_alive.lock().unwrap() = Arc::new(());
util::spawn_detached(self.editor.pool.clone().clean_loaders());
}
}
}
impl EditableImage {
pub fn apply_sparse(
self,
operations: &Operations,
) -> Pin<Box<dyn Future<Output = Result<SparseEdit, Error>> + Send>> {
let operations = operations.to_owned();
Box::pin(self.apply_sparse_internal(operations))
}
async fn apply_sparse_internal(self, operations: Operations) -> Result<SparseEdit, Error> {
match &self.image_editor {
#[cfg(feature = "external")]
ImageEditor::External(editor) => {
let process = editor.process.use_();
let mut editor_output = process
.editor_apply_sparse(&operations, &self)
.await
.err_context(&process)?;
editor_output.final_seal().await?;
SparseEdit::try_from(editor_output.into_fungible())
}
#[cfg(feature = "builtin")]
ImageEditor::Builtin(editor) => {
let editor_function: Box<dyn FnOnce() -> _ + Send>;
match editor {
#[cfg(feature = "builtin-image-rs")]
ImageEditorBuiltin::ImageRs(editor) => {
let editor = editor.clone();
editor_function = Box::new(move || editor.apply_sparse(operations));
}
#[cfg(feature = "builtin-test")]
ImageEditorBuiltin::Test(editor) => {
let editor = editor.clone();
editor_function = Box::new(move || editor.apply_sparse(operations));
}
}
let editor_output = gio::spawn_blocking(|| {
editor_function().map_err(|e| Error::from(e.into_editor_error()))
})
.await
.map_err(|e| ErrorKind::panic(e))??;
SparseEdit::try_from(editor_output)
}
}
}
pub fn apply_complete(
&self,
operations: &Operations,
) -> Pin<Box<dyn Future<Output = Result<Edit, Error>> + Send + '_>> {
let operations = operations.to_owned();
Box::pin(self.apply_complete_internal(operations))
}
async fn apply_complete_internal(&self, operations: Operations) -> Result<Edit, Error> {
match &self.image_editor {
#[cfg(feature = "external")]
ImageEditor::External(editor) => {
let process = editor.process.use_();
let mut editor_output = process
.editor_apply_complete(&operations, self)
.await
.err_context(&process)?
.into_fungible();
editor_output.final_seal().await?;
Ok(Edit {
inner: editor_output,
})
}
#[cfg(feature = "builtin")]
ImageEditor::Builtin(editor) => {
let apply_function: Box<dyn FnOnce() -> _ + Send + 'static>;
match editor {
#[cfg(feature = "builtin-image-rs")]
ImageEditorBuiltin::ImageRs(editor) => {
let editor = editor.clone();
apply_function = Box::new(move || editor.apply_complete(operations));
}
#[cfg(feature = "builtin-test")]
ImageEditorBuiltin::Test(editor) => {
let editor = editor.clone();
apply_function = Box::new(move || editor.apply_complete(operations));
}
}
let editor_output = gio::spawn_blocking(|| {
apply_function().map_err(|e| Error::from(e.into_editor_error()))
})
.await
.map_err(|e| ErrorKind::panic(e))??;
Ok(Edit {
inner: editor_output,
})
}
}
}
pub async fn supported_formats() -> BTreeMap<MimeType, config::ImageEditorConfig> {
let config = config::Config::cached().await;
config.image_editor.clone()
}
#[cfg(feature = "external")]
pub(crate) fn edit_request_path(&self) -> OwnedObjectPath {
#[allow(irrefutable_let_patterns)]
if let ImageEditor::External(editor) = &self.image_editor {
editor.edit_request.clone()
} else {
todo!()
}
}
}
#[derive(Debug)]
enum ImageEditor {
#[cfg(feature = "external")]
External(ImageEditorExternal),
#[cfg(feature = "builtin")]
Builtin(ImageEditorBuiltin),
}
#[cfg(feature = "external")]
#[derive(Debug)]
struct ImageEditorExternal {
pub(crate) process: Arc<PooledProcess<EditorProxy<'static>>>,
edit_request: OwnedObjectPath,
_active_sandbox_mechanism: SandboxMechanism,
editor_alive: std::sync::Mutex<Arc<()>>,
}
#[cfg(feature = "builtin")]
#[derive(Clone)]
enum ImageEditorBuiltin {
#[cfg(feature = "builtin-image-rs")]
ImageRs(Arc<glycin_image_rs::ImgEditor>),
#[cfg(feature = "builtin-test")]
Test(Arc<glycin_test::ImgEditor>),
}
#[cfg(feature = "builtin")]
impl std::fmt::Debug for ImageEditorBuiltin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ImageEditorBuiltin")
}
}
#[derive(Debug)]
pub enum SparseEdit {
Sparse(ByteChanges),
Complete(FungibleMemory),
}
#[derive(Debug)]
pub struct Edit {
inner: CompleteEditorOutput<FungibleMemory>,
}
impl Edit {
pub fn data(&self) -> &[u8] {
&self.inner.data
}
pub fn is_lossless(&self) -> bool {
self.inner.info.lossless
}
}
#[derive(Debug, PartialEq, Eq)]
#[must_use]
pub enum EditOutcome {
Changed,
Unchanged,
}
impl SparseEdit {
pub async fn apply_to(&self, file: gio::File) -> Result<EditOutcome, Error> {
match self {
Self::Sparse(bit_changes) => {
let bit_changes = bit_changes.clone();
util::spawn_blocking(move || {
let stream = file.open_readwrite(gio::Cancellable::NONE)?;
let output_stream = stream.output_stream();
for change in bit_changes.changes {
stream.seek(
change.offset.try_i64()?,
glib::SeekType::Set,
gio::Cancellable::NONE,
)?;
let (_, err) =
output_stream.write_all(&[change.new_value], gio::Cancellable::NONE)?;
if let Some(err) = err {
return Err(err.into());
}
}
Ok(EditOutcome::Changed)
})
.await?
}
Self::Complete(_) => Ok(EditOutcome::Unchanged),
}
}
}
impl TryFrom<SparseEditorOutput<FungibleMemory>> for SparseEdit {
type Error = Error;
fn try_from(
value: SparseEditorOutput<FungibleMemory>,
) -> std::result::Result<Self, Self::Error> {
if value.byte_changes.is_some() && value.data.is_some() {
Err(
ErrorKind::RemoteError(glycin_utils::RemoteError::InternalLoaderError(
"Sparse editor output with 'byte_changes' and 'data' returned.".into(),
))
.into(),
)
} else if let Some(bit_changes) = value.byte_changes {
Ok(Self::Sparse(bit_changes))
} else if let Some(data) = value.data {
Ok(Self::Complete(data.into_fungible()))
} else {
Err(
ErrorKind::RemoteError(glycin_utils::RemoteError::InternalLoaderError(
"Sparse editor output with neither 'bit_changes' nor 'data' returned.".into(),
))
.into(),
)
}
}
}