diff --git a/Cargo.lock b/Cargo.lock
index afb6666..e97f8d5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1542,6 +1542,36 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "gvdb"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6fc5915836ff1779eb9455810dad729811334b1026d4316eaf89c3a415e32ea"
+dependencies = [
+ "byteorder",
+ "flate2",
+ "lazy_static",
+ "memmap2",
+ "quick-xml",
+ "safe-transmute",
+ "serde",
+ "serde_json",
+ "walkdir",
+ "zvariant",
+]
+
+[[package]]
+name = "gvdb-macros"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5166acbf52c6109ec51fc758532aaa47b0eb8c4becd585c3dde6f5965c6529e"
+dependencies = [
+ "gvdb",
+ "litrs",
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "gweather-sys"
version = "4.2.0"
@@ -1930,6 +1960,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
+[[package]]
+name = "litrs"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f17c3668f3cc1132437cdadc93dab05e52d592f06948d3f64828430c36e4a70"
+dependencies = [
+ "proc-macro2",
+]
+
[[package]]
name = "locale_config"
version = "0.3.0"
@@ -1975,6 +2014,7 @@ dependencies = [
"gettext-rs",
"gio",
"gtk4",
+ "gvdb-macros",
"image",
"indexmap",
"kamadak-exif",
@@ -2040,6 +2080,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+[[package]]
+name = "memmap2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "memoffset"
version = "0.7.1"
@@ -2578,6 +2627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41"
dependencies = [
"memchr",
+ "serde",
]
[[package]]
@@ -2788,6 +2838,12 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "ryu"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
[[package]]
name = "safe-transmute"
version = "0.11.2"
@@ -2803,6 +2859,15 @@ dependencies = [
"bytemuck",
]
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -2859,6 +2924,17 @@ dependencies = [
"syn 2.0.15",
]
+[[package]]
+name = "serde_json"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
[[package]]
name = "serde_repr"
version = "0.1.12"
@@ -3284,6 +3360,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
diff --git a/Cargo.toml b/Cargo.toml
index 8292072..46ed7ec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ anyhow = { version = "1.0.66", features = ["backtrace"] }
arc-swap = "1.6.0"
env_logger = "0.10.0"
futures = "0.3.25"
+gvdb-macros = "0.1.6"
indexmap = "1.9.2"
kamadak-exif = "0.5.5"
lcms2 = "5.6.0"
diff --git a/data/gtk/print.ui b/data/gtk/print.ui
new file mode 100644
index 0000000..6e345ca
--- /dev/null
+++ b/data/gtk/print.ui
@@ -0,0 +1,192 @@
+
+
+
+ Layout Image
+ 350
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/loupe.gresource.xml b/data/loupe.gresource.xml
deleted file mode 100644
index 042c912..0000000
--- a/data/loupe.gresource.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
- gtk/help_overlay.ui
- style.css
- style-hc.css
-
-
diff --git a/data/meson.build b/data/meson.build
index ef153bf..76605c0 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -38,29 +38,8 @@ appstream_file = i18n.merge_file(
install_dir: iv_datadir / 'metainfo'
)
-# appstream_util = find_program('appstream-util', required: false)
-# if appstream_util.found()
-# test('Validate appstream file', appstream_util,
-# args: ['validate', appstream_file]
-# )
-# endif
-
-install_data('org.gnome.Loupe.gschema.xml',
- install_dir: iv_datadir / 'glib-2.0' / 'schemas'
-)
-
-compile_schemas = find_program('glib-compile-schemas', required: false)
-if compile_schemas.found()
- test('Validate schema file', compile_schemas,
- args: ['--strict', '--dry-run', meson.current_source_dir()]
- )
-endif
-
-gnome.compile_resources('loupe',
- 'loupe.gresource.xml',
- gresource_bundle: true,
- install: true,
- install_dir: iv_pkgdatadir,
-)
-
subdir('icons')
+
+# Post install
+
+gnome.post_install()
diff --git a/data/org.gnome.Loupe.gschema.xml b/data/org.gnome.Loupe.gschema.xml
deleted file mode 100644
index b985dea..0000000
--- a/data/org.gnome.Loupe.gschema.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/data/gtk/help_overlay.ui b/data/resources/gtk/help-overlay.ui
similarity index 100%
rename from data/gtk/help_overlay.ui
rename to data/resources/gtk/help-overlay.ui
diff --git a/data/resources/icons/scalable/actions/center-on-page-horizontal-symbolic.svg b/data/resources/icons/scalable/actions/center-on-page-horizontal-symbolic.svg
new file mode 100644
index 0000000..877f9e3
--- /dev/null
+++ b/data/resources/icons/scalable/actions/center-on-page-horizontal-symbolic.svg
@@ -0,0 +1,11 @@
+
+
diff --git a/data/resources/icons/scalable/actions/center-on-page-vertical-symbolic.svg b/data/resources/icons/scalable/actions/center-on-page-vertical-symbolic.svg
new file mode 100644
index 0000000..d5ecc77
--- /dev/null
+++ b/data/resources/icons/scalable/actions/center-on-page-vertical-symbolic.svg
@@ -0,0 +1,11 @@
+
+
diff --git a/data/resources/icons/scalable/actions/page-orientation-landscape-symbolic.svg b/data/resources/icons/scalable/actions/page-orientation-landscape-symbolic.svg
new file mode 100644
index 0000000..3858fe2
--- /dev/null
+++ b/data/resources/icons/scalable/actions/page-orientation-landscape-symbolic.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/data/resources/icons/scalable/actions/page-orientation-portrait-symbolic.svg b/data/resources/icons/scalable/actions/page-orientation-portrait-symbolic.svg
new file mode 100644
index 0000000..151f8e7
--- /dev/null
+++ b/data/resources/icons/scalable/actions/page-orientation-portrait-symbolic.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/data/style-hc.css b/data/resources/style-hc.css
similarity index 100%
rename from data/style-hc.css
rename to data/resources/style-hc.css
diff --git a/data/style.css b/data/resources/style.css
similarity index 72%
rename from data/style.css
rename to data/resources/style.css
index 8c8445b..46145a8 100644
--- a/data/style.css
+++ b/data/resources/style.css
@@ -22,5 +22,11 @@ row.property label.subtitle {
margin: 16px;
border-radius: 5px;
outline: 2px solid white;
- box-shadow: 0 0 10px rgba(0.4, 0.4, 0.4, 0.8);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 200);
}
+
+lpprintpreview {
+ background-color: rgb(240, 240, 240);
+ border-radius: 4px;
+ box-shadow: 0 0 14px 1px black;
+}
\ No newline at end of file
diff --git a/src/config.rs.in b/src/config.rs.in
index 448b29f..cc3815e 100644
--- a/src/config.rs.in
+++ b/src/config.rs.in
@@ -1,5 +1,4 @@
pub const APP_ID: &str = "@APP_ID@";
pub const PROFILE: &str = "@PROFILE@";
-pub const PKGDATADIR: &str = "@pkgdatadir@";
pub const VERSION: &str = "@VERSION@";
pub const LOCALEDIR: &str = "@localedir@";
diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs
index 3e9b09d..7dc1687 100644
--- a/src/decoder/tiling.rs
+++ b/src/decoder/tiling.rs
@@ -86,6 +86,11 @@ impl Tile {
let area = round(&self.area().scale(zoom, zoom));
let area_with_bleed = round(&self.area_with_bleed().scale(zoom, zoom));
+ if area_with_bleed.width() < 1. || area_with_bleed.height() < 1. {
+ log::warn!("Trying to draw image with dimensions smaller than 1");
+ return;
+ }
+
// TODO: do not clip outer bounderies of the image
snapshot.push_clip(&area);
if let Some(background_color) = &options.background_color {
diff --git a/src/main.rs b/src/main.rs
index 0e0775c..0949cb8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -65,6 +65,9 @@ mod deps {
use application::LpApplication;
use deps::*;
+static GRESOURCE_BYTES: &[u8] =
+ gvdb_macros::include_gresource_from_dir!("/org/gnome/Loupe", "data/resources");
+
fn main() -> glib::ExitCode {
env_logger::Builder::from_default_env()
.format_timestamp_millis()
@@ -74,9 +77,9 @@ fn main() -> glib::ExitCode {
bindtextdomain("loupe", config::LOCALEDIR).unwrap();
textdomain("loupe").unwrap();
- let res = gio::Resource::load(config::PKGDATADIR.to_owned() + "/loupe.gresource")
- .expect("Could not load resources");
- gio::resources_register(&res);
+ gio::resources_register(
+ &gio::Resource::from_data(&glib::Bytes::from_static(GRESOURCE_BYTES)).unwrap(),
+ );
LpApplication::new().run()
}
diff --git a/src/widgets/image.rs b/src/widgets/image.rs
index d4a2afe..3592e9f 100644
--- a/src/widgets/image.rs
+++ b/src/widgets/image.rs
@@ -999,6 +999,34 @@ impl LpImage {
snapshot.to_paintable(None)
}
+ pub fn print_data(&self, scale: f64) -> Option {
+ let orientation = self.metadata().orientation();
+
+ let render_options = tiling::RenderOptions {
+ scaling_filter: gsk::ScalingFilter::Trilinear,
+ background_color: None,
+ scale_factor: 1,
+ };
+
+ let snapshot = gtk::Snapshot::new();
+
+ self.snapshot_rotate_mirror(
+ &snapshot,
+ -orientation.rotation as f32,
+ orientation.mirrored,
+ );
+
+ self.imp()
+ .frame_buffer
+ .load()
+ .add_to_snapshot(&snapshot, scale, &render_options);
+
+ let node = snapshot.to_node()?;
+ let renderer = self.root()?.renderer();
+
+ Some(renderer.render_texture(&node, None))
+ }
+
fn mirrored(&self) -> bool {
self.imp().mirrored.get()
}
diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs
index 2e017d8..a185eab 100644
--- a/src/widgets/image_view.rs
+++ b/src/widgets/image_view.rs
@@ -29,7 +29,7 @@ use crate::deps::*;
use crate::file_model::LpFileModel;
use crate::util::gettext::*;
use crate::util::{Direction, Position};
-use crate::widgets::{LpImage, LpImagePage, LpSlidingView};
+use crate::widgets::{LpImage, LpImagePage, LpPrint, LpSlidingView};
use crate::util::spawn;
use adw::prelude::*;
@@ -609,6 +609,17 @@ impl LpImageView {
}
pub fn print(&self) -> anyhow::Result<()> {
+ let image = self
+ .current_image()
+ .context("No current image for printing")?;
+
+ let root = self.root().context("Could not get root for widget")?;
+ let window = root
+ .downcast_ref::()
+ .context("Could not downcast to GtkWindow")?;
+
+ LpPrint::new(image, window.clone(), None, None).run();
+
Ok(())
}
diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs
index 3627dc2..a051633 100644
--- a/src/widgets/mod.rs
+++ b/src/widgets/mod.rs
@@ -20,11 +20,15 @@
mod image;
mod image_page;
mod image_view;
+mod print;
+mod print_preview;
mod properties_view;
mod sliding_view;
pub use image::LpImage;
pub use image_page::LpImagePage;
pub use image_view::LpImageView;
+pub use print::LpPrint;
+pub use print_preview::LpPrintPreview;
pub use properties_view::LpPropertiesView;
pub use sliding_view::LpSlidingView;
diff --git a/src/widgets/print.rs b/src/widgets/print.rs
new file mode 100644
index 0000000..1c56b72
--- /dev/null
+++ b/src/widgets/print.rs
@@ -0,0 +1,437 @@
+//! The print layout widget that also coordinates the print process
+
+use crate::deps::*;
+use crate::widgets::{LpImage, LpPrintPreview};
+
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use glib::Properties;
+use gtk::CompositeTemplate;
+use once_cell::sync::OnceCell;
+
+use std::cell::{Cell, RefCell};
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate, Properties)]
+ #[template(file = "../../data/gtk/print.ui")]
+ #[properties(wrapper_type = super::LpPrint)]
+ pub struct LpPrint {
+ #[template_child]
+ pub(super) preview: TemplateChild,
+
+ // ListBox entries
+ #[template_child]
+ pub(super) width: TemplateChild,
+ #[template_child]
+ pub(super) scale: TemplateChild,
+ #[template_child]
+ pub(super) margin_left: TemplateChild,
+ #[template_child]
+ pub(super) margin_top: TemplateChild,
+ #[template_child]
+ pub(super) unit: TemplateChild,
+
+ #[property(get, set, builder().construct_only())]
+ pub(super) image: OnceCell,
+ #[property(get, set, builder().construct_only())]
+ pub(super) parent_window: OnceCell,
+ #[property(get, set, builder().construct_only())]
+ pub(super) print_settings: OnceCell,
+ #[property(get)]
+ pub(super) print_operation: gtk::PrintOperation,
+
+ #[property(get, set)]
+ pub(super) orientation: RefCell,
+ #[property(get, set)]
+ pub(super) unit_factor: Cell,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for LpPrint {
+ const NAME: &'static str = "LpPrint";
+ type Type = super::LpPrint;
+ type ParentType = adw::Window;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ Self::Type::bind_template_callbacks(klass);
+
+ klass.install_action_async("print.print", None, |print, _, _| async move {
+ print.print();
+ });
+
+ klass.install_action_async("print.back", None, |print, _, _| async move {
+ print.back().await;
+ });
+
+ klass.install_action("print.center-horizontally", None, |print, _, _| {
+ print.center_horizontally();
+ });
+
+ klass.install_action("print.center-vertically", None, |print, _, _| {
+ print.center_vertically();
+ });
+
+ klass.install_property_action("print.orientation", "orientation");
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for LpPrint {
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ let obj = self.obj();
+
+ obj.set_unit_factor(1.);
+
+ obj.set_transient_for(Some(&obj.parent_window()));
+ obj.set_modal(true);
+
+ self.print_operation
+ .set_print_settings(Some(&obj.print_settings()));
+ self.print_operation.set_allow_async(true);
+ self.print_operation.set_n_pages(1);
+ self.print_operation.set_current_page(0);
+
+ if let Some(uri) = obj.image().file().map(|x| x.uri()) {
+ obj.print_settings()
+ .set(gtk::PRINT_SETTINGS_OUTPUT_URI, Some(&format!("{uri}.pdf")));
+ }
+
+ self.print_operation.connect_begin_print(move |op, _ctx| {
+ op.set_n_pages(1);
+ });
+
+ self.print_operation.connect_draw_page(
+ glib::clone!(@weak obj => move |_operation, _context, _page_nr| {
+ let imp = obj.imp();
+
+ imp.width.set_increments(1., 5.);
+ imp.margin_left.set_increments(1., 5.);
+ imp.margin_top.set_increments(1., 5.);
+
+ obj.set_ranges();
+
+ imp.width.set_value(obj.image().image_size().0 as f64);
+ obj.center_horizontally();
+
+ obj.unit_selected();
+
+ imp.print_operation.cancel();
+
+ let orientation = match obj.page_setup().orientation() {
+ gtk::PageOrientation::Portrait => "portrait",
+ gtk::PageOrientation::Landscape => "landscape",
+ _ => "other",
+ };
+ obj.set_orientation(orientation);
+
+ obj.set_visible(true);
+ }),
+ );
+ }
+
+ fn properties() -> &'static [glib::ParamSpec] {
+ Self::derived_properties()
+ }
+ fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
+ self.derived_set_property(id, value, pspec)
+ }
+ fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ self.derived_property(id, pspec)
+ }
+ }
+
+ impl WidgetImpl for LpPrint {}
+ impl WindowImpl for LpPrint {}
+ impl AdwWindowImpl for LpPrint {}
+}
+glib::wrapper! {
+ pub struct LpPrint(ObjectSubclass)
+ @extends gtk::Widget, gtk::Window, adw::Window,
+ @implements gtk::Buildable, gtk::ConstraintTarget;
+}
+
+#[gtk::template_callbacks]
+impl LpPrint {
+ pub fn new(
+ image: LpImage,
+ parent_window: gtk::Window,
+ print_settings: Option,
+ page_setup: Option,
+ ) -> Self {
+ let print_settings = print_settings.unwrap_or_default();
+
+ let obj: Self = glib::Object::builder()
+ .property("image", image)
+ .property("print-settings", print_settings)
+ .property("parent-window", parent_window)
+ .build();
+
+ if let Some(page_setup) = page_setup {
+ obj.print_operation()
+ .set_default_page_setup(Some(&page_setup));
+ }
+
+ obj
+ }
+
+ pub fn unit(&self) -> glib::GString {
+ self.imp()
+ .unit
+ .selected_item()
+ .and_downcast::()
+ .map(|x| x.string())
+ .unwrap_or_default()
+ }
+
+ pub fn dpi(&self) -> f64 {
+ self.print_settings().resolution() as f64
+ }
+
+ #[template_callback]
+ pub fn width_changed(&self) {
+ self.imp()
+ .scale
+ .set_label(&format!("{:.2}\u{202F}%", self.user_scale() * 100.));
+
+ self.draw_preview();
+ }
+
+ /// Returns user selected image width in pixels
+ pub fn user_width(&self) -> f64 {
+ f64::max(1., self.imp().width.value() / self.unit_factor())
+ }
+
+ /// Returns scaling of the original image based on selected image size
+ pub fn user_scale(&self) -> f64 {
+ let (orig_width, _) = self.image().image_size();
+
+ self.user_width() / orig_width as f64
+ }
+
+ #[template_callback]
+ pub fn margin_left_changed(&self) {
+ self.draw_preview();
+ }
+
+ /// Returns user selected left margin in pixels
+ pub fn user_margin_left(&self) -> f64 {
+ self.imp().margin_left.value() / self.unit_factor()
+ }
+
+ #[template_callback]
+ pub fn margin_top_changed(&self) {
+ self.draw_preview();
+ }
+
+ /// Returns user selected top margin in pixels
+ pub fn user_margin_top(&self) -> f64 {
+ self.imp().margin_top.value() / self.unit_factor()
+ }
+
+ /// Redraws the preview widget child
+ fn draw_preview(&self) {
+ self.imp().preview.queue_resize();
+ }
+
+ /// Centers the image horizontally
+ fn center_horizontally(&self) {
+ let margin_left =
+ (self.page_setup().paper_width(gtk::Unit::Inch) * self.dpi() - self.user_width()) / 2.;
+
+ self.imp()
+ .margin_left
+ .set_value(margin_left * self.unit_factor());
+ }
+
+ /// Centers the image vertically
+ fn center_vertically(&self) {
+ let user_height = self.image().image_size().1 as f64 * self.user_scale();
+ let margin_top =
+ (self.page_setup().paper_height(gtk::Unit::Inch) * self.dpi() - user_height) / 2.;
+
+ self.imp()
+ .margin_top
+ .set_value(margin_top * self.unit_factor());
+ }
+
+ /// Sets the allowed ranges for the spin button based on current unit
+ fn set_ranges(&self) {
+ let imp = self.imp();
+
+ let max_height =
+ self.page_setup().page_height(gtk::Unit::Inch) * self.dpi() * self.unit_factor();
+
+ let max_width =
+ self.page_setup().page_width(gtk::Unit::Inch) * self.dpi() * self.unit_factor();
+ imp.width.set_range(self.unit_factor(), max_width);
+
+ let min_left_margin =
+ self.page_setup().left_margin(gtk::Unit::Inch) * self.dpi() * self.unit_factor();
+ imp.margin_left
+ .set_range(min_left_margin, min_left_margin + max_width);
+
+ let min_top_margin =
+ self.page_setup().top_margin(gtk::Unit::Inch) * self.dpi() * self.unit_factor();
+ imp.margin_top
+ .set_range(min_top_margin, min_top_margin + max_height);
+ }
+
+ #[template_callback]
+ pub fn orientation_changed(&self) {
+ let orientation = if self.orientation() == "landscape" {
+ gtk::PageOrientation::Landscape
+ } else {
+ gtk::PageOrientation::Portrait
+ };
+ self.page_setup().set_orientation(orientation);
+ self.set_ranges();
+ self.draw_preview();
+ }
+
+ #[template_callback]
+ pub fn unit_selected(&self) {
+ let imp = self.imp();
+
+ let (unit_factor, digits) = match self.unit().as_str() {
+ "cm" => (2.54 / self.dpi(), 2),
+ "in" => (1. / self.dpi(), 2),
+ "px" => (1., 0),
+ unit => {
+ log::error!("Unknown unit '{unit}'");
+ (1., 2)
+ }
+ };
+
+ let update_factor = unit_factor / self.unit_factor();
+ let width = imp.width.value() * update_factor;
+ let margin_left = imp.margin_left.value() * update_factor;
+ let margin_top = imp.margin_top.value() * update_factor;
+
+ self.set_unit_factor(unit_factor);
+ self.set_ranges();
+
+ imp.width.set_digits(digits);
+ imp.width.set_value(width);
+
+ imp.margin_left.set_digits(digits);
+ imp.margin_left.set_value(margin_left);
+
+ imp.margin_top.set_digits(digits);
+ imp.margin_top.set_value(margin_top);
+ }
+
+ pub fn run(&self) {
+ self.print_operation()
+ .run(
+ gtk::PrintOperationAction::PrintDialog,
+ self.imp().parent_window.get(),
+ )
+ .unwrap();
+ }
+
+ pub fn page_setup(&self) -> gtk::PageSetup {
+ self.print_operation().default_page_setup()
+ }
+
+ /// Go back to print dialog from image layout
+ async fn back(&self) {
+ self.close();
+ let print = Self::new(
+ self.image(),
+ self.parent_window(),
+ self.print_operation().print_settings(),
+ Some(self.print_operation().default_page_setup()),
+ );
+ print.run();
+ }
+
+ /// Initialize actual print operation
+ fn print(&self) {
+ self.close();
+
+ let print_operation = gtk::PrintOperation::new();
+
+ let print_settings = self.print_operation().print_settings();
+
+ if let Some(print_settings) = &print_settings {
+ if let Some(uri) = self.image().file().map(|x| x.uri()) {
+ print_settings.set(gtk::PRINT_SETTINGS_OUTPUT_URI, Some(&format!("{uri}.pdf")));
+ }
+ }
+
+ print_operation.set_print_settings(print_settings.as_ref());
+ print_operation.set_default_page_setup(Some(&self.print_operation().default_page_setup()));
+
+ print_operation.connect_begin_print(move |op, _ctx| {
+ op.set_n_pages(1);
+ });
+
+ print_operation.connect_draw_page(
+ glib::clone!(@weak self as obj => move |_operation, context, _page_nr| {
+ obj.draw_page(context);
+ }),
+ );
+
+ print_operation
+ .run(
+ gtk::PrintOperationAction::Print,
+ Some(&self.parent_window()),
+ )
+ .unwrap();
+ }
+
+ /// Draw PDF for printing
+ fn draw_page(&self, print_context: >k::PrintContext) {
+ log::debug!("Drawing image to print");
+ let image = self.image();
+
+ let cairo_dpi = print_context.dpi_x();
+
+ let (orig_width, _) = image.image_size();
+
+ let texture_scale = self.user_width() / orig_width as f64;
+ let cairo_scale = cairo_dpi / self.dpi();
+
+ let texture = image.print_data(texture_scale).unwrap();
+
+ let mut downloader = gdk::TextureDownloader::new(&texture);
+ downloader.set_format(gdk::MemoryFormat::B8g8r8a8Premultiplied);
+
+ let (data, stride) = downloader.download_bytes();
+ let data = data.to_vec();
+
+ let width = texture.width();
+ let height = texture.height();
+
+ let cairo_surface = cairo::ImageSurface::create_for_data(
+ data,
+ cairo::Format::ARgb32,
+ width,
+ height,
+ stride as i32,
+ )
+ .unwrap();
+
+ let cairo_context = print_context.cairo_context();
+ cairo_context.scale(cairo_scale, cairo_scale);
+
+ let margin_left =
+ self.user_margin_left() - self.page_setup().left_margin(gtk::Unit::Inch) * self.dpi();
+ let margin_top =
+ self.user_margin_top() - self.page_setup().top_margin(gtk::Unit::Inch) * self.dpi();
+
+ cairo_context
+ .set_source_surface(cairo_surface, margin_left, margin_top)
+ .unwrap();
+
+ cairo_context.paint().unwrap();
+ }
+}
diff --git a/src/widgets/print_preview.rs b/src/widgets/print_preview.rs
new file mode 100644
index 0000000..2e68c07
--- /dev/null
+++ b/src/widgets/print_preview.rs
@@ -0,0 +1,122 @@
+//! Print preview widget that shows a fake page with the image on it
+
+use crate::deps::*;
+use crate::widgets::LpPrint;
+
+use adw::subclass::prelude::*;
+use glib::Properties;
+use gtk::prelude::*;
+use once_cell::sync::OnceCell;
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default, Properties)]
+ #[properties(wrapper_type = super::LpPrintPreview)]
+ pub struct LpPrintPreview {
+ #[property(get, set)]
+ print: OnceCell,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for LpPrintPreview {
+ const NAME: &'static str = "LpPrintPreview";
+ type Type = super::LpPrintPreview;
+ type ParentType = gtk::Widget;
+ }
+
+ impl ObjectImpl for LpPrintPreview {
+ fn constructed(&self) {
+ self.parent_constructed();
+ self.obj().set_halign(gtk::Align::Center);
+ self.obj().set_valign(gtk::Align::Start);
+ self.obj().set_overflow(gtk::Overflow::Hidden);
+ }
+
+ fn properties() -> &'static [glib::ParamSpec] {
+ Self::derived_properties()
+ }
+ fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
+ self.derived_set_property(id, value, pspec)
+ }
+ fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ self.derived_property(id, pspec)
+ }
+ }
+
+ impl WidgetImpl for LpPrintPreview {
+ fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
+ let (width, height) = self.display_paper_dimensions();
+
+ let size = match orientation {
+ gtk::Orientation::Horizontal => width,
+ _ => height,
+ };
+
+ (size, size, -1, -1)
+ }
+
+ fn snapshot(&self, snapshot: >k::Snapshot) {
+ let obj = self.obj();
+
+ snapshot.save();
+
+ let scale = obj.print().user_scale() * self.display_scale();
+ let margin_left = obj.print().user_margin_left() * self.display_scale();
+ let margin_top = obj.print().user_margin_top() * self.display_scale();
+
+ if let Some(texture) = self.obj().print().image().print_data(scale) {
+ let area = graphene::Rect::new(
+ margin_left as f32,
+ margin_top as f32,
+ texture.width() as f32,
+ texture.height() as f32,
+ );
+
+ snapshot.append_texture(&texture, &area);
+
+ // Border around image
+ snapshot.append_inset_shadow(
+ &gsk::RoundedRect::from_rect(area, 0.),
+ &gdk::RGBA::new(0., 0., 0., 0.3),
+ 0.,
+ 0.,
+ 1.,
+ 0.,
+ );
+ }
+ snapshot.restore();
+ }
+ }
+
+ impl LpPrintPreview {
+ /// Returns scale factor from print pixel to preview size
+ fn display_scale(&self) -> f64 {
+ let print = self.obj().print();
+
+ let height = print.page_setup().paper_height(gtk::Unit::Inch) * print.dpi();
+ let width = print.page_setup().paper_width(gtk::Unit::Inch) * print.dpi();
+
+ 300. / f64::max(width, height)
+ }
+
+ /// Returns total preview size in pixels
+ fn display_paper_dimensions(&self) -> (i32, i32) {
+ let print = self.obj().print();
+
+ let height = print.page_setup().paper_height(gtk::Unit::Inch) * print.dpi();
+ let width = print.page_setup().paper_width(gtk::Unit::Inch) * print.dpi();
+
+ let scale = self.display_scale();
+
+ ((width * scale) as i32, (height * scale) as i32)
+ }
+ }
+}
+glib::wrapper! {
+ pub struct LpPrintPreview(ObjectSubclass)
+ @extends gtk::Widget;
+}
+
+#[gtk::template_callbacks]
+impl LpPrintPreview {}
diff --git a/src/window.rs b/src/window.rs
index 1408f13..15962eb 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -305,11 +305,6 @@ mod imp {
self.status_page
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
- // Set help overlay
- let builder = gtk::Builder::from_resource("/org/gnome/Loupe/gtk/help_overlay.ui");
- let help_overlay = builder.object("help_overlay").unwrap();
- obj.set_help_overlay(Some(&help_overlay));
-
self.drop_target.set_types(&[gdk::FileList::static_type()]);
// For callbacks, you will want to reference the GTK docs on