printing: Add printing support

Part-of: <https://gitlab.gnome.org/Incubator/loupe/-/merge_requests/165>
This commit is contained in:
Sophie Herold 2023-04-02 01:39:23 +02:00 • committed by Marge Bot
parent e96b9ccac9
commit ae5ca76176
22 changed files with 936 additions and 49 deletions

86
Cargo.lock generated
View file

@ -1542,6 +1542,36 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "gweather-sys" name = "gweather-sys"
version = "4.2.0" version = "4.2.0"
@ -1930,6 +1960,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" 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]] [[package]]
name = "locale_config" name = "locale_config"
version = "0.3.0" version = "0.3.0"
@ -1975,6 +2014,7 @@ dependencies = [
"gettext-rs", "gettext-rs",
"gio", "gio",
"gtk4", "gtk4",
"gvdb-macros",
"image", "image",
"indexmap", "indexmap",
"kamadak-exif", "kamadak-exif",
@ -2040,6 +2080,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memmap2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.7.1" version = "0.7.1"
@ -2578,6 +2627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde",
] ]
[[package]] [[package]]
@ -2788,6 +2838,12 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "safe-transmute" name = "safe-transmute"
version = "0.11.2" version = "0.11.2"
@ -2803,6 +2859,15 @@ dependencies = [
"bytemuck", "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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -2859,6 +2924,17 @@ dependencies = [
"syn 2.0.15", "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]] [[package]]
name = "serde_repr" name = "serde_repr"
version = "0.1.12" version = "0.1.12"
@ -3284,6 +3360,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" 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]] [[package]]
name = "wasi" name = "wasi"
version = "0.9.0+wasi-snapshot-preview1" version = "0.9.0+wasi-snapshot-preview1"

View file

@ -14,6 +14,7 @@ anyhow = { version = "1.0.66", features = ["backtrace"] }
arc-swap = "1.6.0" arc-swap = "1.6.0"
env_logger = "0.10.0" env_logger = "0.10.0"
futures = "0.3.25" futures = "0.3.25"
gvdb-macros = "0.1.6"
indexmap = "1.9.2" indexmap = "1.9.2"
kamadak-exif = "0.5.5" kamadak-exif = "0.5.5"
lcms2 = "5.6.0" lcms2 = "5.6.0"

192
data/gtk/print.ui Normal file
View file

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="LpPrint" parent="AdwWindow">
<property name="title" translatable="yes">Layout Image</property>
<property name="default-width">350</property>
<signal name="notify::orientation" handler="orientation_changed" swapped="yes" />
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkShortcutController">
<property name="scope">managed</property>
<child>
<object class="GtkShortcut">
<property name="trigger">Escape|&lt;Ctrl&gt;w</property>
<property name="action">action(window.close)</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkButton">
<property name="icon-name">go-previous-symbolic</property>
<property name="action-name">print.back</property>
</object>
</child>
<style>
<class name="flat" />
</style>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="vexpand">true</property>
<property name="spacing">24</property>
<property name="margin-top">18</property>
<property name="margin-end">18</property>
<property name="margin-bottom">18</property>
<property name="margin-start">18</property>
<child>
<object class="LpPrintPreview" id="preview">
<property name="css-name">lpprintpreview</property>
<binding name="print">
<constant>LpPrint</constant>
</binding>
</object>
</child>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="spacing">36</property>
<child>
<object class="GtkBox">
<style>
<class name="linked" />
</style>
<child>
<object class="GtkToggleButton">
<property name="icon-name">page-orientation-portrait-symbolic</property>
<property name="action-name">print.orientation</property>
<property name="action-target">"portrait"</property>
<property name="tooltip-text" translatable="yes">Portrait page orientation</property>
</object>
</child>
<child>
<object class="GtkToggleButton">
<property name="icon-name">page-orientation-landscape-symbolic</property>
<property name="action-name">print.orientation</property>
<property name="action-target">"landscape"</property>
<property name="tooltip-text" translatable="yes">Landscape page orientation</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<style>
<class name="linked" />
</style>
<child>
<object class="GtkButton">
<property name="icon-name">center-on-page-horizontal-symbolic</property>
<property name="action-name">print.center-horizontally</property>
<property name="tooltip-text" translatable="yes">Center image horizontally</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="icon-name">center-on-page-vertical-symbolic</property>
<property name="action-name">print.center-vertically</property>
<property name="tooltip-text" translatable="yes">Center image vertically</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Image Width</property>
<child>
<object class="GtkSpinButton" id="width">
<property name="valign">center</property>
<property name="max-width-chars">5</property>
<signal name="value-changed" handler="width_changed" swapped="yes" />
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Image Scale</property>
<child>
<object class="GtkLabel" id="scale">
<style>
<class name="dim-label" />
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Left Margin</property>
<child>
<object class="GtkSpinButton" id="margin_left">
<property name="valign">center</property>
<property name="max-width-chars">5</property>
<signal name="value-changed" handler="margin_left_changed" swapped="yes" />
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Top Margin</property>
<child>
<object class="GtkSpinButton" id="margin_top">
<property name="valign">center</property>
<property name="max-width-chars">5</property>
<signal name="value-changed" handler="margin_top_changed" swapped="yes" />
</object>
</child>
</object>
</child>
<child>
<object class="AdwComboRow" id="unit">
<property name="title" translatable="yes">Length Unit</property>
<signal name="notify::selected-item" handler="unit_selected" swapped="yes" />
<property name="model">
<object class="GtkStringList">
<items>
<item>cm</item>
<item>in</item>
<item>px</item>
</items>
</object>
</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Print</property>
<property name="action-name">print.print</property>
<property name="halign">center</property>
<property name="valign">end</property>
<property name="vexpand">true</property>
<property name="width-request">160</property>
<style>
<class name="suggested-action" />
<class name="pill" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/Loupe">
<file compressed="true" preprocess="xml-stripblanks">gtk/help_overlay.ui</file>
<file>style.css</file>
<file>style-hc.css</file>
</gresource>
</gresources>

View file

@ -38,29 +38,8 @@ appstream_file = i18n.merge_file(
install_dir: iv_datadir / 'metainfo' 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') subdir('icons')
# Post install
gnome.post_install()

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="loupe">
<schema id="org.gnome.Loupe" path="/org/gnome/Loupe/">
</schema>
</schemalist>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3 0 c -1.644531 0 -3 1.355469 -3 3 v 10.003906 c 0 1.644532 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355468 3 -3 v -8.503906 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -3.5 -3.5 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 2 h 8 v 1.5 c 0 1 0.5 1.5 1.5 1.5 h 1.5 v 8.003906 c 0 0.574219 -0.429688 1 -1 1 h -10 c -0.570312 0 -1 -0.425781 -1 -1 v -10.003906 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"/>
<path d="m 7 6 h 2 v -2 h -2 z m 0 0"/>
<path d="m 7 9 h 2 v -2 h -2 z m 0 0"/>
<path d="m 7 12 h 2 v -2 h -2 z m 0 0"/>
<g fill-opacity="0.34902">
<path d="m 10 12 h 2 v -2 h -2 z m 0 0"/>
<path d="m 4 12 h 2 v -2 h -2 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 848 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3 0 c -1.644531 0 -3 1.355469 -3 3 v 10.003906 c 0 1.644532 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355468 3 -3 v -8.503906 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -3.5 -3.5 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 2 h 8 v 1.5 c 0 1 0.5 1.5 1.5 1.5 h 1.5 v 8.003906 c 0 0.574219 -0.429688 1 -1 1 h -10 c -0.570312 0 -1 -0.425781 -1 -1 v -10.003906 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"/>
<path d="m 10 7 v 2 h 2 v -2 z m 0 0"/>
<path d="m 7 7 v 2 h 2 v -2 z m 0 0"/>
<path d="m 4 7 v 2 h 2 v -2 z m 0 0"/>
<g fill-opacity="0.34902">
<path d="m 4 10 v 2 h 2 v -2 z m 0 0"/>
<path d="m 4 4 v 2 h 2 v -2 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 841 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 15 11 c 0 1.644531 -1.355469 3 -3 3 h -9 c -1.644531 0 -3 -1.355469 -3 -3 v -6 c 0 -1.644531 1.355469 -3 3 -3 h 7.5 c 0.265625 0 0.519531 0.105469 0.707031 0.292969 l 3.5 3.5 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 z m -2 0 v -4 h -1.5 c -1 0 -1.5 -0.5 -1.5 -1.5 v -1.5 h -7 c -0.570312 0 -1 0.429688 -1 1 v 6 c 0 0.570312 0.429688 1 1 1 h 9 c 0.570312 0 1 -0.429688 1 -1 z m -8.5 -6.035156 c -0.824219 0 -1.5 0.675781 -1.5 1.5 c 0 0.820312 0.675781 1.5 1.5 1.5 s 1.5 -0.679688 1.5 -1.5 c 0 -0.824219 -0.675781 -1.5 -1.5 -1.5 z m 0 1 c 0.28125 0 0.5 0.21875 0.5 0.5 s -0.21875 0.5 -0.5 0.5 s -0.5 -0.21875 -0.5 -0.5 s 0.21875 -0.5 0.5 -0.5 z m 0 0"/>
<path d="m 13.460938 10 h -2.417969 c -0.558594 0 -1.09375 -0.195312 -1.523438 -0.550781 l -1.128906 -0.941407 c -0.804687 -0.671874 -1.972656 -0.675781 -2.78125 -0.003906 l -1.09375 0.898438 c -0.449219 0.375 -1.015625 0.578125 -1.597656 0.578125 h -1.339844 c -0.277344 0 -0.5 0.222656 -0.5 0.5 c 0 0.273437 0.222656 0.5 0.5 0.5 h 1.339844 c 0.816406 0 1.605469 -0.285157 2.234375 -0.804688 l 1.09375 -0.902343 c 0.4375 -0.363282 1.070312 -0.359376 1.503906 0.003906 l 1.128906 0.941406 c 0.609375 0.503906 1.371094 0.78125 2.164063 0.78125 h 2.417969 c 0.277343 0 0.5 -0.222656 0.5 -0.5 s -0.222657 -0.5 -0.5 -0.5 z m 0 0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 5 1 c -1.644531 0 -3 1.355469 -3 3 v 9 c 0 1.644531 1.355469 3 3 3 h 6 c 1.644531 0 3 -1.355469 3 -3 v -7.5 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -3.5 -3.5 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 2 h 4 v 1.5 c 0 1 0.5 1.5 1.5 1.5 h 1.5 v 7 c 0 0.570312 -0.429688 1 -1 1 h -6 c -0.570312 0 -1 -0.429688 -1 -1 v -9 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"/>
<path d="m 8 6 c -1.097656 0 -2 0.902344 -2 2 c 0 0.53125 0.210938 1.015625 0.550781 1.375 c -0.015625 0.011719 -0.03125 0.015625 -0.050781 0.027344 c -0.925781 0.535156 -1.5 1.527344 -1.5 2.597656 c 0 0.542969 0.441406 0.980469 0.980469 0.980469 h 4.039062 c 0.539063 0 0.980469 -0.4375 0.980469 -0.980469 c 0 -1.070312 -0.574219 -2.0625 -1.5 -2.597656 c -0.015625 -0.011719 -0.035156 -0.019532 -0.054688 -0.027344 c 0.34375 -0.359375 0.554688 -0.84375 0.554688 -1.375 c 0 -1.097656 -0.902344 -2 -2 -2 z m 0 1 c 0.558594 0 1 0.441406 1 1 s -0.441406 1 -1 1 s -1 -0.441406 -1 -1 s 0.441406 -1 1 -1 z m 0 3 c 0.34375 0 0.691406 0.089844 1 0.269531 c 0.566406 0.324219 0.933594 0.902344 0.992188 1.542969 c 0.007812 0.105469 -0.074219 0.1875 -0.179688 0.1875 h -3.605469 c -0.113281 0 -0.207031 -0.09375 -0.195312 -0.207031 c 0.0625 -0.632813 0.429687 -1.203125 0.988281 -1.523438 c 0.308594 -0.179687 0.65625 -0.269531 1 -0.269531 z m 0 0" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -22,5 +22,11 @@ row.property label.subtitle {
margin: 16px; margin: 16px;
border-radius: 5px; border-radius: 5px;
outline: 2px solid white; 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;
}

View file

@ -1,5 +1,4 @@
pub const APP_ID: &str = "@APP_ID@"; pub const APP_ID: &str = "@APP_ID@";
pub const PROFILE: &str = "@PROFILE@"; pub const PROFILE: &str = "@PROFILE@";
pub const PKGDATADIR: &str = "@pkgdatadir@";
pub const VERSION: &str = "@VERSION@"; pub const VERSION: &str = "@VERSION@";
pub const LOCALEDIR: &str = "@localedir@"; pub const LOCALEDIR: &str = "@localedir@";

View file

@ -86,6 +86,11 @@ impl Tile {
let area = round(&self.area().scale(zoom, zoom)); let area = round(&self.area().scale(zoom, zoom));
let area_with_bleed = round(&self.area_with_bleed().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 // TODO: do not clip outer bounderies of the image
snapshot.push_clip(&area); snapshot.push_clip(&area);
if let Some(background_color) = &options.background_color { if let Some(background_color) = &options.background_color {

View file

@ -65,6 +65,9 @@ mod deps {
use application::LpApplication; use application::LpApplication;
use deps::*; use deps::*;
static GRESOURCE_BYTES: &[u8] =
gvdb_macros::include_gresource_from_dir!("/org/gnome/Loupe", "data/resources");
fn main() -> glib::ExitCode { fn main() -> glib::ExitCode {
env_logger::Builder::from_default_env() env_logger::Builder::from_default_env()
.format_timestamp_millis() .format_timestamp_millis()
@ -74,9 +77,9 @@ fn main() -> glib::ExitCode {
bindtextdomain("loupe", config::LOCALEDIR).unwrap(); bindtextdomain("loupe", config::LOCALEDIR).unwrap();
textdomain("loupe").unwrap(); textdomain("loupe").unwrap();
let res = gio::Resource::load(config::PKGDATADIR.to_owned() + "/loupe.gresource") gio::resources_register(
.expect("Could not load resources"); &gio::Resource::from_data(&glib::Bytes::from_static(GRESOURCE_BYTES)).unwrap(),
gio::resources_register(&res); );
LpApplication::new().run() LpApplication::new().run()
} }

View file

@ -999,6 +999,34 @@ impl LpImage {
snapshot.to_paintable(None) snapshot.to_paintable(None)
} }
pub fn print_data(&self, scale: f64) -> Option<gdk::Texture> {
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 { fn mirrored(&self) -> bool {
self.imp().mirrored.get() self.imp().mirrored.get()
} }

View file

@ -29,7 +29,7 @@ use crate::deps::*;
use crate::file_model::LpFileModel; use crate::file_model::LpFileModel;
use crate::util::gettext::*; use crate::util::gettext::*;
use crate::util::{Direction, Position}; use crate::util::{Direction, Position};
use crate::widgets::{LpImage, LpImagePage, LpSlidingView}; use crate::widgets::{LpImage, LpImagePage, LpPrint, LpSlidingView};
use crate::util::spawn; use crate::util::spawn;
use adw::prelude::*; use adw::prelude::*;
@ -609,6 +609,17 @@ impl LpImageView {
} }
pub fn print(&self) -> anyhow::Result<()> { 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::<gtk::Window>()
.context("Could not downcast to GtkWindow")?;
LpPrint::new(image, window.clone(), None, None).run();
Ok(()) Ok(())
} }

View file

@ -20,11 +20,15 @@
mod image; mod image;
mod image_page; mod image_page;
mod image_view; mod image_view;
mod print;
mod print_preview;
mod properties_view; mod properties_view;
mod sliding_view; mod sliding_view;
pub use image::LpImage; pub use image::LpImage;
pub use image_page::LpImagePage; pub use image_page::LpImagePage;
pub use image_view::LpImageView; pub use image_view::LpImageView;
pub use print::LpPrint;
pub use print_preview::LpPrintPreview;
pub use properties_view::LpPropertiesView; pub use properties_view::LpPropertiesView;
pub use sliding_view::LpSlidingView; pub use sliding_view::LpSlidingView;

437
src/widgets/print.rs Normal file
View file

@ -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<LpPrintPreview>,
// ListBox entries
#[template_child]
pub(super) width: TemplateChild<gtk::SpinButton>,
#[template_child]
pub(super) scale: TemplateChild<gtk::Label>,
#[template_child]
pub(super) margin_left: TemplateChild<gtk::SpinButton>,
#[template_child]
pub(super) margin_top: TemplateChild<gtk::SpinButton>,
#[template_child]
pub(super) unit: TemplateChild<adw::ComboRow>,
#[property(get, set, builder().construct_only())]
pub(super) image: OnceCell<LpImage>,
#[property(get, set, builder().construct_only())]
pub(super) parent_window: OnceCell<gtk::Window>,
#[property(get, set, builder().construct_only())]
pub(super) print_settings: OnceCell<gtk::PrintSettings>,
#[property(get)]
pub(super) print_operation: gtk::PrintOperation,
#[property(get, set)]
pub(super) orientation: RefCell<String>,
#[property(get, set)]
pub(super) unit_factor: Cell<f64>,
}
#[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<Self>) {
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<imp::LpPrint>)
@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<gtk::PrintSettings>,
page_setup: Option<gtk::PageSetup>,
) -> 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::<gtk::StringObject>()
.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: &gtk::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();
}
}

View file

@ -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<LpPrint>,
}
#[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: &gtk::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<imp::LpPrintPreview>)
@extends gtk::Widget;
}
#[gtk::template_callbacks]
impl LpPrintPreview {}

View file

@ -305,11 +305,6 @@ mod imp {
self.status_page self.status_page
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID))); .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()]); self.drop_target.set_types(&[gdk::FileList::static_type()]);
// For callbacks, you will want to reference the GTK docs on // For callbacks, you will want to reference the GTK docs on