Add color mode feature, rendering e.g. in dark mode
This commit is contained in:
parent
c8795588d8
commit
b5239e14b6
30
src/cache.rs
30
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<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
131
src/color_mode.rs
Normal 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,
|
||||
)
|
||||
}
|
20
src/draw.rs
20
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<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();
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
97
src/ui.rs
97
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<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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user