From ae5ca76176e8cd7ffe62e087e1f1bac97b80e217 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Sun, 2 Apr 2023 01:39:23 +0200 Subject: [PATCH] printing: Add printing support Part-of: --- Cargo.lock | 86 ++++ Cargo.toml | 1 + data/gtk/print.ui | 192 ++++++++ data/loupe.gresource.xml | 8 - data/meson.build | 29 +- data/org.gnome.Loupe.gschema.xml | 5 - .../gtk/help-overlay.ui} | 0 .../center-on-page-horizontal-symbolic.svg | 11 + .../center-on-page-vertical-symbolic.svg | 11 + .../page-orientation-landscape-symbolic.svg | 5 + .../page-orientation-portrait-symbolic.svg | 5 + data/{ => resources}/style-hc.css | 0 data/{ => resources}/style.css | 8 +- src/config.rs.in | 1 - src/decoder/tiling.rs | 5 + src/main.rs | 9 +- src/widgets/image.rs | 28 ++ src/widgets/image_view.rs | 13 +- src/widgets/mod.rs | 4 + src/widgets/print.rs | 437 ++++++++++++++++++ src/widgets/print_preview.rs | 122 +++++ src/window.rs | 5 - 22 files changed, 936 insertions(+), 49 deletions(-) create mode 100644 data/gtk/print.ui delete mode 100644 data/loupe.gresource.xml delete mode 100644 data/org.gnome.Loupe.gschema.xml rename data/{gtk/help_overlay.ui => resources/gtk/help-overlay.ui} (100%) create mode 100644 data/resources/icons/scalable/actions/center-on-page-horizontal-symbolic.svg create mode 100644 data/resources/icons/scalable/actions/center-on-page-vertical-symbolic.svg create mode 100644 data/resources/icons/scalable/actions/page-orientation-landscape-symbolic.svg create mode 100644 data/resources/icons/scalable/actions/page-orientation-portrait-symbolic.svg rename data/{ => resources}/style-hc.css (100%) rename data/{ => resources}/style.css (72%) create mode 100644 src/widgets/print.rs create mode 100644 src/widgets/print_preview.rs 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 @@ + + + + \ 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