Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions openslide/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
This package provides Python bindings for the OpenSlide library.
"""

from abc import ABCMeta, abstractmethod
from collections.abc import Mapping
from io import BytesIO
import os

from PIL import Image, ImageCms

Expand Down Expand Up @@ -53,7 +55,7 @@
PROPERTY_NAME_BOUNDS_HEIGHT = 'openslide.bounds-height'


class AbstractSlide:
class AbstractSlide(metaclass=ABCMeta):
"""The base class of a slide object."""

def __init__(self):
Expand All @@ -67,22 +69,26 @@ def __exit__(self, exc_type, exc_val, exc_tb):
return False

@classmethod
@abstractmethod
def detect_format(cls, filename):
"""Return a string describing the format of the specified file.

If the file format is not recognized, return None."""
raise NotImplementedError

@abstractmethod
def close(self):
"""Close the slide."""
raise NotImplementedError

@property
@abstractmethod
def level_count(self):
"""The number of levels in the image."""
raise NotImplementedError

@property
@abstractmethod
def level_dimensions(self):
"""A list of (width, height) tuples, one for each level of the image.

Expand All @@ -95,20 +101,23 @@ def dimensions(self):
return self.level_dimensions[0]

@property
@abstractmethod
def level_downsamples(self):
"""A list of downsampling factors for each level of the image.

level_downsample[n] contains the downsample factor of level n."""
raise NotImplementedError

@property
@abstractmethod
def properties(self):
"""Metadata about the image.

This is a map: property name -> property value."""
raise NotImplementedError

@property
@abstractmethod
def associated_images(self):
"""Images associated with this whole-slide image.

Expand All @@ -122,10 +131,12 @@ def color_profile(self):
return None
return ImageCms.getOpenProfile(BytesIO(self._profile))

@abstractmethod
def get_best_level_for_downsample(self, downsample):
"""Return the best level for displaying the given downsample."""
raise NotImplementedError

@abstractmethod
def read_region(self, location, level, size):
"""Return a PIL.Image containing the contents of the region.

Expand All @@ -135,6 +146,7 @@ def read_region(self, location, level, size):
size: (width, height) tuple giving the region size."""
raise NotImplementedError

@abstractmethod
def set_cache(self, cache):
"""Use the specified cache to store recently decoded slide tiles.

Expand Down Expand Up @@ -176,7 +188,10 @@ def __init__(self, filename):
"""Open a whole-slide image."""
AbstractSlide.__init__(self)
self._filename = filename
self._osr = lowlevel.open(str(filename))
try:
self._osr = lowlevel.open(os.fspath(filename))
except TypeError:
raise OpenSlideUnsupportedFormatError
if lowlevel.read_icc_profile.available:
self._profile = lowlevel.read_icc_profile(self._osr)

Expand All @@ -188,7 +203,7 @@ def detect_format(cls, filename):
"""Return a string describing the format vendor of the specified file.

If the file format is not recognized, return None."""
return lowlevel.detect_vendor(str(filename))
return lowlevel.detect_vendor(os.fspath(filename))

def close(self):
"""Close the OpenSlide object."""
Expand Down Expand Up @@ -281,6 +296,7 @@ def __len__(self):
def __iter__(self):
return iter(self._keys())

@abstractmethod
def _keys(self):
# Private method; always returns list.
raise NotImplementedError()
Expand Down Expand Up @@ -350,7 +366,7 @@ def __init__(self, file):
self._image = file
else:
self._close = True
self._image = Image.open(file)
self._image = Image.open(os.fspath(file))
self._profile = self._image.info.get('icc_profile')

def __repr__(self):
Expand All @@ -362,15 +378,16 @@ def detect_format(cls, filename):

If the file format is not recognized, return None."""
try:
with Image.open(filename) as img:
with Image.open(os.fspath(filename)) as img:
return img.format
except OSError:
return None

def close(self):
"""Close the slide object."""
if self._close:
self._image.close()
if self._image is not None:
self._image.close()
self._close = False
self._image = None

Expand All @@ -379,12 +396,18 @@ def level_count(self):
"""The number of levels in the image."""
return 1

@property
def _image_size(self):
if self._image is None:
raise AttributeError("Cannot read from an already closed slide")
return self._image.size

@property
def level_dimensions(self):
"""A list of (width, height) tuples, one for each level of the image.

level_dimensions[n] contains the dimensions of level n."""
return (self._image.size,)
return (self._image_size,)

@property
def level_downsamples(self):
Expand Down Expand Up @@ -422,15 +445,17 @@ def read_region(self, location, level, size):
raise OpenSlideError("Invalid level")
if ['fail' for s in size if s < 0]:
raise OpenSlideError(f"Size {size} must be non-negative")
if self._image is None:
raise AttributeError("Cannot read from an already closed slide")
# Any corner of the requested region may be outside the bounds of
# the image. Create a transparent tile of the correct size and
# paste the valid part of the region into the correct location.
image_topleft = [
max(0, min(l, limit - 1)) for l, limit in zip(location, self._image.size)
max(0, min(l, limit - 1)) for l, limit in zip(location, self._image_size)
]
image_bottomright = [
max(0, min(l + s - 1, limit - 1))
for l, s, limit in zip(location, size, self._image.size)
for l, s, limit in zip(location, size, self._image_size)
]
tile = Image.new("RGBA", size, (0,) * 4)
if not [
Expand Down
55 changes: 28 additions & 27 deletions openslide/lowlevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,34 +59,36 @@ def try_load(names):
except OSError:
if name == names[-1]:
raise
raise FileNotFoundError

if platform.system() == 'Windows':
try:
library_name = "library"
err_hint = "Is OpenSlide installed correctly?"
try:
if platform.system() == 'Windows':
library_name = "DLL"
err_hint = "Did you call os.add_dll_directory()?"
return try_load(['libopenslide-1.dll', 'libopenslide-0.dll'])
except FileNotFoundError:
raise ModuleNotFoundError(
"Couldn't locate OpenSlide DLL. "
"Did you call os.add_dll_directory()? "
"https://openslide.org/api/python/#installing"
)
elif platform.system() == 'Darwin':
try:
return try_load(['libopenslide.1.dylib', 'libopenslide.0.dylib'])
except OSError:
# MacPorts doesn't add itself to the dyld search path, but
# does add itself to the find_library() search path
# (DEFAULT_LIBRARY_FALLBACK in ctypes.macholib.dyld).
import ctypes.util

lib = ctypes.util.find_library('openslide')
if lib is None:
raise ModuleNotFoundError(
"Couldn't locate OpenSlide dylib. Is OpenSlide installed "
"correctly? https://openslide.org/api/python/#installing"
)
return cdll.LoadLibrary(lib)
else:
return try_load(['libopenslide.so.1', 'libopenslide.so.0'])
elif platform.system() == 'Darwin':
library_name = "dylib"
try:
return try_load(['libopenslide.1.dylib', 'libopenslide.0.dylib'])
except OSError:
# MacPorts doesn't add itself to the dyld search path, but
# does add itself to the find_library() search path
# (DEFAULT_LIBRARY_FALLBACK in ctypes.macholib.dyld).
import ctypes.util

lib = ctypes.util.find_library('openslide')
if lib is None:
raise FileNotFoundError
return cdll.LoadLibrary(lib)
else:
return try_load(['libopenslide.so.1', 'libopenslide.so.0'])
except FileNotFoundError:
raise ModuleNotFoundError(
f"Couldn't locate OpenSlide {library_name}. {err_hint} "
"https://openslide.org/api/python/#installing"
)


_lib = _load_library()
Expand Down Expand Up @@ -446,7 +448,6 @@ def read_associated_image_icc_profile(slide, name):
'openslide_set_cache',
None,
[_OpenSlide, _OpenSlideCache],
None,
minimum_version='4.0.0',
)

Expand Down