use serde::Deserialize;
use super::container;
use super::meta::ControlRef;
use super::{GeneratorRef, Limits, Meta, Metadata, PortableError, RendererRef};
#[derive(Debug, Deserialize)]
struct Header {
schema: u32,
generator: GeneratorRef,
renderer: RendererRef,
#[serde(default)]
meta: Option<Meta>,
#[serde(default)]
timeline: Option<serde::de::IgnoredAny>,
#[serde(default)]
controls: Vec<ControlRef>,
#[serde(default)]
figure: FigureHead,
}
#[derive(Debug, Default, Deserialize)]
struct FigureHead {
#[serde(default)]
suptitle_size: Option<serde::de::IgnoredAny>,
#[serde(default)]
supxlabel: Option<serde::de::IgnoredAny>,
#[serde(default)]
supylabel: Option<serde::de::IgnoredAny>,
#[serde(default)]
texts: Vec<serde::de::IgnoredAny>,
#[serde(default)]
rc: RcHead,
#[serde(default)]
axes: Vec<AxesHead>,
}
#[derive(Debug, Default, Deserialize)]
struct RcHead {
#[serde(default)]
path_simplify: Option<serde::de::IgnoredAny>,
#[serde(default)]
path_simplify_threshold: Option<serde::de::IgnoredAny>,
#[serde(default)]
axes_axisbelow: Option<serde::de::IgnoredAny>,
#[serde(default)]
axes_formatter_min_exponent: Option<serde::de::IgnoredAny>,
#[serde(default)]
lines_solid_capstyle: Option<serde::de::IgnoredAny>,
#[serde(default)]
lines_solid_joinstyle: Option<serde::de::IgnoredAny>,
#[serde(default)]
lines_dash_capstyle: Option<serde::de::IgnoredAny>,
#[serde(default)]
lines_dash_joinstyle: Option<serde::de::IgnoredAny>,
}
#[derive(Debug, Deserialize)]
struct AxesHead {
#[serde(default)]
title_style: Option<serde::de::IgnoredAny>,
#[serde(default)]
text_size: Option<serde::de::IgnoredAny>,
#[serde(default)]
artist_order: Option<serde::de::IgnoredAny>,
#[serde(default)]
legend: Vec<LegendHead>,
#[serde(default)]
lines: Vec<LineHead>,
#[serde(default)]
quivers: Vec<serde::de::IgnoredAny>,
#[serde(default)]
images: Vec<ImageHead>,
#[serde(default)]
colorbar: Option<serde::de::IgnoredAny>,
#[serde(default)]
meshes: Vec<ScaledHead>,
#[serde(default)]
collections: Vec<ScaledHead>,
#[serde(default)]
sky: Option<serde::de::IgnoredAny>,
#[serde(default)]
axisbelow: Option<serde::de::IgnoredAny>,
#[serde(default)]
poly_collections: Vec<ScaledHead>,
xaxis: AxisHead,
yaxis: AxisHead,
}
#[derive(Debug, Deserialize)]
struct ScaledHead {
#[serde(default)]
cmap: Option<String>,
#[serde(default)]
norm: Option<NormHead>,
#[serde(default)]
bad: Option<serde::de::IgnoredAny>,
#[serde(default)]
over: Option<serde::de::IgnoredAny>,
#[serde(default)]
under: Option<serde::de::IgnoredAny>,
#[serde(default)]
join: Option<serde::de::IgnoredAny>,
}
impl ScaledHead {
fn present(&self) -> bool {
self.cmap.is_some()
|| self.norm.is_some()
|| self.bad.is_some()
|| self.over.is_some()
|| self.under.is_some()
}
}
#[derive(Debug, Deserialize)]
struct ImageHead {
#[serde(default)]
cmap: Option<String>,
#[serde(default)]
norm: Option<NormHead>,
#[serde(default)]
bad: Option<serde::de::IgnoredAny>,
#[serde(default)]
over: Option<serde::de::IgnoredAny>,
#[serde(default)]
under: Option<serde::de::IgnoredAny>,
}
#[derive(Debug, Deserialize)]
struct NormHead {
kind: String,
}
impl NormHead {
fn is_image_normalize(norm: &Option<NormHead>) -> bool {
norm.as_ref().is_some_and(|n| n.kind == "image_normalize")
}
}
#[derive(Debug, Deserialize)]
struct LineHead {
#[serde(default)]
marker: Option<serde::de::IgnoredAny>,
#[serde(default)]
dash_cap: Option<serde::de::IgnoredAny>,
#[serde(default)]
dash_join: Option<serde::de::IgnoredAny>,
}
#[derive(Debug, Deserialize)]
struct LegendHead {
#[serde(default)]
marker: Option<serde::de::IgnoredAny>,
#[serde(default)]
line_marker: Option<serde::de::IgnoredAny>,
}
#[derive(Debug, Deserialize)]
struct AxisHead {
#[serde(default)]
minor: Option<MinorHead>,
#[serde(default)]
locator: TickerHead,
#[serde(default)]
formatter: TickerHead,
}
#[derive(Debug, Deserialize)]
struct MinorHead {
#[serde(default)]
locator: Option<TickerHead>,
}
#[derive(Debug, Default)]
struct TickerHead {
log_ticks: bool,
}
#[derive(Deserialize)]
struct TickerFields {
#[serde(default)]
auto: Option<bool>,
#[serde(default)]
min_exponent: Option<serde::de::IgnoredAny>,
}
impl<'de> Deserialize<'de> for TickerHead {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = TickerHead;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a locator or formatter")
}
fn visit_str<E>(self, _: &str) -> Result<TickerHead, E> {
Ok(TickerHead::default())
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<TickerHead, A::Error> {
let mut log_ticks = false;
while map.next_key::<serde::de::IgnoredAny>()?.is_some() {
let fields: TickerFields = map.next_value()?;
log_ticks |= fields.auto == Some(true) || fields.min_exponent.is_some();
}
Ok(TickerHead { log_ticks })
}
}
deserializer.deserialize_any(Visitor)
}
}
impl FigureHead {
fn uses_axis_styling(&self) -> bool {
self.axes.iter().any(|axes| {
axes.title_style.is_some() || axes.xaxis.minor.is_some() || axes.yaxis.minor.is_some()
})
}
fn uses_line_styles(&self) -> bool {
let rc = &self.rc;
rc.lines_solid_capstyle.is_some()
|| rc.lines_solid_joinstyle.is_some()
|| rc.lines_dash_capstyle.is_some()
|| rc.lines_dash_joinstyle.is_some()
|| self.axes.iter().any(|axes| {
axes.lines
.iter()
.any(|l| l.dash_cap.is_some() || l.dash_join.is_some())
})
}
fn uses_log_ticks(&self) -> bool {
let log_ticks = |axis: &AxisHead| {
axis.locator.log_ticks
|| axis.formatter.log_ticks
|| axis
.minor
.as_ref()
.and_then(|m| m.locator.as_ref())
.is_some_and(|l| l.log_ticks)
};
self.rc.axes_formatter_min_exponent.is_some()
|| self
.axes
.iter()
.any(|axes| log_ticks(&axes.xaxis) || log_ticks(&axes.yaxis))
}
fn uses_text_sizes(&self) -> bool {
self.suptitle_size.is_some() || self.axes.iter().any(|axes| axes.text_size.is_some())
}
fn uses_image_normalize(&self) -> bool {
self.axes.iter().any(|axes| {
axes.images
.iter()
.any(|i| NormHead::is_image_normalize(&i.norm))
|| axes
.meshes
.iter()
.chain(&axes.collections)
.any(|s| NormHead::is_image_normalize(&s.norm))
})
}
fn uses_embedded_colormaps(&self) -> bool {
let embedded = |cmap: &Option<String>| {
cmap.as_deref()
.is_some_and(crate::core::color::CmapSpec::is_embedded_wire)
};
self.axes.iter().any(|axes| {
axes.images.iter().any(|i| embedded(&i.cmap))
|| axes
.meshes
.iter()
.chain(&axes.collections)
.chain(&axes.poly_collections)
.any(|s| embedded(&s.cmap))
})
}
fn uses_colorbars(&self) -> bool {
self.axes.iter().any(|axes| {
axes.colorbar.is_some()
|| axes.meshes.iter().any(ScaledHead::present)
|| axes.collections.iter().any(ScaledHead::present)
})
}
fn uses_image_norms(&self) -> bool {
self.axes.iter().any(|axes| {
axes.images.iter().any(|image| {
image.norm.is_some()
|| image.bad.is_some()
|| image.over.is_some()
|| image.under.is_some()
})
})
}
fn uses_figure_text(&self) -> bool {
!self.texts.is_empty() || self.supxlabel.is_some() || self.supylabel.is_some()
}
fn uses_legend_markers(&self) -> bool {
self.axes.iter().any(|axes| {
axes.artist_order.is_some() || axes.legend.iter().any(|entry| entry.marker.is_some())
})
}
}
pub fn inspect(bytes: &[u8], limits: &Limits) -> Result<Metadata, PortableError> {
let dir = container::directory(bytes, limits)?;
let json = container::chunk(bytes, &dir, container::TAG_JSON).expect("directory checked JSON");
if json.len() > limits.max_json_bytes {
return Err(PortableError::Budget(format!(
"JSON chunk is {} bytes, over the {} byte limit",
json.len(),
limits.max_json_bytes
)));
}
let header: Header = serde_json::from_slice(json)?;
if let Some(meta) = &header.meta {
if let Some(poster) = &meta.poster {
if poster.bytes > limits.max_poster_bytes {
return Err(PortableError::Budget(format!(
"poster is {} bytes, over the {} byte limit",
poster.bytes, limits.max_poster_bytes
)));
}
let actual = container::chunk(bytes, &dir, container::TAG_PSTR).map(<[u8]>::len);
if actual != Some(poster.bytes) {
return Err(PortableError::Malformed(format!(
"spec declares a {}-byte poster but the PSTR chunk holds {}",
poster.bytes,
actual.map_or_else(|| "nothing".to_string(), |n| n.to_string())
)));
}
}
let pixels = (meta.width_px as usize).saturating_mul(meta.height_px as usize);
if pixels > limits.max_canvas_pixels {
return Err(PortableError::Budget(format!(
"figure is {}x{} = {pixels} pixels, over the {} pixel limit",
meta.width_px, meta.height_px, limits.max_canvas_pixels
)));
}
}
let required = super::required_schema(&[
(2, header.meta.is_some()),
(3, header.timeline.is_some()),
(4, !header.controls.is_empty()),
(5, header.figure.uses_axis_styling()),
(6, header.figure.uses_text_sizes()),
(7, header.figure.uses_legend_markers()),
(8, header.figure.uses_figure_text()),
(9, header.figure.uses_image_norms()),
(10, header.figure.uses_colorbars()),
(11, header.figure.uses_image_normalize()),
(
12,
header.figure.rc.path_simplify.is_some()
|| header.figure.rc.path_simplify_threshold.is_some(),
),
(13, header.figure.axes.iter().any(|axes| axes.sky.is_some())),
(
14,
header
.figure
.axes
.iter()
.any(|axes| !axes.poly_collections.is_empty()),
),
(15, header.figure.uses_embedded_colormaps()),
(
16,
header.figure.rc.axes_axisbelow.is_some()
|| header
.figure
.axes
.iter()
.any(|axes| axes.axisbelow.is_some()),
),
(
17,
header.figure.axes.iter().any(|axes| {
axes.lines.iter().any(|l| l.marker.is_some())
|| axes.legend.iter().any(|e| e.line_marker.is_some())
}),
),
(
18,
header
.figure
.axes
.iter()
.any(|axes| !axes.quivers.is_empty()),
),
(
19,
header
.figure
.axes
.iter()
.any(|axes| axes.collections.iter().any(|c| c.join.is_some())),
),
(20, header.figure.uses_log_ticks()),
(21, header.figure.uses_line_styles()),
]);
if header.schema < required {
return Err(PortableError::Malformed(format!(
"declares schema {} but carries features requiring schema {required}",
header.schema
)));
}
if header.controls.len() > limits.max_controls {
return Err(PortableError::Budget(format!(
"artifact declares {} controls, over the {} control limit",
header.controls.len(),
limits.max_controls
)));
}
if let Some(long) = header
.controls
.iter()
.find(|c| c.label.len() > limits.max_control_label_bytes)
{
return Err(PortableError::Budget(format!(
"a control label is {} bytes, over the {} byte limit",
long.label.len(),
limits.max_control_label_bytes
)));
}
for (i, c) in header.controls.iter().enumerate() {
if !(c.min.is_finite() && c.max.is_finite() && c.default.is_finite())
|| c.min >= c.max
|| !(c.min..=c.max).contains(&c.default)
{
return Err(PortableError::Malformed(format!(
"control {i} range [{}, {}] with default {} is not finite, \
increasing, and containing",
c.min, c.max, c.default
)));
}
if let Some(step) = c.step.filter(|step| !(step.is_finite() && *step > 0.0)) {
return Err(PortableError::Malformed(format!(
"control {i} step {step} is not finite and positive"
)));
}
}
Ok(Metadata {
schema: header.schema,
schema_supported: (super::SCHEMA_MIN..=super::SCHEMA_VERSION).contains(&header.schema),
generator: header.generator,
renderer: header.renderer,
meta: header.meta,
controls: header.controls,
chunks: dir,
total_bytes: bytes.len(),
})
}