diff --git a/src/bffile/_biofile.py b/src/bffile/_biofile.py index 8fbeb83..6b6f3e6 100644 --- a/src/bffile/_biofile.py +++ b/src/bffile/_biofile.py @@ -147,6 +147,12 @@ class BioFile(Sequence[Series]): See: https://docs.openmicroscopy.org/bio-formats/latest/formats/options.html For example: to turn off chunkmap table reading for ND2 files, use `options={"nativend2.chunkmap": False}` + channel_filler : bool or None, optional + Whether to wrap the reader with Bio-Formats' ChannelFiller, which + expands indexed-color data (e.g. GIF palettes) into RGB. If `True`, + always use ChannelFiller. If `False`, never use it. If `None` + (default), automatically apply it when the file contains true + indexed-color data. """ def __init__( @@ -157,6 +163,7 @@ def __init__( original_meta: bool = False, memoize: int | bool = 0, options: dict[str, bool] | None = None, + channel_filler: bool | None = None, ): self._path = str(path) self._lock = RLock() @@ -164,6 +171,7 @@ def __init__( self._original_meta = original_meta self._memoize = memoize self._options = options + self._channel_filler = channel_filler # Reader and finalizer created in open() self._java_reader: IFormatReader | None = None @@ -281,6 +289,10 @@ def open(self) -> Self: r = jimport("loci.formats.ImageReader")() r.setFlattenedResolutions(False) + if self._channel_filler is True: + ChannelFiller = jimport("loci.formats.ChannelFiller") + r = ChannelFiller(r) + if self._memoize > 0: Memoizer = jimport("loci.formats.Memoizer") if BIOFORMATS_MEMO_DIR is not None: @@ -301,6 +313,17 @@ def open(self) -> Self: try: r.setId(self._path) + + # Auto-detect: wrap with ChannelFiller for true indexed color + if ( + self._channel_filler is None + and r.isIndexed() + and not r.isFalseColor() + ): + ChannelFiller = jimport("loci.formats.ChannelFiller") + r = ChannelFiller(r) + r.setId(self._path) + core_meta = self._get_core_metadata(r) except Exception: # pragma: no cover with suppress(Exception): diff --git a/src/bffile/_core_metadata.py b/src/bffile/_core_metadata.py index 76bd755..60359df 100644 --- a/src/bffile/_core_metadata.py +++ b/src/bffile/_core_metadata.py @@ -103,6 +103,7 @@ def from_java(cls, meta: loci.formats.CoreMetadata) -> Self: t=meta.sizeT, rgb=rgb_count, ), + rgb_count=rgb_count, thumb_size_x=meta.thumbSizeX, thumb_size_y=meta.thumbSizeY, bits_per_pixel=meta.bitsPerPixel, diff --git a/tests/conftest.py b/tests/conftest.py index ba90e2c..cbd3906 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,12 @@ def any_file(request: pytest.FixtureRequest) -> Path: return request.param +@pytest.fixture +def data_dir() -> Path: + """Path to the test data directory.""" + return TEST_DATA + + @pytest.fixture def simple_file() -> Path: """Small TIFF file for fast unit tests (7KB).""" diff --git a/tests/test_biofile.py b/tests/test_biofile.py index 1a47054..a9f0591 100644 --- a/tests/test_biofile.py +++ b/tests/test_biofile.py @@ -79,9 +79,37 @@ def test_core_meta_returns_metadata(opened_biofile: BioFile) -> None: assert hasattr(meta, "dtype") assert hasattr(meta, "dimension_order") assert len(meta.shape) == 6 # CoreMetadata always has 6 elements (TCZYX + rgb) + assert meta.rgb_count == meta.shape.rgb assert isinstance(meta.dtype, np.dtype) +@pytest.mark.parametrize( + ("channel_filler", "expect_rgb", "expect_indexed"), + [(None, 3, False), (True, 3, False), (False, 1, True)], + ids=["auto", "forced", "disabled"], +) +def test_channel_filler_gif( + data_dir: Path, + channel_filler: bool | None, + expect_rgb: int, + expect_indexed: bool, +) -> None: + with BioFile(data_dir / "example.gif", channel_filler=channel_filler) as bf: + meta = bf.core_metadata() + assert meta.shape.rgb == expect_rgb + assert meta.is_indexed is expect_indexed + + +def test_false_color_indexed_file_not_expanded(data_dir: Path) -> None: + with BioFile(data_dir / "ND2_dims_c2y32x32.nd2") as bf: + meta = bf.core_metadata() + assert meta.shape.c == 2 + assert meta.shape.rgb == 1 + assert meta.is_rgb is False + assert meta.is_indexed + assert meta.is_false_color + + def test_ome_xml_property(opened_biofile: BioFile) -> None: xml = opened_biofile.ome_xml assert isinstance(xml, str)