1use std::collections::BTreeSet;
2
3use serde::Deserialize;
4
5use crate::{Error, Result};
6
7use super::super::json::validate_unique_object_keys;
8use super::super::model::{AlgorithmIdentification, DicomCode};
9use super::super::profile::{ProfileAlgorithm, ProfileCode};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum RasterInputFormat {
14 Tiff,
15 Npy,
16 Zarr,
17 TiledManifest,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum RasterChannelSelection {
22 Auto,
23 Index(usize),
24 Name(String),
25 All,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
29#[serde(rename_all = "kebab-case")]
30pub(crate) enum RasterDType {
31 Float32,
32 Float64,
33 Int8,
34 Int16,
35 Int32,
36 Int64,
37 Uint8,
38 Uint16,
39 Uint32,
40 Uint64,
41}
42
43impl RasterDType {
44 pub(crate) const fn is_integer(self) -> bool {
45 !matches!(self, Self::Float32 | Self::Float64)
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub(crate) enum RasterAxis {
52 Y,
53 X,
54 Channel,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
58#[serde(rename_all = "kebab-case")]
59pub(crate) enum RasterCoordinateSpace {
60 Level0Pixels,
61 SourcePixels,
62 SlideMm,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
66#[serde(rename_all = "lowercase")]
67pub(crate) enum RasterOutputPrecision {
68 Float32,
69 Float64,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
73#[serde(untagged)]
74pub(crate) enum IntegerSentinel {
75 Signed(i64),
76 Unsigned(u64),
77}
78
79#[derive(Debug, Clone, PartialEq)]
80pub(crate) struct IntegerScaling {
81 pub(crate) slope: f64,
82 pub(crate) intercept: f64,
83 pub(crate) missing_sentinel: IntegerSentinel,
84 pub(crate) output_precision: RasterOutputPrecision,
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub(crate) struct RasterChannel {
89 pub(crate) name: String,
90 pub(crate) quantity: DicomCode,
91 pub(crate) unit: DicomCode,
92}
93
94#[derive(Debug, Clone)]
95pub struct RasterProfile {
96 pub(crate) input_format: RasterInputFormat,
97 pub(crate) dtype: RasterDType,
98 pub(crate) axes: Vec<RasterAxis>,
99 pub(crate) grid_origin: GridOrigin,
100 pub(crate) sample_spacing: SampleSpacing,
101 pub(crate) coordinate_space: RasterCoordinateSpace,
102 pub(crate) channels: Vec<RasterChannel>,
103 pub(crate) algorithm: AlgorithmIdentification,
104 pub(crate) integer_scaling: Option<IntegerScaling>,
105 pub(crate) zarr_array_path: Option<String>,
106}
107
108impl RasterProfile {
109 pub fn from_json(json: &[u8]) -> Result<Self> {
110 validate_unique_object_keys(json, "raster profile")?;
111 let raw: RawRasterProfile = serde_json::from_slice(json).map_err(|error| {
112 Error::InvalidInput(format!("raster profile is not valid JSON: {error}"))
113 })?;
114 raw.validate_and_build()
115 }
116
117 #[must_use]
118 pub const fn input_format(&self) -> RasterInputFormat {
119 self.input_format
120 }
121
122 #[must_use]
123 pub fn channel_count(&self) -> usize {
124 self.channels.len()
125 }
126
127 #[must_use]
128 pub fn channel_name(&self, index: usize) -> Option<&str> {
129 self.channels
130 .get(index)
131 .map(|channel| channel.name.as_str())
132 }
133
134 pub fn select_channels(&self, selection: RasterChannelSelection) -> Result<Vec<usize>> {
135 if self.channels.len() == 1 {
136 return match selection {
137 RasterChannelSelection::Auto => Ok(vec![0]),
138 _ => Err(Error::InvalidInput(
139 "single-channel raster inputs do not accept a channel option".into(),
140 )),
141 };
142 }
143 match selection {
144 RasterChannelSelection::Auto => Err(Error::InvalidInput(
145 "multichannel raster input requires exactly one channel or all channels".into(),
146 )),
147 RasterChannelSelection::Index(index) if index < self.channels.len() => Ok(vec![index]),
148 RasterChannelSelection::Index(index) => Err(Error::InvalidInput(format!(
149 "channel index {index} is outside 0..{}",
150 self.channels.len()
151 ))),
152 RasterChannelSelection::Name(name) => self
153 .channels
154 .iter()
155 .position(|channel| channel.name == name)
156 .map(|index| vec![index])
157 .ok_or_else(|| {
158 Error::InvalidInput(format!("channel name {name:?} is not declared"))
159 }),
160 RasterChannelSelection::All => Ok((0..self.channels.len()).collect()),
161 }
162 }
163
164 pub(crate) fn axis_index(&self, axis: RasterAxis) -> Option<usize> {
165 self.axes.iter().position(|candidate| *candidate == axis)
166 }
167}
168
169#[derive(Debug, Deserialize)]
170#[serde(deny_unknown_fields)]
171struct RawRasterProfile {
172 schema_version: u32,
173 input_format: RasterInputFormat,
174 dtype: RasterDType,
175 axes: Vec<RasterAxis>,
176 grid_origin: GridOrigin,
177 sample_spacing: SampleSpacing,
178 coordinate_space: RasterCoordinateSpace,
179 channels: Vec<RawRasterChannel>,
180 algorithm: ProfileAlgorithm,
181 #[serde(default)]
182 integer_scaling: Option<RawIntegerScaling>,
183 #[serde(default)]
184 zarr_array_path: Option<String>,
185}
186
187impl RawRasterProfile {
188 fn validate_and_build(self) -> Result<RasterProfile> {
189 if self.schema_version != 1 {
190 return Err(Error::Unsupported(format!(
191 "raster profile schema version {} is not supported",
192 self.schema_version
193 )));
194 }
195 validate_axes(&self.axes)?;
196 self.grid_origin.validate(self.coordinate_space)?;
197 self.sample_spacing.validate()?;
198 let channels = build_channels(self.channels)?;
199 let integer_scaling = self
200 .integer_scaling
201 .map(RawIntegerScaling::build)
202 .transpose()?;
203 if self.dtype.is_integer() != integer_scaling.is_some() {
204 return Err(Error::InvalidInput(
205 "integer dtypes require integer_scaling, which is forbidden for float dtypes"
206 .into(),
207 ));
208 }
209 validate_zarr_path(self.input_format, self.zarr_array_path.as_deref())?;
210 Ok(RasterProfile {
211 input_format: self.input_format,
212 dtype: self.dtype,
213 axes: self.axes,
214 grid_origin: self.grid_origin,
215 sample_spacing: self.sample_spacing,
216 coordinate_space: self.coordinate_space,
217 channels,
218 algorithm: self.algorithm.into_algorithm()?,
219 integer_scaling,
220 zarr_array_path: self.zarr_array_path,
221 })
222 }
223}
224
225fn validate_axes(axes: &[RasterAxis]) -> Result<()> {
226 if !matches!(axes.len(), 2 | 3)
227 || axes.iter().filter(|axis| **axis == RasterAxis::Y).count() != 1
228 || axes.iter().filter(|axis| **axis == RasterAxis::X).count() != 1
229 || axes
230 .iter()
231 .filter(|axis| **axis == RasterAxis::Channel)
232 .count()
233 != axes.len() - 2
234 {
235 return Err(Error::InvalidInput(
236 "axes must contain y and x exactly once, plus at most one channel axis".into(),
237 ));
238 }
239 Ok(())
240}
241
242fn build_channels(raw: Vec<RawRasterChannel>) -> Result<Vec<RasterChannel>> {
243 if raw.is_empty() {
244 return Err(Error::InvalidInput(
245 "raster profile must declare at least one channel".into(),
246 ));
247 }
248 let mut names = BTreeSet::new();
249 raw.into_iter()
250 .map(|channel| {
251 if channel.name.trim().is_empty()
252 || channel.name.contains('\0')
253 || !names.insert(channel.name.clone())
254 {
255 return Err(Error::InvalidInput(
256 "raster channel names must be nonempty, NUL-free, and unique".into(),
257 ));
258 }
259 Ok(RasterChannel {
260 name: channel.name,
261 quantity: channel.quantity.into_code()?,
262 unit: channel.unit.into_code()?,
263 })
264 })
265 .collect()
266}
267
268fn validate_zarr_path(format: RasterInputFormat, path: Option<&str>) -> Result<()> {
269 match (format, path) {
270 (RasterInputFormat::Zarr, Some(path))
271 if !path.trim_matches('/').is_empty()
272 && !path.split('/').any(|part| matches!(part, "" | "." | "..")) =>
273 {
274 Ok(())
275 }
276 (RasterInputFormat::Zarr, _) => Err(Error::InvalidInput(
277 "zarr input requires an explicit local zarr_array_path".into(),
278 )),
279 (_, None) => Ok(()),
280 (_, Some(_)) => Err(Error::InvalidInput(
281 "zarr_array_path is valid only for zarr input".into(),
282 )),
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
287#[serde(deny_unknown_fields)]
288pub(crate) struct GridOrigin {
289 pub(crate) x: f64,
290 pub(crate) y: f64,
291 #[serde(default)]
292 pub(crate) z: Option<f64>,
293}
294
295impl GridOrigin {
296 fn validate(self, coordinate_space: RasterCoordinateSpace) -> Result<()> {
297 if !self.x.is_finite() || !self.y.is_finite() || self.z.is_some_and(|z| !z.is_finite()) {
298 return Err(Error::InvalidInput(
299 "grid origin coordinates must be finite".into(),
300 ));
301 }
302 if (coordinate_space == RasterCoordinateSpace::SlideMm) != self.z.is_some() {
303 return Err(Error::InvalidInput(
304 "slide-mm grid origin requires z; pixel coordinate origins must omit z".into(),
305 ));
306 }
307 Ok(())
308 }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
312#[serde(deny_unknown_fields)]
313pub(crate) struct SampleSpacing {
314 pub(crate) x: f64,
315 pub(crate) y: f64,
316}
317
318impl SampleSpacing {
319 fn validate(self) -> Result<()> {
320 if !self.x.is_finite() || !self.y.is_finite() || self.x <= 0.0 || self.y <= 0.0 {
321 return Err(Error::InvalidInput(
322 "sample spacing must contain positive finite x and y values".into(),
323 ));
324 }
325 Ok(())
326 }
327}
328
329#[derive(Debug, Deserialize)]
330#[serde(deny_unknown_fields)]
331struct RawRasterChannel {
332 name: String,
333 quantity: ProfileCode,
334 unit: ProfileCode,
335}
336
337#[derive(Debug, Deserialize)]
338#[serde(deny_unknown_fields)]
339struct RawIntegerScaling {
340 slope: f64,
341 intercept: f64,
342 missing_sentinel: IntegerSentinel,
343 output_precision: RasterOutputPrecision,
344}
345
346impl RawIntegerScaling {
347 fn build(self) -> Result<IntegerScaling> {
348 if !self.slope.is_finite() || !self.intercept.is_finite() || self.slope == 0.0 {
349 return Err(Error::InvalidInput(
350 "integer scaling slope must be nonzero and slope/intercept must be finite".into(),
351 ));
352 }
353 Ok(IntegerScaling {
354 slope: self.slope,
355 intercept: self.intercept,
356 missing_sentinel: self.missing_sentinel,
357 output_precision: self.output_precision,
358 })
359 }
360}