image: Use mipmaps for zoom-out and NN for zoom-in
Before we always used "linear" filtering. This gave a harsh image with moire patterns when zooming out and washed out images when zooming in. Closes #68, #71 Part-of: <https://gitlab.gnome.org/Incubator/loupe/-/merge_requests/125>
This commit is contained in:
parent
6d0c5a4665
commit
67207bb215
3 changed files with 73 additions and 41 deletions
|
|
@ -27,6 +27,10 @@ impl ImageFormat {
|
||||||
ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng
|
ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_svg(&self) -> bool {
|
||||||
|
matches!(self, Self::Svg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ImageFormat {
|
impl std::fmt::Display for ImageFormat {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,19 @@ pub struct Tile {
|
||||||
pub texture: gdk::Texture,
|
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 {
|
impl Tile {
|
||||||
pub fn area(&self) -> graphene::Rect {
|
pub fn area(&self) -> graphene::Rect {
|
||||||
let bleed = self.bleed as f32;
|
let bleed = self.bleed as f32;
|
||||||
|
|
@ -62,15 +75,20 @@ impl Tile {
|
||||||
/// Add tile to snapshot
|
/// Add tile to snapshot
|
||||||
///
|
///
|
||||||
/// This is where parts of images actually land on the screen
|
/// This is where parts of images actually land on the screen
|
||||||
pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, _options: &RenderOptions) {
|
pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, zoom: f64, options: &RenderOptions) {
|
||||||
let zoom = self.zoom();
|
let zoom = zoom as f32;
|
||||||
let area = self.area().scale(1. / zoom, 1. / zoom);
|
let texture_zoom = self.zoom();
|
||||||
let area_with_bleed = self.area_with_bleed().scale(1. / zoom, 1. / 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
|
// TODO: do not clip outer bounderies of the image
|
||||||
snapshot.push_clip(&area);
|
snapshot.push_clip(&area);
|
||||||
snapshot.append_color(&gdk::RGBA::new(0.118, 0.118, 0.118, 1.), &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();
|
snapshot.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +195,7 @@ impl TiledImage {
|
||||||
log::trace!("Rendering {} tiles", tiles.len());
|
log::trace!("Rendering {} tiles", tiles.len());
|
||||||
// reverse to put least fitting tiles in the background
|
// reverse to put least fitting tiles in the background
|
||||||
for (_, tile) in tiles.iter().rev() {
|
for (_, tile) in tiles.iter().rev() {
|
||||||
tile.add_to_snapshot(snapshot, options);
|
tile.add_to_snapshot(snapshot, zoom, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -507,14 +507,16 @@ mod imp {
|
||||||
|
|
||||||
let applicable_zoom = widget.applicable_zoom();
|
let applicable_zoom = widget.applicable_zoom();
|
||||||
|
|
||||||
let scaling_filter = gsk::ScalingFilter::Linear;
|
let scaling_filter = if self.format.borrow().map_or(false, |x| x.is_svg()) {
|
||||||
/*
|
// Looks better in SVG animations and avoids rendering issues
|
||||||
let scaling_filter = if applicable_zoom < 1. {
|
gsk::ScalingFilter::Linear
|
||||||
gsk::ScalingFilter::Trilinear
|
} else if applicable_zoom < 1. {
|
||||||
} else {
|
// Uses mipmaps to avoid moiré patterns
|
||||||
gsk::ScalingFilter::Nearest
|
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 };
|
let render_options = tiling::RenderOptions { scaling_filter };
|
||||||
|
|
||||||
|
|
@ -525,26 +527,23 @@ mod imp {
|
||||||
// Apply the scrolling position to the image
|
// Apply the scrolling position to the image
|
||||||
if let Some(adj) = self.hadjustment.borrow().as_ref() {
|
if let Some(adj) = self.hadjustment.borrow().as_ref() {
|
||||||
let x = -(adj.value() - (adj.upper() - display_width) / 2.);
|
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() {
|
if let Some(adj) = self.vadjustment.borrow().as_ref() {
|
||||||
let y = -(adj.value() - (adj.upper() - display_height) / 2.);
|
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)
|
// Centering in widget when no scrolling (black bars around image)
|
||||||
let x = f64::max((widget_width - display_width) / 2.0, 0.).floor();
|
let x = widget.round_f64(f64::max((widget_width - display_width) / 2.0, 0.));
|
||||||
let y = f64::max((widget_height - display_height) / 2.0, 0.).floor();
|
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
|
// Round to pixel values to not have a half pixel offset to physical pixels
|
||||||
// The offset would leading to a blurry output
|
// The offset would leading to a blurry output
|
||||||
snapshot.translate(&graphene::Point::new(
|
snapshot.translate(&graphene::Point::new(
|
||||||
widget.round(x) as f32,
|
widget.round_f64(x) as f32,
|
||||||
widget.round(y) as f32,
|
widget.round_f64(y) as f32,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Apply zoom
|
|
||||||
snapshot.scale(applicable_zoom as f32, applicable_zoom as f32);
|
|
||||||
|
|
||||||
// Apply rotation and mirroring
|
// Apply rotation and mirroring
|
||||||
widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored());
|
widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored());
|
||||||
|
|
||||||
|
|
@ -876,7 +875,6 @@ impl LpImage {
|
||||||
|
|
||||||
let snapshot = gtk::Snapshot::new();
|
let snapshot = gtk::Snapshot::new();
|
||||||
|
|
||||||
snapshot.scale(scale, scale);
|
|
||||||
self.snapshot_rotate_mirror(
|
self.snapshot_rotate_mirror(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
-orientation.rotation as f32,
|
-orientation.rotation as f32,
|
||||||
|
|
@ -1358,7 +1356,7 @@ impl LpImage {
|
||||||
fn configure_adjustments(&self) {
|
fn configure_adjustments(&self) {
|
||||||
let hadjustment = self.hadjustment();
|
let hadjustment = self.hadjustment();
|
||||||
// round to application pixels to avoid tiny rounding errors from zoom
|
// 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();
|
let widget_width = self.widget_width();
|
||||||
|
|
||||||
hadjustment.configure(
|
hadjustment.configure(
|
||||||
|
|
@ -1378,7 +1376,7 @@ impl LpImage {
|
||||||
|
|
||||||
let vadjustment = self.vadjustment();
|
let vadjustment = self.vadjustment();
|
||||||
// round to application pixels to avoid tiny rounding errors from zoom
|
// 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();
|
let widget_height = self.widget_height();
|
||||||
|
|
||||||
vadjustment.configure(
|
vadjustment.configure(
|
||||||
|
|
@ -1424,22 +1422,20 @@ impl LpImage {
|
||||||
///
|
///
|
||||||
/// After the operation the image is positioned such that it's origin
|
/// After the operation the image is positioned such that it's origin
|
||||||
/// is a `(0, 0)` again.
|
/// is a `(0, 0)` again.
|
||||||
pub fn snapshot_rotate_mirror(&self, snapshot: >k::Snapshot, rotation: f32, mirrored: bool) {
|
fn snapshot_rotate_mirror(&self, snapshot: >k::Snapshot, rotation: f32, mirrored: bool) {
|
||||||
let applicable_zoom = self.applicable_zoom();
|
if rotation == 0. && !mirrored {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zoom = self.applicable_zoom() as f32;
|
||||||
let (original_width, original_height) = self.original_dimensions();
|
let (original_width, original_height) = self.original_dimensions();
|
||||||
let display_width = self.image_displayed_width();
|
let displayed_width = self.image_displayed_width() as f32;
|
||||||
let display_height = self.image_displayed_height();
|
let displayed_height = self.image_displayed_height() as f32;
|
||||||
|
|
||||||
// Put image origin at (0, 0) again with rotation
|
// Put image origin at (0, 0) again with rotation
|
||||||
snapshot.translate(&graphene::Point::new(
|
snapshot.translate(&graphene::Point::new(
|
||||||
-(original_width as f32 - display_width as f32 / applicable_zoom as f32) / 2.,
|
self.round_f32(displayed_width / 2.),
|
||||||
-(original_height as f32 - display_height as f32 / applicable_zoom as f32) / 2.,
|
self.round_f32(displayed_height / 2.),
|
||||||
));
|
|
||||||
|
|
||||||
// Undo centering in coordinates
|
|
||||||
snapshot.translate(&graphene::Point::new(
|
|
||||||
original_width as f32 / 2.,
|
|
||||||
original_height as f32 / 2.,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
// Apply the transformations from properties
|
// Apply the transformations from properties
|
||||||
|
|
@ -1452,8 +1448,8 @@ impl LpImage {
|
||||||
// Needed for rotating around the center of the image, and
|
// Needed for rotating around the center of the image, and
|
||||||
// mirroring the image does not put it to a completely different position.
|
// mirroring the image does not put it to a completely different position.
|
||||||
snapshot.translate(&graphene::Point::new(
|
snapshot.translate(&graphene::Point::new(
|
||||||
-original_width as f32 / 2.,
|
-self.round_f32(original_width as f32 * zoom / 2.),
|
||||||
-original_height as f32 / 2.,
|
-self.round_f32(original_height as f32 * zoom / 2.),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1496,8 +1492,22 @@ impl LpImage {
|
||||||
/// Returns scaling aware rounded application pixel
|
/// Returns scaling aware rounded application pixel
|
||||||
///
|
///
|
||||||
/// One physical pixel is 0.5 application pixels
|
/// 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;
|
let scale = self.scale_factor() as f64;
|
||||||
(number * scale).round() / scale
|
(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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue