Skip to main content

async_geotiff/style/raster/
compiler.rs

1//! SDK adaptation and supported-capability validation.
2use super::{Colors, RasterMosaic, RasterPlan};
3use crate::{
4    error::{AgError, AgResult},
5    render::{colormap::builtin_lut, expression},
6    style::{
7        stretch::{Statistics, Stretch},
8        RasterStyle, TileFormat, TileStyle,
9    },
10    warp::resampler::Resampling,
11};
12use serde_json::Value;
13use std::sync::Arc;
14
15fn unsupported(path: &str) -> AgError {
16    AgError::query(path, "not supported by the Raster Style adapter")
17}
18
19fn sdk_error(error: raster_style::Error) -> AgError {
20    AgError::query(
21        if error.path.is_empty() {
22            "style"
23        } else {
24            &error.path
25        },
26        error.to_string(),
27    )
28}
29
30// The SDK has already checked types. This second boundary rejects capabilities
31// we cannot execute instead of silently accepting fields from the larger spec.
32fn only(value: &Value, allowed: &[&str], path: &str) -> AgResult<()> {
33    if let Some(object) = value.as_object() {
34        for key in object.keys() {
35            if !allowed.contains(&key.as_str()) {
36                return Err(unsupported(&format!("{path}/{key}")));
37            }
38        }
39    }
40    Ok(())
41}
42
43impl TileStyle {
44    /// Parse Raster Style 3.0 JSON with versioned Mosaic and Tile extensions.
45    /// Unsupported renderer capabilities fail before any raster is opened.
46    pub fn from_json(json: &str) -> AgResult<Self> {
47        Self::from_raster_style(&RasterStyle::from_json(json).map_err(sdk_error)?)
48    }
49
50    /// Parse an original, percent-encoded Q3 query. Do not URL-decode it first.
51    pub fn from_raster_query(query: &str) -> AgResult<Self> {
52        Self::from_raster_style(&raster_style::decode_query(query).map_err(sdk_error)?)
53    }
54
55    /// Adapt an SDK-validated document to the supported renderer.
56    pub fn from_raster_style(style: &RasterStyle) -> AgResult<Self> {
57        let supported: &[&str] = if cfg!(feature = "mosaic") {
58            &[
59                raster_style::TILE_SCHEMA_URL,
60                raster_style::MOSAIC_SCHEMA_URL,
61            ]
62        } else {
63            &[raster_style::TILE_SCHEMA_URL]
64        };
65        style
66            .require_extension_support(supported)
67            .map_err(sdk_error)?;
68        let v = style.as_value();
69        only(
70            v,
71            &[
72                "version",
73                "renderer",
74                "stretch",
75                "resampling",
76                "nodata",
77                "effects",
78                "opacity",
79                "extensions",
80                "raster:tile",
81                "calibration",
82                "raster:mosaic",
83                "statistics",
84            ],
85            "",
86        )?;
87        let renderer = &v["renderer"];
88        only(
89            renderer,
90            &[
91                "type",
92                "bidx",
93                "expression",
94                "language",
95                "colormap_name",
96                "colormap",
97                "renderer_invert",
98                "color",
99            ],
100            "/renderer",
101        )?;
102        if v.pointer("/calibration/mode")
103            .is_some_and(|mode| mode != "none")
104        {
105            return Err(unsupported("/calibration"));
106        }
107        let kind = renderer["type"].as_str().unwrap_or_default();
108        if !["gray", "rgb", "pseudocolor", "categorized", "single_color"].contains(&kind) {
109            return Err(unsupported("/renderer/type"));
110        }
111        let inputs = compile_inputs(renderer)?;
112        let mut dependencies: Vec<_> = inputs
113            .iter()
114            .flatten()
115            .filter_map(|token| {
116                if let expression::Token::Band(b) = token {
117                    Some(*b)
118                } else {
119                    None
120                }
121            })
122            .collect();
123        dependencies.sort_unstable();
124        dependencies.dedup();
125
126        let mosaic = if let Some(mosaic) = v.get("raster:mosaic") {
127            if !cfg!(feature = "mosaic") {
128                return Err(unsupported("/raster:mosaic (enable the mosaic feature)"));
129            }
130            let selection = mosaic["pixel_selection"]
131                .as_str()
132                .expect("validated selection")
133                .to_owned();
134            let after_channels = mosaic["stage"] == "after_channels";
135            let rank = mosaic["rank_channel"].as_f64().unwrap_or(1.0) as usize - 1;
136            let channels = if after_channels {
137                inputs.len()
138            } else {
139                dependencies.len()
140            };
141            if matches!(selection.as_str(), "highest" | "lowest") && rank >= channels {
142                return Err(AgError::query(
143                    "/raster:mosaic/rank_channel",
144                    "outside the compiled channel dependencies",
145                ));
146            }
147            Some(RasterMosaic {
148                #[cfg(feature = "mosaic")]
149                selection,
150                after_channels,
151                #[cfg(feature = "mosaic")]
152                rank,
153            })
154        } else {
155            None
156        };
157
158        let stretch = &v["stretch"];
159        let method = stretch["method"].as_str().unwrap_or("none");
160        let transfer = Stretch::parse(stretch, inputs.len());
161        let statistics = Statistics::parse(&v["statistics"])?;
162
163        only(&v["effects"], &["color_formula"], "/effects")?;
164        let mut gamma = Vec::new();
165        if let Some(ops) = v
166            .pointer("/effects/color_formula")
167            .and_then(Value::as_array)
168        {
169            for op in ops {
170                if op["op"] != "gamma" {
171                    return Err(unsupported("/effects/color_formula"));
172                }
173                gamma.push((
174                    op["channels"]
175                        .as_str()
176                        .expect("normalized channels")
177                        .to_owned(),
178                    op["value"].as_f64().expect("validated gamma"),
179                ));
180            }
181        }
182        let data_domain = method == "none" && gamma.is_empty();
183        let colors = compile_colors(renderer, kind, data_domain)?;
184        only(
185            &v["raster:tile"],
186            &["format", "tilesize", "lossless"],
187            "/raster:tile",
188        )?;
189        let format = match v
190            .pointer("/raster:tile/format")
191            .and_then(Value::as_str)
192            .unwrap_or("png")
193        {
194            "png" => TileFormat::Png,
195            "webp" if v.pointer("/raster:tile/lossless") == Some(&Value::Bool(true)) => {
196                TileFormat::Webp
197            }
198            _ => {
199                return Err(unsupported(
200                    "/raster:tile/format (use png or lossless webp)",
201                ))
202            }
203        };
204        format.ensure_available()?;
205        let resampler = |key: &str| -> AgResult<Resampling> {
206            let name = v["resampling"][key].as_str().unwrap_or("nearest");
207            if name == "mode" {
208                return Err(unsupported(&format!("/resampling/{key} (mode Alpha)")));
209            }
210            Resampling::parse(name).ok_or_else(|| unsupported(&format!("/resampling/{key}")))
211        };
212        let (statistics_key, stretch_key) = if transfer.needs_statistics() {
213            let key = serde_json::json!({
214                "bidx": renderer.get("bidx"), "expression": renderer.get("expression"),
215                "language": renderer.get("language"), "resampling": v.get("resampling"),
216                "nodata": v.get("nodata"), "calibration": v.get("calibration"),
217                "raster:mosaic": v.get("raster:mosaic"), "statistics": v.get("statistics")
218            })
219            .to_string();
220            let mut stretch = stretch.clone();
221            if let Some(object) = stretch.as_object_mut() {
222                object.remove("range_policy");
223            }
224            (key, stretch.to_string())
225        } else {
226            (String::new(), String::new())
227        };
228        let out = Self {
229            nodata: v["nodata"]
230                .as_f64()
231                .or_else(|| (v["nodata"] == "nan").then_some(f64::NAN)),
232            resampling: resampler("read")?,
233            reproject: resampler("reproject")?,
234            tilesize: v
235                .pointer("/raster:tile/tilesize")
236                .and_then(Value::as_f64)
237                .unwrap_or(256.0) as u32,
238            format,
239            raster: Arc::new(RasterPlan {
240                key: style.to_json().map_err(sdk_error)?,
241                categorical: kind == "categorized",
242                mosaic,
243                inputs,
244                dependencies,
245                stretch: transfer,
246                statistics,
247                transparent: stretch["range_policy"] == "transparent",
248                colors,
249                gamma,
250                opacity: v["opacity"].as_f64().unwrap_or(1.0),
251                statistics_key,
252                stretch_key,
253                rgb_luts: Arc::default(),
254            }),
255        };
256        if !crate::style::params::ALLOWED_TILE_SIZES.contains(&out.tilesize) {
257            return Err(unsupported(
258                "/raster:tile/tilesize (use 64, 128, 256, 512 or 1024)",
259            ));
260        }
261        Ok(out)
262    }
263
264    /// The normalized Raster Style protocol document.
265    pub fn raster_style_json(&self) -> &str {
266        &self.raster.key
267    }
268}
269
270fn rgba_color(v: &Value) -> [u8; 4] {
271    std::array::from_fn(|i| v[i].as_f64().expect("validated RGBA") as u8)
272}
273
274fn hex_color(text: &str) -> [u8; 4] {
275    std::array::from_fn(|i| {
276        u8::from_str_radix(&text[1 + i * 2..3 + i * 2], 16).expect("normalized color")
277    })
278}
279
280fn compile_inputs(renderer: &Value) -> AgResult<Vec<Vec<expression::Token>>> {
281    let inputs: Vec<Vec<expression::Token>> = if let Some(bands) = renderer["bidx"].as_array() {
282        bands
283            .iter()
284            .map(|b| {
285                vec![expression::Token::Band(
286                    b.as_f64().expect("validated band") as usize - 1,
287                )]
288            })
289            .collect()
290    } else if let Some(text) = renderer["expression"].as_str() {
291        text.split(';')
292            .map(expression::parse_expression)
293            .collect::<AgResult<_>>()?
294    } else {
295        return Err(unsupported("/renderer/index"));
296    };
297    Ok(inputs)
298}
299
300fn compile_colors(renderer: &Value, kind: &str, data_domain: bool) -> AgResult<Colors> {
301    let colors = match kind {
302        "rgb" => Colors::Rgb,
303        "gray" => Colors::Gray(renderer["renderer_invert"].as_bool().unwrap_or(false)),
304        "single_color" => Colors::Solid(hex_color(
305            renderer["color"].as_str().expect("validated color"),
306        )),
307        _ => {
308            if let Some(name) = renderer["colormap_name"].as_str() {
309                if kind == "categorized" {
310                    return Err(unsupported("/renderer/colormap_name"));
311                }
312                Colors::Named(Box::new(builtin_lut(name)?))
313            } else if let Some(map) = renderer["colormap"].as_object() {
314                let mut entries = map
315                    .iter()
316                    .map(|(key, rgba)| {
317                        Ok((
318                            key.parse::<f64>()
319                                .map_err(|_| unsupported("/renderer/colormap"))?,
320                            rgba_color(rgba),
321                        ))
322                    })
323                    .collect::<AgResult<Vec<_>>>()?;
324                entries.sort_unstable_by(|a, b| a.0.total_cmp(&b.0));
325                Colors::Exact(entries, data_domain)
326            } else if let Some(intervals) = renderer["colormap"].as_array() {
327                Colors::Intervals(
328                    intervals
329                        .iter()
330                        .map(|entry| {
331                            (
332                                entry[0][0].as_f64().expect("validated bound"),
333                                entry[0][1].as_f64().expect("validated bound"),
334                                rgba_color(&entry[1]),
335                            )
336                        })
337                        .collect(),
338                    data_domain,
339                )
340            } else {
341                return Err(unsupported("/renderer/color_mapping"));
342            }
343        }
344    };
345    Ok(colors)
346}