diff --git a/po/POTFILES.in b/po/POTFILES.in index f257e9a..9127299 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -11,6 +11,7 @@ src/decoder/formats/svg.rs src/decoder/tiling.rs src/editing.rs src/file_model.rs +src/lib.rs src/main.rs src/metadata.rs src/metadata/file.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..121ea45 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,121 @@ +// Copyright (c) 2024 Sophie Herold +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#![allow(clippy::new_without_default)] + +/*! +# Loupe Image Viewer + +Code documentation + +# Widget Structure + +Simplified widget arrangement including the most important widgets. + +- [`LpWindow`] + - [`AdwFlap`] + - [`LpPropertiesView`] + - [`LpImageView`] + - [`LpSlidingView`] + - [`LpImagePage`] + - [`GtkScrolledWindow`] + - [`LpImage`] + - … + +[`AdwFlap`]: adw::Flap +[`GtkScrolledWindow`]: gtk::ScrolledWindow +[`LpImagePage`]: widgets::LpImagePage +[`LpImageView`]: widgets::LpImageView +[`LpImage`]: widgets::LpImage +[`LpPropertiesView`]: widgets::LpPropertiesView +[`LpSlidingView`]: widgets::LpSlidingView +[`LpWindow`]: window::LpWindow +*/ + +use gettextrs::*; +use gtk::gio::prelude::*; +use gtk::gio::{self}; + +pub mod about; +pub mod application; +pub mod config; +pub mod decoder; +pub mod editing; +pub mod file_model; +pub mod metadata; +pub mod util; +pub mod widgets; +pub mod window; + +mod deps { + pub use gtk::{cairo, gdk, gio, glib, graphene, gsk}; +} + +use application::LpApplication; +use deps::*; +use tracing_subscriber::layer::*; +use tracing_subscriber::util::*; + +static GRESOURCE_BYTES: &[u8] = + gvdb_macros::include_gresource_from_dir!("/org/gnome/Loupe", "data/resources"); + +pub fn main() -> glib::ExitCode { + // Don't use ngl renderer by default due to performance reasons + // + if std::env::var("GSK_RENDERER").map_or(true, |x| x.is_empty()) { + std::env::set_var("GSK_RENDERER", "gl"); + } + + // Don't use gl renderer does not properly support fractional scaling + // + if std::env::var("GDK_DEBUG").is_err() { + std::env::set_var("GDK_DEBUG", "gl-no-fractional"); + } + + // Follow G_MESSAGES_DEBUG env variable + let default_level = + if !glib::log_writer_default_would_drop(glib::LogLevel::Debug, Some("loupe")) { + tracing_subscriber::filter::LevelFilter::DEBUG + } else { + tracing_subscriber::filter::LevelFilter::ERROR + }; + + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(default_level.into()) + .from_env_lossy(), + ) + .with(tracing_subscriber::fmt::Layer::default().compact()) + .init(); + + log::debug!("Logger initialized"); + + setlocale(LocaleCategory::LcAll, ""); + bindtextdomain("loupe", config::LOCALEDIR).unwrap(); + textdomain("loupe").unwrap(); + + log::trace!("gettext initialized"); + + gio::resources_register( + &gio::Resource::from_data(&glib::Bytes::from_static(GRESOURCE_BYTES)).unwrap(), + ); + + log::trace!("Gio resources registered"); + + LpApplication::new().run() +} diff --git a/src/main.rs b/src/main.rs index c7da1ab..7916dbe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -// Copyright (c) 2020-2022 Christopher Davis -// Copyright (c) 2023-2024 Sophie Herold +// Copyright (c) 2020 Christopher Davis +// Copyright (c) 2024 Sophie Herold // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,107 +16,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -#![allow(clippy::new_without_default)] - -/*! -# Loupe Image Viewer - -Code documentation - -# Widget Structure - -Simplified widget arrangement including the most important widgets. - -- [`LpWindow`] - - [`AdwFlap`] - - [`LpPropertiesView`] - - [`LpImageView`] - - [`LpSlidingView`] - - [`LpImagePage`] - - [`GtkScrolledWindow`] - - [`LpImage`] - - … - -[`AdwFlap`]: adw::Flap -[`GtkScrolledWindow`]: gtk::ScrolledWindow -[`LpImagePage`]: widgets::LpImagePage -[`LpImageView`]: widgets::LpImageView -[`LpImage`]: widgets::LpImage -[`LpPropertiesView`]: widgets::LpPropertiesView -[`LpSlidingView`]: widgets::LpSlidingView -[`LpWindow`]: window::LpWindow -*/ - -use gettextrs::*; -use gtk::gio::prelude::*; -use gtk::gio::{self}; - -mod about; -mod application; -mod config; -mod decoder; -mod editing; -mod file_model; -mod metadata; -mod util; -mod widgets; -mod window; - -mod deps { - pub use gtk::{cairo, gdk, gio, glib, graphene, gsk}; -} - -use application::LpApplication; -use deps::*; -use tracing_subscriber::layer::*; -use tracing_subscriber::util::*; - -static GRESOURCE_BYTES: &[u8] = - gvdb_macros::include_gresource_from_dir!("/org/gnome/Loupe", "data/resources"); - -fn main() -> glib::ExitCode { - // Don't use ngl renderer by default due to performance reasons - // - if std::env::var("GSK_RENDERER").map_or(true, |x| x.is_empty()) { - std::env::set_var("GSK_RENDERER", "gl"); - } - - // Don't use gl renderer does not properly support fractional scaling - // - if std::env::var("GDK_DEBUG").is_err() { - std::env::set_var("GDK_DEBUG", "gl-no-fractional"); - } - - // Follow G_MESSAGES_DEBUG env variable - let default_level = - if !glib::log_writer_default_would_drop(glib::LogLevel::Debug, Some("loupe")) { - tracing_subscriber::filter::LevelFilter::DEBUG - } else { - tracing_subscriber::filter::LevelFilter::ERROR - }; - - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::builder() - .with_default_directive(default_level.into()) - .from_env_lossy(), - ) - .with(tracing_subscriber::fmt::Layer::default().compact()) - .init(); - - log::debug!("Logger initialized"); - - setlocale(LocaleCategory::LcAll, ""); - bindtextdomain("loupe", config::LOCALEDIR).unwrap(); - textdomain("loupe").unwrap(); - - log::trace!("gettext initialized"); - - gio::resources_register( - &gio::Resource::from_data(&glib::Bytes::from_static(GRESOURCE_BYTES)).unwrap(), - ); - - log::trace!("Gio resources registered"); - - LpApplication::new().run() +use gtk::glib; +pub fn main() -> glib::ExitCode { + loupe::main() } diff --git a/src/metadata.rs b/src/metadata.rs index 8e48f54..10fa204 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -244,7 +244,11 @@ impl Metadata { .as_ref() .and_then(|exif| exif.get_field(exif::Tag::FNumber, exif::In::PRIMARY)) { - return Some(format!("ƒ\u{2215}{}", field.display_value())); + // Translators: Display of the f-number . {} will be replaced with the number + return Some(gettext_f( + r"\u{192}\u{2215}{}", + [field.display_value().to_string()], + )); } None @@ -259,7 +263,7 @@ impl Metadata { if speed <= 0.5 { let exposure = format!("{:.0}", 1. / speed); // Translators: Fractional exposure time (photography) in seconds - return Some(gettext_f("1\u{2215}{}\u{202F}s", [exposure])); + return Some(gettext_f(r"1\u{2215}{}\u{202F}s", [exposure])); } else { let exposure = if speed < 5. { format!("{:.1}", speed) @@ -267,7 +271,7 @@ impl Metadata { format!("{:.0}", speed) }; // Translators: Exposure time (photography) in seconds - return Some(gettext_f("{}\u{202F}s", [exposure])); + return Some(gettext_f(r"{}\u{202F}s", [exposure])); } } } @@ -293,7 +297,7 @@ impl Metadata { if let exif::Value::Rational(rational) = &field.value { let length = format!("{:.0}", rational.first()?.to_f32()); // Translators: Unit for focal length in millimeters - return Some(gettext_f("{}\u{202F}mm", [length])); + return Some(gettext_f(r"{}\u{202F}mm", [length])); } } diff --git a/src/util/gettext.rs b/src/util/gettext.rs index 110a25c..9686cbb 100644 --- a/src/util/gettext.rs +++ b/src/util/gettext.rs @@ -26,6 +26,64 @@ fn freplace(mut s: String, args: impl IntoIterator>) -> S } pub fn gettext_f(format: &str, args: impl IntoIterator>) -> String { - let s = gettextrs::gettext(format); + let s = ugettext(format); freplace(s, args) } + +/// Same as `gettext` but evaluates unicode escape sequences +pub fn ugettext(msgid: &str) -> String { + let msg = gettext(msgid); + apply_unicode_escapes(&msg).unwrap_or(msg) +} + +enum State { + None, + UnicodeHex, +} + +/// Replace unicode escape sequences with the actual `char` +/// +/// ``` +/// assert_eq!( +/// loupe::util::gettext::apply_unicode_escapes(r"abc \u{03a6} \u{2764} d"), +/// Some("abc \u{03a6} \u{2764} d".into()) +/// ); +pub fn apply_unicode_escapes(s: impl AsRef) -> Option { + let mut state = State::None; + let mut new = String::new(); + let mut hex = String::new(); + let mut char_iter = s.as_ref().chars(); + while let Some(c) = char_iter.next() { + match state { + State::None if c == '\\' => { + if let (Some(c1), Some(c2)) = (char_iter.next(), char_iter.next()) { + if c1 == 'u' && c2 == '{' { + state = State::UnicodeHex; + } + } + } + State::None => new.push(c), + State::UnicodeHex => { + if c == '}' { + new.push(hex_to_char(&hex)?); + hex.clear(); + state = State::None; + } else { + hex.push(c); + } + } + } + } + + Some(new) +} + +/// Convert hex string to char +/// +/// ``` +/// assert_eq!(loupe::util::gettext::hex_to_char("03a6"), Some('Φ')) +/// ``` +pub fn hex_to_char(hex: &str) -> Option { + let u = u32::from_str_radix(hex, 16).ok()?; + char::from_u32(u) +} diff --git a/src/widgets/image/input_handling.rs b/src/widgets/image/input_handling.rs index 47b82e4..472964c 100644 --- a/src/widgets/image/input_handling.rs +++ b/src/widgets/image/input_handling.rs @@ -1,4 +1,5 @@ // Copyright (c) 2023-2024 Sophie Herold +// Copyright (c) 2024 Fina Wilke // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/src/widgets/image/loading.rs b/src/widgets/image/loading.rs index 44bfa68..5ff03dc 100644 --- a/src/widgets/image/loading.rs +++ b/src/widgets/image/loading.rs @@ -1,4 +1,5 @@ // Copyright (c) 2023-2024 Sophie Herold +// Copyright (c) 2024 Fina Wilke // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs index c81edaf..b79237f 100644 --- a/src/widgets/image_page.rs +++ b/src/widgets/image_page.rs @@ -1,5 +1,5 @@ // Copyright (c) 2022-2024 Sophie Herold -// Copyright (c) 2022 Christopher Davis +// Copyright (c) 2022, 2024 Christopher Davis // Copyright (c) 2024 Anders Jonsson // // This program is free software: you can redistribute it and/or modify diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index b4a7098..6b75436 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -4,6 +4,7 @@ // Copyright (c) 2023 FineFindus // Copyright (c) 2023 Huan Nguyen // Copyright (c) 2023 Philipp Kiemle +// Copyright (c) 2024 Fina Wilke // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/src/widgets/properties_view.rs b/src/widgets/properties_view.rs index 9eb9d8b..213baca 100644 --- a/src/widgets/properties_view.rs +++ b/src/widgets/properties_view.rs @@ -1,5 +1,7 @@ // Copyright (c) 2022-2024 Sophie Herold // Copyright (c) 2022 Christopher Davis +// Copyright (c) 2024 Lukáš Tyrychtr +// Copyright (c) 2024 Fina Wilke // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -264,7 +266,7 @@ mod imp { if let Some(bit_depth) = metadata.bit_depth() { // Translators: Addition of bit size to format name - format.push_str(&gettext_f(", {}\u{202F}bit", [bit_depth.to_string()])); + format.push_str(&gettext_f(r", {}\u{202F}bit", [bit_depth.to_string()])); } Some(format) @@ -279,7 +281,11 @@ mod imp { _ => { let (width, height) = image.image_size(); if width > 0 && height > 0 { - Some(format!("{width}\u{202F}\u{D7}\u{202F}{height}")) + // Translators: Image " x " + Some(gettext_f( + r"{}\u{202F}\u{D7}\u{202F}{}", + [width.to_string(), height.to_string()], + )) } else { None } diff --git a/src/widgets/sliding_view.rs b/src/widgets/sliding_view.rs index d20d916..583de20 100644 --- a/src/widgets/sliding_view.rs +++ b/src/widgets/sliding_view.rs @@ -2,6 +2,7 @@ // Copyright (c) 2023 Alice Mikhaylenko // Copyright (c) 2023 Christopher Davis // Copyright (c) 2023 Lubosz Sarnecki +// Copyright (c) 2024 Fina Wilke // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/src/window.rs b/src/window.rs index 3136fd2..bec1b2a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,6 +6,7 @@ // Copyright (c) 2023 FineFindus // Copyright (c) 2023 qwel // Copyright (c) 2023 Huan Thieu Nguyen +// Copyright (c) 2024 Fina Wilke // Copyright (c) 2024 DaPigGuy // Copyright (c) 2024 James Frost //