From b5239e14b63ce241fcbc8e7b1be6b88f6940f021 Mon Sep 17 00:00:00 2001 From: Julian Mutter Date: Wed, 27 Nov 2024 15:48:58 +0100 Subject: [PATCH] Add color mode feature, rendering e.g. in dark mode --- src/cache.rs | 30 ++++++++--- src/color_mode.rs | 131 ++++++++++++++++++++++++++++++++++++++++++++++ src/draw.rs | 20 +++++-- src/main.rs | 3 +- src/ui.rs | 97 +++++++++++++++++++++++++++++++--- 5 files changed, 261 insertions(+), 20 deletions(-) create mode 100644 src/color_mode.rs diff --git a/src/cache.rs b/src/cache.rs index f71faf4..a3b068b 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,4 +1,4 @@ -use crate::draw; +use crate::{color_mode::{self, ColorMode}, draw}; use anyhow::{anyhow, bail, Result}; use glib::timeout_future; use gtk::{gdk::Texture, prelude::TextureExt}; @@ -19,15 +19,17 @@ pub struct PageCache { max_num_stored_pages: usize, pages: BTreeMap>, last_requested_page_number: PageNumber, + color_mode: ColorMode, } impl PageCache { - pub fn new(document: Document, max_num_stored_pages: usize) -> Self { + pub fn new(document: Document, max_num_stored_pages: usize, color_mode : ColorMode) -> Self { PageCache { document, max_num_stored_pages, pages: BTreeMap::new(), last_requested_page_number: 0, + color_mode, } } @@ -70,7 +72,7 @@ impl PageCache { if let Some(page) = self.document.page(page_number as i32) { let pages = vec![Rc::new(page)]; - let texture = draw::draw_pages_to_texture(&pages, height); + let texture = draw::draw_pages_to_texture(&pages, height, &self.color_mode); let page = Rc::new(texture); // Overwrite page with lower resolution if exists @@ -162,6 +164,9 @@ pub struct CachePageCommand { height: i32, } +#[derive(Debug)] +pub struct CacheKillSignal; + #[derive(Debug)] pub enum RetrievePagesCommand { GetCurrentTwoPages { page_left_number: PageNumber }, @@ -186,6 +191,7 @@ pub struct SyncCacheCommandChannel { retrieve_commands: Vec, cache_commands: VecDeque, priority_cache_commands: Vec, + kill_signals : Vec } pub struct SyncCacheCommandSender { @@ -193,6 +199,7 @@ pub struct SyncCacheCommandSender { } pub struct SyncCacheCommandReceiver { + cache_closed : bool, channel: Rc>, } @@ -202,13 +209,14 @@ impl SyncCacheCommandChannel { retrieve_commands: Vec::new(), cache_commands: VecDeque::new(), priority_cache_commands: Vec::new(), + kill_signals: Vec::new(), }; let channel = Rc::new(RefCell::new(channel)); let sender = SyncCacheCommandSender { channel: Rc::clone(&channel), }; - let receiver = SyncCacheCommandReceiver { channel }; + let receiver = SyncCacheCommandReceiver { cache_closed: false, channel }; (sender, receiver) } } @@ -246,6 +254,10 @@ impl SyncCacheCommandSender { .push_back(CachePageCommand { page, height }); } } + + pub fn send_kill_signal(&self) { + self.channel.borrow_mut().kill_signals.push(CacheKillSignal); + } } impl SyncCacheCommandReceiver { @@ -255,6 +267,10 @@ impl SyncCacheCommandReceiver { pub fn receive_most_important_command(&self) -> Option { let mut channel = self.channel.borrow_mut(); + if let Some(command) = channel.kill_signals.pop() { + // self.cache_closed = true; + return None; + } if let Some(command) = channel.priority_cache_commands.pop() { return Some(CacheCommand::Cache(command)); } else if let Some(command) = channel.retrieve_commands.pop() { @@ -266,17 +282,17 @@ impl SyncCacheCommandReceiver { } } -pub fn spawn_sync_cache(document: Document, receiver: F) -> SyncCacheCommandSender +pub fn spawn_sync_cache(document: Document, color_mode:ColorMode,receiver: F) -> SyncCacheCommandSender where F: Fn(CacheResponse) + 'static, { let (command_sender, command_receiver) = SyncCacheCommandChannel::open(); - let mut cache = PageCache::new(document, 30); + let mut cache = PageCache::new(document, 30, color_mode); // Besides the name, it is not in another thread glib::spawn_future_local(async move { - while command_receiver.is_channel_open() { + while command_receiver.is_channel_open() && !command_receiver.cache_closed { // Add delay to tell gtk to give rendering priority timeout_future(Duration::from_millis(1)).await; diff --git a/src/color_mode.rs b/src/color_mode.rs new file mode 100644 index 0000000..45e6359 --- /dev/null +++ b/src/color_mode.rs @@ -0,0 +1,131 @@ +use cairo::ImageSurface; + +#[derive(Clone)] +pub enum ColorMode { + Normal, + Dark, + Sepia, +} + +impl ColorMode { + fn transform_color(&self, r: u8, g: u8, b: u8) -> (u8, u8, u8) { + // l of black is 0.13 (approx 0x22) + match self { + ColorMode::Normal => (r, g, b), + ColorMode::Dark => { + let (mut h, mut s, mut l) = rgb_to_hsl(r, g, b); + l = (1.0 - l) * (1.0 - 0.13) + 0.13; + hsl_to_rgb(h, s, l) + } + ColorMode::Sepia => { + let (mut h, mut s, mut l) = rgb_to_hsl(r, g, b); + h = h + 30.0; + if h < 0.0 { + h += 360.0; + } + s += 0.2; + if s > 1.0 { + s = 1.0; + } + + l = l * (1.0 - 0.13); + hsl_to_rgb(h, s, l) + } + } + } + + /// Apply color mode transformation to an RGB24 surface. + pub fn apply_to_rgb24_surface(&self, surface: &mut ImageSurface) { + let width = surface.width(); + let height = surface.height(); + let stride = surface.stride(); + + let mut data = surface.data().expect("Failed to get source surface data"); + + for y in 0..height { + for x in 0..width { + let offset = (y * stride + x * 4) as usize; // Rgb24: 3 bytes per pixel (R, G, B) + + // Read source pixel + let r = data[offset + 2]; + let g = data[offset + 1]; + let b = data[offset]; + + // Apply color transformation + let (tr, tg, tb) = self.transform_color(r, g, b); + + // Write transformed pixel to destination surface + data[offset + 2] = tr; + data[offset + 1] = tg; + data[offset] = tb; + } + } + } +} + +// h from 0 to 360 +// s from 0 to 1 +// l from 0 to 1 +fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) { + let r = r as f64 / 255.0; + let g = g as f64 / 255.0; + let b = b as f64 / 255.0; + + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let delta = max - min; + + // Calculate Lightness + let l = (max + min) / 2.0; + + // Calculate Saturation + let s = if delta == 0.0 { + 0.0 + } else if l < 0.5 { + delta / (max + min) + } else { + delta / (2.0 - max - min) + }; + + // Calculate Hue + let h = if delta == 0.0 { + 0.0 + } else if max == r { + 60.0 * (((g - b) / delta) % 6.0) + } else if max == g { + 60.0 * (((b - r) / delta) + 2.0) + } else { + 60.0 * (((r - g) / delta) + 4.0) + }; + + let h = if h < 0.0 { h + 360.0 } else { h }; + + (h, s, l) +} + +fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) { + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; // Chroma + let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + + let (r, g, b) = if (0.0..60.0).contains(&h) { + (c, x, 0.0) + } else if (60.0..120.0).contains(&h) { + (x, c, 0.0) + } else if (120.0..180.0).contains(&h) { + (0.0, c, x) + } else if (180.0..240.0).contains(&h) { + (0.0, x, c) + } else if (240.0..300.0).contains(&h) { + (x, 0.0, c) + } else { + (c, 0.0, x) + }; + + // Convert back to [0, 255] + ( + ((r + m) * 255.0).round() as u8, + ((g + m) * 255.0).round() as u8, + ((b + m) * 255.0).round() as u8, + ) +} diff --git a/src/draw.rs b/src/draw.rs index 620da75..c454388 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,12 +1,18 @@ use std::rc::Rc; -use cairo::{Context, ImageSurface}; +use cairo::{Context, ImageSurface, ImageSurfaceData}; use glib::Bytes; use gtk::gdk::Texture; use log::debug; use poppler::Page; -pub fn draw_pages_to_texture(pages: &[Rc], area_height: i32) -> Texture { +use crate::color_mode::ColorMode; + +pub fn draw_pages_to_texture( + pages: &[Rc], + area_height: i32, + color_mode: &ColorMode, +) -> Texture { let area_height = i32::max(100, area_height); let total_width_normalized: f64 = pages .iter() @@ -15,9 +21,13 @@ pub fn draw_pages_to_texture(pages: &[Rc], area_height: i32) -> Texture { .sum(); let area_width = (total_width_normalized * area_height as f64 + 0.5) as i32; - let surface = ImageSurface::create(cairo::Format::Rgb24, area_width, area_height).unwrap(); - let context = Context::new(&surface).unwrap(); - draw_pages(pages, &context, area_width, area_height); + let mut surface = ImageSurface::create(cairo::Format::Rgb24, area_width, area_height).unwrap(); + { + let context = Context::new(&surface).unwrap(); + draw_pages(pages, &context, area_width, area_height); + } // Assure the context gets dropped and the data of the surface can be accessed again + + color_mode.apply_to_rgb24_surface(&mut surface); let mut stream: Vec = Vec::new(); surface.write_to_png(&mut stream).unwrap(); diff --git a/src/main.rs b/src/main.rs index 8782a7a..8adbc26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod cache; +mod color_mode; mod draw; mod ui; @@ -28,7 +29,7 @@ fn main() { app.connect_activate(move |app| { let ui = build_ui(app); if let Some(file) = cli.file.as_ref() { - ui::load_document(file, Rc::clone(&ui)); + ui::load_document(file, Rc::clone(&ui), 0); } }); diff --git a/src/ui.rs b/src/ui.rs index db84d70..2a43a75 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -7,11 +7,14 @@ use std::{ use gtk::{ glib, Application, ApplicationWindow, Box, Button, FileChooserAction, FileChooserDialog, - HeaderBar, Label, Overlay, Picture, ResponseType, + HeaderBar, Label, Overlay, Picture, ResponseType, ToggleButton, }; use log::debug; -use crate::cache::{self, PageNumber, SyncCacheCommandSender}; +use crate::{ + cache::{self, PageNumber, SyncCacheCommandSender}, + color_mode::{self, ColorMode}, +}; use glib::clone; use gtk::prelude::*; @@ -20,6 +23,7 @@ pub struct Ui { bottom_bar: gtk::Box, header_bar: gtk::HeaderBar, page_indicator: gtk::Label, + pub color_mode: ColorMode, pub app_wrapper: Overlay, pub image_container: Box, pub image_left: Picture, @@ -29,14 +33,16 @@ pub struct Ui { } pub struct DocumentCanvas { + pub document_path: PathBuf, pub current_page_number: usize, pub num_pages: Option, page_cache_sender: SyncCacheCommandSender, } impl DocumentCanvas { - pub fn new(page_cache_sender: SyncCacheCommandSender) -> Self { + pub fn new(document_path: PathBuf, page_cache_sender: SyncCacheCommandSender) -> Self { DocumentCanvas { + document_path, current_page_number: 0, num_pages: None, page_cache_sender, @@ -66,6 +72,10 @@ impl DocumentCanvas { ); } + pub fn purge_cache(&self) { + self.page_cache_sender.send_kill_signal(); + } + pub fn cache_surrounding_pages(&self, area_height: i32) { self.page_cache_sender.send_cache_commands( &[ @@ -206,6 +216,31 @@ impl Ui { pub fn build(app: &Application) -> Rc> { debug!("building ui"); let open_file_button = Button::from_icon_name("document-open"); + let normal_color_mode_button = ToggleButton::builder().label("Std").active(true).build(); + let dark_color_mode_button = ToggleButton::builder() + .label("Dark") + .group(&normal_color_mode_button) + .build(); + let sepia_color_mode_button = ToggleButton::builder() + .label("Sepia") + .group(&normal_color_mode_button) + .build(); + + let button_container = Box::builder() + .spacing(5) + .hexpand(true) + .orientation(gtk::Orientation::Horizontal) + .build(); + let color_mode_button_container = Box::builder() + .spacing(0) + .hexpand(true) + .orientation(gtk::Orientation::Horizontal) + .build(); + color_mode_button_container.append(&normal_color_mode_button); + color_mode_button_container.append(&dark_color_mode_button); + color_mode_button_container.append(&sepia_color_mode_button); + button_container.append(&open_file_button); + button_container.append(&color_mode_button_container); let image_container = Box::builder() .spacing(0) @@ -260,10 +295,11 @@ impl Ui { image_right, document_canvas: None, last_touch_time: None, + color_mode: ColorMode::Normal, }; let ui = Rc::new(RefCell::new(ui)); - ui.borrow().header_bar.pack_start(&open_file_button); + ui.borrow().header_bar.pack_start(&button_container); ui.borrow().app_wrapper.add_overlay(&ui.borrow().bottom_bar); ui.borrow().bottom_bar.append(&ui.borrow().page_indicator); @@ -291,12 +327,53 @@ impl Ui { choose_file(Rc::clone(&ui), &ui.borrow().window); }), ); + normal_color_mode_button.connect_clicked( + glib::clone!(@strong ui => @default-panic, move |_button| { + switch_color_mode(Rc::clone(&ui), ColorMode::Normal); + }), + ); + dark_color_mode_button.connect_clicked( + glib::clone!(@strong ui => @default-panic, move |_button| { + switch_color_mode(Rc::clone(&ui), ColorMode::Dark); + }), + ); + sepia_color_mode_button.connect_clicked( + glib::clone!(@strong ui => @default-panic, move |_button| { + switch_color_mode(Rc::clone(&ui), ColorMode::Sepia); + }), + ); ui.borrow().window.present(); ui } } +fn switch_color_mode(ui: Rc>, color_mode: ColorMode) { + ui.borrow_mut().color_mode = color_mode; + + if ui.borrow().document_canvas.is_none() { + return; + } + + ui.borrow().document_canvas.as_ref().unwrap().purge_cache(); + + let path = ui + .borrow() + .document_canvas + .as_ref() + .unwrap() + .document_path + .clone(); + let current_page_number = ui + .borrow() + .document_canvas + .as_ref() + .unwrap() + .current_page_number; + + load_document(path, Rc::clone(&ui), current_page_number); +} + fn choose_file(ui: Rc>, window: &ApplicationWindow) { let filechooser = FileChooserDialog::builder() .title("Choose a PDF...") @@ -309,23 +386,28 @@ fn choose_file(ui: Rc>, window: &ApplicationWindow) { filechooser.connect_response(move |d, response| { if response == ResponseType::Accept { let path = d.file().unwrap().path().unwrap(); - load_document(path, Rc::clone(&ui)); + + ui.borrow().document_canvas.as_ref().unwrap().purge_cache(); + + load_document(path, Rc::clone(&ui), 0); } d.destroy(); }); filechooser.show() } -pub fn load_document(file: impl AsRef, ui: Rc>) { +pub fn load_document(file: impl AsRef, ui: Rc>, initial_page_number: usize) { debug!("Loading file..."); // TODO: catch errors, maybe show error dialog let path: PathBuf = file.as_ref().to_path_buf(); let uri = format!("file://{}", path.to_str().unwrap()); let document = poppler::Document::from_file(&uri, None).unwrap(); let num_pages = document.n_pages() as usize; + let color_mode = ui.borrow().color_mode.clone(); let sender = cache::spawn_sync_cache( document, + color_mode, clone!(@weak ui => move |cache_response| match cache_response { cache::CacheResponse::SinglePageRetrieved { page } => { ui.borrow_mut().image_left.set_paintable(Some(page.as_ref())); @@ -360,8 +442,9 @@ pub fn load_document(file: impl AsRef, ui: Rc>) { }), ); - let mut document_canvas = DocumentCanvas::new(sender); + let mut document_canvas = DocumentCanvas::new(path, sender); document_canvas.num_pages = Some(num_pages); + document_canvas.current_page_number = initial_page_number; document_canvas.cache_initial_pages(ui.borrow().image_container.height()); ui.borrow_mut().document_canvas = Some(document_canvas);