11"""Handler for wsi files using OpenSlide."""
22
3+ import re
34from pathlib import Path
45from typing import Any
56
67import defusedxml .ElementTree as ET # noqa: N817
78import openslide
89from loguru import logger
910from openslide import ImageSlide , OpenSlide , open_slide
11+ from packaging import version
1012from PIL .Image import Image
1113
1214TIFF_IMAGE_DESCRIPTION = "tiff.ImageDescription"
@@ -62,6 +64,34 @@ def _detect_format(self) -> str | None:
6264
6365 return base_format
6466
67+ @staticmethod
68+ def _get_mpp_correction_factor (props : dict [str , Any ]) -> float :
69+ """Handle a scaling bug in libvips<8.8.3 for tiff files.
70+
71+ libvips<8.8.3 had a bug which wrote the tiff.XResolution as px / mm, but it should be
72+ px / cm. Therefore, the resolution is 10x smaller than expected. To counteract, one has
73+ to multiply the mpp with 0.1. Source: https://github.com/libvips/libvips/issues/1421
74+
75+ Returns:
76+ float: Correction factor (0.1 for buggy versions, 1.0 otherwise).
77+ """
78+ LEGACY_MPP_FACTOR = 1 / 10 # noqa: N806
79+
80+ try :
81+ xml_string = props [TIFF_IMAGE_DESCRIPTION ]
82+
83+ # Match custom metadata for library version used during export
84+ libvips_version_match = re .findall (r"libVips-version.*?(\d+\.\d+\.\d+)" , xml_string , re .DOTALL )
85+ if not libvips_version_match :
86+ return LEGACY_MPP_FACTOR
87+
88+ if version .parse (libvips_version_match [0 ]) >= version .parse ("8.8.3" ):
89+ # Bug-free libvips version was used during initial pyramid export
90+ return 1.0
91+ return LEGACY_MPP_FACTOR
92+ except Exception :
93+ return LEGACY_MPP_FACTOR
94+
6595 def get_thumbnail (self ) -> Image :
6696 """Get thumbnail of the slide.
6797
@@ -122,7 +152,7 @@ def _parse_xml_image_description(self, xml_string: str) -> dict[str, Any]: # no
122152 except ET .ParseError :
123153 return {}
124154
125- def _get_level_info (self ) -> list [dict [str , Any ]]:
155+ def _get_level_info (self , mpp_correction_factor : float ) -> list [dict [str , Any ]]:
126156 """Get detailed information for each level.
127157
128158 Returns:
@@ -131,8 +161,9 @@ def _get_level_info(self) -> list[dict[str, Any]]:
131161 """
132162 levels = []
133163 props = dict (self .slide .properties )
134- base_mpp_x = float (props .get (openslide .PROPERTY_NAME_MPP_X , 0 ))
135- base_mpp_y = float (props .get (openslide .PROPERTY_NAME_MPP_Y , 0 ))
164+ mpp_correction_factor = self ._get_mpp_correction_factor (props ) if "tiff.XResolution" in props else 1.0
165+ base_mpp_x = float (props .get (openslide .PROPERTY_NAME_MPP_X , 0 )) * mpp_correction_factor
166+ base_mpp_y = float (props .get (openslide .PROPERTY_NAME_MPP_Y , 0 )) * mpp_correction_factor
136167
137168 for level in range (self .slide .level_count ):
138169 width , height = self .slide .level_dimensions [level ]
@@ -178,6 +209,7 @@ def get_metadata(self) -> dict[str, Any]:
178209 props = dict (self .slide .properties )
179210 file_size = self .path .stat ().st_size
180211 base_width , base_height = self .slide .dimensions
212+ mpp_correction_factor = self ._get_mpp_correction_factor (props )
181213
182214 metadata = {
183215 "format" : self ._detect_format (),
@@ -188,8 +220,8 @@ def get_metadata(self) -> dict[str, Any]:
188220 },
189221 "dimensions" : {"width" : base_width , "height" : base_height },
190222 "resolution" : {
191- "mpp_x" : float (props .get (openslide .PROPERTY_NAME_MPP_X , 0 )),
192- "mpp_y" : float (props .get (openslide .PROPERTY_NAME_MPP_Y , 0 )),
223+ "mpp_x" : float (props .get (openslide .PROPERTY_NAME_MPP_X , 0 )) * mpp_correction_factor ,
224+ "mpp_y" : float (props .get (openslide .PROPERTY_NAME_MPP_Y , 0 )) * mpp_correction_factor ,
193225 "unit" : props .get ("tiff.ResolutionUnit" , "unknown" ),
194226 "x_resolution" : float (props .get ("tiff.XResolution" , 0 )),
195227 "y_resolution" : float (props .get ("tiff.YResolution" , 0 )),
@@ -204,7 +236,7 @@ def get_metadata(self) -> dict[str, Any]:
204236 "width" : int (props .get ("openslide.level[0].tile-width" , 256 )),
205237 "height" : int (props .get ("openslide.level[0].tile-height" , 256 )),
206238 },
207- "levels" : {"count" : self .slide .level_count , "data" : self ._get_level_info ()},
239+ "levels" : {"count" : self .slide .level_count , "data" : self ._get_level_info (mpp_correction_factor )},
208240 "extra" : ", " .join ([
209241 props .get ("dicom.ImageType[0]" , "0" ),
210242 props .get ("dicom.ImageType[1]" , "1" ),
0 commit comments