use std::path::PathBuf;
use std::time::Duration;
use crate::{
CodecValidation, EncodeBackendPreference, Error, ExportOptions, MetadataSource, TransferSyntax,
};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ExportRequest {
pub source_path: PathBuf,
pub output_dir: PathBuf,
pub options: ExportOptions,
pub metadata: MetadataSource,
pub level_filter: Option<u32>,
}
impl ExportRequest {
pub fn new(
source_path: PathBuf,
output_dir: PathBuf,
options: ExportOptions,
metadata: MetadataSource,
) -> Result<Self, Error> {
options.validate()?;
Ok(Self {
source_path,
output_dir,
options,
metadata,
level_filter: None,
})
}
pub fn validate(&self) -> Result<(), Error> {
self.options.validate()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct FrameSamples<'a> {
pub data: &'a [u8],
pub width: u32,
pub height: u32,
pub components: u8,
pub bit_depth: u8,
pub signed: bool,
}
impl<'a> FrameSamples<'a> {
pub fn new(
data: &'a [u8],
width: u32,
height: u32,
components: u8,
bit_depth: u8,
signed: bool,
) -> Result<Self, Error> {
let samples = Self {
data,
width,
height,
components,
bit_depth,
signed,
};
samples.to_j2k()?;
Ok(samples)
}
pub(crate) fn to_j2k(self) -> Result<j2k::J2kLosslessSamples<'a>, Error> {
j2k::J2kLosslessSamples::new(
self.data,
self.width,
self.height,
u16::from(self.components),
self.bit_depth,
self.signed,
)
.map_err(|err| Error::UnsupportedPixelData {
reason: err.to_string(),
})
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct J2kFrameEncodeRequest<'a> {
pub samples: FrameSamples<'a>,
pub transfer_syntax: TransferSyntax,
pub encode_backend: EncodeBackendPreference,
pub codec_validation: CodecValidation,
}
impl<'a> J2kFrameEncodeRequest<'a> {
pub fn new(
samples: FrameSamples<'a>,
transfer_syntax: TransferSyntax,
encode_backend: EncodeBackendPreference,
codec_validation: CodecValidation,
) -> Self {
Self {
samples,
transfer_syntax,
encode_backend,
codec_validation,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct RouteProfileRequest {
pub source_path: PathBuf,
pub options: ExportOptions,
pub source_aware_transfer_syntax: bool,
pub level: u32,
pub max_frames: u64,
}
impl RouteProfileRequest {
pub fn new(source_path: PathBuf, options: ExportOptions, level: u32, max_frames: u64) -> Self {
Self {
source_path,
options,
source_aware_transfer_syntax: true,
level,
max_frames,
}
}
pub fn with_source_aware_transfer_syntax(mut self, source_aware: bool) -> Self {
self.source_aware_transfer_syntax = source_aware;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DefaultTransferSyntaxRequest {
pub source_path: PathBuf,
pub tile_size: u32,
pub level_filter: Option<u32>,
pub max_levels: Option<u32>,
}
impl DefaultTransferSyntaxRequest {
pub fn new(source_path: PathBuf, tile_size: u32) -> Self {
Self {
source_path,
tile_size,
level_filter: None,
max_levels: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RouteCoverageTarget {
Source(PathBuf),
Corpus(PathBuf),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RouteProgressSink {
Stderr,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct RouteCoverageRequest {
pub target: RouteCoverageTarget,
pub options: ExportOptions,
pub source_aware_transfer_syntax: bool,
pub max_frames_per_level: u64,
pub max_levels: Option<u32>,
pub max_level_elapsed: Option<Duration>,
pub progress: Option<RouteProgressSink>,
pub max_sources: usize,
pub max_depth: usize,
}
impl RouteCoverageRequest {
pub fn new(source_path: PathBuf, options: ExportOptions) -> Self {
Self {
target: RouteCoverageTarget::Source(source_path),
options,
source_aware_transfer_syntax: true,
max_frames_per_level: 1,
max_levels: None,
max_level_elapsed: None,
progress: None,
max_sources: 100_000,
max_depth: 64,
}
}
pub fn new_corpus(source_root: PathBuf, options: ExportOptions) -> Self {
Self {
target: RouteCoverageTarget::Corpus(source_root),
options,
source_aware_transfer_syntax: true,
max_frames_per_level: 1,
max_levels: None,
max_level_elapsed: None,
progress: None,
max_sources: 100_000,
max_depth: 64,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_constructors_preserve_defaults_and_validate_options() {
let source = PathBuf::from("slide.svs");
let output = PathBuf::from("dicom-out");
let options = ExportOptions::default();
let export = ExportRequest::new(
source.clone(),
output.clone(),
options.clone(),
MetadataSource::ResearchPlaceholder,
)
.unwrap();
assert_eq!(export.source_path, source);
assert_eq!(export.output_dir, output);
assert_eq!(export.metadata, MetadataSource::ResearchPlaceholder);
assert_eq!(export.level_filter, None);
export.validate().unwrap();
let err = ExportRequest::new(
PathBuf::from("slide.svs"),
PathBuf::from("dicom-out"),
ExportOptions {
tile_size: 0,
..ExportOptions::default()
},
MetadataSource::ResearchPlaceholder,
)
.expect_err("zero tile size should be rejected");
assert!(err.to_string().contains("tile_size"));
let profile = RouteProfileRequest::new(PathBuf::from("slide.svs"), options.clone(), 2, 16);
assert!(profile.source_aware_transfer_syntax);
assert_eq!(profile.level, 2);
assert_eq!(profile.max_frames, 16);
let default_transfer = DefaultTransferSyntaxRequest::new(PathBuf::from("slide.svs"), 512);
assert_eq!(default_transfer.tile_size, 512);
assert_eq!(default_transfer.level_filter, None);
assert_eq!(default_transfer.max_levels, None);
let coverage = RouteCoverageRequest::new(PathBuf::from("slide.svs"), options.clone());
assert_eq!(
coverage.target,
RouteCoverageTarget::Source(PathBuf::from("slide.svs"))
);
assert!(coverage.source_aware_transfer_syntax);
assert_eq!(coverage.max_frames_per_level, 1);
assert_eq!(coverage.max_levels, None);
assert_eq!(coverage.max_level_elapsed, None);
assert_eq!(coverage.progress, None);
assert_eq!(coverage.max_sources, 100_000);
assert_eq!(coverage.max_depth, 64);
let corpus = RouteCoverageRequest::new_corpus(PathBuf::from("slides"), options);
assert_eq!(
corpus.target,
RouteCoverageTarget::Corpus(PathBuf::from("slides"))
);
assert!(corpus.source_aware_transfer_syntax);
assert_eq!(corpus.max_frames_per_level, 1);
assert_eq!(corpus.max_levels, None);
assert_eq!(corpus.max_level_elapsed, None);
assert_eq!(corpus.progress, None);
assert_eq!(corpus.max_sources, 100_000);
assert_eq!(corpus.max_depth, 64);
}
#[test]
fn frame_encode_request_constructors_validate_sample_geometry() {
let pixels = [0_u8; 2 * 2 * 3];
let samples = FrameSamples::new(&pixels, 2, 2, 3, 8, false).unwrap();
assert_eq!(samples.width, 2);
assert_eq!(samples.height, 2);
assert_eq!(samples.components, 3);
let request = J2kFrameEncodeRequest::new(
samples,
TransferSyntax::Htj2kLosslessRpcl,
EncodeBackendPreference::CpuOnly,
CodecValidation::RoundTrip,
);
assert_eq!(request.transfer_syntax, TransferSyntax::Htj2kLosslessRpcl);
assert_eq!(request.encode_backend, EncodeBackendPreference::CpuOnly);
assert_eq!(request.codec_validation, CodecValidation::RoundTrip);
let err = FrameSamples::new(&pixels[..3], 2, 2, 3, 8, false)
.expect_err("short frame data should be rejected");
assert!(err.to_string().contains("pixel"));
}
}