Add color mode feature, rendering e.g. in dark mode

This commit is contained in:
Julian Mutter 2024-11-27 15:48:58 +01:00
parent c8795588d8
commit b5239e14b6
5 changed files with 261 additions and 20 deletions

View File

@ -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<usize, Rc<MyPageType>>,
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<RetrievePagesCommand>,
cache_commands: VecDeque<CachePageCommand>,
priority_cache_commands: Vec<CachePageCommand>,
kill_signals : Vec<CacheKillSignal>
}
pub struct SyncCacheCommandSender {
@ -193,6 +199,7 @@ pub struct SyncCacheCommandSender {
}
pub struct SyncCacheCommandReceiver {
cache_closed : bool,
channel: Rc<RefCell<SyncCacheCommandChannel>>,
}
@ -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<CacheCommand> {
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<F>(document: Document, receiver: F) -> SyncCacheCommandSender
pub fn spawn_sync_cache<F>(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;

131
src/color_mode.rs Normal file
View File

@ -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,
)
}

View File

@ -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<Page>], area_height: i32) -> Texture {
use crate::color_mode::ColorMode;
pub fn draw_pages_to_texture(
pages: &[Rc<Page>],
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<Page>], 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<u8> = Vec::new();
surface.write_to_png(&mut stream).unwrap();

View File

@ -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);
}
});

View File

@ -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<usize>,
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<RefCell<Ui>> {
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<RefCell<Ui>>, 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<RefCell<Ui>>, window: &ApplicationWindow) {
let filechooser = FileChooserDialog::builder()
.title("Choose a PDF...")
@ -309,23 +386,28 @@ fn choose_file(ui: Rc<RefCell<Ui>>, 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<Path>, ui: Rc<RefCell<Ui>>) {
pub fn load_document(file: impl AsRef<Path>, ui: Rc<RefCell<Ui>>, 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<Path>, ui: Rc<RefCell<Ui>>) {
}),
);
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);