- Commit
- 50d04f8709483d0fd21ad90a06532a720e44ec3f
- Parent
- 04d70d11e8ddf12551f6b736fdcc31c7e69db199
- Author
- Pablo <pablo-pie@riseup.net>
- Date
Integrated the MuPDF rendering code into the application
Custum build of stapix for tikz.pablopie.xyz
Integrated the MuPDF rendering code into the application
8 files changed, 512 insertions, 240 deletions
| Status | Name | Changes | Insertions | Deletions |
| Modified | Cargo.lock | 2 files changed | 159 | 22 |
| Modified | Cargo.toml | 2 files changed | 1 | 2 |
| Modified | mupdf-sys/src/lib.rs | 2 files changed | 0 | 22 |
| Modified | mupdf-sys/src/wrapper.c | 2 files changed | 25 | 25 |
| Modified | src/image.rs | 2 files changed | 2 | 2 |
| Modified | src/log.rs | 2 files changed | 2 | 2 |
| Modified | src/main.rs | 2 files changed | 212 | 165 |
| Added | src/mupdf.rs | 1 file changed | 111 | 0 |
diff --git a/Cargo.lock b/Cargo.lock @@ -3,6 +3,39 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -13,6 +46,38 @@ dependencies = [ ] [[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -31,12 +96,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] name = "indexmap" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -47,6 +106,15 @@ dependencies = [ ] [[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -68,6 +136,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] name = "libwebp-sys" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -79,13 +157,35 @@ dependencies = [ ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mupdf-sys" +version = "1.0.0" dependencies = [ - "hermit-abi", - "libc", + "bindgen", + "cc", + "pkg-config", + "regex", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] @@ -113,6 +213,41 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -152,6 +287,12 @@ dependencies = [ ] [[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -169,23 +310,13 @@ dependencies = [ ] [[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] name = "tikz_gallery_generator" version = "1.0.0" dependencies = [ "libwebp-sys", - "num_cpus", + "mupdf-sys", "serde", "serde_yaml", - "threadpool", "zune-core", "zune-jpeg", "zune-png", @@ -204,6 +335,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] name = "zune-core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml @@ -13,8 +13,7 @@ libwebp-sys = "0.14" zune-core = "0.5" zune-jpeg = "0.5" zune-png = "0.5" -threadpool = "1.8.1" -num_cpus = "1.16.0" +mupdf-sys = { path = "./mupdf-sys/" } [profile.release] debug = true
diff --git a/mupdf-sys/src/lib.rs b/mupdf-sys/src/lib.rs @@ -15,25 +15,3 @@ pub unsafe fn fz_new_context( let version = FZ_VERSION.as_ptr() as *const i8; fz_new_context_imp(alloc, locks, max_store, version) } - -/// Wrapper for [`fz_new_context`]. -pub unsafe fn mupdf_new_context() -> *mut fz_context { - use core::ptr; - - let ctx = fz_new_context( - ptr::null(), - ptr::null(), - FZ_STORE_DEFAULT as usize, - ); - if ctx.is_null() { return ctx; } - - // SAFETY: this should really be wrapped with fz_try, but it can only fail if - // ctx does not contain a document handler list (?) or the list is - // already full. seems safe to assume fz_new_documment will handle - // us a sane ctx - fz_register_document_handlers(ctx); - - fz_set_warning_callback(ctx, None, ptr::null_mut()); - fz_set_error_callback(ctx, None, ptr::null_mut()); - ctx -}
diff --git a/mupdf-sys/src/wrapper.c b/mupdf-sys/src/wrapper.c @@ -9,14 +9,13 @@ typedef struct { enum fz_error_type type; } mupdf_result_t; -fz_document* mupdf_open_doc_from_bytes(fz_context* ctx, - uint8_t* bytes, size_t size, - mupdf_result_t* result) +mupdf_result_t mupdf_open_doc_from_bytes( + fz_context* ctx, + uint8_t* bytes, size_t size, + fz_document** doc, uint32_t* page_count) { - fz_document* doc = NULL; - fz_stream* stream = NULL; - - memset(result, 0, sizeof *result); + mupdf_result_t result = {0}; + fz_stream* stream = NULL; fz_try(ctx) { @@ -26,7 +25,10 @@ fz_document* mupdf_open_doc_from_bytes(fz_context* ctx, buf.len = size; stream = fz_open_buffer(ctx, &buf); - doc = (fz_document*)pdf_open_document_with_stream(ctx, stream); + *doc = (fz_document*)pdf_open_document_with_stream(ctx, stream); + if (*doc == NULL) break; + + *page_count = fz_count_pages(ctx, *doc); } fz_always(ctx) { @@ -34,39 +36,38 @@ fz_document* mupdf_open_doc_from_bytes(fz_context* ctx, } fz_catch(ctx) { - result->err_msg = fz_caught_message(ctx); - result->type = fz_caught(ctx); - doc = NULL; + result.err_msg = fz_caught_message(ctx); + result.type = fz_caught(ctx); } - return doc; + return result; } -fz_buffer* mupdf_page_to_svg(fz_context* ctx, fz_document* doc, - size_t page_number, mupdf_result_t* result) +mupdf_result_t mupdf_page_to_svg(fz_context* ctx, fz_document* doc, + size_t page_number, fz_buffer** buf) { - fz_buffer* buf = NULL; + mupdf_result_t result = {0}; fz_page* page = NULL; fz_output* out = NULL; fz_device* dev = NULL; - memset(result, 0, sizeof *result); - fz_try(ctx) { - // TODO: assert page != NULL // TODO: page bound checks when running in debug mode? page = fz_load_page(ctx, doc, page_number); + if (page == NULL) break; // skip to fz_always fz_rect bbox = fz_bound_page(ctx, page); float width = bbox.x1 - bbox.x0, height = bbox.y1 - bbox.y0; - buf = fz_new_buffer(ctx, 1024); // TODO: assert buf != NULL - out = fz_new_output_with_buffer(ctx, buf); // TODO: assert out != NULL + *buf = fz_new_buffer(ctx, 1024); + if (*buf == NULL) break; // skip to fz_always + out = fz_new_output_with_buffer(ctx, *buf); + if (out == NULL) break; // skip to fz_always - // TODO: assert dev != NULL dev = fz_new_svg_device(ctx, out, width, height, FZ_SVG_TEXT_AS_PATH, 0); + if (dev == NULL) break; // skip to fz_always fz_run_page(ctx, page, dev, fz_identity, NULL); fz_close_device(ctx, dev); @@ -80,10 +81,9 @@ fz_buffer* mupdf_page_to_svg(fz_context* ctx, fz_document* doc, } fz_catch(ctx) { - result->err_msg = fz_caught_message(ctx); - result->type = fz_caught(ctx); - buf = NULL; + result.err_msg = fz_caught_message(ctx); + result.type = fz_caught(ctx); } - return buf; + return result; }
diff --git a/src/image.rs b/src/image.rs @@ -43,8 +43,8 @@ use crate::create_file; #[derive(Debug)] pub struct Image { - width: usize, - height: usize, + width: usize, + height: usize, pixels: PixelBuffer, }
diff --git a/src/log.rs b/src/log.rs @@ -77,8 +77,8 @@ pub fn version(program_name: &str) { } pub fn usage(program: &str) { - use crate::{FULL_BUILD_OPT, N_THREADS_OPT}; - println!(" {BOLD_YELLOW}Usage{RESET} {program} [{FULL_BUILD_OPT}] [{N_THREADS_OPT} <jobs>] <config.yml>"); + use crate::FULL_BUILD_OPT; + println!(" {BOLD_YELLOW}Usage{RESET} {program} [{FULL_BUILD_OPT}] <config.yml>"); } pub fn usage_config() {
diff --git a/src/main.rs b/src/main.rs @@ -1,15 +1,12 @@ use std::{ - cmp, env, fmt::{self, Display}, fs::{self, File}, - io::{self, Write}, + io::{self, Read, Write}, path::{Path, PathBuf}, - process::{ExitCode, Command}, - sync::mpsc, + process::ExitCode, time::Instant, }; -use threadpool::ThreadPool; use gallery_entry::{GalleryEntry, FileFormat, LicenseType}; use escape::Escaped; @@ -17,9 +14,16 @@ use escape::Escaped; #[macro_use] mod log; mod image; +mod mupdf; mod gallery_entry; mod escape; +struct RenderingJob<'a> { + pub path: &'a Path, + pub file_name: &'a str, + pub thumb_path: PathBuf, +} + /// A wrapper for displaying the path for the thumbnail of a given path pub struct ThumbPath<'a>(pub &'a GalleryEntry); @@ -27,8 +31,6 @@ pub struct ThumbPath<'a>(pub &'a GalleryEntry); pub struct HtmlFileName<'a>(pub &'a str); const FULL_BUILD_OPT: &str = "-B"; -const N_THREADS_OPT: &str = "-j"; -const BOTH_OPTS: &str = "-Bj"; const TARGET_PATH: &str = "./site"; const PAGES_PATH: &str = "figures"; @@ -56,39 +58,10 @@ fn main() -> ExitCode { }; let mut full_build = false; - let total_cores = num_cpus::get(); - let mut num_cores = total_cores - 1; - while let Some(arg) = args.next() { - let mut is_valid_arg = false; - - if arg == FULL_BUILD_OPT || arg == BOTH_OPTS { + for arg in args { + if arg == FULL_BUILD_OPT { full_build = true; - is_valid_arg = true; - } - - if arg == N_THREADS_OPT || arg == BOTH_OPTS { - is_valid_arg = true; - - let val = match args.next() { - Some(val) => val, - None => { - errorln!("Expected one more argument, got none"); - log::usage(&program); - return ExitCode::FAILURE; - } - }; - - match val.parse() { - Ok(val) => num_cores = val, - Err(_) => { - errorln!("Expected a number, got {val:?}"); - log::usage(&program); - return ExitCode::FAILURE; - } - } - } - - if !is_valid_arg { + } else { if arg.starts_with("-") { errorln!("Unknown option: {arg:?}"); } else { @@ -111,7 +84,7 @@ fn main() -> ExitCode { log::usage_config(); return ExitCode::FAILURE; } - Ok(Ok(pics)) => if render_gallery(pics, full_build, num_cores, total_cores).is_err() { + Ok(Ok(pics)) => if render_gallery(pics, full_build).is_err() { return ExitCode::FAILURE; }, } @@ -123,18 +96,20 @@ fn main() -> ExitCode { fn render_gallery( pics: Vec<GalleryEntry>, full_build: bool, - num_cores: usize, - total_cores: usize, ) -> Result<(), ()> { struct Job { pic_id: usize, image_path: PathBuf, - thumb_path: PathBuf, page_path: PathBuf, } let start = Instant::now(); + let mut tex_jobs = Vec::with_capacity(pics.len()); + let mut svg_jobs = Vec::new(); + let mut png_jobs = Vec::new(); + let mut jpeg_jobs = Vec::new(); + let mut skipped = 0; let mut jobs = Vec::with_capacity(pics.len()); for (pic_id, pic) in pics.iter().enumerate() { @@ -142,8 +117,6 @@ fn render_gallery( image_path.push(IMAGES_PATH); image_path.push(&pic.file_name); - let thumb_path: PathBuf = ThumbPath(pic).into(); - let mut page_path = PathBuf::from(TARGET_PATH); page_path.push(PAGES_PATH); page_path.push(format!("{}", HtmlFileName(&pic.file_name))); @@ -156,9 +129,39 @@ fn render_gallery( } if full_build || needs_update(pic, &image_path) - || needs_update(pic, &thumb_path) || needs_update(pic, &page_path) { - jobs.push(Job { pic_id, image_path, thumb_path, page_path, }); + jobs.push(Job { pic_id, image_path, page_path, }); + + match pic.file_format { + FileFormat::TeX => { + tex_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}.svg", pic.file_name).into(), + }); + } + FileFormat::Svg => { + svg_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}", pic.file_name).into(), + }); + } + FileFormat::Png => { + png_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}.webp", pic.file_name).into(), + }); + } + FileFormat::Jpeg => { + jpeg_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}.webp", pic.file_name).into(), + }); + } + } } else { skipped += 1; } @@ -196,50 +199,30 @@ fn render_gallery( infoln!("Copied image files to the target directory"); // ======================================================================== - let num_cores = cmp::min(num_cores, jobs.len()); - - // NOTE: only spawn the threads if necessary - if num_cores > 1 { - infoln!("Rendering thumbnails... (using {num_cores}/{total_cores} cores)"); - let rendering_pool = ThreadPool::with_name( - String::from("thumbnails renderer"), - num_cores, - ); - let (sender, reciever) = mpsc::channel(); - - for Job { pic_id, thumb_path, .. } in &jobs { - let pic_id = *pic_id; - let thumb_path = thumb_path.clone(); - let pic = pics[pic_id].clone(); - let sender = sender.clone(); - - rendering_pool.execute(move || { - // NOTE: we need to send the picture id back so that the main thread - // knows how to log the fact we finished rendering it - let _ = sender.send( - render_thumbnail(&pic, &thumb_path).map(|()| pic_id) - ); - }); - } + infoln!("Rendering thumbnails..."); - for _ in 0..jobs.len() { - let msg = reciever.recv(); - // propagate the panic to the main thread: reciever.recv should - // only fail if some of the rendering threads panicked - if msg.is_err() { panic!("rendering thread panicked!"); } + // TODO: log something while compiling the TeX? + render_tikz_thumbnails(&tex_jobs)?; - let pic_id = msg.unwrap()?; - let pic = &pics[pic_id]; - log::job_finished(&pic.file_name); - } - } else { - infoln!("Rendering thumbnails... (using 1/{total_cores} core)"); - for Job { pic_id, thumb_path, .. } in &jobs { - let pic = &pics[*pic_id]; + for pic in svg_jobs { + let mut src_path = PathBuf::from(TARGET_PATH); + src_path.push(IMAGES_PATH); + src_path.push(pic.file_name); - render_thumbnail(pic, thumb_path)?; - log::job_finished(&pic.file_name); - } + copy(&src_path, &pic.thumb_path)?; + log::job_finished(&pic.file_name); + } + + const TARGET_HEIGHT: usize = 500; + for pic in png_jobs { + let img = image::parse_png(pic.path, TARGET_HEIGHT)?; + image::encode_webp(&img, &pic.thumb_path)?; + log::job_finished(&pic.file_name); + } + for pic in jpeg_jobs { + let img = image::parse_jpeg(pic.path, TARGET_HEIGHT)?; + image::encode_webp(&img, &pic.thumb_path)?; + log::job_finished(&pic.file_name); } // ========================================================================== @@ -433,67 +416,133 @@ fn write_license(f: &mut File) -> io::Result<()> { writeln!(f, "{}", LICENSE_COMMENT) } -fn render_thumbnail(pic: &GalleryEntry, thumb_path: &Path) -> Result<(), ()> { - const TARGET_HEIGHT: usize = 500; +fn render_tikz_thumbnails(pics: &[RenderingJob<'_>]) -> Result<(), ()> { + let mut tmp_dir = env::temp_dir(); + tmp_dir.push(random_dir_name()); - match pic.file_format { - FileFormat::TeX => { - // tikztosvg -o thumb_path - // -p relsize - // -p xfrac - // -l matrix - // -l patterns - // -l shapes.geometric - // -l arrows - // -q - // pic.path - let mut tikztosvg_cmd = Command::new("tikztosvg"); - tikztosvg_cmd.arg("-o") - .arg(thumb_path) - .args([ - "-p", "relsize", - "-p", "xfrac", - "-l", "matrix", - "-l", "patterns", - "-l", "shapes.geometric", - "-l", "arrows", - "-q", - ]) - .arg(&pic.path); - - let exit_code = tikztosvg_cmd - .status() - .map_err(|e| errorln!("Failed to run tikztosvg: {e}"))?; - - if !exit_code.success() { - errorln!( - "Failed to run tikztosvg: {tikztosvg_cmd:?} returned exit code {exit_code}" - ); - return Err(()); - } - } - FileFormat::Svg => { - let mut src_path = PathBuf::from(TARGET_PATH); - src_path.push(IMAGES_PATH); - src_path.push(&pic.file_name); + if let Err(e) = fs::create_dir(&tmp_dir) { + errorln!("Could not create {tmp_dir:?}: {e}"); + return Err(()); + } + + let result = render_impl(pics, &tmp_dir); + if let Err(e) = fs::remove_dir_all(&tmp_dir) { + errorln!("Could not delete {tmp_dir:?}: {e}"); + return Err(()); + } + + return result; - copy(&src_path, thumb_path)?; + fn render_impl(pics: &[RenderingJob<'_>], tmp_dir: &Path) -> Result<(), ()> { + use std::process::{Command, Stdio}; + + const TEX_ENGINE: &str = "lualatex"; + const TEX_FILE_PATH: &str = "drawings.tex"; + const PDF_FILE_PATH: &str = "drawings.pdf"; + + // ======================================================================== + let mut tex_path = PathBuf::from(tmp_dir); + tex_path.push(TEX_FILE_PATH); + + let mut tex_f = create_file(&tex_path).map_err(|_| ())?; + + let tex_contents = generate_tex_contents(pics)?; + if let Err(e) = tex_f.write_all(tex_contents.as_bytes()) { + errorln!("Could not write to {tex_path:?}: {e}"); + return Err(()); } - FileFormat::Jpeg => { - // NOTE: even if the picture is no taller than TARGET_HEIGHT * 2, it is - // faster to downsample and then encode - let img = image::parse_jpeg(&pic.path, TARGET_HEIGHT)?; - image::encode_webp(&img, thumb_path)?; + drop(tex_f); + + let mut tex_cmd = Command::new(TEX_ENGINE); + tex_cmd + .arg("-halt-on-error") + .arg(format!("-output-directory={}", tmp_dir.to_string_lossy())) + .arg(&tex_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + let exit_code = tex_cmd + .status() + .map_err(|e| errorln!("Failed to run {TEX_ENGINE}: {e}"))?; + + if !exit_code.success() { + errorln!( + "Failed to run {TEX_ENGINE}: {tex_cmd:?} returned exit code {exit_code}" + ); + return Err(()); } - FileFormat::Png => { - // NOTE: even if the picture is no taller than TARGET_HEIGHT * 2, it is - // faster to downsample and then encode - let img = image::parse_png(&pic.path, TARGET_HEIGHT)?; - image::encode_webp(&img, thumb_path)?; + + // ======================================================================== + let mut pdf_path = PathBuf::from(tmp_dir); + pdf_path.push(PDF_FILE_PATH); + + let pdf_contents = fs::read(&pdf_path) + .map_err(|e| { errorln!("Could not read {pdf_path:?}: {e}") })?; + + let pdf_doc = mupdf::Document::open_from_bytes(&pdf_contents) + .map_err(|e| errorln!("Could not parse {pdf_path:?}: {e}"))?; + + for (page, pic) in pics.iter().enumerate() { + let buf = pdf_doc.render_page_to_svg(page) + .map_err(|e| { + errorln!("Could not render {:?} to SVG: {e}", pic.file_name) + })?; + + let mut svg_f = create_file(&pic.thumb_path).map_err(|_| ())?; + if let Err(e) = svg_f.write_all(&buf) { + errorln!("Could not write to {:?}: {e}", pic.thumb_path); + return Err(()); + } + + log::job_finished(&pic.file_name); } + + Ok(()) } +} - Ok(()) +fn generate_tex_contents(pics: &[RenderingJob<'_>]) -> Result<String, ()> { + use std::fmt::Write; + + const LATEX_PACKAGES: &[&str] = &["tikz", "pgfplots", "amsmath", "amssymb"]; + const TIKZ_LIBRARIES: &[&str] = &[ + "matrix", + "patterns", + "shapes.geometric", + "arrows", + ]; + + let mut sb = String::with_capacity(1024); + + let _ = writeln!(&mut sb, "\\documentclass[crop, tikz]{{standalone}}"); + + let _ = write!(&mut sb, "\\usepackage{{"); + for (i, pkg) in LATEX_PACKAGES.iter().enumerate() { + if i != 0 { sb.push_str(", "); } + sb.push_str(pkg); + } + let _ = writeln!(&mut sb, "}}"); + + let _ = write!(&mut sb, "\\usetikzlibrary{{"); + for (i, lib) in TIKZ_LIBRARIES.iter().enumerate() { + if i != 0 { sb.push_str(", "); } + sb.push_str(lib); + } + let _ = writeln!(&mut sb, "}}"); + + let _ = writeln!(&mut sb, "\\pgfplotsset{{compat=1.18}}"); + let _ = writeln!(&mut sb, "\\begin{{document}}"); + + for pic in pics { + sb.push('\n'); + let _ = File::open(pic.path) + .and_then(|mut f| f.read_to_string(&mut sb)) + .map_err(|e| errorln!("Could not read {:?}: {e}", pic.path))?; + } + + let _ = writeln!(&mut sb, "\n\\end{{document}}"); + + Ok(sb) } fn needs_update(pic: &GalleryEntry, dst: &Path) -> bool { @@ -518,6 +567,27 @@ fn copy(from: &Path, to: &Path) -> Result<(), ()> { .map_err(|e| errorln!("Failed to copy {from:?} to {to:?}: {e}")) } +fn random_dir_name() -> String { + use std::time::{self, SystemTime}; + + const RND_DIR_NAME_SIZE: usize = 16; + const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789"; + + let mut sb = String::with_capacity(RND_DIR_NAME_SIZE); + let rng = SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let mut rng = (rng % (usize::MAX as u128)) as usize; + + for _ in 0..RND_DIR_NAME_SIZE { + rng = rng.wrapping_mul(1103515245).wrapping_add(12345) % CHARSET.len(); + sb.push(CHARSET[rng] as char); + } + + sb +} + impl Display for ThumbPath<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{THUMBS_PATH}/{name}", name = Escaped(&self.0.file_name))?; @@ -532,29 +602,6 @@ impl Display for ThumbPath<'_> { } } -impl From<ThumbPath<'_>> for PathBuf { - fn from(thumb_path: ThumbPath<'_>) -> Self { - let pic = thumb_path.0; - - let mut result = PathBuf::from(TARGET_PATH); - result.push(THUMBS_PATH); - - match pic.file_format { - FileFormat::TeX => { - result.push(pic.file_name.clone() + ".svg"); - } - FileFormat::Svg => { - result.push(&pic.file_name); - } - FileFormat::Jpeg | FileFormat::Png => { - result.push(pic.file_name.clone() + ".webp"); - } - } - - result - } -} - impl Display for HtmlFileName<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{}.html", self.0)
diff --git /dev/null b/src/mupdf.rs @@ -0,0 +1,111 @@ +//! Safe MuPDF wrappers +use std::{ptr, slice, ops::Deref, ffi::CStr}; +use mupdf_sys::*; + +#[derive(Debug)] +pub struct Document { + ctx: *mut fz_context, + inner: *mut fz_document, + + page_count: usize, +} + +#[derive(Debug)] +pub struct Buffer<'doc> { + doc: &'doc Document, + inner: *mut fz_buffer, +} + +impl Document { + pub fn open_from_bytes(bytes: &[u8]) -> Result<Self, String> { + let ctx = unsafe { + fz_new_context(ptr::null(), ptr::null(), FZ_STORE_DEFAULT as usize) + }; + assert!(!ctx.is_null()); + + // SAFETY: this should really be wrapped with fz_try, but it can only + // fail if ctx does not contain a document handler list (?) or + // the list is already full. seems safe to assume + // fz_new_documment will handle us a sane ctx + unsafe { fz_register_document_handlers(ctx); } + + unsafe { + fz_set_warning_callback(ctx, None, ptr::null_mut()); + fz_set_error_callback(ctx, None, ptr::null_mut()); + } + + let mut doc = ptr::null_mut(); + let mut page_count = 0; + let result = unsafe { + mupdf_open_doc_from_bytes( + ctx, + bytes.as_ptr() as *mut _, + bytes.len(), + &mut doc, + &mut page_count, + ) + }; + + if result.type_ != FZ_ERROR_NONE { + return Err(unsafe { + CStr::from_ptr(result.err_msg).to_string_lossy().to_string() + }); + } + + assert!(!doc.is_null()); + let page_count = page_count as usize; + Ok(Self { ctx, inner: doc, page_count, }) + } + + pub fn render_page_to_svg( + &self, + page: usize, + ) -> Result<Buffer<'_>, String> { + assert!(page < self.page_count, + "page {page} is out of bounds: document only has {} pages", + self.page_count); + + let mut buf = ptr::null_mut(); + let result = unsafe { + mupdf_page_to_svg(self.ctx, self.inner, page, &mut buf) + }; + + if result.type_ != FZ_ERROR_NONE { + return Err(unsafe { + CStr::from_ptr(result.err_msg).to_string_lossy().to_string() + }); + } + + assert!(!buf.is_null()); + Ok(Buffer { doc: self, inner: buf, }) + } +} + +impl Drop for Document { + fn drop(&mut self) { + unsafe { + fz_drop_document(self.ctx, self.inner); + fz_drop_context(self.ctx); + } + } +} + +impl<'doc> Deref for Buffer<'doc> { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + (*self.inner).data as *const _, + (*self.inner).len + ) + } + } +} + +impl<'doc> Drop for Buffer<'doc> { + fn drop(&mut self) { + unsafe { fz_drop_buffer(self.doc.ctx, self.inner); } + } +} +