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}