Custom Colors (#70)

* First implementation of custom colors

* Remove use-statement

Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com>

* Cleaned up help-text

Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com>

* Removed superfluous features from csscolorparser

* Fix for clippy

* Clarify how to pass in custom colors

* Explicitly install clippy and rustfmt in CI

* Better error handling when colors can not be parsed

Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com>

* More elegant type conversion

Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com>

* Made clippy happy

---------

Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com>
Co-authored-by: itsjunetime <junewelker@gmail.com>
This commit is contained in:
JanNeuendorf
2025-05-28 19:16:07 +02:00
committed by GitHub
parent 2f4e2a54bc
commit e16163efb8
8 changed files with 76 additions and 20 deletions
+2
View File
@@ -28,6 +28,8 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libfontconfig1-dev libgoogle-perftools-dev google-perftools sudo apt-get install -y libfontconfig1-dev libgoogle-perftools-dev google-perftools
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install clippy and fmt
run: rustup component add clippy rustfmt
- name: Clippy - name: Clippy
run: cargo clippy -- -D warnings run: cargo clippy -- -D warnings
- name: Check fmt - name: Check fmt
Generated
+11 -1
View File
@@ -711,6 +711,15 @@ dependencies = [
"phf", "phf",
] ]
[[package]]
name = "csscolorparser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f9a16a848a7fb95dd47ce387ac1ee9a6df879ba784b815537fcd388a1a8288"
dependencies = [
"phf",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.11" version = "0.20.11"
@@ -2866,6 +2875,7 @@ dependencies = [
"cpuprofiler", "cpuprofiler",
"criterion", "criterion",
"crossterm", "crossterm",
"csscolorparser 0.7.0",
"flume", "flume",
"futures-util", "futures-util",
"image", "image",
@@ -3473,7 +3483,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296"
dependencies = [ dependencies = [
"csscolorparser", "csscolorparser 0.6.2",
"deltae", "deltae",
"lazy_static", "lazy_static",
"wezterm-dynamic", "wezterm-dynamic",
+1
View File
@@ -43,6 +43,7 @@ rayon = { version = "*", default-features = false }
# for tracing with tokio-console # for tracing with tokio-console
console-subscriber = { version = "0.4.0", optional = true } console-subscriber = { version = "0.4.0", optional = true }
csscolorparser = { version = "0.7.0" }
[profile.production] [profile.production]
inherits = "release" inherits = "release"
+1 -1
View File
@@ -9,5 +9,5 @@ async fn main() {
.nth(1) .nth(1)
.expect("Please enter a file to profile"); .expect("Please enter a file to profile");
utils::render_doc(file, None).await; utils::render_doc(file, None, 0, 1000).await;
} }
+12 -9
View File
@@ -23,11 +23,14 @@ const FILES: [&str; 3] = [
"benches/geotopo.pdf" "benches/geotopo.pdf"
]; ];
const BLACK: i32 = 0;
const WHITE: i32 = 1000;
fn render_full(c: &mut Criterion) { fn render_full(c: &mut Criterion) {
for file in FILES { for file in FILES {
c.bench_with_input(BenchmarkId::new("render_full", file), &file, |b, &file| { c.bench_with_input(BenchmarkId::new("render_full", file), &file, |b, &file| {
b.to_async(tokio::runtime::Runtime::new().unwrap()) b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| render_doc(file, None)) .iter(|| render_doc(file, None, BLACK, WHITE))
}); });
} }
} }
@@ -39,7 +42,7 @@ fn render_to_first_page(c: &mut Criterion) {
&file, &file,
|b, &file| { |b, &file| {
b.to_async(tokio::runtime::Runtime::new().unwrap()) b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| render_first_page(file)) .iter(|| render_first_page(file, BLACK, WHITE))
} }
); );
} }
@@ -48,7 +51,7 @@ fn render_to_first_page(c: &mut Criterion) {
fn only_converting(c: &mut Criterion) { fn only_converting(c: &mut Criterion) {
for file in FILES { for file in FILES {
let runtime = tokio::runtime::Runtime::new().unwrap(); let runtime = tokio::runtime::Runtime::new().unwrap();
let all_rendered = runtime.block_on(render_all_files(file)); let all_rendered = runtime.block_on(render_all_files(file, BLACK, WHITE));
c.bench_with_input( c.bench_with_input(
BenchmarkId::new("only_converting", file), BenchmarkId::new("only_converting", file),
@@ -68,7 +71,7 @@ fn search_short_common(c: &mut Criterion) {
&file, &file,
|b, &file| { |b, &file| {
b.to_async(tokio::runtime::Runtime::new().unwrap()) b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| render_doc(file, Some("an"))) .iter(|| render_doc(file, Some("an"), BLACK, WHITE))
} }
); );
} }
@@ -81,20 +84,20 @@ fn search_long_rare(c: &mut Criterion) {
&file, &file,
|b, &file| { |b, &file| {
b.to_async(tokio::runtime::Runtime::new().unwrap()) b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| render_doc(file, Some("this is long and rare"))) .iter(|| render_doc(file, Some("this is long and rare"), BLACK, WHITE))
} }
); );
} }
} }
pub async fn render_first_page(path: impl AsRef<Path>) { pub async fn render_first_page(path: impl AsRef<Path>, black: i32, white: i32) {
let RenderState { let RenderState {
mut from_render_rx, mut from_render_rx,
mut from_converter_rx, mut from_converter_rx,
mut pages, mut pages,
mut to_converter_tx, mut to_converter_tx,
to_render_tx to_render_tx
} = start_all_rendering(path); } = start_all_rendering(path, black, white);
// we only want to render until the first page is ready to be printed // we only want to render until the first page is ready to be printed
while pages.iter().all(Option::is_none) { while pages.iter().all(Option::is_none) {
@@ -114,8 +117,8 @@ pub async fn render_first_page(path: impl AsRef<Path>) {
drop(to_render_tx); drop(to_render_tx);
} }
async fn render_all_files(path: &'static str) -> Vec<PageInfo> { async fn render_all_files(path: &'static str, black: i32, white: i32) -> Vec<PageInfo> {
let (mut from_render_rx, to_render_tx) = start_rendering_loop(path); let (mut from_render_rx, to_render_tx) = start_rendering_loop(path, black, white);
let mut pages = Vec::<Option<PageInfo>>::new(); let mut pages = Vec::<Option<PageInfo>>::new();
while let Some(info) = from_render_rx.next().await { while let Some(info) = from_render_rx.next().await {
+10 -6
View File
@@ -57,7 +57,9 @@ pub struct RenderState {
const FONT_SIZE: (u16, u16) = (8, 14); const FONT_SIZE: (u16, u16) = (8, 14);
pub fn start_rendering_loop( pub fn start_rendering_loop(
path: impl AsRef<Path> path: impl AsRef<Path>,
black: i32,
white: i32
) -> ( ) -> (
RecvStream<'static, Result<RenderInfo, RenderError>>, RecvStream<'static, Result<RenderInfo, RenderError>>,
Sender<RenderNotif> Sender<RenderNotif>
@@ -91,7 +93,9 @@ pub fn start_rendering_loop(
to_main_tx, to_main_tx,
from_main_rx, from_main_rx,
size, size,
tdf::PrerenderLimit::All tdf::PrerenderLimit::All,
black,
white
) )
}); });
@@ -122,8 +126,8 @@ pub fn start_converting_loop(
(from_converter_rx, to_converter_tx) (from_converter_rx, to_converter_tx)
} }
pub fn start_all_rendering(path: impl AsRef<Path>) -> RenderState { pub fn start_all_rendering(path: impl AsRef<Path>, black: i32, white: i32) -> RenderState {
let (from_render_rx, to_render_tx) = start_rendering_loop(path); let (from_render_rx, to_render_tx) = start_rendering_loop(path, black, white);
let (from_converter_rx, to_converter_tx) = start_converting_loop(20); let (from_converter_rx, to_converter_tx) = start_converting_loop(20);
let pages: Vec<Option<ConvertedPage>> = Vec::new(); let pages: Vec<Option<ConvertedPage>> = Vec::new();
@@ -137,14 +141,14 @@ pub fn start_all_rendering(path: impl AsRef<Path>) -> RenderState {
} }
} }
pub async fn render_doc(path: impl AsRef<Path>, search_term: Option<&str>) { pub async fn render_doc(path: impl AsRef<Path>, search_term: Option<&str>, black: i32, white: i32) {
let RenderState { let RenderState {
mut from_render_rx, mut from_render_rx,
mut from_converter_rx, mut from_converter_rx,
mut pages, mut pages,
mut to_converter_tx, mut to_converter_tx,
to_render_tx to_render_tx
} = start_all_rendering(path); } = start_all_rendering(path, black, white);
if let Some(term) = search_term { if let Some(term) = search_term {
to_render_tx to_render_tx
+29 -1
View File
@@ -51,11 +51,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// The number of pages to prerender surrounding the currently-shown page; 0 means no /// The number of pages to prerender surrounding the currently-shown page; 0 means no
/// limit. By default, there is no limit. /// limit. By default, there is no limit.
optional -p,--prerender prerender: usize optional -p,--prerender prerender: usize
/// Custom white color, specified in css format (e.g. "FFFFFF" or "rgb(255, 255, 255)")
optional -w,--white-color white: String
/// Custom black color, specified in css format (e.g "000000" or "rgb(0, 0, 0)")
optional -b,--black-color black: String
/// PDF file to read /// PDF file to read
required file: PathBuf required file: PathBuf
}; };
let path = flags.file.canonicalize()?; let path = flags.file.canonicalize()?;
let black = parse_color_to_i32(&flags.black_color.unwrap_or("000000".into())).map_err(|e| {
BadTermSizeStdin(format!(
"Couldn't parse black color: {e} - is it formatted like a CSS color?"
))
})?;
let white = parse_color_to_i32(&flags.white_color.unwrap_or("FFFFFF".into())).map_err(|e| {
BadTermSizeStdin(format!(
"Couldn't parse while color: {e} - is it formatted like a CSS color?"
))
})?;
let (watch_to_render_tx, render_rx) = flume::unbounded(); let (watch_to_render_tx, render_rx) = flume::unbounded();
let tui_tx = watch_to_render_tx.clone(); let tui_tx = watch_to_render_tx.clone();
@@ -141,7 +156,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.and_then(NonZeroUsize::new) .and_then(NonZeroUsize::new)
.map_or(PrerenderLimit::All, PrerenderLimit::Limited); .map_or(PrerenderLimit::All, PrerenderLimit::Limited);
std::thread::spawn(move || { std::thread::spawn(move || {
renderer::start_rendering(&file_path, render_tx, render_rx, window_size, prerender) renderer::start_rendering(
&file_path,
render_tx,
render_rx,
window_size,
prerender,
black,
white
)
}); });
let mut ev_stream = crossterm::event::EventStream::new(); let mut ev_stream = crossterm::event::EventStream::new();
@@ -286,3 +309,8 @@ fn on_notify_ev(
} }
} }
} }
fn parse_color_to_i32(cs: &str) -> Result<i32, csscolorparser::ParseColorError> {
let color = csscolorparser::parse(cs)?;
let [r, g, b, _] = color.to_rgba8();
Ok(i32::from_be_bytes([0, r, g, b]))
}
+10 -2
View File
@@ -76,7 +76,9 @@ pub fn start_rendering(
sender: Sender<Result<RenderInfo, RenderError>>, sender: Sender<Result<RenderInfo, RenderError>>,
receiver: Receiver<RenderNotif>, receiver: Receiver<RenderNotif>,
size: WindowSize, size: WindowSize,
prerender: PrerenderLimit prerender: PrerenderLimit,
black: i32,
white: i32
) -> Result<(), SendError<Result<RenderInfo, RenderError>>> { ) -> Result<(), SendError<Result<RenderInfo, RenderError>>> {
// We want this outside of 'reload so that if the doc reloads, the search term that somebody // We want this outside of 'reload so that if the doc reloads, the search term that somebody
// set will still get highlighted in the reloaded doc // set will still get highlighted in the reloaded doc
@@ -282,6 +284,8 @@ pub fn start_rendering(
search_term.as_deref(), search_term.as_deref(),
rendered, rendered,
invert, invert,
black,
white,
(area_w, area_h) (area_w, area_h)
) { ) {
// If that fn returned Some, that means it needed to be re-rendered for some // If that fn returned Some, that means it needed to be re-rendered for some
@@ -421,6 +425,8 @@ fn render_single_page_to_ctx(
search_term: Option<&str>, search_term: Option<&str>,
prev_render: &PrevRender, prev_render: &PrevRender,
invert: bool, invert: bool,
black: i32,
white: i32,
(area_w, area_h): (f32, f32) (area_w, area_h): (f32, f32)
) -> Result<RenderedContext, mupdf::error::Error> { ) -> Result<RenderedContext, mupdf::error::Error> {
let result_rects = match prev_render.num_search_found { let result_rects = match prev_render.num_search_found {
@@ -461,7 +467,9 @@ fn render_single_page_to_ctx(
let mut pixmap = page.to_pixmap(&matrix, &colorspace, false, false)?; let mut pixmap = page.to_pixmap(&matrix, &colorspace, false, false)?;
if invert { if invert {
pixmap.invert()?; pixmap.tint(white, black)?;
} else {
pixmap.tint(black, white)?;
} }
let (x_res, y_res) = pixmap.resolution(); let (x_res, y_res) = pixmap.resolution();