use hdf5::filters::Filter;
use crate::h5::data::Layout;
use crate::json::{repr_list, repr_str};
use crate::{Error, Result};
pub const COMPRESS_MIN_BYTES: usize = 64 * 1024;
pub const BULK_MIN_BYTES: usize = 1024 * 1024;
pub const BLOSC2_FILTER_ID: i32 = 32026;
pub const BLOSC_FILTER_ID: i32 = 32001;
pub const BUILTIN_FILTER_IDS: [i32; 6] = [1, 2, 3, 4, 5, 6];
const ZSTD_FILTER_ID: i32 = 32015;
const LZF_FILTER_ID: i32 = 32000;
pub const DEFAULT_PROFILE: &str = "balanced";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
Image,
Label,
Aux,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Codec {
pub name: &'static str,
pub blosc2: Option<(&'static str, u32, &'static str)>,
pub gzip_level: Option<u8>,
pub shuffle: bool,
}
impl Codec {
pub fn filters(&self) -> Vec<Filter> {
if let Some((cname, clevel, shuffle)) = self.blosc2 {
let compcode = match cname {
"blosclz" => 0,
"lz4" => 1,
"lz4hc" => 2,
"zlib" => 4,
"zstd" => 5,
_ => 0,
};
let mode = match shuffle {
"shuffle" => 1,
"bitshuffle" => 2,
_ => 0,
};
return vec![Filter::user(BLOSC2_FILTER_ID as _, &[0, 0, 0, 0, clevel, mode, compcode])];
}
if let Some(level) = self.gzip_level {
let mut out = Vec::new();
if self.shuffle {
out.push(Filter::shuffle());
}
out.push(Filter::deflate(level));
return out;
}
Vec::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CodecProfile {
pub name: &'static str,
pub image: Codec,
pub label: Codec,
pub description: &'static str,
}
impl CodecProfile {
pub fn codec(&self, role: Role) -> &Codec {
if role == Role::Image {
&self.image
} else {
&self.label
}
}
}
const fn blosc2(name: &'static str, cname: &'static str, clevel: u32, shuffle: &'static str) -> Codec {
Codec { name, blosc2: Some((cname, clevel, shuffle)), gzip_level: None, shuffle: true }
}
const fn gzip(level: u8) -> Codec {
Codec { name: "gzip:4", blosc2: None, gzip_level: Some(level), shuffle: true }
}
pub fn profiles() -> Vec<CodecProfile> {
vec![
CodecProfile {
name: "archive",
image: blosc2("blosc2:zstd:9:bitshuffle", "zstd", 9, "bitshuffle"),
label: blosc2("blosc2:zstd:9:bitshuffle", "zstd", 9, "bitshuffle"),
description: "smallest on disk; cold storage and distribution",
},
CodecProfile {
name: "balanced",
image: blosc2("blosc2:zstd:3:shuffle", "zstd", 3, "shuffle"),
label: blosc2("blosc2:zstd:3:bitshuffle", "zstd", 3, "bitshuffle"),
description: "general use; the default",
},
CodecProfile { name: "portable", image: gzip(4), label: gzip(4), description: "readable without hdf5plugin" },
CodecProfile {
name: "training",
image: blosc2("blosc2:lz4:1:shuffle", "lz4", 1, "shuffle"),
label: blosc2("blosc2:lz4:1:shuffle", "lz4", 1, "shuffle"),
description: "fastest decompression; hot dataloader path",
},
]
}
pub fn profile_names() -> Vec<&'static str> {
profiles().iter().map(|p| p.name).collect()
}
pub fn resolve_profile(name: Option<&str>) -> Result<CodecProfile> {
let wanted = name.unwrap_or(DEFAULT_PROFILE);
profiles().into_iter().find(|p| p.name == wanted).ok_or_else(|| {
Error::invalid(format!(
"unknown codec profile {}; expected one of {}",
repr_str(wanted),
profile_names().join(", ")
))
})
}
pub fn dataset_layout(
shape: &[usize],
itemsize: usize,
profile: &CodecProfile,
role: Role,
chunks: Option<Vec<usize>>,
) -> Layout {
let nbytes: usize = if shape.is_empty() { 0 } else { shape.iter().product::<usize>() * itemsize };
if nbytes < COMPRESS_MIN_BYTES || shape.contains(&0) {
return Layout::contiguous();
}
let chunks = chunks.unwrap_or_else(|| super::chunking::guess_chunk(shape, itemsize));
Layout { chunks: Some(chunks), filters: profile.codec(role).filters() }
}
const BLOSC_CNAMES: [&str; 6] = ["blosclz", "lz4", "lz4hc", "snappy", "zlib", "zstd"];
const BLOSC_SHUFFLE: [&str; 3] = ["noshuffle", "shuffle", "bitshuffle"];
fn describe_blosc(values: &[u32]) -> String {
if values.len() < 7 {
return "blosc2".into();
}
let (clevel, shuffle, cname) = (values[4], values[5], values[6]);
let cname = BLOSC_CNAMES.get(cname as usize).map(|s| s.to_string()).unwrap_or_else(|| cname.to_string());
let shuffle = BLOSC_SHUFFLE.get(shuffle as usize).map(|s| s.to_string()).unwrap_or_else(|| shuffle.to_string());
format!("blosc2:{cname}:{clevel}+{shuffle}")
}
pub fn describe_filters(ds: &hdf5::Dataset) -> Result<String> {
if crate::h5::data::chunks(ds).is_none() {
return Ok("contiguous".into());
}
let pipeline = crate::h5::data::filters(ds)?;
let named_compression = pipeline.iter().find_map(|(id, values)| match *id {
1 => Some(format!("gzip:{}", values.first().copied().unwrap_or(0))),
LZF_FILTER_ID => Some("lzf".to_string()),
4 => Some("szip".to_string()),
_ => None,
});
let has_shuffle = pipeline.iter().any(|(id, _)| *id == 2);
let mut parts: Vec<String> = Vec::new();
let mut named = false;
for (id, values) in &pipeline {
match *id {
BLOSC2_FILTER_ID => parts.push(describe_blosc(values)),
BLOSC_FILTER_ID => {
let d = describe_blosc(values);
parts.push(format!("blosc:{}", d.split_once(':').map(|x| x.1).unwrap_or("")));
}
ZSTD_FILTER_ID => {
parts.push(values.first().map_or_else(|| "zstd".to_string(), |level| format!("zstd:{level}")));
}
other if !BUILTIN_FILTER_IDS.contains(&other) && other != LZF_FILTER_ID => {
parts.push(format!("filter:{other}"));
}
_ if !named => {
if let Some(c) = &named_compression {
named = true;
parts.push(c.clone());
}
}
_ => {}
}
}
if has_shuffle {
parts.push("shuffle".into());
}
Ok(if parts.is_empty() { "chunked".into() } else { parts.join("+") })
}
pub fn profile_family(root: &hdf5::Group) -> Result<&'static str> {
let mut plugin = false;
crate::h5::ops::visit(root, &mut |_, node| {
if let crate::h5::ops::Node::Dataset(ds) = node {
plugin = needs_plugin(ds)?;
}
Ok(!plugin)
})?;
Ok(if plugin { "balanced" } else { "portable" })
}
pub(crate) fn needs_plugin(ds: &hdf5::Dataset) -> Result<bool> {
if crate::h5::data::chunks(ds).is_none() {
return Ok(false);
}
Ok(crate::h5::data::filters(ds)?.iter().any(|(id, _)| !BUILTIN_FILTER_IDS.contains(id)))
}
pub(crate) fn builtin_only(profile: &CodecProfile) -> bool {
[&profile.image, &profile.label].iter().all(|codec| codec.blosc2.is_none())
}
pub fn is_bulk(ds: &hdf5::Dataset) -> bool {
crate::h5::data::nbytes(ds).map(|n| n >= BULK_MIN_BYTES).unwrap_or(false)
}
pub fn profiles_repr() -> String {
repr_list(&profile_names())
}