1717from tiffslide import TiffSlide
1818
1919from .config import settings
20+ from .identity import IDENTITY_VERSION , TILE_METADATA_SCHEMA_VERSION , decode_policy_version
2021from .metrics import DECODE_SOURCE_PIXELS
2122
2223TILE_SIZE = settings .tile_size
23- DECODE_POLICY_VERSION = (
24- f"geometry-v1;tile-max={ settings .max_decode_pixels } ;"
25- f"thumbnail-max={ settings .thumbnail_max_decode_pixels } "
26- )
24+ DECODE_POLICY_VERSION = decode_policy_version ()
2725
2826
2927class OverviewTooLarge (RuntimeError ):
@@ -115,7 +113,65 @@ def max_zoom(slide: TiffSlide) -> int:
115113 rounding to the nearest power-of-two pyramid level).
116114 """
117115 w , h = slide .dimensions
118- return math .ceil (math .log2 (max (w , h ) / TILE_SIZE ))
116+ return max (0 , math .ceil (math .log2 (max (w , h ) / TILE_SIZE )))
117+
118+
119+ def _best_level_for_downsample (level_downsamples : list [float ], downsample : float ) -> int :
120+ """Match tiffslide's level selection without opening or decoding a slide."""
121+ if downsample <= 1.0 :
122+ return 0
123+ for level , level_downsample in enumerate (level_downsamples ):
124+ if level_downsample > downsample :
125+ return max (0 , level - 1 )
126+ return len (level_downsamples ) - 1
127+
128+
129+ def safe_min_level_from_geometry (
130+ * ,
131+ width : int ,
132+ height : int ,
133+ level_dimensions : list [tuple [int , int ]],
134+ level_downsamples : list [float ],
135+ tile_size : int ,
136+ max_decode_pixels : int ,
137+ ) -> int | None :
138+ """Compute the first safe ZXY level using only intrinsic pyramid geometry."""
139+ if (
140+ width <= 0
141+ or height <= 0
142+ or not level_dimensions
143+ or len (level_dimensions ) != len (level_downsamples )
144+ or tile_size <= 0
145+ or max_decode_pixels <= 0
146+ ):
147+ return None
148+ pyramid_max_zoom = max (0 , math .ceil (math .log2 (max (width , height ) / tile_size )))
149+ for z in range (pyramid_max_zoom + 1 ):
150+ target_ds = 2 ** (pyramid_max_zoom - z )
151+ tiles_x = max (1 , math .ceil (width / (tile_size * target_ds )))
152+ tiles_y = max (1 , math .ceil (height / (tile_size * target_ds )))
153+ worst_pixels = 0
154+ best_level = _best_level_for_downsample (level_downsamples , target_ds )
155+ level_w , level_h = level_dimensions [best_level ]
156+ level_ds = max (1.0 , float (level_downsamples [best_level ]))
157+ for x in {0 , tiles_x - 1 }:
158+ for y in {0 , tiles_y - 1 }:
159+ x0 = x * tile_size * target_ds
160+ y0 = y * tile_size * target_ds
161+ src_w = min (tile_size * target_ds , width - x0 )
162+ src_h = min (tile_size * target_ds , height - y0 )
163+ read_w = min (
164+ math .ceil (src_w / level_ds ),
165+ level_w - math .floor (x0 / level_ds ),
166+ )
167+ read_h = min (
168+ math .ceil (src_h / level_ds ),
169+ level_h - math .floor (y0 / level_ds ),
170+ )
171+ worst_pixels = max (worst_pixels , max (0 , read_w ) * max (0 , read_h ))
172+ if worst_pixels <= max_decode_pixels :
173+ return z
174+ return None
119175
120176
121177def safe_min_level (slide : TiffSlide ) -> int | None :
@@ -125,33 +181,17 @@ def safe_min_level(slide: TiffSlide) -> int | None:
125181 result is emitted into offline tile metadata and remains advisory because
126182 the request path independently enforces the same pixel budget.
127183 """
128- slide_w , slide_h = slide .dimensions
129- for z in range (max_zoom (slide ) + 1 ):
130- target_ds = 2 ** (max_zoom (slide ) - z )
131- tiles_x = max (1 , math .ceil (slide_w / (TILE_SIZE * target_ds )))
132- tiles_y = max (1 , math .ceil (slide_h / (TILE_SIZE * target_ds )))
133- worst_pixels = 0
134- for x in {0 , tiles_x - 1 }:
135- for y in {0 , tiles_y - 1 }:
136- try :
137- _ , _ , x0 , y0 , src_w , src_h , _ , _ = _tile_geometry (slide , z , x , y )
138- best_level = slide .get_best_level_for_downsample (target_ds )
139- level_ds = max (1.0 , float (slide .level_downsamples [best_level ]))
140- level_w , level_h = slide .level_dimensions [best_level ]
141- read_w = min (
142- math .ceil (src_w / level_ds ),
143- level_w - math .floor (x0 / level_ds ),
144- )
145- read_h = min (
146- math .ceil (src_h / level_ds ),
147- level_h - math .floor (y0 / level_ds ),
148- )
149- worst_pixels = max (worst_pixels , max (0 , read_w ) * max (0 , read_h ))
150- except (IndexError , ValueError , TypeError ):
151- return None
152- if worst_pixels <= settings .max_decode_pixels :
153- return z
154- return None
184+ try :
185+ return safe_min_level_from_geometry (
186+ width = int (slide .dimensions [0 ]),
187+ height = int (slide .dimensions [1 ]),
188+ level_dimensions = [(int (width ), int (height )) for width , height in slide .level_dimensions ],
189+ level_downsamples = [float (value ) for value in slide .level_downsamples ],
190+ tile_size = TILE_SIZE ,
191+ max_decode_pixels = settings .max_decode_pixels ,
192+ )
193+ except (IndexError , ValueError , TypeError , ZeroDivisionError ):
194+ return None
155195
156196
157197def _slide_properties_metadata (slide : TiffSlide ) -> tuple [float , float , str , int | None ]:
@@ -187,7 +227,8 @@ def slide_metadata(slide: TiffSlide) -> dict:
187227 "objective_power" : objective_power ,
188228 "vendor" : vendor ,
189229 "safe_min_level" : safe_min_level (slide ),
190- "identity_version" : "v2" ,
230+ "identity_version" : IDENTITY_VERSION ,
231+ "tile_metadata_schema_version" : TILE_METADATA_SCHEMA_VERSION ,
191232 "decode_policy_version" : DECODE_POLICY_VERSION ,
192233 "max_decode_pixels" : settings .max_decode_pixels ,
193234 "thumbnail_max_decode_pixels" : settings .thumbnail_max_decode_pixels ,
0 commit comments