use crate::error::Error; use ndarray::Array2; use ome_metadata::Ome; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::path::{Path, PathBuf}; use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape}; use itertools::Itertools; use j4rs::{Instance, InvocationArg, JavaOpt, Jvm, JvmBuilder}; use std::cell::OnceCell; use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::rc::Rc; use std::sync::Mutex; use thread_local::ThreadLocal; include!(concat!(env!("OUT_DIR"), "/constants.rs")); thread_local! { static JVM: OnceCell> = const { OnceCell::new() } } static DOWNLOAD_LOCK: Mutex<()> = Mutex::new(()); static JVM_BUILT: Mutex = Mutex::new(false); /// Ensure 1 jvm per thread fn jvm() -> Rc { JVM.with(|cell| { cell.get_or_init(move || { #[cfg(feature = "python")] let path = crate::py::ndbioimage_file(); #[cfg(not(feature = "python"))] let path = std::env::current_exe() .unwrap() .parent() .unwrap() .to_path_buf(); let class_path = if path.join("jassets").exists() { path.as_path() } else { path.parent().unwrap() }; // download jars if needed, but make sure only one thread will do this { let _guard = DOWNLOAD_LOCK.lock().unwrap(); let jassets = class_path.join("jassets"); if !jassets.exists() { std::fs::create_dir_all(&jassets).unwrap(); } if !jassets.join(format!("j4rs-{}-jar-with-dependencies.jar", J4RS_VERSION)).exists() { println!("downloading j4rs-{}-jar-with-dependencies.jar into {}", J4RS_VERSION, jassets.display()); let download = downloader::Download::new(&format!( "https://github.com/astonbitecode/j4rs/raw/v{}/rust/jassets/j4rs-{}-jar-with-dependencies.jar", J4RS_VERSION, J4RS_VERSION )); let mut downloader = downloader::Downloader::builder() .download_folder(&jassets) .build().unwrap(); downloader .download(&[download]).unwrap() .into_iter() .collect::, _>>().unwrap(); } if !jassets.join(format!("bioformats_package-{}.jar", BIOFORMATS_VERSION)).exists() { println!("downloading bioformats_package-{}.jar into {}", BIOFORMATS_VERSION, jassets.display()); let download = downloader::Download::new(&format!( "https://artifacts.openmicroscopy.org/artifactory/ome.releases/ome/bioformats_package/{}/bioformats_package-{}.jar", BIOFORMATS_VERSION, BIOFORMATS_VERSION )); let mut downloader = downloader::Downloader::builder() .download_folder(&jassets) .build().unwrap(); downloader .download(&[download]).unwrap() .into_iter() .collect::, _>>().unwrap(); } #[cfg(feature = "gpl-formats")] if !jassets.join(format!("formats-gpl-{}.jar", BIOFORMATS_VERSION)).exists() { println!("downloading formats-gpl-{}.jar into {}", BIOFORMATS_VERSION, jassets.display()); let download = downloader::Download::new(&format!( "https://artifacts.openmicroscopy.org/artifactory/ome.releases/ome/formats-gpl/{}/formats-gpl-{}.jar", BIOFORMATS_VERSION, BIOFORMATS_VERSION )); let mut downloader = downloader::Downloader::builder() .download_folder(&jassets) .build().unwrap(); downloader .download(&[download]).unwrap() .into_iter() .collect::, _>>().unwrap(); } } { let mut jvm_built = JVM_BUILT.lock().unwrap(); Rc::new(if *jvm_built { Jvm::attach_thread().expect("Failed to attach to JVM") } else { *jvm_built = true; let j = JvmBuilder::new() .skip_setting_native_lib() .with_base_path(class_path.to_str().unwrap()) .java_opt(JavaOpt::new("-XX:+IgnoreUnrecognizedVMOptions")) .java_opt(JavaOpt::new("--enable-native-access=ALL-UNNAMED")) .java_opt(JavaOpt::new("--sun-misc-unsafe-memory-access=allow")) .build() .expect("Failed to build JVM"); if let Ok(e) = InvocationArg::try_from("ERROR") { let _ = j.invoke_static( "loci.common.DebugTools", "setRootLevel", &[e], ); } j }) } }) .clone() }) } macro_rules! method_return { ($R:ty$(|c)?) => { Result<$R, Error> }; () => { Result<(), Error> }; } macro_rules! method_arg { ($n:tt: $t:ty|p) => { InvocationArg::try_from($n)?.into_primitive()? }; ($n:tt: $t:ty) => { InvocationArg::try_from($n)? }; } macro_rules! method { ($name:ident, $method:expr $(,[$($n:tt: $t:ty$(|$p:tt)?),*])? $(=> $tt:ty$(|$c:tt)?)?) => { #[allow(dead_code)] pub(crate) fn $name(&self, $($($n: $t),*)?) -> method_return!($($tt)?) { let args: Vec = vec![$($( method_arg!($n:$t$(|$p)?) ),*)?]; let _result = jvm().invoke(&self.0, $method, &args)?; macro_rules! method_result { ($R:ty|c) => { Ok(jvm().to_rust(_result)?) }; ($R:ty|d) => { Ok(jvm().to_rust_deserialized(_result)?) }; ($R:ty) => { Ok(_result) }; () => { Ok(()) }; } method_result!($($tt$(|$c)?)?) } }; } fn transmute_vec(vec: Vec) -> Vec { unsafe { // Ensure the original vector is not dropped. let mut v_clone = std::mem::ManuallyDrop::new(vec); Vec::from_raw_parts( v_clone.as_mut_ptr() as *mut U, v_clone.len(), v_clone.capacity(), ) } } /// Wrapper around bioformats java class loci.common.DebugTools pub struct DebugTools; impl DebugTools { /// set debug root level: ERROR, DEBUG, TRACE, INFO, OFF pub fn set_root_level(level: &str) -> Result<(), Error> { jvm().invoke_static( "loci.common.DebugTools", "setRootLevel", &[InvocationArg::try_from(level)?], )?; Ok(()) } } /// Wrapper around bioformats java class loci.formats.ChannelSeparator pub(crate) struct ChannelSeparator(Instance); impl ChannelSeparator { pub(crate) fn new(image_reader: &ImageReader) -> Result { let jvm = jvm(); let channel_separator = jvm.create_instance( "loci.formats.ChannelSeparator", &[InvocationArg::from(jvm.clone_instance(&image_reader.0)?)], )?; Ok(ChannelSeparator(channel_separator)) } pub(crate) fn open_bytes(&self, index: i32) -> Result, Error> { Ok(transmute_vec(self.open_bi8(index)?)) } method!(open_bi8, "openBytes", [index: i32|p] => Vec|c); method!(get_index, "getIndex", [z: i32|p, c: i32|p, t: i32|p] => i32|c); } /// Wrapper around bioformats java class loci.formats.ImageReader pub struct ImageReader(Instance); impl Drop for ImageReader { fn drop(&mut self) { self.close().unwrap() } } impl ImageReader { pub(crate) fn new() -> Result { let reader = jvm().create_instance("loci.formats.ImageReader", InvocationArg::empty())?; Ok(ImageReader(reader)) } pub(crate) fn open_bytes(&self, index: i32) -> Result, Error> { Ok(transmute_vec(self.open_bi8(index)?)) } pub(crate) fn ome_xml(&self) -> Result { let mds = self.get_metadata_store()?; Ok(jvm() .chain(&mds)? .cast("loci.formats.ome.OMEPyramidStore")? .invoke("dumpXML", InvocationArg::empty())? .to_rust()?) } method!(close, "close"); method!(is_indexed, "isIndexed" => bool|c); method!(is_interleaved, "isInterleaved" => bool|c); method!(is_little_endian, "isLittleEndian" => bool|c); method!(is_rgb, "isRGB" => bool|c); method!(get_8bit_lookup_table, "get8BitLookupTable" => Instance); method!(get_16bit_lookup_table, "get16BitLookupTable" => Instance); method!(get_dimension_order, "getDimensionOrder" => String|c); method!(set_id, "setId", [id: &str]); method!(get_index, "getIndex", [z: i32|p, c: i32|p, t: i32|p] => i32|c); method!(set_metadata_store, "setMetadataStore", [ome_data: Instance]); method!(get_metadata_store, "getMetadataStore" => Instance); method!(get_pixel_type, "getPixelType" => i32|c); method!(get_rgb_channel_count, "getRGBChannelCount" => i32|c); method!(get_series, "getSeries" => i32|c); method!(set_series, "setSeries", [series: i32|p]); method!(get_series_count, "getSeriesCount" => i32|c); method!(get_size_x, "getSizeX" => i32|c); method!(get_size_y, "getSizeY" => i32|c); method!(get_size_c, "getSizeC" => i32|c); method!(get_size_t, "getSizeT" => i32|c); method!(get_size_z, "getSizeZ" => i32|c); method!(open_bi8, "openBytes", [index: i32|p] => Vec|c); } /// Wrapper around bioformats java class loci.formats.MetadataTools pub(crate) struct MetadataTools(Instance); impl MetadataTools { pub(crate) fn new() -> Result { let meta_data_tools = jvm().create_instance("loci.formats.MetadataTools", InvocationArg::empty())?; Ok(MetadataTools(meta_data_tools)) } method!(create_ome_xml_metadata, "createOMEXMLMetadata" => Instance); } /// Reader interface to file. Use get_frame to get data. #[derive(Serialize, Deserialize)] pub struct BioFormatsJavaReader { #[serde(skip)] reader: ThreadLocal, /// path to file path: PathBuf, /// which (if more than 1) of the series in the file to open series: usize, shape: Shape, pixel_type: PixelType, little_endian: bool, } impl From for DynReader { fn from(value: BioFormatsJavaReader) -> Self { DynReader::BioFormatsJava(value) } } impl Hash for BioFormatsJavaReader { fn hash(&self, state: &mut H) { self.path.hash(state); self.series.hash(state); } } impl PartialEq for BioFormatsJavaReader { fn eq(&self, other: &Self) -> bool { self.path == other.path && self.series == other.series && self.shape == other.shape && self.pixel_type == other.pixel_type && self.little_endian == other.little_endian } } impl Eq for BioFormatsJavaReader {} impl Deref for BioFormatsJavaReader { type Target = ImageReader; fn deref(&self) -> &Self::Target { self.get_reader().unwrap() } } impl Clone for BioFormatsJavaReader { fn clone(&self) -> Self { // BioFormatsReader::new(&self.path, self.series, 0).unwrap() Self { reader: ThreadLocal::default(), path: self.path.clone(), series: self.series, shape: self.shape.clone(), pixel_type: self.pixel_type, little_endian: self.little_endian, } } } impl Debug for BioFormatsJavaReader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BioFormatsJavaReader") .field("path", &self.path) .field("series", &self.series) .field("shape", &self.shape) .field("pixel_type", &self.pixel_type) .field("little_endian", &self.little_endian) .finish() } } impl BioFormatsJavaReader { fn get_reader(&self) -> Result<&ImageReader, Error> { self.reader.get_or_try(|| { let reader = ImageReader::new()?; let meta_data_tools = MetadataTools::new()?; let ome_meta = meta_data_tools.create_ome_xml_metadata()?; reader.set_metadata_store(ome_meta)?; reader.set_id(self.path.to_str().ok_or(Error::InvalidFileName)?)?; reader.set_series(self.series as i32)?; Ok(reader) }) } // pub fn set_reader(&self) -> Result<(), Error> { // self.get_reader().map(|_| ()) // } /// Get ome metadata as ome structure pub fn get_ome(&self) -> Result { let mut ome = Ome::from_xml(self.ome_xml()?)?; if ome.image.len() > 1 { ome.image = vec![ome.image[self.series].clone()]; } Ok(ome) } /// Get ome metadata as xml string pub fn get_ome_xml(&self) -> Result { self.ome_xml() } fn deinterleave(&self, bytes: Vec, channel: usize) -> Result, Error> { let chunk_size = match self.pixel_type { PixelType::Bool => 1, PixelType::I8 => 1, PixelType::U8 => 1, PixelType::I16 => 2, PixelType::U16 => 2, PixelType::I32 => 4, PixelType::U32 => 4, PixelType::F32 => 4, PixelType::F64 => 8, PixelType::I64 => 8, PixelType::U64 => 8, PixelType::I128 => 16, PixelType::U128 => 16, PixelType::F128 => 8, }; Ok(bytes .chunks(chunk_size) .skip(channel) .step_by(self.shape.c) .flat_map(|a| a.to_vec()) .collect()) } fn bytes_to_frame(&self, bytes: Vec) -> Result { macro_rules! get_frame { (bool, <$n:expr) => { Ok(ArrayT::from(Array2::from_shape_vec( (self.shape.y, self.shape.x), bytes .iter() .map(|x| [x & 128, x & 64, x & 32, x & 16, x & 8, x & 4, x & 2, x & 1]) .flatten() .collect(), )?)) }; (bool, >$n:expr) => { Ok(ArrayT::from(Array2::from_shape_vec( (self.shape.y, self.shape.x), bytes .iter() .map(|x| [x & 1, x & 2, x & 4, x & 8, x & 16, x & 32, x & 64, x & 128]) .flatten() .collect(), )?)) }; ($t:tt, <$n:expr) => { Ok(ArrayT::from(Array2::from_shape_vec( (self.shape.y, self.shape.x), bytes .chunks($n) .map(|x| $t::from_le_bytes(x.try_into().unwrap())) .collect(), )?)) }; ($t:tt, >$n:expr) => { Ok(ArrayT::from(Array2::from_shape_vec( (self.shape.y, self.shape.x), bytes .chunks($n) .map(|x| $t::from_be_bytes(x.try_into().unwrap())) .collect(), )?)) }; } match (&self.pixel_type, self.little_endian) { (PixelType::Bool, true) => get_frame!(bool, <1), (PixelType::I8, true) => get_frame!(i8, <1), (PixelType::U8, true) => get_frame!(u8, <1), (PixelType::I16, true) => get_frame!(i16, <2), (PixelType::U16, true) => get_frame!(u16, <2), (PixelType::I32, true) => get_frame!(i32, <4), (PixelType::U32, true) => get_frame!(u32, <4), (PixelType::F32, true) => get_frame!(f32, <4), (PixelType::F64, true) => get_frame!(f64, <8), (PixelType::I64, true) => get_frame!(i64, <8), (PixelType::U64, true) => get_frame!(u64, <8), (PixelType::I128, true) => get_frame!(i128, <16), (PixelType::U128, true) => get_frame!(u128, <16), (PixelType::F128, true) => get_frame!(f64, <8), (PixelType::Bool, false) => get_frame!(bool, >1), (PixelType::I8, false) => get_frame!(i8, >1), (PixelType::U8, false) => get_frame!(u8, >1), (PixelType::I16, false) => get_frame!(i16, >2), (PixelType::U16, false) => get_frame!(u16, >2), (PixelType::I32, false) => get_frame!(i32, >4), (PixelType::U32, false) => get_frame!(u32, >4), (PixelType::F32, false) => get_frame!(f32, >4), (PixelType::F64, false) => get_frame!(f64, >8), (PixelType::I64, false) => get_frame!(i64, >8), (PixelType::U64, false) => get_frame!(u64, >8), (PixelType::I128, false) => get_frame!(i128, >16), (PixelType::U128, false) => get_frame!(u128, >16), (PixelType::F128, false) => get_frame!(f64, >8), } } } impl Drop for BioFormatsJavaReader { fn drop(&mut self) { if let Ok(reader) = self.get_reader() { reader.close().unwrap(); } } } fn find_tiff(path: &Path) -> Result, Error> { if let Some(ext) = path.extension() && path.is_file() && (["tif", "tiff"].contains(&ext.to_string_lossy().to_lowercase().as_str())) { return Ok(Some(path.to_path_buf())); } else if path.is_dir() { for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) { if let Ok(Some(file)) = find_tiff(file.path().as_path()) { return Ok(Some(file)); } } } Ok(None) } impl Reader for BioFormatsJavaReader { /// Create a new reader for the image file at a path, and open series #. fn new

(path: P, series: usize, _position: usize) -> Result where P: AsRef, { let mut path = path.as_ref().to_path_buf(); if path.is_dir() { let orig = path.clone(); path = find_tiff(&path)?.ok_or_else(|| { Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string()) })?; } let mut new = Self { reader: ThreadLocal::default(), path, series, shape: Shape::default(), pixel_type: PixelType::I8, little_endian: false, }; // new.set_reader()?; new.shape.x = new.get_size_x()? as usize; new.shape.y = new.get_size_y()? as usize; new.shape.c = new.get_size_c()? as usize; new.shape.z = new.get_size_z()? as usize; new.shape.t = new.get_size_t()? as usize; new.pixel_type = PixelType::try_from(new.get_pixel_type()?)?; new.little_endian = new.is_little_endian()?; Ok(new) } fn metadata(&self) -> Result { self.get_ome() } /// Retrieve fame at channel c, slize z and time t. fn get_frame(&self, c: usize, z: usize, t: usize) -> Result { let bytes = if self.is_rgb()? && self.is_interleaved()? { let index = self.get_index(z as i32, 0, t as i32)?; self.deinterleave(self.open_bytes(index)?, c)? } else if self.get_rgb_channel_count()? > 1 { let channel_separator = ChannelSeparator::new(self)?; let index = channel_separator.get_index(z as i32, c as i32, t as i32)?; channel_separator.open_bytes(index)? } else { let index = self.get_index(z as i32, c as i32, t as i32)?; self.open_bytes(index)? }; self.bytes_to_frame(bytes) } fn path(&self) -> &Path { &self.path } fn series(&self) -> usize { self.series } fn position(&self) -> usize { 0 } fn shape(&self) -> &Shape { &self.shape } fn pixel_type(&self) -> &PixelType { &self.pixel_type } fn get_available_positions

(_path: P, _series: usize) -> Result, Error> where P: AsRef, { Ok(HashSet::from([0])) } fn get_available_series

(path: P) -> Result, Error> where P: AsRef, { let mut path = path.as_ref().to_path_buf(); if path.is_dir() { let orig = path.clone(); path = find_tiff(&path)?.ok_or_else(|| { Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string()) })?; } let new = BioFormatsJavaReader { reader: ThreadLocal::default(), path, series: 0, shape: Shape::default(), pixel_type: PixelType::I8, little_endian: false, }; Ok(HashSet::from_iter(0..(new.get_series_count()? as usize))) } } #[cfg(test)] mod tests { use super::*; fn open(file: &str) -> Result { let path = std::env::current_dir()? .join("tests") .join("files") .join(file); BioFormatsJavaReader::new(&path, 0, 0) } macro_rules! test_metadata { ($($name:ident: $file:expr $(,)?)*) => { $( #[test] fn $name() -> Result<(), Error> { let bf = open($file)?; println!("{}", bf.view().squeeze()?.summary()?); Ok(()) } )* }; } test_metadata! { metadata_a: "czi/1xp53-01-AP1.czi", metadata_b: "czi/beads_2023_05_04__19_00_22.czi", metadata_c: "czi/Experiment-2029.czi", metadata_d: "czi/MK022_cE9_1-01-Airyscan Processing-01-Scene-2-P1.czi", metadata_e: "czi/YTL1849A131_2023_05_04__13_36_36.czi", metadata_f: "czi/EU_UV_t=1-01.czi", metadata_g: "tiffseq/4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif", metadata_h: "tiffseq/20-Pos_005_005/img_000000000_Cy3-Cy3_filter_000.tif", metadata_i: "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1", } #[test] fn ome_xml() -> Result<(), Error> { let file = "czi/Experiment-2029.czi"; let path = std::env::current_dir()? .join("tests") .join("files") .join(file); let reader = BioFormatsJavaReader::new(&path, 0, 0)?; let xml = reader.get_ome_xml()?; println!("{}", xml); Ok(()) } }