diff --git a/src/decoder/formats/mod.rs b/src/decoder/formats/mod.rs index f965dc6..8e086a7 100644 --- a/src/decoder/formats/mod.rs +++ b/src/decoder/formats/mod.rs @@ -27,6 +27,10 @@ impl ImageFormat { ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng ) } + + pub fn is_svg(&self) -> bool { + matches!(self, Self::Svg) + } } impl std::fmt::Display for ImageFormat { diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs index 451133f..57ffa92 100644 --- a/src/decoder/tiling.rs +++ b/src/decoder/tiling.rs @@ -36,6 +36,19 @@ pub struct Tile { pub texture: gdk::Texture, } +/// Temporary function until HighDPI is sorted out +/// +/// TODO: Does this has to follow scaling factor? +#[must_use] +pub fn round(r: &graphene::Rect) -> graphene::Rect { + graphene::Rect::new( + r.x().round(), + r.y().round(), + r.width().round(), + r.height().round(), + ) +} + impl Tile { pub fn area(&self) -> graphene::Rect { let bleed = self.bleed as f32; @@ -62,15 +75,20 @@ impl Tile { /// Add tile to snapshot /// /// This is where parts of images actually land on the screen - pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, _options: &RenderOptions) { - let zoom = self.zoom(); - let area = self.area().scale(1. / zoom, 1. / zoom); - let area_with_bleed = self.area_with_bleed().scale(1. / zoom, 1. / zoom); + pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, zoom: f64, options: &RenderOptions) { + let zoom = zoom as f32; + let texture_zoom = self.zoom(); + let area = round(&self.area().scale(zoom / texture_zoom, zoom / texture_zoom)); + let area_with_bleed = round( + &self + .area_with_bleed() + .scale(zoom / texture_zoom, zoom / texture_zoom), + ); // TODO: do not clip outer bounderies of the image snapshot.push_clip(&area); snapshot.append_color(&gdk::RGBA::new(0.118, 0.118, 0.118, 1.), &area); - snapshot.append_texture(&self.texture, &area_with_bleed); + snapshot.append_scaled_texture(&self.texture, options.scaling_filter, &area_with_bleed); snapshot.pop(); } @@ -177,7 +195,7 @@ impl TiledImage { log::trace!("Rendering {} tiles", tiles.len()); // reverse to put least fitting tiles in the background for (_, tile) in tiles.iter().rev() { - tile.add_to_snapshot(snapshot, options); + tile.add_to_snapshot(snapshot, zoom, options); } } diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 191423f..a2050c2 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -507,14 +507,16 @@ mod imp { let applicable_zoom = widget.applicable_zoom(); - let scaling_filter = gsk::ScalingFilter::Linear; - /* - let scaling_filter = if applicable_zoom < 1. { - gsk::ScalingFilter::Trilinear - } else { - gsk::ScalingFilter::Nearest - }; - */ + let scaling_filter = if self.format.borrow().map_or(false, |x| x.is_svg()) { + // Looks better in SVG animations and avoids rendering issues + gsk::ScalingFilter::Linear + } else if applicable_zoom < 1. { + // Uses mipmaps to avoid moiré patterns + gsk::ScalingFilter::Trilinear + } else { + // Show pixels when zooming in because making images blurry looks worse + gsk::ScalingFilter::Nearest + }; let render_options = tiling::RenderOptions { scaling_filter }; @@ -525,26 +527,23 @@ mod imp { // Apply the scrolling position to the image if let Some(adj) = self.hadjustment.borrow().as_ref() { let x = -(adj.value() - (adj.upper() - display_width) / 2.); - snapshot.translate(&graphene::Point::new(x as f32, 0.)); + snapshot.translate(&graphene::Point::new(widget.round_f64(x) as f32, 0.)); } if let Some(adj) = self.vadjustment.borrow().as_ref() { let y = -(adj.value() - (adj.upper() - display_height) / 2.); - snapshot.translate(&graphene::Point::new(0., y as f32)); + snapshot.translate(&graphene::Point::new(0., widget.round_f64(y) as f32)); } // Centering in widget when no scrolling (black bars around image) - let x = f64::max((widget_width - display_width) / 2.0, 0.).floor(); - let y = f64::max((widget_height - display_height) / 2.0, 0.).floor(); + let x = widget.round_f64(f64::max((widget_width - display_width) / 2.0, 0.)); + let y = widget.round_f64(f64::max((widget_height - display_height) / 2.0, 0.)); // Round to pixel values to not have a half pixel offset to physical pixels // The offset would leading to a blurry output snapshot.translate(&graphene::Point::new( - widget.round(x) as f32, - widget.round(y) as f32, + widget.round_f64(x) as f32, + widget.round_f64(y) as f32, )); - // Apply zoom - snapshot.scale(applicable_zoom as f32, applicable_zoom as f32); - // Apply rotation and mirroring widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored()); @@ -876,7 +875,6 @@ impl LpImage { let snapshot = gtk::Snapshot::new(); - snapshot.scale(scale, scale); self.snapshot_rotate_mirror( &snapshot, -orientation.rotation as f32, @@ -1358,7 +1356,7 @@ impl LpImage { fn configure_adjustments(&self) { let hadjustment = self.hadjustment(); // round to application pixels to avoid tiny rounding errors from zoom - let content_width = self.round(self.image_displayed_width()); + let content_width = self.round_f64(self.image_displayed_width()); let widget_width = self.widget_width(); hadjustment.configure( @@ -1378,7 +1376,7 @@ impl LpImage { let vadjustment = self.vadjustment(); // round to application pixels to avoid tiny rounding errors from zoom - let content_height = self.round(self.image_displayed_height()); + let content_height = self.round_f64(self.image_displayed_height()); let widget_height = self.widget_height(); vadjustment.configure( @@ -1424,22 +1422,20 @@ impl LpImage { /// /// After the operation the image is positioned such that it's origin /// is a `(0, 0)` again. - pub fn snapshot_rotate_mirror(&self, snapshot: >k::Snapshot, rotation: f32, mirrored: bool) { - let applicable_zoom = self.applicable_zoom(); + fn snapshot_rotate_mirror(&self, snapshot: >k::Snapshot, rotation: f32, mirrored: bool) { + if rotation == 0. && !mirrored { + return; + } + + let zoom = self.applicable_zoom() as f32; let (original_width, original_height) = self.original_dimensions(); - let display_width = self.image_displayed_width(); - let display_height = self.image_displayed_height(); + let displayed_width = self.image_displayed_width() as f32; + let displayed_height = self.image_displayed_height() as f32; // Put image origin at (0, 0) again with rotation snapshot.translate(&graphene::Point::new( - -(original_width as f32 - display_width as f32 / applicable_zoom as f32) / 2., - -(original_height as f32 - display_height as f32 / applicable_zoom as f32) / 2., - )); - - // Undo centering in coordinates - snapshot.translate(&graphene::Point::new( - original_width as f32 / 2., - original_height as f32 / 2., + self.round_f32(displayed_width / 2.), + self.round_f32(displayed_height / 2.), )); // Apply the transformations from properties @@ -1452,8 +1448,8 @@ impl LpImage { // Needed for rotating around the center of the image, and // mirroring the image does not put it to a completely different position. snapshot.translate(&graphene::Point::new( - -original_width as f32 / 2., - -original_height as f32 / 2., + -self.round_f32(original_width as f32 * zoom / 2.), + -self.round_f32(original_height as f32 * zoom / 2.), )); } @@ -1496,8 +1492,22 @@ impl LpImage { /// Returns scaling aware rounded application pixel /// /// One physical pixel is 0.5 application pixels - pub fn round(&self, number: f64) -> f64 { + pub fn round_f64(&self, number: f64) -> f64 { + // Do not round during animation to avoid wiggling around + if self.zoom_animation().state() == adw::AnimationState::Playing { + return number; + } + let scale = self.scale_factor() as f64; (number * scale).round() / scale } + + pub fn round_f32(&self, number: f32) -> f32 { + if self.zoom_animation().state() == adw::AnimationState::Playing { + return number; + } + + let scale = self.scale_factor() as f32; + (number * scale).round() / scale + } }