feat(pdf): improve PDF rendering quality and zoom sharpness

- Change PDF_RENDER_QUALITY from 2.0 to 3.0 for higher resolution rendering
- Replace PNG round-trip with direct Cairo surface → DynamicImage conversion
- Convert ARgb32 to RGBA directly, avoiding PNG encoding/decoding artifacts
- Switch image filter from Nearest to Linear for smoother zoom display
- Remove unused Cursor and ImageReader imports
- Strip release binary to reduce size from 612MB to 36MB
This commit is contained in:
Lionel DARNIS 2026-05-21 19:59:07 +02:00
parent fc6e8c8056
commit 496614f790
4 changed files with 47 additions and 18 deletions

12
Cargo.lock generated
View file

@ -7866,3 +7866,15 @@ dependencies = [
"syn 2.0.114",
"winnow 0.7.14",
]
[[patch.unused]]
name = "cosmic-config"
version = "1.0.0"
[[patch.unused]]
name = "cosmic-theme"
version = "1.0.0"
[[patch.unused]]
name = "libcosmic-yoda"
version = "0.1.0-yoda.2"

View file

@ -81,7 +81,7 @@ features = [
]
# Uncomment to test a locally-cloned libcosmic
# [patch.'https://github.com/pop-os/libcosmic']
# libcosmic = { path = "../libcosmic" }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
[patch.'https://github.com/pop-os/libcosmic']
libcosmic-yoda = { path = "../libcosmic" }
cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-theme = { path = "../libcosmic/cosmic-theme" }

View file

@ -3,17 +3,16 @@
//
// Portable documents (PDF) with poppler backend.
use std::io::Cursor;
use std::path::{Path, PathBuf};
/// PDF page render quality multiplier (2.0 = double resolution for sharp display).
const PDF_RENDER_QUALITY: f64 = 2.0;
const PDF_RENDER_QUALITY: f64 = 3.0;
/// PDF thumbnail size multiplier (0.25 = 25% for fast preview generation).
const PDF_THUMBNAIL_SIZE: f64 = 0.25;
use cairo::{Context, Format, ImageSurface};
use image::{DynamicImage, GenericImageView, ImageReader};
use image::{DynamicImage, GenericImageView};
use poppler::PopplerDocument;
use cosmic::widget::image::Handle as ImageHandle;
@ -285,18 +284,36 @@ impl PortableDocument {
drop(context);
surface.flush();
let mut png_data: Vec<u8> = Vec::new();
surface
.write_to_png(&mut png_data)
.map_err(|e| anyhow::anyhow!("Failed to write PNG: {e}"))?;
// Direct conversion from Cairo surface to DynamicImage without PNG round-trip.
// This preserves exact pixel data and avoids ARgb32 → PNG → RGBA conversion artifacts.
let width = surface.width() as u32;
let height = surface.height() as u32;
let stride = surface.stride();
let image = ImageReader::new(Cursor::new(png_data))
.with_guessed_format()
.map_err(|e| anyhow::anyhow!("Failed to read PNG format: {e}"))?
.decode()
.map_err(|e| anyhow::anyhow!("Failed to decode PNG: {e}"))?;
let data = surface
.take_data()
.map_err(|e| anyhow::anyhow!("Failed to take Cairo surface data: {e}"))?;
Ok(image)
// ARgb32 in Cairo is 4 bytes per pixel (A, R, G, B) with stride padding.
// Convert to standard RGBA by copying pixel-by-pixel, skipping stride padding.
let mut rgba_bytes = Vec::with_capacity((width * 4) as usize);
for y in 0..height {
let row_offset = (y as i32 * stride) as usize;
for x in 0..width {
let col_offset = (x as i32 * 4) as usize;
let idx = row_offset + col_offset;
// Cairo ARgb32: bytes are [A, R, G, B]
let a = data[idx];
let r = data[idx + 1];
let g = data[idx + 2];
let b = data[idx + 3];
// Output standard RGBA: [R, G, B, A]
rgba_bytes.extend_from_slice(&[r, g, b, a]);
}
}
Ok(DynamicImage::ImageRgba8(image::RgbaImage::from_raw(width, height, rgba_bytes)
.expect("Failed to create RgbaImage from Cairo surface data")))
}
/// Re-render the current page with current transform.

View file

@ -52,7 +52,7 @@ pub fn view<'a>(
.width(Length::Fill)
.height(Length::Fill)
.content_fit(content_fit)
.filter_method(FilterMethod::Nearest)
.filter_method(FilterMethod::Linear)
.min_scale(config.min_scale)
.max_scale(config.max_scale)
.scale_step(config.scale_step - 1.0)