diff --git a/README.md b/README.md index de01df0..b98f9c6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,219 @@ # libCZIrw-sys -Crate linking to [libCZIAPI](https://github.com/ZEISS/libczi). -This crate attempts to provide save wrappers to objects and functions in libCZIAPI. -Direct often unsafe access using pointer is available through the sys module. +Crate linking to [libCZIAPI](https://github.com/ZEISS/libczi). This crate attempts to provide safe wrappers to objects +and functions in libCZIAPI. Direct often unsafe access using pointer is available through the `sys` module. -By default, libCZIAPI will be statically linked. The feature 'dynamic' will switch it to dynamic linking. +By default, libCZIAPI will be statically linked. The feature `dynamic` will switch it to dynamic linking. -This code is licensed with an MIT or APACHE 2 license, but Zeiss' libCZI which is included as a submodule has a LGPL license. \ No newline at end of file +This code is licensed with an MIT or APACHE 2 license, but Zeiss' libCZI which is included as a submodule has a LGPL +license. + +### Reading a CZI file + +The typical pattern is: create a reader, open it with an input stream, and query statistics or sub-blocks: + +```rust +use libczirw_sys::{CziReader, Dimension, InputStream, ReaderOpenInfo}; + +fn main() -> Result<(), Box> { + // create the reader and a stream for the file + let czi = CziReader::create()?; + let stream = InputStream::create_from_file_utf8("path/to/file.czi")?; + czi.open(ReaderOpenInfo::new(&stream))?; + + // get the (simple) statistics about the sub-blocks + let statistics = czi.get_statistics_simple()?; + println!("number of sub-blocks: {}", statistics.get_sub_block_count()); + + // iterate over the dimensions that are present (Z, C, T, ...) + let dim_bounds = statistics.get_dim_bounds(); + let dimensions = Dimension::vec_from_bitflags(dim_bounds.get_dimensions_valid()); + for (i, dim) in dimensions.iter().enumerate() { + println!("{:?}: {}", dim, dim_bounds.get_size()[i]); + } + + // the bounding box of all sub-blocks + let bbox = statistics.get_bounding_box(); + println!( + "overall extent: {} x {} (offset {}, {})", + bbox.get_w(), + bbox.get_h(), + bbox.get_x(), + bbox.get_y() + ); + Ok(()) +} +``` + +### Reading pixel data + +Each sub-block can be decoded into a bitmap. Locking the bitmap gives access to the raw pixels: + +```rust +use libczirw_sys::{CziReader, InputStream, ReaderOpenInfo}; + +fn main() -> Result<(), Box> { + let czi = CziReader::create()?; + let stream = InputStream::create_from_file_utf8("path/to/file.czi")?; + czi.open(ReaderOpenInfo::new(&stream))?; + + // read the first sub-block and turn it into a bitmap + let sub_block = czi.read_sub_block(0)?; + let bitmap = sub_block.create_bitmap()?; + let info = bitmap.get_info()?; + println!( + "width: {}, height: {}, pixel type: {:?}", + info.get_width(), + info.get_height(), + info.get_pixel_type()? + ); + + // locking gives access to the raw pixel data + let locked = bitmap.lock()?; + let stride = locked.lock_info.get_stride(); + let pixels = locked.lock_info.get_data_roi(); // copies the pixel bytes + + // the bitmap is automatically unlocked when `locked` goes out of scope + Ok(()) +} +``` + +### Reading the metadata + +CZI files store metadata as an XML document, which can be retrieved as a string: + +```rust +use libczirw_sys::{CziReader, InputStream, ReaderOpenInfo}; + +fn main() -> Result<(), Box> { + let czi = CziReader::create()?; + let stream = InputStream::create_from_file_utf8("path/to/file.czi")?; + czi.open(ReaderOpenInfo::new(&stream))?; + + let metadata_segment = czi.get_metadata_segment()?; + let xml = metadata_segment.get_metadata_as_xml()?; + let xml: String = (&xml).try_into()?; + println!("{}", xml); + + // document-level information (title, user, creation date, ...) as JSON + let doc_info = metadata_segment.get_czi_document_info()?; + let general_info = doc_info.get_general_document_info()?; + println!("{}", general_info); + Ok(()) +} +``` + +### Reading attachments + +Attachments hold additional data (e.g. microscope setup or experiment info) and are decoded based on their content type: + +```rust +use libczirw_sys::{AttachmentData, CziReader, InputStream, ReaderOpenInfo}; + +fn main() -> Result<(), Box> { + let czi = CziReader::create()?; + let stream = InputStream::create_from_file_utf8("path/to/file.czi")?; + czi.open(ReaderOpenInfo::new(&stream))?; + + for index in 0..czi.get_attachment_count()? { + let info = czi.get_attachment_info_from_directory(index)?; + println!( + "attachment '{}' of type {}", + info.get_name()?, + info.get_content_file_type()? + ); + + let attachment = czi.read_attachment(index)?; + match attachment.get_data()? { + AttachmentData::Float(values) => println!(" floats: {:?}", values), + AttachmentData::Xml(xml) => println!(" xml: {}", &xml[..xml.len().min(100)]), + AttachmentData::Unknown(bytes) => println!(" raw bytes: {}", bytes.len()), + } + } + Ok(()) +} +``` + +### Writing a CZI file + +Create a writer, add sub-blocks and metadata, and close the file to finalize it: + +```rust +use libczirw_sys::{ + AddSubBlockInfo, Coordinate, CziWriter, OutputStream, PixelType, WriteMetadataInfo, +}; + +fn main() -> Result<(), Box> { + let writer = CziWriter::create(r#"{"allow_duplicate_subblocks": true}"#)?; + let stream = OutputStream::create_for_file_utf8("output.czi", true)?; + writer.init(&stream, r#"{"minimum_m_index": 0, "maximum_m_index": 0}"#)?; + + // a single gray-8-bit sub-block of 100 x 100 pixels + let width = 100; + let height = 100; + let pixels: Vec = (0..(width * height)).map(|i| i as u8).collect(); + + // bit 0 => dimension Z is valid (Z is the first dimension, value 1); + // the Z coordinate of this sub-block is 0 + let coordinate = Coordinate::new(1 << 0, [0, 0, 0, 0, 0, 0, 0, 0, 0]); + + let sub_block = AddSubBlockInfo::new( + coordinate, + 0, // m_index_valid + 0, // m_index + 0, // x + 0, // y + width, // logical width + height, // logical height + width, // physical width + height, // physical height + PixelType::Gray8, + 0, // compression mode (none) + &pixels, // raw pixel data + b"", // sub-block metadata + b"", // attachment data + ); + writer.add_sub_block(sub_block)?; + + let xml_metadata = br#" + + + + My document + + +"#; + writer.write_metadata(WriteMetadataInfo::new(xml_metadata))?; + + // finalize the file - required to produce a valid CZI + writer.close()?; + Ok(()) +} +``` + +### Version and build information + +```rust +use libczirw_sys::{LibCZIBuildInformation, LibCZIVersionInfo}; + +fn main() -> Result<(), Box> { + let version = LibCZIVersionInfo::get_lib_czi_version_info()?; + println!( + "libCZI version {}.{}.{}", + version.get_major(), + version.get_minor(), + version.get_patch() + ); + + let build_info = LibCZIBuildInformation::get()?; + println!("compiler: {}", build_info.get_compiler_information()); + println!("repository: {}", build_info.get_repository_url()); + Ok(()) +} +``` + +## Error handling + +All fallible operations return a `Result` with the crate's `Error` type, which maps libCZIAPI error codes onto +descriptive variants (see `src/error.rs`). The `?` operator can be used directly; conversions from UTF-8 and +null-termination errors are provided via `From` implementations on the error type. diff --git a/build.rs b/build.rs index 13e0431..bc1cd47 100644 --- a/build.rs +++ b/build.rs @@ -236,13 +236,7 @@ fn main() -> Result<(), Box> { let _importexport_guard = patch_importexport(&libcziapi_inc)?; let dst = cmake_config.build(); - #[cfg(not(feature = "dynamic"))] - let bindings = bindgen::Builder::default(); - - #[cfg(feature = "dynamic")] - let bindings = bindgen::Builder::default(); - - let bindings = bindings + let bindings = bindgen::Builder::default() .merge_extern_blocks(true) .clang_args([ "-fms-extensions", @@ -268,11 +262,9 @@ fn main() -> Result<(), Box> { .ok_or(Error::msg("cannot into string"))?, ) .generate()?; - bindings.write_to_file(dst.join("lib_czi_api.rs"))?; - #[cfg(not(feature = "dynamic"))] - { + if cfg!(any(not(feature = "dynamic"), target_feature = "crt-static")) { let libcziapi_dir = dst.join("build/Src/libCZIAPI"); let libcziapi_dir2 = dst.join("Src/libCZIAPI"); let libczi_dir = dst.join("build/Src/libCZI"); @@ -367,10 +359,7 @@ fn main() -> Result<(), Box> { println!("cargo::rustc-link-lib=shell32"); println!("cargo::rustc-link-lib=Windowscodecs"); } - } - - #[cfg(feature = "dynamic")] - { + } else { println!( "cargo::rustc-link-search=native={}", dst.join("build/Src/libCZIAPI").display() diff --git a/src/lib.rs b/src/lib.rs index 03bb908..9017dbc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,14 +17,11 @@ mod tests { use crate::handle::{CziReader, InputStream}; use crate::interop::{LibCZIBuildInformation, ReaderOpenInfo}; use crate::misc::Dimension; - use std::env; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; #[test] fn test_read_shape() -> Result<(), Box> { - let path = env::home_dir() - .unwrap() - .join("code/rust/ndbioimage/tests/files/Experiment-2029.czi"); + let path = Path::new("test-files/Experiment-2029.czi"); assert!(path.exists()); let czi = CziReader::create()?; let stream = InputStream::create_from_file_utf8(path.to_string_lossy().as_ref())?; @@ -48,9 +45,7 @@ mod tests { #[test] fn test_read_bytes() -> Result<(), Box> { - let path = env::home_dir() - .unwrap() - .join("code/rust/ndbioimage/tests/files/Experiment-2029.czi"); + let path = Path::new("test-files/Experiment-2029.czi"); assert!(path.exists()); let czi = CziReader::create()?; let stream = InputStream::create_from_file_utf8(path.to_string_lossy().as_ref())?; @@ -72,9 +67,7 @@ mod tests { #[test] fn test_libczi_xml() -> Result<(), Box> { - let path = env::home_dir() - .unwrap() - .join("code/rust/ndbioimage/tests/files/Experiment-2029.czi"); + let path = Path::new("test-files/Experiment-2029.czi"); assert!(path.exists()); let czi = CziReader::create()?; let stream = InputStream::create_from_file_utf8(path.to_string_lossy().as_ref())?;