1132 lines
35 KiB
Rust
1132 lines
35 KiB
Rust
pub mod error;
|
|
#[cfg(feature = "python")]
|
|
mod py;
|
|
|
|
use crate::error::Error;
|
|
use chrono::Utc;
|
|
use colorcet::ColorMap;
|
|
use colorgrad::{Gradient, LinearGradient};
|
|
use css_color::Srgb;
|
|
use flate2::write::ZlibEncoder;
|
|
use ndarray::{ArcArray2, AsArray, Ix2, s};
|
|
use num::{Complex, FromPrimitive, Rational32, traits::ToBytes};
|
|
use rayon::prelude::*;
|
|
use std::collections::HashSet;
|
|
use std::fs::{File, OpenOptions};
|
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
use std::io::{Read, Seek, SeekFrom, Write};
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
use std::{cmp::Ordering, collections::HashMap};
|
|
use std::{
|
|
thread,
|
|
thread::{JoinHandle, available_parallelism, sleep},
|
|
};
|
|
use zstd::zstd_safe::CompressionLevel;
|
|
use zstd::{DEFAULT_COMPRESSION_LEVEL, stream::Encoder};
|
|
|
|
const TAG_SIZE: usize = 20;
|
|
const OFFSET_SIZE: usize = 8;
|
|
const OFFSET: u64 = 16;
|
|
|
|
/// Compression: deflate or zstd
|
|
#[derive(Clone, Debug)]
|
|
pub enum Compression {
|
|
Deflate,
|
|
Zstd(CompressionLevel),
|
|
}
|
|
|
|
impl Compression {
|
|
fn index(&self) -> u16 {
|
|
match self {
|
|
Compression::Deflate => 8,
|
|
Compression::Zstd(_) => 50000,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Image File Directory
|
|
#[allow(clippy::upper_case_acronyms)]
|
|
#[derive(Clone, Debug)]
|
|
struct IFD {
|
|
tags: HashSet<Tag>,
|
|
}
|
|
|
|
impl IFD {
|
|
/// new IFD with empty set of tags
|
|
pub fn new() -> Self {
|
|
IFD {
|
|
tags: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
fn write(
|
|
&mut self,
|
|
ijtifffile: &mut IJTiffFile,
|
|
where_to_write_offset: u64,
|
|
) -> Result<u64, Error> {
|
|
let mut tags = self.tags.drain().collect::<Vec<_>>();
|
|
tags.sort();
|
|
ijtifffile.file.seek(SeekFrom::End(0))?;
|
|
if ijtifffile.file.stream_position()? % 2 == 1 {
|
|
ijtifffile.file.write_all(&[0])?;
|
|
}
|
|
let offset = ijtifffile.file.stream_position()?;
|
|
ijtifffile
|
|
.file
|
|
.write_all(&(tags.len() as u64).to_le_bytes())?;
|
|
|
|
for tag in tags.iter_mut() {
|
|
tag.write_tag(ijtifffile)?;
|
|
}
|
|
let where_to_write_next_ifd_offset = ijtifffile.file.stream_position()?;
|
|
ijtifffile.file.write_all(&[0; OFFSET_SIZE])?;
|
|
for tag in tags.iter() {
|
|
tag.write_data(ijtifffile)?;
|
|
}
|
|
ijtifffile
|
|
.file
|
|
.seek(SeekFrom::Start(where_to_write_offset))?;
|
|
ijtifffile.file.write_all(&offset.to_le_bytes())?;
|
|
Ok(where_to_write_next_ifd_offset)
|
|
}
|
|
}
|
|
|
|
/// Tiff tag, use one of the constructors to get a tag of a specific type
|
|
#[derive(Clone, Debug, Eq)]
|
|
pub struct Tag {
|
|
code: u16,
|
|
bytes: Vec<u8>,
|
|
ttype: u16,
|
|
offset: u64,
|
|
}
|
|
|
|
impl PartialOrd<Self> for Tag {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for Tag {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.code.cmp(&other.code)
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Tag {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.code == other.code
|
|
}
|
|
}
|
|
|
|
impl Hash for Tag {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.code.hash(state);
|
|
}
|
|
}
|
|
|
|
impl Tag {
|
|
pub fn new(code: u16, bytes: Vec<u8>, ttype: u16) -> Self {
|
|
Tag {
|
|
code,
|
|
bytes,
|
|
ttype,
|
|
offset: 0,
|
|
}
|
|
}
|
|
|
|
pub fn byte(code: u16, value: &[u8]) -> Self {
|
|
Tag::new(code, value.to_vec(), 1)
|
|
}
|
|
|
|
pub fn ascii(code: u16, value: &str) -> Self {
|
|
let mut bytes = value.as_bytes().to_vec();
|
|
bytes.push(0);
|
|
Tag::new(code, bytes, 2)
|
|
}
|
|
|
|
pub fn short(code: u16, value: &[u16]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
3,
|
|
)
|
|
}
|
|
|
|
pub fn long(code: u16, value: &[u32]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
4,
|
|
)
|
|
}
|
|
|
|
pub fn rational(code: u16, value: &[Rational32]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value
|
|
.iter()
|
|
.flat_map(|x| {
|
|
u32::try_from(*x.denom())
|
|
.unwrap()
|
|
.to_le_bytes()
|
|
.into_iter()
|
|
.chain(u32::try_from(*x.numer()).unwrap().to_le_bytes())
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.collect(),
|
|
5,
|
|
)
|
|
}
|
|
|
|
pub fn sbyte(code: u16, value: &[i8]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
6,
|
|
)
|
|
}
|
|
|
|
pub fn sshort(code: u16, value: &[i16]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
8,
|
|
)
|
|
}
|
|
|
|
pub fn slong(code: u16, value: &[i32]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
9,
|
|
)
|
|
}
|
|
|
|
pub fn srational(code: u16, value: &[Rational32]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value
|
|
.iter()
|
|
.flat_map(|x| {
|
|
x.denom()
|
|
.to_le_bytes()
|
|
.into_iter()
|
|
.chain(x.numer().to_le_bytes())
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.collect(),
|
|
10,
|
|
)
|
|
}
|
|
|
|
pub fn float(code: u16, value: &[f32]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
11,
|
|
)
|
|
}
|
|
|
|
pub fn double(code: u16, value: &[f64]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
12,
|
|
)
|
|
}
|
|
|
|
pub fn ifd(code: u16, value: &[u32]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
13,
|
|
)
|
|
}
|
|
|
|
pub fn unicode(code: u16, value: &str) -> Self {
|
|
let mut bytes: Vec<u8> = value.encode_utf16().flat_map(|x| x.to_le_bytes()).collect();
|
|
bytes.push(0);
|
|
Tag::new(code, bytes, 14)
|
|
}
|
|
|
|
pub fn complex(code: u16, value: &[Complex<f32>]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value
|
|
.iter()
|
|
.flat_map(|x| {
|
|
x.re.to_le_bytes()
|
|
.into_iter()
|
|
.chain(x.im.to_le_bytes())
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.collect(),
|
|
15,
|
|
)
|
|
}
|
|
|
|
pub fn long8(code: u16, value: &[u64]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
16,
|
|
)
|
|
}
|
|
|
|
pub fn slong8(code: u16, value: &[i64]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
17,
|
|
)
|
|
}
|
|
|
|
pub fn ifd8(code: u16, value: &[u64]) -> Self {
|
|
Tag::new(
|
|
code,
|
|
value.iter().flat_map(|x| x.to_le_bytes()).collect(),
|
|
18,
|
|
)
|
|
}
|
|
|
|
pub fn short_long_or_long8(code: u16, value: &[u64]) -> Self {
|
|
let m = *value.iter().max().unwrap();
|
|
if m < 65536 {
|
|
Tag::short(code, &value.iter().map(|x| *x as u16).collect::<Vec<_>>())
|
|
} else if m < 4294967296 {
|
|
Tag::long(code, &value.iter().map(|x| *x as u32).collect::<Vec<_>>())
|
|
} else {
|
|
Tag::long8(code, value)
|
|
}
|
|
}
|
|
|
|
/// get the number of values in the tag
|
|
pub fn count(&self) -> u64 {
|
|
let c = match self.ttype {
|
|
1 => self.bytes.len(), // BYTE
|
|
2 => self.bytes.len(), // ASCII
|
|
3 => self.bytes.len() / 2, // SHORT
|
|
4 => self.bytes.len() / 4, // LONG
|
|
5 => self.bytes.len() / 8, // RATIONAL
|
|
6 => self.bytes.len(), // SBYTE
|
|
7 => self.bytes.len(), // UNDEFINED
|
|
8 => self.bytes.len() / 2, // SSHORT
|
|
9 => self.bytes.len() / 4, // SLONG
|
|
10 => self.bytes.len() / 8, // SRATIONAL
|
|
11 => self.bytes.len() / 4, // FLOAT
|
|
12 => self.bytes.len() / 8, // DOUBLE
|
|
13 => self.bytes.len() / 4, // IFD
|
|
14 => self.bytes.len() / 2, // UNICODE
|
|
15 => self.bytes.len() / 8, // COMPLEX
|
|
16 => self.bytes.len() / 8, // LONG8
|
|
17 => self.bytes.len() / 8, // SLONG8
|
|
18 => self.bytes.len() / 8, // IFD8
|
|
_ => self.bytes.len(),
|
|
};
|
|
c as u64
|
|
}
|
|
|
|
fn write_tag(&mut self, ijtifffile: &mut IJTiffFile) -> Result<(), Error> {
|
|
self.offset = ijtifffile.file.stream_position()?;
|
|
ijtifffile.file.write_all(&self.code.to_le_bytes())?;
|
|
ijtifffile.file.write_all(&self.ttype.to_le_bytes())?;
|
|
ijtifffile.file.write_all(&self.count().to_le_bytes())?;
|
|
if self.bytes.len() <= OFFSET_SIZE {
|
|
ijtifffile.file.write_all(&self.bytes)?;
|
|
ijtifffile
|
|
.file
|
|
.write_all(&vec![0; OFFSET_SIZE - self.bytes.len()])?;
|
|
} else {
|
|
ijtifffile.file.write_all(&[0; OFFSET_SIZE])?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn write_data(&self, ijtifffile: &mut IJTiffFile) -> Result<(), Error> {
|
|
if self.bytes.len() > OFFSET_SIZE {
|
|
ijtifffile.file.seek(SeekFrom::End(0))?;
|
|
let offset = ijtifffile.write(&self.bytes)?;
|
|
ijtifffile.file.seek(SeekFrom::Start(
|
|
self.offset + (TAG_SIZE - OFFSET_SIZE) as u64,
|
|
))?;
|
|
ijtifffile.file.write_all(&offset.to_le_bytes())?;
|
|
if ijtifffile.file.stream_position()? % 2 == 1 {
|
|
ijtifffile.file.write_all(&[0])?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CompressedFrame {
|
|
bytes: Vec<Vec<u8>>,
|
|
image_width: u32,
|
|
image_length: u32,
|
|
tile_width: usize,
|
|
tile_length: usize,
|
|
bits_per_sample: u16,
|
|
sample_format: u16,
|
|
}
|
|
|
|
impl CompressedFrame {
|
|
fn new<T>(frame: ArcArray2<T>, compression: Compression) -> CompressedFrame
|
|
where
|
|
T: Bytes + Send + Sync,
|
|
{
|
|
let shape = frame.shape();
|
|
let tile_size = 2usize
|
|
.pow(((shape[0] as f64 * shape[1] as f64 / 2f64).log2() / 2f64).round() as u32)
|
|
.clamp(16, 1024);
|
|
|
|
let tile_width = tile_size;
|
|
let tile_length = tile_size;
|
|
let n = shape[0] / tile_width;
|
|
let m = shape[1] / tile_length;
|
|
let mut slices = Vec::new();
|
|
for i in 0..n {
|
|
for j in 0..m {
|
|
slices.push((
|
|
i * tile_width,
|
|
(i + 1) * tile_width,
|
|
j * tile_length,
|
|
(j + 1) * tile_length,
|
|
));
|
|
}
|
|
if shape[1] % tile_length != 0 {
|
|
slices.push((
|
|
i * tile_width,
|
|
(i + 1) * tile_width,
|
|
m * tile_length,
|
|
shape[1],
|
|
));
|
|
}
|
|
}
|
|
if shape[0] % tile_width != 0 {
|
|
for j in 0..m {
|
|
slices.push((
|
|
n * tile_width,
|
|
shape[0],
|
|
j * tile_length,
|
|
(j + 1) * tile_length,
|
|
));
|
|
}
|
|
if shape[1] % tile_length != 0 {
|
|
slices.push((n * tile_width, shape[0], m * tile_length, shape[1]));
|
|
}
|
|
}
|
|
|
|
let bytes: Vec<_> = match compression {
|
|
Compression::Deflate => {
|
|
if slices.len() > 4 {
|
|
slices
|
|
.into_par_iter()
|
|
.map(|slice| {
|
|
CompressedFrame::compress_tile_deflate(
|
|
frame.clone(),
|
|
slice,
|
|
tile_size,
|
|
tile_size,
|
|
)
|
|
.unwrap()
|
|
})
|
|
.collect()
|
|
} else {
|
|
slices
|
|
.into_iter()
|
|
.map(|slice| {
|
|
CompressedFrame::compress_tile_deflate(
|
|
frame.clone(),
|
|
slice,
|
|
tile_size,
|
|
tile_size,
|
|
)
|
|
.unwrap()
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
Compression::Zstd(level) => {
|
|
if slices.len() > 4 {
|
|
slices
|
|
.into_par_iter()
|
|
.map(|slice| {
|
|
CompressedFrame::compress_tile_zstd(
|
|
frame.clone(),
|
|
slice,
|
|
tile_size,
|
|
tile_size,
|
|
level,
|
|
)
|
|
.unwrap()
|
|
})
|
|
.collect()
|
|
} else {
|
|
slices
|
|
.into_iter()
|
|
.map(|slice| {
|
|
CompressedFrame::compress_tile_zstd(
|
|
frame.clone(),
|
|
slice,
|
|
tile_size,
|
|
tile_size,
|
|
level,
|
|
)
|
|
.unwrap()
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
};
|
|
|
|
CompressedFrame {
|
|
bytes,
|
|
image_width: shape[1] as u32,
|
|
image_length: shape[0] as u32,
|
|
tile_width,
|
|
tile_length,
|
|
bits_per_sample: T::BITS_PER_SAMPLE,
|
|
sample_format: T::SAMPLE_FORMAT,
|
|
}
|
|
}
|
|
|
|
fn encode<W, T>(
|
|
mut encoder: W,
|
|
frame: ArcArray2<T>,
|
|
slice: (usize, usize, usize, usize),
|
|
tile_width: usize,
|
|
tile_length: usize,
|
|
) -> Result<W, Error>
|
|
where
|
|
W: Write,
|
|
T: Bytes,
|
|
{
|
|
let bytes_per_sample = (T::BITS_PER_SAMPLE / 8) as usize;
|
|
let shape = (slice.1 - slice.0, slice.3 - slice.2);
|
|
for i in 0..shape.0 {
|
|
encoder.write_all(
|
|
&frame
|
|
.slice(s![slice.0..slice.1, slice.2..slice.3])
|
|
.slice(s![i, ..])
|
|
.map(|x| x.bytes())
|
|
.into_iter()
|
|
.flatten()
|
|
.collect::<Vec<_>>(),
|
|
)?;
|
|
encoder.write_all(&vec![0; bytes_per_sample * (tile_width - shape.1)])?;
|
|
}
|
|
encoder.write_all(&vec![
|
|
0;
|
|
bytes_per_sample * tile_width * (tile_length - shape.0)
|
|
])?;
|
|
Ok(encoder)
|
|
}
|
|
|
|
fn compress_tile_deflate<T>(
|
|
frame: ArcArray2<T>,
|
|
slice: (usize, usize, usize, usize),
|
|
tile_width: usize,
|
|
tile_length: usize,
|
|
) -> Result<Vec<u8>, Error>
|
|
where
|
|
T: Bytes,
|
|
{
|
|
let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
|
encoder = CompressedFrame::encode(encoder, frame, slice, tile_width, tile_length)?;
|
|
Ok(encoder.finish()?)
|
|
}
|
|
|
|
fn compress_tile_zstd<T>(
|
|
frame: ArcArray2<T>,
|
|
slice: (usize, usize, usize, usize),
|
|
tile_width: usize,
|
|
tile_length: usize,
|
|
compression_level: i32,
|
|
) -> Result<Vec<u8>, Error>
|
|
where
|
|
T: Bytes,
|
|
{
|
|
let mut dest = Vec::new();
|
|
let mut encoder = Encoder::new(&mut dest, compression_level)?;
|
|
let bytes_per_sample = (T::BITS_PER_SAMPLE / 8) as usize;
|
|
encoder.include_contentsize(true)?;
|
|
encoder.set_pledged_src_size(Some((bytes_per_sample * tile_width * tile_length) as u64))?;
|
|
encoder.include_checksum(true)?;
|
|
encoder = CompressedFrame::encode(encoder, frame, slice, tile_width, tile_length)?;
|
|
encoder.finish()?;
|
|
Ok(dest)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct Frame {
|
|
offsets: Vec<u64>,
|
|
bytecounts: Vec<u64>,
|
|
image_width: u32,
|
|
image_length: u32,
|
|
bits_per_sample: u16,
|
|
sample_format: u16,
|
|
tile_width: u16,
|
|
tile_length: u16,
|
|
}
|
|
|
|
impl Frame {
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn new(
|
|
offsets: Vec<u64>,
|
|
bytecounts: Vec<u64>,
|
|
image_width: u32,
|
|
image_length: u32,
|
|
bits_per_sample: u16,
|
|
sample_format: u16,
|
|
tile_width: u16,
|
|
tile_length: u16,
|
|
) -> Self {
|
|
Frame {
|
|
offsets,
|
|
bytecounts,
|
|
image_width,
|
|
image_length,
|
|
bits_per_sample,
|
|
sample_format,
|
|
tile_width,
|
|
tile_length,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// trait to convert numbers to bytes
|
|
pub trait Bytes {
|
|
const BITS_PER_SAMPLE: u16;
|
|
/// 1: unsigned int, 2: signed int, 3: float
|
|
const SAMPLE_FORMAT: u16;
|
|
|
|
fn bytes(&self) -> Vec<u8>;
|
|
}
|
|
|
|
macro_rules! bytes_impl {
|
|
($T:ty, $bits_per_sample:expr, $sample_format:expr) => {
|
|
impl Bytes for $T {
|
|
const BITS_PER_SAMPLE: u16 = $bits_per_sample;
|
|
const SAMPLE_FORMAT: u16 = $sample_format;
|
|
|
|
#[inline]
|
|
fn bytes(&self) -> Vec<u8> {
|
|
self.to_le_bytes().to_vec()
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
bytes_impl!(u8, 8, 1);
|
|
bytes_impl!(u16, 16, 1);
|
|
bytes_impl!(u32, 32, 1);
|
|
bytes_impl!(u64, 64, 1);
|
|
bytes_impl!(u128, 128, 1);
|
|
#[cfg(target_pointer_width = "64")]
|
|
bytes_impl!(usize, 64, 1);
|
|
#[cfg(target_pointer_width = "32")]
|
|
bytes_impl!(usize, 32, 1);
|
|
bytes_impl!(i8, 8, 2);
|
|
bytes_impl!(i16, 16, 2);
|
|
bytes_impl!(i32, 32, 2);
|
|
bytes_impl!(i64, 64, 2);
|
|
bytes_impl!(i128, 128, 2);
|
|
#[cfg(target_pointer_width = "64")]
|
|
bytes_impl!(isize, 64, 2);
|
|
#[cfg(target_pointer_width = "32")]
|
|
bytes_impl!(isize, 32, 2);
|
|
bytes_impl!(f32, 32, 3);
|
|
bytes_impl!(f64, 64, 3);
|
|
|
|
/// what colormap to save in the tiff;
|
|
#[derive(Clone, Debug)]
|
|
pub enum Colors {
|
|
None,
|
|
/// gradient from black to rgb color, 1 vec per channel
|
|
Colors(Vec<Vec<u8>>),
|
|
/// vec of rgb colors
|
|
Colormap(Vec<Vec<u8>>),
|
|
}
|
|
|
|
/// save 2d arrays in a tif file compatible with Fiji/ImageJ
|
|
#[derive(Debug)]
|
|
pub struct IJTiffFile {
|
|
file: File,
|
|
frames: HashMap<(usize, usize, usize), Frame>,
|
|
hashes: HashMap<u64, u64>,
|
|
threads: HashMap<(usize, usize, usize), JoinHandle<CompressedFrame>>,
|
|
/// zstd: -7 ..= 22
|
|
pub compression: Compression,
|
|
pub colors: Colors,
|
|
pub comment: Option<String>,
|
|
/// um per pixel
|
|
pub px_size: Option<f64>,
|
|
/// um per slice
|
|
pub delta_z: Option<f64>,
|
|
/// s per frame
|
|
pub time_interval: Option<f64>,
|
|
/// extra tags; per frame: key = Some((c, z, t)), global: key = None
|
|
pub extra_tags: HashMap<Option<(usize, usize, usize)>, Vec<Tag>>,
|
|
}
|
|
|
|
impl Drop for IJTiffFile {
|
|
fn drop(&mut self) {
|
|
if let Err(e) = self.close() {
|
|
println!("Error closing IJTiffFile: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IJTiffFile {
|
|
/// create new tifffile from path, use it's save() method to save frames
|
|
/// the file is finalized when it goes out of scope
|
|
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
|
let mut file = OpenOptions::new()
|
|
.create(true)
|
|
.truncate(true)
|
|
.write(true)
|
|
.read(true)
|
|
.open(path)?;
|
|
file.write_all(b"II")?;
|
|
file.write_all(&43u16.to_le_bytes())?;
|
|
file.write_all(&8u16.to_le_bytes())?;
|
|
file.write_all(&0u16.to_le_bytes())?;
|
|
file.write_all(&OFFSET.to_le_bytes())?;
|
|
Ok(IJTiffFile {
|
|
file,
|
|
frames: HashMap::new(),
|
|
hashes: HashMap::new(),
|
|
threads: HashMap::new(),
|
|
compression: Compression::Zstd(DEFAULT_COMPRESSION_LEVEL),
|
|
colors: Colors::None,
|
|
comment: None,
|
|
px_size: None,
|
|
delta_z: None,
|
|
time_interval: None,
|
|
extra_tags: HashMap::new(),
|
|
})
|
|
}
|
|
|
|
/// set compression: zstd(level) or deflate
|
|
pub fn set_compression(&mut self, compression: Compression) {
|
|
self.compression = compression;
|
|
}
|
|
|
|
/// set colors from css color names and #C01085
|
|
/// (see [css_color](https://crates.io/crates/css-color))
|
|
pub fn set_colors<S: AsRef<str>>(&mut self, colors: &[S]) -> Result<(), Error> {
|
|
self.colors = Colors::Colors(
|
|
colors
|
|
.iter()
|
|
.map(|c| {
|
|
let c = c.as_ref();
|
|
let lc = c.to_lowercase();
|
|
let c = match lc.as_str() {
|
|
"r" => "#ff0000",
|
|
"g" => "#008000",
|
|
"b" => "#0000ff",
|
|
"c" => "#00bfbf",
|
|
"m" => "#bf00bf",
|
|
"y" => "#bfbf00",
|
|
"k" => "#000000",
|
|
"w" => "#ffffff",
|
|
_ => c,
|
|
};
|
|
match c.parse::<Srgb>() {
|
|
Ok(c) => Ok(vec![
|
|
(255.0 * c.red).round() as u8,
|
|
(255.0 * c.green).round() as u8,
|
|
(255.0 * c.blue).round() as u8,
|
|
]),
|
|
Err(_) => Err(Error::ColorParse(c.to_string())),
|
|
}
|
|
})
|
|
.collect::<Result<Vec<_>, Error>>()?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// set colormap from named colormap (see [colorcet](https://crates.io/crates/colorcet))
|
|
pub fn set_colormap<S: AsRef<str>>(&mut self, name: S) -> Result<(), Error> {
|
|
let name = name.as_ref();
|
|
let colormap: LinearGradient = name
|
|
.parse::<ColorMap>()?
|
|
.try_into()
|
|
.map_err(|_| Error::Conversion)?;
|
|
let mut colormap = colormap
|
|
.colors(256)
|
|
.into_iter()
|
|
.map(|c| {
|
|
vec![
|
|
(c.r * 255.0).round() as u8,
|
|
(c.g * 255.0).round() as u8,
|
|
(c.b * 255.0).round() as u8,
|
|
]
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if name.starts_with("glasbey") || name.ends_with("glasbey") {
|
|
colormap[0] = vec![255, 255, 255];
|
|
colormap[255] = vec![0, 0, 0];
|
|
}
|
|
self.colors = Colors::Colormap(colormap);
|
|
Ok(())
|
|
}
|
|
|
|
/// to be saved in description tag (270)
|
|
pub fn description(&self, c_size: usize, z_size: usize, t_size: usize) -> String {
|
|
let mut desc: String = String::from("ImageJ=1.11a");
|
|
if let Colors::None = self.colors {
|
|
desc += &format!("\nimages={}", c_size);
|
|
desc += &format!("\nslices={}", z_size);
|
|
desc += &format!("\nframes={}", t_size);
|
|
} else {
|
|
desc += &format!("\nimages={}", c_size * z_size * t_size);
|
|
desc += &format!("\nchannels={}", c_size);
|
|
desc += &format!("\nslices={}", z_size);
|
|
desc += &format!("\nframes={}", t_size);
|
|
};
|
|
if c_size == 1 {
|
|
desc += "\nmode=grayscale";
|
|
} else {
|
|
desc += "\nmode=composite";
|
|
}
|
|
desc += "\nhyperstack=true\nloop=false\nunit=micron";
|
|
if let Some(delta_z) = self.delta_z {
|
|
desc += &format!("\nspacing={}", delta_z);
|
|
}
|
|
if let Some(timeinterval) = self.time_interval {
|
|
desc += &format!("\ninterval={}", timeinterval);
|
|
}
|
|
if let Some(comment) = &self.comment {
|
|
desc += &format!("\ncomment={}", comment);
|
|
}
|
|
desc
|
|
}
|
|
|
|
fn get_czt(
|
|
&self,
|
|
frame_number: usize,
|
|
channel: u8,
|
|
c_size: usize,
|
|
z_size: usize,
|
|
) -> (usize, usize, usize) {
|
|
if let Colors::None = self.colors {
|
|
(
|
|
channel as usize,
|
|
frame_number % z_size,
|
|
frame_number / z_size,
|
|
)
|
|
} else {
|
|
(
|
|
frame_number % c_size,
|
|
frame_number / c_size % z_size,
|
|
frame_number / c_size / z_size,
|
|
)
|
|
}
|
|
}
|
|
|
|
fn spp_and_n_frames(&self, c_size: usize, z_size: usize, t_size: usize) -> (u8, usize) {
|
|
if let Colors::None = &self.colors {
|
|
(c_size as u8, z_size * t_size)
|
|
} else {
|
|
(1, c_size * z_size * t_size)
|
|
}
|
|
}
|
|
|
|
fn hash<T: Hash>(value: &T) -> u64 {
|
|
let mut hasher = DefaultHasher::new();
|
|
value.hash(&mut hasher);
|
|
hasher.finish()
|
|
}
|
|
|
|
fn hash_check(&mut self, bytes: &Vec<u8>, offset: u64) -> Result<bool, Error> {
|
|
let current_offset = self.file.stream_position()?;
|
|
self.file.seek(SeekFrom::Start(offset))?;
|
|
let mut buffer = vec![0; bytes.len()];
|
|
self.file.read_exact(&mut buffer)?;
|
|
let same = bytes == &buffer;
|
|
self.file.seek(SeekFrom::Start(current_offset))?;
|
|
Ok(same)
|
|
}
|
|
|
|
fn write(&mut self, bytes: &Vec<u8>) -> Result<u64, Error> {
|
|
let hash = IJTiffFile::hash(&bytes);
|
|
if self.hashes.contains_key(&hash)
|
|
&& self.hash_check(bytes, *self.hashes.get(&hash).unwrap())?
|
|
{
|
|
Ok(*self.hashes.get(&hash).unwrap())
|
|
} else {
|
|
if self.file.stream_position()? % 2 == 1 {
|
|
self.file.write_all(&[0])?;
|
|
}
|
|
let offset = self.file.stream_position()?;
|
|
self.hashes.insert(hash, offset);
|
|
self.file.write_all(bytes)?;
|
|
Ok(offset)
|
|
}
|
|
}
|
|
|
|
/// save a 2d array to the tiff file at channel c, slice z, and time t
|
|
pub fn save<'a, A, T>(&mut self, frame: A, c: usize, z: usize, t: usize) -> Result<(), Error>
|
|
where
|
|
A: AsArray<'a, T, Ix2>,
|
|
T: Bytes + Clone + Send + Sync + 'static,
|
|
{
|
|
let n_threads = usize::from(available_parallelism()?);
|
|
loop {
|
|
self.collect_threads(false)?;
|
|
if self.threads.len() < n_threads {
|
|
break;
|
|
}
|
|
sleep(Duration::from_millis(100));
|
|
}
|
|
let compression = self.compression.clone();
|
|
let frame = frame.into().to_shared();
|
|
self.threads.insert(
|
|
(c, z, t),
|
|
thread::spawn(move || CompressedFrame::new(frame, compression)),
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_threads(&mut self, block: bool) -> Result<(), Error> {
|
|
for (c, z, t) in self.threads.keys().cloned().collect::<Vec<_>>() {
|
|
if block || self.threads[&(c, z, t)].is_finished() {
|
|
if let Some(thread) = self.threads.remove(&(c, z, t)) {
|
|
self.write_frame(thread.join().unwrap(), c, z, t)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn write_frame(
|
|
&mut self,
|
|
frame: CompressedFrame,
|
|
c: usize,
|
|
z: usize,
|
|
t: usize,
|
|
) -> Result<(), Error> {
|
|
let mut offsets = Vec::new();
|
|
let mut bytecounts = Vec::new();
|
|
for tile in frame.bytes {
|
|
bytecounts.push(tile.len() as u64);
|
|
offsets.push(self.write(&tile)?);
|
|
}
|
|
let frame = Frame::new(
|
|
offsets,
|
|
bytecounts,
|
|
frame.image_width,
|
|
frame.image_length,
|
|
frame.bits_per_sample,
|
|
frame.sample_format,
|
|
frame.tile_width as u16,
|
|
frame.tile_length as u16,
|
|
);
|
|
self.frames.insert((c, z, t), frame);
|
|
Ok(())
|
|
}
|
|
|
|
fn get_colormap(&self, colormap: &Vec<Vec<u8>>, bits_per_sample: u16) -> Vec<u16> {
|
|
let mut r = Vec::new();
|
|
let mut g = Vec::new();
|
|
let mut b = Vec::new();
|
|
let n = 2usize.pow(bits_per_sample as u32 - 8);
|
|
for color in colormap {
|
|
r.extend(vec![(color[0] as u16) * 257; n]);
|
|
g.extend(vec![(color[1] as u16) * 257; n]);
|
|
b.extend(vec![(color[2] as u16) * 257; n]);
|
|
}
|
|
r.extend(g);
|
|
r.extend(b);
|
|
r
|
|
}
|
|
|
|
fn get_color(&self, colors: &Vec<u8>, bits_per_sample: u16) -> Vec<u16> {
|
|
let mut c = Vec::new();
|
|
let n = 2usize.pow(bits_per_sample as u32 - 8);
|
|
for color in colors {
|
|
for i in 0..256 {
|
|
c.extend(vec![i * (*color as u16) / 255 * 257; n])
|
|
}
|
|
}
|
|
c
|
|
}
|
|
|
|
fn close(&mut self) -> Result<(), Error> {
|
|
self.collect_threads(true)?;
|
|
let mut c_size = 1;
|
|
let mut z_size = 1;
|
|
let mut t_size = 1;
|
|
for (c, z, t) in self.frames.keys() {
|
|
c_size = c_size.max(c + 1);
|
|
z_size = z_size.max(z + 1);
|
|
t_size = t_size.max(t + 1);
|
|
}
|
|
|
|
let mut where_to_write_next_ifd_offset = OFFSET - OFFSET_SIZE as u64;
|
|
let mut warn = Vec::new();
|
|
let (samples_per_pixel, n_frames) = self.spp_and_n_frames(c_size, t_size, z_size);
|
|
for frame_number in 0..n_frames {
|
|
if let Some(frame) = self
|
|
.frames
|
|
.get(&self.get_czt(frame_number, 0, c_size, z_size))
|
|
{
|
|
let mut offsets = Vec::new();
|
|
let mut bytecounts = Vec::new();
|
|
let mut frame_count = 0;
|
|
for channel in 0..samples_per_pixel {
|
|
if let Some(frame_n) =
|
|
self.frames
|
|
.get(&self.get_czt(frame_number, channel, c_size, z_size))
|
|
{
|
|
offsets.extend(frame_n.offsets.iter());
|
|
bytecounts.extend(frame_n.bytecounts.iter());
|
|
frame_count += 1;
|
|
} else {
|
|
warn.push((frame_number, channel));
|
|
}
|
|
}
|
|
let mut ifd = IFD::new();
|
|
ifd.tags.insert(Tag::long(256, &[frame.image_width]));
|
|
ifd.tags.insert(Tag::long(257, &[frame.image_length]));
|
|
ifd.tags
|
|
.insert(Tag::short(258, &vec![frame.bits_per_sample; frame_count]));
|
|
ifd.tags
|
|
.insert(Tag::short(259, &[self.compression.index()]));
|
|
ifd.tags
|
|
.insert(Tag::ascii(270, &self.description(c_size, z_size, t_size)));
|
|
ifd.tags.insert(Tag::short(277, &[frame_count as u16]));
|
|
ifd.tags.insert(Tag::ascii(305, "tiffwrite_rs"));
|
|
ifd.tags.insert(Tag::short(322, &[frame.tile_width]));
|
|
ifd.tags.insert(Tag::short(323, &[frame.tile_length]));
|
|
ifd.tags.insert(Tag::short_long_or_long8(324, &offsets));
|
|
ifd.tags.insert(Tag::short_long_or_long8(325, &bytecounts));
|
|
if frame.sample_format > 1 {
|
|
ifd.tags.insert(Tag::short(339, &[frame.sample_format]));
|
|
}
|
|
if let Some(px_size) = self.px_size {
|
|
let r = [Rational32::from_f64(px_size).unwrap()];
|
|
ifd.tags.insert(Tag::rational(282, &r));
|
|
ifd.tags.insert(Tag::rational(283, &r));
|
|
ifd.tags.insert(Tag::short(296, &[1]));
|
|
}
|
|
if let Colors::Colormap(_) = &self.colors {
|
|
ifd.tags.insert(Tag::short(262, &[3]));
|
|
} else if let Colors::None = self.colors {
|
|
ifd.tags.insert(Tag::short(262, &[1]));
|
|
}
|
|
if frame_number == 0 {
|
|
if let Colors::Colormap(colormap) = &self.colors {
|
|
ifd.tags.insert(Tag::short(
|
|
320,
|
|
&self.get_colormap(colormap, frame.bits_per_sample),
|
|
));
|
|
}
|
|
}
|
|
if frame_number < c_size {
|
|
if let Colors::Colors(colors) = &self.colors {
|
|
ifd.tags.insert(Tag::short(
|
|
320,
|
|
&self.get_color(&colors[frame_number], frame.bits_per_sample),
|
|
));
|
|
ifd.tags.insert(Tag::short(262, &[3]));
|
|
}
|
|
}
|
|
if let Colors::None = &self.colors {
|
|
if c_size > 1 {
|
|
ifd.tags.insert(Tag::short(284, &[2]));
|
|
}
|
|
}
|
|
for channel in 0..samples_per_pixel {
|
|
let czt = self.get_czt(frame_number, channel, c_size, z_size);
|
|
if let Some(extra_tags) = self.extra_tags.get(&Some(czt)) {
|
|
for tag in extra_tags {
|
|
ifd.tags.insert(tag.to_owned());
|
|
}
|
|
}
|
|
}
|
|
if let Some(extra_tags) = self.extra_tags.get(&None) {
|
|
for tag in extra_tags {
|
|
ifd.tags.insert(tag.to_owned());
|
|
}
|
|
}
|
|
if frame_number == 0 {
|
|
ifd.tags.insert(Tag::ascii(
|
|
306,
|
|
&format!("{}", Utc::now().format("%Y:%m:%d %H:%M:%S")),
|
|
));
|
|
}
|
|
where_to_write_next_ifd_offset = ifd.write(self, where_to_write_next_ifd_offset)?;
|
|
} else {
|
|
warn.push((frame_number, 0));
|
|
}
|
|
if !warn.is_empty() {
|
|
println!("The following frames were not added to the tif file:");
|
|
for (frame_number, channel) in &warn {
|
|
let (c, z, t) = self.get_czt(*frame_number, *channel, c_size, z_size);
|
|
println!("c: {c}, z: {z}, t: {t}")
|
|
}
|
|
println!(
|
|
"Either you forgot them, \
|
|
or an error occurred and the tif file was closed prematurely."
|
|
)
|
|
}
|
|
}
|
|
self.file
|
|
.seek(SeekFrom::Start(where_to_write_next_ifd_offset))?;
|
|
self.file.write_all(&0u64.to_le_bytes())?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ndarray::Array2;
|
|
|
|
#[test]
|
|
/// An example of generating julia fractals.
|
|
fn julia_test() -> Result<(), Error> {
|
|
let imgx = 800;
|
|
let imgy = 600;
|
|
|
|
let scalex = 3.0 / imgx as f32;
|
|
let scaley = 3.0 / imgy as f32;
|
|
|
|
let mut im_r = Array2::<u8>::zeros((imgy, imgx));
|
|
let mut im_g = Array2::<u8>::zeros((imgy, imgx));
|
|
let mut im_b = Array2::<u8>::zeros((imgy, imgx));
|
|
for x in 0..imgx {
|
|
for y in 0..imgy {
|
|
im_r[[y, x]] = (0.3 * x as f32) as u8;
|
|
im_b[[y, x]] = (0.3 * y as f32) as u8;
|
|
|
|
let cx = y as f32 * scalex - 1.5;
|
|
let cy = x as f32 * scaley - 1.5;
|
|
|
|
let c = Complex::new(-0.4, 0.6);
|
|
let mut z = Complex::new(cx, cy);
|
|
|
|
let mut i = 0;
|
|
while i < 255 && z.norm() <= 2.0 {
|
|
z = z * z + c;
|
|
i += 1;
|
|
}
|
|
|
|
im_g[[y, x]] = i as u8;
|
|
}
|
|
}
|
|
|
|
let mut f = IJTiffFile::new("julia.tif")?;
|
|
f.save(&im_r, 0, 0, 0)?;
|
|
f.save(&im_g, 1, 0, 0)?;
|
|
f.save(&im_b, 2, 0, 0)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|