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<Rc<Jvm>> = const { OnceCell::new() }
}
static DOWNLOAD_LOCK: Mutex<()> = Mutex::new(());
static JVM_BUILT: Mutex<bool> = Mutex::new(false);
fn jvm() -> Rc<Jvm> {
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()
};
{
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::<Result<Vec<_>, _>>().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::<Result<Vec<_>, _>>().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::<Result<Vec<_>, _>>().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<InvocationArg> = 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<T, U>(vec: Vec<T>) -> Vec<U> {
unsafe {
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(),
)
}
}
pub struct DebugTools;
impl DebugTools {
pub fn set_root_level(level: &str) -> Result<(), Error> {
jvm().invoke_static(
"loci.common.DebugTools",
"setRootLevel",
&[InvocationArg::try_from(level)?],
)?;
Ok(())
}
}
pub(crate) struct ChannelSeparator(Instance);
impl ChannelSeparator {
pub(crate) fn new(image_reader: &ImageReader) -> Result<Self, Error> {
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<Vec<u8>, Error> {
Ok(transmute_vec(self.open_bi8(index)?))
}
method!(open_bi8, "openBytes", [index: i32|p] => Vec<i8>|c);
method!(get_index, "getIndex", [z: i32|p, c: i32|p, t: i32|p] => i32|c);
}
pub struct ImageReader(Instance);
impl Drop for ImageReader {
fn drop(&mut self) {
self.close().unwrap()
}
}
impl ImageReader {
pub(crate) fn new() -> Result<Self, Error> {
let reader = jvm().create_instance("loci.formats.ImageReader", InvocationArg::empty())?;
Ok(ImageReader(reader))
}
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>, Error> {
Ok(transmute_vec(self.open_bi8(index)?))
}
pub(crate) fn ome_xml(&self) -> Result<String, Error> {
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<i8>|c);
}
pub(crate) struct MetadataTools(Instance);
impl MetadataTools {
pub(crate) fn new() -> Result<Self, Error> {
let meta_data_tools =
jvm().create_instance("loci.formats.MetadataTools", InvocationArg::empty())?;
Ok(MetadataTools(meta_data_tools))
}
method!(create_ome_xml_metadata, "createOMEXMLMetadata" => Instance);
}
#[derive(Serialize, Deserialize)]
pub struct BioFormatsJavaReader {
#[serde(skip)]
reader: ThreadLocal<ImageReader>,
path: PathBuf,
series: usize,
shape: Shape,
pixel_type: PixelType,
little_endian: bool,
}
impl From<BioFormatsJavaReader> for DynReader {
fn from(value: BioFormatsJavaReader) -> Self {
DynReader::BioFormatsJava(value)
}
}
impl Hash for BioFormatsJavaReader {
fn hash<H: Hasher>(&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 {
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 get_ome(&self) -> Result<Ome, Error> {
let mut ome = Ome::from_xml(self.ome_xml()?)?;
if ome.image.len() > 1 {
ome.image = vec![ome.image[self.series].clone()];
}
Ok(ome)
}
pub fn get_ome_xml(&self) -> Result<String, Error> {
self.ome_xml()
}
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, Error> {
let chunk_size = match self.pixel_type {
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<u8>) -> Result<Frame, Error> {
macro_rules! get_frame {
($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::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::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<Option<PathBuf>, 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 {
fn new<P>(path: P, series: usize, _position: usize) -> Result<Self, Error>
where
P: AsRef<Path>,
{
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 = BioFormatsJavaReader {
reader: ThreadLocal::default(),
path,
series,
shape: Shape::default(),
pixel_type: PixelType::I8,
little_endian: false,
};
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<Ome, Error> {
self.get_ome()
}
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
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<P>(_path: P, _series: usize) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>,
{
Ok(HashSet::from([0]))
}
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>,
{
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<BioFormatsJavaReader, Error> {
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(())
}
}