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
//