diff --git a/lib/galaxy/tool_util/verify/__init__.py b/lib/galaxy/tool_util/verify/__init__.py
index 976c8706b684..dd40e8cacf62 100644
--- a/lib/galaxy/tool_util/verify/__init__.py
+++ b/lib/galaxy/tool_util/verify/__init__.py
@@ -32,6 +32,10 @@
from PIL import Image
except ImportError:
pass
+try:
+ import tifffile
+except ImportError:
+ pass
from galaxy.tool_util.parser.util import (
DEFAULT_DELTA,
@@ -496,14 +500,29 @@ def get_image_metric(
raise ValueError(f'No such metric: "{metric_name}"')
+def _load_image(filepath: str) -> "numpy.typing.NDArray":
+ """
+ Reads the given image, trying tifffile and Pillow for reading.
+ """
+ # Try reading with tifffile first. It fails if the file is not a TIFF.
+ try:
+ arr = tifffile.imread(filepath)
+
+ # If tifffile failed, then the file is not a tifffile. In that case, try with Pillow.
+ except tifffile.TiffFileError:
+ with Image.open(filepath) as im:
+ arr = numpy.array(im)
+
+ # Return loaded image
+ return arr
+
+
def files_image_diff(file1: str, file2: str, attributes: Optional[Dict[str, Any]] = None) -> None:
"""Check the pixel data of 2 image files for differences."""
attributes = attributes or {}
- with Image.open(file1) as im1:
- arr1 = numpy.array(im1)
- with Image.open(file2) as im2:
- arr2 = numpy.array(im2)
+ arr1 = _load_image(file1)
+ arr2 = _load_image(file2)
if arr1.dtype != arr2.dtype:
raise AssertionError(f"Image data types did not match ({arr1.dtype}, {arr2.dtype}).")
diff --git a/lib/galaxy/tool_util/verify/asserts/image.py b/lib/galaxy/tool_util/verify/asserts/image.py
index f0fbc6e8326e..7513be700f5e 100644
--- a/lib/galaxy/tool_util/verify/asserts/image.py
+++ b/lib/galaxy/tool_util/verify/asserts/image.py
@@ -18,6 +18,10 @@
from PIL import Image
except ImportError:
pass
+try:
+ import tifffile
+except ImportError:
+ pass
if TYPE_CHECKING:
import numpy.typing
@@ -58,18 +62,17 @@ def assert_has_image_width(
"""
Asserts the specified output is an image and has a width of the specified value.
"""
- buf = io.BytesIO(output_bytes)
- with Image.open(buf) as im:
- _assert_number(
- im.size[0],
- width,
- delta,
- min,
- max,
- negate,
- "{expected} width {n}+-{delta}",
- "{expected} width to be in [{min}:{max}]",
- )
+ im_arr = _get_image(output_bytes)
+ _assert_number(
+ im_arr.shape[1],
+ width,
+ delta,
+ min,
+ max,
+ negate,
+ "{expected} width {n}+-{delta}",
+ "{expected} width to be in [{min}:{max}]",
+ )
def assert_has_image_height(
@@ -83,18 +86,17 @@ def assert_has_image_height(
"""
Asserts the specified output is an image and has a height of the specified value.
"""
- buf = io.BytesIO(output_bytes)
- with Image.open(buf) as im:
- _assert_number(
- im.size[1],
- height,
- delta,
- min,
- max,
- negate,
- "{expected} height {n}+-{delta}",
- "{expected} height to be in [{min}:{max}]",
- )
+ im_arr = _get_image(output_bytes)
+ _assert_number(
+ im_arr.shape[0],
+ height,
+ delta,
+ min,
+ max,
+ negate,
+ "{expected} height {n}+-{delta}",
+ "{expected} height to be in [{min}:{max}]",
+ )
def assert_has_image_channels(
@@ -108,18 +110,18 @@ def assert_has_image_channels(
"""
Asserts the specified output is an image and has the specified number of channels.
"""
- buf = io.BytesIO(output_bytes)
- with Image.open(buf) as im:
- _assert_number(
- len(im.getbands()),
- channels,
- delta,
- min,
- max,
- negate,
- "{expected} image channels {n}+-{delta}",
- "{expected} image channels to be in [{min}:{max}]",
- )
+ im_arr = _get_image(output_bytes)
+ n_channels = 1 if im_arr.ndim < 3 else im_arr.shape[2] # we assume here that the image is a 2-D image
+ _assert_number(
+ n_channels,
+ channels,
+ delta,
+ min,
+ max,
+ negate,
+ "{expected} image channels {n}+-{delta}",
+ "{expected} image channels to be in [{min}:{max}]",
+ )
def _compute_center_of_mass(im_arr: "numpy.typing.NDArray") -> Tuple[float, float]:
@@ -139,10 +141,20 @@ def _get_image(
) -> "numpy.typing.NDArray":
"""
Returns the output image or a specific channel.
+
+ The function tries to read the image using tifffile and Pillow.
"""
buf = io.BytesIO(output_bytes)
- with Image.open(buf) as im:
- im_arr = numpy.array(im)
+
+ # Try reading with tifffile first. It fails if the file is not a TIFF.
+ try:
+ im_arr = tifffile.imread(buf)
+
+ # If tifffile failed, then the file is not a tifffile. In that case, try with Pillow.
+ except tifffile.TiffFileError:
+ buf.seek(0)
+ with Image.open(buf) as im:
+ im_arr = numpy.array(im)
# Select the specified channel (if any).
if channel is not None:
diff --git a/packages/tool_util/setup.cfg b/packages/tool_util/setup.cfg
index 5d6b9b9a378d..848a609a6b2c 100644
--- a/packages/tool_util/setup.cfg
+++ b/packages/tool_util/setup.cfg
@@ -70,6 +70,7 @@ extended-assertions =
numpy
pysam
pillow
+ tifffile
[options.packages.find]
exclude =
diff --git a/test-data/im4_float.tif b/test-data/im4_float.tif
new file mode 100644
index 000000000000..21f0e11bb096
Binary files /dev/null and b/test-data/im4_float.tif differ
diff --git a/test/functional/tools/image_diff.xml b/test/functional/tools/image_diff.xml
index a079992e5fe5..adb17639c603 100644
--- a/test/functional/tools/image_diff.xml
+++ b/test/functional/tools/image_diff.xml
@@ -22,6 +22,11 @@
+
+
+
+
+
diff --git a/test/functional/tools/validation_image.xml b/test/functional/tools/validation_image.xml
index a1a7791fa6ec..a3c93ea84f67 100644
--- a/test/functional/tools/validation_image.xml
+++ b/test/functional/tools/validation_image.xml
@@ -93,6 +93,18 @@
+
+
+
+
+