Skip to main content

wsi_rs/formats/
svcache.rs

1mod build;
2mod paths;
3mod reader;
4mod storage;
5
6#[cfg(test)]
7mod tests;
8
9pub use build::{
10    build_svcache, build_svcache_tile_payloads_merge, build_svcache_tile_payloads_replace,
11    build_svcache_tiles, build_svcache_tiles_replace,
12};
13pub(crate) use paths::resolve_open_path_with_policy;
14pub use paths::{cache_dir_svcache_path, default_svcache_path, svcache_candidate_paths};
15pub use storage::svcache_matches_source;
16
17use std::collections::{HashMap, HashSet};
18use std::fs::File;
19use std::io::{Read, Seek, SeekFrom, Write};
20use std::path::{Path, PathBuf};
21use std::sync::Mutex;
22use std::time::UNIX_EPOCH;
23
24use serde::{Deserialize, Serialize};
25use sha2::{Digest, Sha256};
26
27use crate::core::registry::{
28    DatasetReader, FormatProbe, FormatRegistry, ProbeConfidence, ProbeResult, Slide, SlideReader,
29};
30use crate::core::types::{
31    AssociatedImage, AxesShape, ChannelInfo, ColorSpace, CpuTile, CpuTileLayout, Dataset,
32    DatasetId, Level, LevelIdx, PlaneIdx, PlaneSelection, SampleType, Scene, SceneId, Series,
33    SeriesId, TileLayout, TileOutputPreference, TilePixels, TileRequest, TileViewRequest,
34};
35use crate::error::WsiError;
36use crate::properties::Properties;
37
38const MAGIC: &[u8; 8] = b"SVCACHE1";
39const SCHEMA_VERSION: u32 = 2;
40const DEFAULT_TILE_SIZE: u32 = 256;
41
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum SvcachePolicy {
45    #[default]
46    Off,
47    PreferFresh,
48    RequireFresh,
49}
50
51impl SvcachePolicy {
52    pub fn from_env_value(value: Option<&str>) -> Self {
53        match value.unwrap_or("off").trim().to_ascii_lowercase().as_str() {
54            "prefer" | "on" | "true" | "1" => Self::PreferFresh,
55            "required" | "require" => Self::RequireFresh,
56            _ => Self::Off,
57        }
58    }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62struct SvcacheMetadata {
63    schema_version: u32,
64    #[serde(default = "default_cache_complete")]
65    complete: bool,
66    source: SourceFingerprint,
67    properties: Vec<(String, String)>,
68    scenes: Vec<SceneMeta>,
69    associated: Vec<AssociatedMeta>,
70}
71
72fn default_cache_complete() -> bool {
73    true
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77struct SourceFingerprint {
78    path: String,
79    len: u64,
80    modified_unix_nanos: Option<u128>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84struct SceneMeta {
85    id: String,
86    name: Option<String>,
87    series: Vec<SeriesMeta>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91struct SeriesMeta {
92    id: String,
93    axes: AxesMeta,
94    sample_type: SampleTypeMeta,
95    channels: Vec<ChannelMeta>,
96    levels: Vec<LevelMeta>,
97}
98
99#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
100struct AxesMeta {
101    z: u32,
102    c: u32,
103    t: u32,
104}
105
106#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
107enum SampleTypeMeta {
108    Uint8,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112struct ChannelMeta {
113    name: Option<String>,
114    color: Option<[u8; 3]>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118struct LevelMeta {
119    dimensions: (u64, u64),
120    downsample: f64,
121    tile_width: u32,
122    tile_height: u32,
123    tiles_across: u64,
124    tiles_down: u64,
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    tiles: Vec<Option<TileMeta>>,
127    #[serde(default, skip_serializing_if = "Vec::is_empty")]
128    sparse_tiles: Vec<SparseTileMeta>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132struct SparseTileMeta {
133    index: u64,
134    tile: TileMeta,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138struct AssociatedMeta {
139    name: String,
140    dimensions: (u32, u32),
141    tile: TileMeta,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145struct TileMeta {
146    payload_offset: u64,
147    payload_len: u64,
148    decoded_len: usize,
149    width: u32,
150    height: u32,
151    channels: u16,
152    color_space: ColorSpaceMeta,
153    codec: PayloadCodec,
154    sha256: String,
155}
156
157impl LevelMeta {
158    fn tile_meta_for_index(&self, index: u64) -> Option<&TileMeta> {
159        if !self.tiles.is_empty() {
160            return usize::try_from(index)
161                .ok()
162                .and_then(|idx| self.tiles.get(idx))
163                .and_then(Option::as_ref);
164        }
165        self.sparse_tiles
166            .binary_search_by_key(&index, |entry| entry.index)
167            .ok()
168            .map(|idx| &self.sparse_tiles[idx].tile)
169    }
170
171    fn insert_tile_for_index(&mut self, index: u64, tile: TileMeta) {
172        if !self.tiles.is_empty() {
173            if let Ok(idx) = usize::try_from(index) {
174                if let Some(slot) = self.tiles.get_mut(idx) {
175                    *slot = Some(tile);
176                }
177            }
178            return;
179        }
180
181        match self
182            .sparse_tiles
183            .binary_search_by_key(&index, |entry| entry.index)
184        {
185            Ok(idx) => self.sparse_tiles[idx].tile = tile,
186            Err(idx) => self
187                .sparse_tiles
188                .insert(idx, SparseTileMeta { index, tile }),
189        }
190    }
191}
192
193#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
194enum PayloadCodec {
195    Zstd,
196}
197
198#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
199enum ColorSpaceMeta {
200    Rgb,
201    Rgba,
202    Grayscale,
203}
204
205pub struct SvcacheBackend;
206
207pub struct SvcacheReader {
208    file: Mutex<File>,
209    payload_start: u64,
210    metadata: SvcacheMetadata,
211    dataset: Dataset,
212    associated_index: HashMap<String, usize>,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216#[non_exhaustive]
217pub struct SvcacheTileSelection {
218    pub scene: SceneId,
219    pub series: SeriesId,
220    pub level: LevelIdx,
221    pub plane: PlaneIdx,
222    pub col: i64,
223    pub row: i64,
224}
225
226impl SvcacheTileSelection {
227    pub fn new(
228        scene: impl Into<SceneId>,
229        series: impl Into<SeriesId>,
230        level: impl Into<LevelIdx>,
231        col: i64,
232        row: i64,
233    ) -> Self {
234        Self {
235            scene: scene.into(),
236            series: series.into(),
237            level: level.into(),
238            plane: PlaneIdx::default(),
239            col,
240            row,
241        }
242    }
243
244    pub fn with_plane(mut self, plane: impl Into<PlaneIdx>) -> Self {
245        self.plane = plane.into();
246        self
247    }
248}