Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-tifffile for openSUSE:Factory
checked in at 2026-06-28 21:10:55
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-tifffile (Old)
and /work/SRC/openSUSE:Factory/.python-tifffile.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tifffile"
Sun Jun 28 21:10:55 2026 rev:25 rq:1362180 version:2026.6.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-tifffile/python-tifffile.changes
2026-05-20 15:25:30.264570037 +0200
+++
/work/SRC/openSUSE:Factory/.python-tifffile.new.11887/python-tifffile.changes
2026-06-28 21:12:23.541943838 +0200
@@ -1,0 +2,12 @@
+Sun Jun 28 14:38:36 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2026.6.1:
+ * Replace NullContext with contextlib.nullcontext (breaking).
+ * Fix writing monochrome linear_raw (#328).
+ * Fix keyboard axis selection in imshow interactive viewer
+ (#327).
+ * Fix reading short ASCII string tag values from NDPI.
+ * Add option to suppress writing extrasamples tag.
+ * Verify origin of codecs.
+
+-------------------------------------------------------------------
Old:
----
tifffile-2026.5.15.tar.gz
New:
----
tifffile-2026.6.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-tifffile.spec ++++++
--- /var/tmp/diff_new_pack.7VZEek/_old 2026-06-28 21:12:24.865988610 +0200
+++ /var/tmp/diff_new_pack.7VZEek/_new 2026-06-28 21:12:24.913990233 +0200
@@ -26,7 +26,7 @@
%endif
%global skip_python311 1
Name: python-tifffile%{psuffix}
-Version: 2026.5.15
+Version: 2026.6.1
Release: 0
Summary: Read and write TIFF files
License: BSD-3-Clause
++++++ tifffile-2026.5.15.tar.gz -> tifffile-2026.6.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/CHANGES.rst
new/tifffile-2026.6.1/CHANGES.rst
--- old/tifffile-2026.5.15/CHANGES.rst 2026-05-15 22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/CHANGES.rst 2026-06-01 01:57:12.000000000 +0200
@@ -1,6 +1,15 @@
Revisions
=========
+2026.6.1
+
+- Replace NullContext with contextlib.nullcontext (breaking).
+- Fix writing monochrome linear_raw (#328).
+- Fix keyboard axis selection in imshow interactive viewer (#327).
+- Fix reading short ASCII string tag values from NDPI.
+- Add option to suppress writing extrasamples tag.
+- Verify origin of codecs.
+
2026.5.15
- Update ZarrFileSequenceStore to zarr format 3 (breaking).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/MANIFEST.in
new/tifffile-2026.6.1/MANIFEST.in
--- old/tifffile-2026.5.15/MANIFEST.in 2026-05-15 22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/MANIFEST.in 2026-06-01 01:57:12.000000000 +0200
@@ -28,6 +28,4 @@
include examples/issue125.py
include examples/write_dng.py
-include docs/conf.py
-include docs/make.py
-include docs/_static/custom.css
+recursive-exclude docs *
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/README.rst
new/tifffile-2026.6.1/README.rst
--- old/tifffile-2026.5.15/README.rst 2026-05-15 22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/README.rst 2026-06-01 01:57:12.000000000 +0200
@@ -36,7 +36,7 @@
:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
:License: BSD-3-Clause
-:Version: 2026.5.15
+:Version: 2026.6.1
:DOI: `10.5281/zenodo.6795860 <https://doi.org/10.5281/zenodo.6795860>`_
Quickstart
@@ -73,14 +73,14 @@
(other versions may work):
- `CPython <https://www.python.org>`_ 3.12.10, 3.13.13, 3.14.5, 3.15.0b1 64-bit
-- `NumPy <https://pypi.org/project/numpy>`_ 2.4.4
+- `NumPy <https://pypi.org/project/numpy>`_ 2.4.6
- `Imagecodecs <https://pypi.org/project/imagecodecs/>`_ 2026.5.10
(required for encoding or decoding LZW, JPEG, etc. compressed segments)
- `Xarray <https://pypi.org/project/xarray>`_ 2026.4.0
(required only for reading xarray DataArrays)
- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.9
(required for plotting)
-- `Lxml <https://pypi.org/project/lxml/>`_ 6.1.0
+- `Lxml <https://pypi.org/project/lxml/>`_ 6.1.1
(required only for validating and printing XML)
- `Zarr <https://pypi.org/project/zarr/>`_ 3.2.1
(required only for using Zarr stores)
@@ -90,6 +90,15 @@
Revisions
---------
+2026.6.1
+
+- Replace NullContext with contextlib.nullcontext (breaking).
+- Fix writing monochrome linear_raw (#328).
+- Fix keyboard axis selection in imshow interactive viewer (#327).
+- Fix reading short ASCII string tag values from NDPI.
+- Add option to suppress writing extrasamples tag.
+- Verify origin of codecs.
+
2026.5.15
- Update ZarrFileSequenceStore to zarr format 3 (breaking).
@@ -235,6 +244,7 @@
performs poorly. BitsPerSample, SamplesPerPixel, and
PhotometricInterpretation tags may contain wrong values, which can be
corrected using the value of tag 65441.
+ Short ASCII string tag values are not stored inline.
- **Philips TIFF** slides store padded ImageWidth and ImageLength tag values
for tiled pages. The values can be corrected using the DICOM_PIXEL_SPACING
attributes of the XML formatted description of the first page. Tile offsets
@@ -532,7 +542,7 @@
* Z (Z) float64 456B 0.0 3.947 ... 221.1
* Y (Y) float32 1kB 0.0 2.675 ... 682.3
* X (X) float32 1kB 0.0 2.675 ... 682.3
- Attributes:
+ Attributes...
photometric: minisblack
mode: grayscale
...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/docs/_static/custom.css
new/tifffile-2026.6.1/docs/_static/custom.css
--- old/tifffile-2026.5.15/docs/_static/custom.css 2026-05-15
22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/docs/_static/custom.css 1970-01-01
01:00:00.000000000 +0100
@@ -1,8 +0,0 @@
-dl {
- margin: 0;
- margin-top: 1em;
- margin-right: 0px;
- margin-bottom: 0px;
- margin-left: 0px;
- padding: 0;
-}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/docs/conf.py
new/tifffile-2026.6.1/docs/conf.py
--- old/tifffile-2026.5.15/docs/conf.py 2026-05-15 22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/docs/conf.py 1970-01-01 01:00:00.000000000 +0100
@@ -1,62 +0,0 @@
-# tifffile/docs/conf.py
-
-import os
-import sys
-
-here = os.path.dirname(__file__)
-sys.path.insert(0, os.path.split(here)[0])
-
-import tifffile
-
-project = 'tifffile'
-copyright = '2008-2026, Christoph Gohlke'
-author = 'Christoph Gohlke'
-version = tifffile.__version__
-
-extensions = [
- 'sphinx.ext.napoleon',
- 'sphinx.ext.autodoc',
- 'sphinx.ext.autosummary',
- 'sphinx.ext.doctest',
- # 'sphinxcontrib.spelling',
- # 'sphinx.ext.viewcode',
- # 'sphinx.ext.autosectionlabel',
- # 'numpydoc',
- # 'sphinx_issues',
-]
-
-templates_path = ['_templates']
-
-html_theme = 'alabaster'
-
-html_static_path = ['_static']
-html_css_files = ['custom.css']
-html_show_sourcelink = False
-
-autodoc_member_order = 'bysource' # bysource, groupwise
-autodoc_default_flags = ['members']
-autodoc_typehints = 'description'
-autodoc_type_aliases = {'ArrayLike': 'numpy.ArrayLike'}
-autoclass_content = 'class'
-autosectionlabel_prefix_document = True
-autosummary_generate = True
-
-napoleon_google_docstring = True
-napoleon_numpy_docstring = False
-
-html_theme_options = {
- 'nosidebar': False,
-}
-
-
-def add_api(app, what, name, obj, options, lines):
- if what == 'module':
- lines.extend(('API', '---'))
-
-
-def setup(app):
- pass
- # app.connect('autodoc-process-docstring', add_api)
-
-
-# mypy: allow-untyped-defs, allow-untyped-calls
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/docs/make.py
new/tifffile-2026.6.1/docs/make.py
--- old/tifffile-2026.5.15/docs/make.py 2026-05-15 22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/docs/make.py 1970-01-01 01:00:00.000000000 +0100
@@ -1,221 +0,0 @@
-# tifffile/docs/make.py
-
-"""Make documentation for tifffile package using Sphinx."""
-
-import os
-import sys
-
-from sphinx.cmd.build import main
-
-here = os.path.dirname(__file__)
-sys.path.insert(0, os.path.split(here)[0])
-path = os.environ.get('PATH')
-if path:
- os.environ['PATH'] = os.path.join(sys.exec_prefix, 'Scripts') + ';' + path
-
-import tifffile # noqa: E402
-
-members = [
- 'imread',
- 'imwrite',
- 'memmap',
- 'TiffWriter',
- 'TiffFile',
- # 'TiffFileError',
- 'TiffFormat',
- 'TiffPage',
- 'TiffFrame',
- 'TiffPages',
- 'TiffTag',
- 'TiffTags',
- 'TiffTagRegistry',
- 'TiffPageSeries',
- 'TiffSeries',
- 'TiffSequence',
- 'FileSequence',
- 'zarr.ZarrStore',
- 'zarr.ZarrTiffStore',
- 'zarr.ZarrFileSequenceStore',
- # Constants
- 'DATATYPE',
- 'SAMPLEFORMAT',
- 'PLANARCONFIG',
- 'COMPRESSION',
- 'PREDICTOR',
- 'EXTRASAMPLE',
- 'FILETYPE',
- 'PHOTOMETRIC',
- 'RESUNIT',
- 'CHUNKMODE',
- 'TIFF',
- # classes
- 'FileHandle',
- 'OmeXml',
- # 'OmeXmlError',
- 'Timer',
- 'NullContext',
- 'StoredShape',
- 'TiledSequence',
- # functions
- 'logger',
- 'rational',
- 'repeat_nd',
- 'natural_sorted',
- 'parse_filenames',
- 'matlabstr2py',
- 'strptime',
- 'imagej_metadata_tag',
- 'imagej_description',
- # 'read_scanimage_metadata',
- 'read_micromanager_metadata',
- 'read_ndtiff_index',
- 'create_output',
- 'hexdump',
- 'xml2dict',
- 'tiffcomment',
- 'tiff2fsspec',
- 'lsm2bin',
- 'validate_jhove',
- 'imshow',
- '.geodb',
-]
-
-title = f'tifffile {tifffile.__version__}'
-underline = '=' * len(title)
-members_ = []
-for name in members:
- if not name:
- continue
- if '.' in name[1:]:
- name = name.rsplit('.', 1)[-1]
- members_.append(name.replace('.', '').lower())
-memberlist = '\n '.join(members_)
-
-with open(here + '/index.rst', 'w') as fh:
- fh.write(f""".. tifffile documentation
-
-.. currentmodule:: tifffile
-
-{title}
-{underline}
-
-.. automodule:: tifffile
-
-.. toctree::
- :hidden:
- :maxdepth: 2
-
- genindex
- license
- revisions
- examples
-
-
-.. toctree::
- :hidden:
- :maxdepth: 2
-
- {memberlist}
-
-
-""")
-
-
-with open(here + '/genindex.rst', 'w') as fh:
- fh.write("""
-Index
-=====
-
-""")
-
-with open(here + '/license.rst', 'w') as fh:
- fh.write("""
-License
-=======
-
-.. include:: ../LICENSE
-""")
-
-
-with open(here + '/examples.rst', 'w') as fh:
- fh.write("""
-Examples
-========
-
-See `#examples <index.html#examples>`_.
-""")
-
-
-with open(here + '/revisions.rst', 'w') as fh:
- fh.write(""".. include:: ../CHANGES.rst""")
-
-
-with open('tiff.rst', 'w') as fh:
- fh.write("""
-.. currentmodule:: tifffile
-
-TIFF
-====
-
-.. autoclass:: tifffile.TIFF
- :members:
-
-.. autoclass:: tifffile._TIFF
- :members:
-""")
-
-
-automodule = """.. currentmodule:: {module}
-
-{name}
-{size}
-
-.. automodule:: {module}.{name}
- :members:
-
-"""
-
-autoclass = """.. currentmodule:: {module}
-
-{name}
-{size}
-
-.. autoclass:: {module}.{name}
- :members:
-
-"""
-
-automethod = """.. currentmodule:: {module}
-
-{name}
-{size}
-
-.. autofunction:: {name}
-
-"""
-
-for name in members:
- if not name or name == 'TIFF':
- continue
-
- if '.' in name[1:]:
- module, name = name.rsplit('.', 1)
- module = f'tifffile.{module}'
- else:
- module = 'tifffile'
-
- if name[0] == '.':
- template = automodule
- name = name[1:]
- elif name[0].isupper():
- template = autoclass
- else:
- template = automethod
- size = '=' * len(name)
-
- with open(f'{here}/{name.lower()}.rst', 'w') as fh:
- fh.write(template.format(module=module, name=name, size=size))
-
-main(['-b', 'html', here, here + '/html'])
-
-os.system('start html/index.html') # noqa: S605, S607
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/setup.py
new/tifffile-2026.6.1/setup.py
--- old/tifffile-2026.5.15/setup.py 2026-05-15 22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/setup.py 2026-06-01 01:57:12.000000000 +0200
@@ -95,7 +95,7 @@
project_urls={
'Bug Tracker': 'https://github.com/cgohlke/tifffile/issues',
'Source Code': 'https://github.com/cgohlke/tifffile',
- # 'Documentation': 'https://',
+ 'Documentation': 'https://www.cgohlke.com/docs/tifffile/',
},
packages=['tifffile'],
package_data={'tifffile': ['py.typed']},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/tests/test_tifffile.py
new/tifffile-2026.6.1/tests/test_tifffile.py
--- old/tifffile-2026.5.15/tests/test_tifffile.py 2026-05-15
22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/tests/test_tifffile.py 2026-06-01
01:57:12.000000000 +0200
@@ -31,7 +31,7 @@
"""Unittests for the tifffile package.
-:Version: 2026.5.15
+:Version: 2026.6.1
"""
@@ -3349,6 +3349,31 @@
assert series[1].asarray().dtype == numpy.uint16
[email protected](SKIP_FILE, reason=REASON)
[email protected]('samples', [1, 3])
+def test_issue_linearraw(samples):
+ """Test write LINEAR_RAW images without extrasamples."""
+ # https://github.com/cgohlke/tifffile/issues/328
+ with TempFileName(f'test_issue_linearraw_{samples}') as filename:
+ imwrite(
+ filename,
+ shape=(27, 23, samples),
+ dtype=numpy.uint8,
+ photometric='linear_raw',
+ planarconfig='contig',
+ extrasamples=False,
+ )
+ with TiffFile(filename) as tif:
+ assert len(tif.pages) == 1
+ page = tif.pages.first
+ assert 'Extrasamples' not in page.tags
+ assert page.photometric == PHOTOMETRIC.LINEAR_RAW
+ assert page.planarconfig == PLANARCONFIG.CONTIG
+ assert page.samplesperpixel == samples
+ assert page.extrasamples == ()
+ assert page.asarray().shape == (27, 23, samples)[: 2 + (samples > 1)]
+
+
class TestExceptions:
"""Test various Exceptions and Warnings."""
@@ -11242,6 +11267,52 @@
SKIP_FILE or SKIP_CODECS or not imagecodecs.JPEG.available,
reason=REASON,
)
+def test_read_ndpi_ascii():
+ """Test read Hamamatsu NDPI slide with short ASCII strings."""
+ # https://openslide.cs.cmu.edu/download/openslide-testdata/Hamamatsu/
+ # short ASCII string tag values are not stored inline
+ filename = _file('HamamatsuNDPI/Hamamatsu-2.ndpi')
+ with TiffFile(filename) as tif:
+ assert tif.is_ndpi
+ assert len(tif.pages) == 8
+ assert len(tif.series) == 3
+
+ # first page
+ page = tif.pages.first
+ assert page.tags[65427].value == 'A1' # not K
+ assert page.ndpi_tags['SlideLabel'] == 'A1'
+ assert page.databytecounts[0] == 2697
+ assert page.shape == (39168, 30720, 3)
+ assert page.photometric == PHOTOMETRIC.YCBCR
+ assert page.compression == COMPRESSION.JPEG
+
+ # last page
+ page = tif.pages[-1]
+ assert page.is_ndpi
+ assert page.tags[65476].value == 'IVR'
+ assert page.shape == (205, 600, 1) # TODO: wrong!
+ assert page.photometric == PHOTOMETRIC.RGB # TODO: wrong!
+ assert page.compression == COMPRESSION.NONE
+
+ with open(filename, 'rb') as fh:
+ data = fh.read()
+
+ bio = BytesIO(data)
+ with TiffFile(bio) as tif:
+ tif.pages.first.tags[65427].overwrite('A 2')
+ tif.pages[-1].tags[65476].overwrite('IVR modified')
+
+ bio.seek(0)
+ with TiffFile(bio) as tif:
+ assert tif.is_ndpi
+ assert tif.pages.first.tags[65427].value == 'A 2'
+ assert tif.pages[-1].tags[65476].value == 'IVR modified'
+
+
[email protected](
+ SKIP_FILE or SKIP_CODECS or not imagecodecs.JPEG.available,
+ reason=REASON,
+)
def test_read_svs_cmu1():
"""Test read Aperio SVS slide, JPEG and LZW."""
filename = _file('AperioSVS/CMU-1.svs')
@@ -19449,6 +19520,55 @@
image = tif.asarray()
assert_array_equal(data, image)
assert_aszarr_method(tif, image)
+ assert__str__(tif)
+
+
+def test_write_extrasamples_false_gray():
+ """Test write grayscale with omitted extrasamples tag."""
+ data = random_data(numpy.uint8, (301, 219, 2))
+ with TempFileName('write_extrasamples_false_gray') as filename:
+ imwrite(filename, data, planarconfig='contig', extrasamples=False)
+ assert_valid_tiff(filename)
+ with TiffFile(filename) as tif:
+ assert len(tif.pages) == 1
+ page = tif.pages.first
+ assert page.is_contiguous
+ assert page.photometric == PHOTOMETRIC.MINISBLACK
+ assert page.planarconfig == PLANARCONFIG.CONTIG
+ assert page.imagewidth == 219
+ assert page.imagelength == 301
+ assert page.samplesperpixel == 2
+ assert 'ExtraSamples' not in page.tags
+ image = tif.asarray()
+ assert_array_equal(data, image)
+ assert_aszarr_method(tif, image)
+ assert__str__(tif)
+
+
+def test_write_extrasamples_false_gray_planar():
+ """Test write planar grayscale with omitted extrasamples tag."""
+ data = random_data(numpy.uint8, (2, 301, 219))
+ with TempFileName('write_extrasamples_false_gray_planar') as filename:
+ imwrite(
+ filename,
+ data,
+ planarconfig=PLANARCONFIG.SEPARATE,
+ extrasamples=False,
+ )
+ assert_valid_tiff(filename)
+ with TiffFile(filename) as tif:
+ assert len(tif.pages) == 1
+ page = tif.pages.first
+ assert page.is_contiguous
+ assert page.photometric == PHOTOMETRIC.MINISBLACK
+ assert page.planarconfig == PLANARCONFIG.SEPARATE
+ assert page.imagewidth == 219
+ assert page.imagelength == 301
+ assert page.samplesperpixel == 2
+ assert 'ExtraSamples' not in page.tags
+ image = tif.asarray()
+ assert_array_equal(data, image)
+ assert_aszarr_method(tif, image)
assert__str__(tif)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/tifffile/numcodecs.py
new/tifffile-2026.6.1/tifffile/numcodecs.py
--- old/tifffile-2026.5.15/tifffile/numcodecs.py 2026-05-15
22:04:55.000000000 +0200
+++ new/tifffile-2026.6.1/tifffile/numcodecs.py 2026-06-01 01:57:12.000000000
+0200
@@ -78,7 +78,9 @@
# TiffWriter.write
photometric: PHOTOMETRIC | int | str | None = None,
planarconfig: PLANARCONFIG | int | str | None = None,
- extrasamples: Sequence[EXTRASAMPLE | int | str] | None = None,
+ extrasamples: (
+ Sequence[EXTRASAMPLE | int | str] | Literal[False] | None
+ ) = None,
volumetric: bool = False,
tile: Sequence[int] | None = None,
rowsperstrip: int | None = None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/tifffile/tifffile.py
new/tifffile-2026.6.1/tifffile/tifffile.py
--- old/tifffile-2026.5.15/tifffile/tifffile.py 2026-05-15 22:04:55.000000000
+0200
+++ new/tifffile-2026.6.1/tifffile/tifffile.py 2026-06-01 01:57:12.000000000
+0200
@@ -63,7 +63,7 @@
:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
:License: BSD-3-Clause
-:Version: 2026.5.15
+:Version: 2026.6.1
:DOI: `10.5281/zenodo.6795860 <https://doi.org/10.5281/zenodo.6795860>`_
Quickstart
@@ -100,14 +100,14 @@
(other versions may work):
- `CPython <https://www.python.org>`_ 3.12.10, 3.13.13, 3.14.5, 3.15.0b1 64-bit
-- `NumPy <https://pypi.org/project/numpy>`_ 2.4.4
+- `NumPy <https://pypi.org/project/numpy>`_ 2.4.6
- `Imagecodecs <https://pypi.org/project/imagecodecs/>`_ 2026.5.10
(required for encoding or decoding LZW, JPEG, etc. compressed segments)
- `Xarray <https://pypi.org/project/xarray>`_ 2026.4.0
(required only for reading xarray DataArrays)
- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.9
(required for plotting)
-- `Lxml <https://pypi.org/project/lxml/>`_ 6.1.0
+- `Lxml <https://pypi.org/project/lxml/>`_ 6.1.1
(required only for validating and printing XML)
- `Zarr <https://pypi.org/project/zarr/>`_ 3.2.1
(required only for using Zarr stores)
@@ -117,6 +117,15 @@
Revisions
---------
+2026.6.1
+
+- Replace NullContext with contextlib.nullcontext (breaking).
+- Fix writing monochrome linear_raw (#328).
+- Fix keyboard axis selection in imshow interactive viewer (#327).
+- Fix reading short ASCII string tag values from NDPI.
+- Add option to suppress writing extrasamples tag.
+- Verify origin of codecs.
+
2026.5.15
- Update ZarrFileSequenceStore to zarr format 3 (breaking).
@@ -262,6 +271,7 @@
performs poorly. BitsPerSample, SamplesPerPixel, and
PhotometricInterpretation tags may contain wrong values, which can be
corrected using the value of tag 65441.
+ Short ASCII string tag values are not stored inline.
- **Philips TIFF** slides store padded ImageWidth and ImageLength tag values
for tiled pages. The values can be corrected using the DICOM_PIXEL_SPACING
attributes of the XML formatted description of the first page. Tile offsets
@@ -527,7 +537,7 @@
* Z (Z) float64 456B 0.0 3.947 ... 221.1
* Y (Y) float32 1kB 0.0 2.675 ... 682.3
* X (X) float32 1kB 0.0 2.675 ... 682.3
-Attributes:
+Attributes...
photometric: minisblack
mode: grayscale
...
@@ -801,7 +811,7 @@
from __future__ import annotations
-__version__ = '2026.5.15'
+__version__ = '2026.6.1'
__all__ = [
'CHUNKMODE',
@@ -822,7 +832,6 @@
'FileCache',
'FileHandle',
'FileSequence',
- 'NullContext',
'OmeXml',
'OmeXmlError',
'StoredShape',
@@ -1391,7 +1400,9 @@
dtype: DTypeLike | None = None,
photometric: PHOTOMETRIC | int | str | None = None,
planarconfig: PLANARCONFIG | int | str | None = None,
- extrasamples: Sequence[EXTRASAMPLE | int | str] | None = None,
+ extrasamples: (
+ Sequence[EXTRASAMPLE | int | str] | Literal[False] | None
+ ) = None,
volumetric: bool = False,
tile: Sequence[int] | None = None,
rowsperstrip: int | None = None,
@@ -1960,7 +1971,9 @@
dtype: DTypeLike | None = None,
photometric: PHOTOMETRIC | int | str | None = None,
planarconfig: PLANARCONFIG | int | str | None = None,
- extrasamples: Sequence[EXTRASAMPLE | int | str] | None = None,
+ extrasamples: (
+ Sequence[EXTRASAMPLE | int | str] | Literal[False] | None
+ ) = None,
volumetric: bool = False,
tile: Sequence[int] | None = None,
rowsperstrip: int | None = None,
@@ -2069,7 +2082,8 @@
*UNSPECIFIED*: no transparency information (default).
*ASSOCALPHA*: true transparency with premultiplied color.
*UNASSALPHA*: independent transparency masks.
- The values are written to the ExtraSamples tag.
+ The values are written to the ExtraSamples tag unless
+ extrasamples is *False*.
volumetric:
Volumetric image stored on single page via SGI ImageDepth tag.
The volumetric format is not part of the TIFF specification,
@@ -2549,16 +2563,23 @@
photometric = enumarg(PHOTOMETRIC, photometric)
if planarconfig:
planarconfig = enumarg(PLANARCONFIG, planarconfig)
+ omit_extrasamples = extrasamples is False
if extrasamples is not None:
- # TODO: deprecate non-sequence extrasamples
- extrasamples = tuple(
- int(enumarg(EXTRASAMPLE, x))
- for x in (
- extrasamples
- if isinstance(extrasamples, (tuple, list))
- else (extrasamples,)
+ if isinstance(extrasamples, bool):
+ if extrasamples:
+ msg = 'extrasamples=True not supported'
+ raise ValueError(msg)
+ extrasamples = None
+ else:
+ # TODO: deprecate non-sequence extrasamples
+ extrasamples = tuple(
+ int(enumarg(EXTRASAMPLE, x))
+ for x in (
+ extrasamples
+ if isinstance(extrasamples, (tuple, list))
+ else (extrasamples,)
+ )
)
- )
if compression:
if isinstance(compression, str):
@@ -3024,6 +3045,7 @@
if (
planarconfig is not None
+ and storedshape.samples > 1
and storedshape.planarconfig != planarconfig
):
msg = f'{planarconfig!r} does not match {storedshape=!r}'
@@ -3212,7 +3234,7 @@
)
else:
addtag(tags, 258, 3, 1, bitspersample)
- if storedshape.extrasamples > 0:
+ if storedshape.extrasamples > 0 and not omit_extrasamples:
if extrasamples is not None:
if storedshape.extrasamples != len(extrasamples):
msg = (
@@ -7418,7 +7440,7 @@
def segments(
self,
*,
- lock: threading.RLock | NullContext | None = ...,
+ lock: contextlib.AbstractContextManager[Any] | None = ...,
maxworkers: int | None = ...,
func: None = ...,
sort: bool = ...,
@@ -7430,7 +7452,7 @@
def segments(
self,
*,
- lock: threading.RLock | NullContext | None = ...,
+ lock: contextlib.AbstractContextManager[Any] | None = ...,
maxworkers: int | None = ...,
func: Callable[[DecodeResult], Any],
sort: bool = ...,
@@ -7441,7 +7463,7 @@
def segments(
self,
*,
- lock: threading.RLock | NullContext | None = None,
+ lock: contextlib.AbstractContextManager[Any] | None = None,
maxworkers: int | None = None,
func: Callable[[DecodeResult], Any] | None = None,
sort: bool = False,
@@ -7540,7 +7562,7 @@
*,
out: OutputType = None,
squeeze: bool = True,
- lock: threading.RLock | NullContext | None = None,
+ lock: contextlib.AbstractContextManager[Any] | None = None,
maxworkers: int | None = None,
buffersize: int | None = None,
) -> NDArray[Any]:
@@ -10296,6 +10318,7 @@
valuesize = count * struct.calcsize(valueformat)
if (
valuesize > tiff.tagoffsetthreshold
+ or (tiff.is_ndpi and dtype == 2) # NDPI string never stored inline
or code in TIFF.TAG_READERS # TODO: only works with offsets?
):
valueoffset = struct.unpack(tiff.offsetformat, value)[0]
@@ -10683,6 +10706,10 @@
newsize = len(packedvalue)
oldsize = self.count * struct.calcsize(TIFF.DATA_FORMATS[self.dtype])
valueoffset = self.valueoffset
+ valueoffset_inline = (
+ self.offset + 4 + struct.calcsize(tiff.tagformat2[:2])
+ )
+ force_outofline = tiff.is_ndpi and dtype == 2
pos = fh.tell()
try:
@@ -10691,8 +10718,11 @@
fh.seek(self.offset + 2)
fh.write(struct.pack(tiff.byteorder + 'H', dtype))
- if oldsize <= tiff.tagoffsetthreshold:
- if newsize <= tiff.tagoffsetthreshold:
+ if (
+ oldsize <= tiff.tagoffsetthreshold
+ and self.valueoffset == valueoffset_inline
+ ):
+ if newsize <= tiff.tagoffsetthreshold and not force_outofline:
# inline -> inline: overwrite
fh.seek(self.offset + 4)
fh.write(struct.pack(tiff.tagformat2, count, packedvalue))
@@ -10717,11 +10747,9 @@
fh.seek(valueoffset)
fh.write(packedvalue)
- elif newsize <= tiff.tagoffsetthreshold:
+ elif newsize <= tiff.tagoffsetthreshold and not force_outofline:
# separate -> inline: erase old value
- valueoffset = (
- self.offset + 4 + struct.calcsize(tiff.tagformat2[:2])
- )
+ valueoffset = valueoffset_inline
fh.seek(self.offset + 4)
fh.write(struct.pack(tiff.tagformat2, count, packedvalue))
if erase:
@@ -12697,7 +12725,7 @@
_offset: int
_size: int
_close: bool
- _lock: threading.RLock | NullContext
+ _lock: contextlib.AbstractContextManager[Any]
def __init__(
self,
@@ -12719,7 +12747,7 @@
self._offset = -1 if offset is None else offset
self._size = -1 if size is None else size
self._close = True
- self._lock = NullContext()
+ self._lock = contextlib.nullcontext()
self.open()
assert self._fh is not None
@@ -13122,7 +13150,7 @@
length: int | None = None,
*,
sort: bool = True,
- lock: threading.RLock | NullContext | None = None,
+ lock: contextlib.AbstractContextManager[Any] | None = None,
buffersize: int | None = None,
flat: bool = True,
) -> (
@@ -13356,7 +13384,7 @@
return self._fh is None
@property
- def lock(self) -> threading.RLock | NullContext:
+ def lock(self) -> contextlib.AbstractContextManager[Any]:
"""Reentrant lock to synchronize reads and writes."""
return self._lock
@@ -13366,13 +13394,15 @@
def set_lock(self, lock: bool) -> None: # noqa: FBT001
"""Set reentrant lock to synchronize reads and writes."""
- if bool(lock) == isinstance(self._lock, NullContext):
- self._lock = threading.RLock() if lock else NullContext()
+ if bool(lock) == isinstance(self._lock, contextlib.nullcontext):
+ self._lock = (
+ threading.RLock() if lock else contextlib.nullcontext()
+ )
@property
def has_lock(self) -> bool:
"""File uses reentrant lock to synchronize reads and writes."""
- return not isinstance(self._lock, NullContext)
+ return not isinstance(self._lock, contextlib.nullcontext)
@property
def is_file(self) -> bool:
@@ -13408,20 +13438,20 @@
past: list[FileHandle]
"""FIFO list of opened files."""
- lock: threading.RLock | NullContext
+ lock: contextlib.AbstractContextManager[Any]
"""Reentrant lock to synchronize reads and writes."""
def __init__(
self,
size: int | None = None,
*,
- lock: threading.RLock | NullContext | None = None,
+ lock: contextlib.AbstractContextManager[Any] | None = None,
) -> None:
self.past = []
self.files = {}
self.keep = set()
self.size = 8 if size is None else int(size)
- self.lock = NullContext() if lock is None else lock
+ self.lock = contextlib.nullcontext() if lock is None else lock
def open(self, fh: FileHandle, /) -> None:
"""Open file, re-open if necessary."""
@@ -13778,51 +13808,6 @@
@final
-class NullContext:
- """Null context manager and dummy reentrant lock.
-
- Can replace :py:class:`threading.RLock` where no synchronization
- is needed. Implements both the context manager and lock interfaces.
-
- >>> with NullContext():
- ... pass
- ...
-
- """
-
- __slots__ = ()
-
- def __enter__(self) -> Self:
- return self
-
- def __exit__(
- self,
- exc_type: type[BaseException] | None,
- exc_value: BaseException | None,
- traceback: TracebackType | None,
- ) -> Literal[False]:
- return False
-
- def acquire(
- self,
- blocking: bool = True, # noqa: FBT001, FBT002
- timeout: float = -1,
- ) -> bool:
- """Acquire lock immediately and return True."""
- return True
-
- def release(self) -> None:
- """Release lock (no-op)."""
-
- def locked(self) -> bool:
- """Return False; lock is never held."""
- return False
-
- def __repr__(self) -> str:
- return 'NullContext()'
-
-
-@final
class Timer:
"""Stopwatch for timing execution speed.
@@ -15155,6 +15140,9 @@
except NotImplementedError as exc:
msg = f'{COMPRESSION(key)!r} not implemented'
raise KeyError(msg) from exc
+ mod = getattr(codec, '__module__', '') or ''
+ if mod.split('.', 1)[0] not in {'imagecodecs', 'tifffile'}:
+ raise RuntimeError(mod, codec)
self._codecs[key] = codec
return codec
@@ -15285,6 +15273,9 @@
except NotImplementedError as exc:
msg = f'{PREDICTOR(key)!r} not implemented'
raise KeyError(msg) from exc
+ mod = getattr(codec, '__module__', '') or ''
+ if mod.split('.', 1)[0] not in {'imagecodecs', 'tifffile'}:
+ raise RuntimeError(mod, codec)
self._codecs[key] = codec
return codec
@@ -16803,11 +16794,11 @@
9: 3, # ICCLAB
10: 3, # ITULAB
32803: 1, # CFA
- 32844: 1, # LOGL ?
+ 32844: 1, # LOGL
32845: 3, # LOGLUV
- 34892: 3, # LINEAR_RAW ?
- 51177: 1, # DEPTH_MAP ?
- 52527: 1, # SEMANTIC_MASK ?
+ 34892: 1, # LINEAR_RAW
+ 51177: 1, # DEPTH_MAP
+ 52527: 1, # SEMANTIC_MASK
}
@cached_property
@@ -25040,7 +25031,7 @@
/,
*,
tiled: TiledSequence | None = None,
- lock: threading.RLock | NullContext | None = None,
+ lock: contextlib.AbstractContextManager[Any] | None = None,
maxworkers: int | None = None,
out: OutputType = None,
**kwargs: Any,
@@ -25132,7 +25123,7 @@
index: int,
out: Any = out,
filecache: FileCache = filecache,
- lock: threading.RLock | NullContext | None = lock,
+ lock: contextlib.AbstractContextManager[Any] | None = lock,
kwargs: dict[str, Any] = kwargs,
/,
) -> None:
@@ -25163,7 +25154,7 @@
# index: tuple[int | slice, ...],
# out: Any = out,
# filecache: FileCache = filecache,
- # lock: threading.RLock | NullContext | None = lock,
+ # lock: contextlib.AbstractContextManager[Any] | None = lock,
# kwargs: dict[str, Any] = kwargs,
# /,
# ) -> None:
@@ -27055,8 +27046,11 @@
if dims:
current = list((0,) * dims)
- curaxdat = [0, data[tuple(current)].squeeze()]
slider_axes = [axis for axis in range(dims) if data.shape[axis] > 1]
+ curaxdat = [
+ slider_axes[0] if slider_axes else 0,
+ data[tuple(current)].squeeze(),
+ ]
sliders = [
Slider(
ax=pyplot.axes((0.125, 0.03 * (i + 1), 0.725, 0.025)),
@@ -27096,7 +27090,9 @@
current[axis] = index
set_image(current)
- def on_keypressed(event, data=data, current=current):
+ def on_keypressed(
+ event, data=data, current=current, slider_axes=slider_axes
+ ):
# callback function for key press event
key = event.key
axis = curaxdat[0]
@@ -27107,9 +27103,19 @@
elif key == 'left':
on_changed(current[axis] - 1, axis)
elif key == 'up':
- curaxdat[0] = 0 if axis == len(data.shape) - 1 else axis + 1
+ if slider_axes:
+ n = len(slider_axes)
+ idx = (
+ slider_axes.index(axis)
+ if axis in slider_axes
+ else n - 1
+ )
+ curaxdat[0] = slider_axes[(idx + 1) % n]
elif key == 'down':
- curaxdat[0] = len(data.shape) - 1 if axis == 0 else axis - 1
+ if slider_axes:
+ n = len(slider_axes)
+ idx = slider_axes.index(axis) if axis in slider_axes else 0
+ curaxdat[0] = slider_axes[(idx - 1) % n]
elif key == 'end':
on_changed(data.shape[axis] - 1, axis)
elif key == 'home':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tifffile-2026.5.15/tifffile/zarr.py
new/tifffile-2026.6.1/tifffile/zarr.py
--- old/tifffile-2026.5.15/tifffile/zarr.py 2026-05-15 22:04:55.000000000
+0200
+++ new/tifffile-2026.6.1/tifffile/zarr.py 2026-06-01 01:57:12.000000000
+0200
@@ -76,7 +76,6 @@
ByteOrder,
FileCache,
FileSequence,
- NullContext,
TagTuple,
TiffFile,
TiffFrame,
@@ -93,7 +92,6 @@
if TYPE_CHECKING:
import os
- import threading
from collections.abc import (
AsyncIterator,
Callable,
@@ -131,7 +129,7 @@
# TiffWriter.write
photometric: str | None = None
planarconfig: str | None = None
- extrasamples: tuple[str, ...] | None = None
+ extrasamples: tuple[str, ...] | Literal[False] | None = None
volumetric: bool = False
tile: tuple[int, ...] | None = None
rowsperstrip: int | None = None
@@ -158,7 +156,9 @@
byteorder: ByteOrder | None = None,
photometric: PHOTOMETRIC | int | str | None = None,
planarconfig: PLANARCONFIG | int | str | None = None,
- extrasamples: Sequence[EXTRASAMPLE | int | str] | None = None,
+ extrasamples: (
+ Sequence[EXTRASAMPLE | int | str] | Literal[False] | None
+ ) = None,
volumetric: bool = False,
tile: Sequence[int] | None = None,
rowsperstrip: int | None = None,
@@ -172,6 +172,12 @@
truncate: bool = False,
maxworkers: int | None = None,
) -> None:
+ if extrasamples is not None and not isinstance(extrasamples, bool):
+ extrasamples = tuple(
+ _enum_name(e, EXTRASAMPLE) # type: ignore[misc]
+ for e in extrasamples
+ )
+
_setattrs(
self,
key=key,
@@ -184,11 +190,7 @@
byteorder=byteorder,
photometric=_enum_name(photometric, PHOTOMETRIC),
planarconfig=_enum_name(planarconfig, PLANARCONFIG),
- extrasamples=(
- tuple(_enum_name(e, EXTRASAMPLE) for e in extrasamples)
- if extrasamples is not None
- else None
- ),
+ extrasamples=extrasamples,
volumetric=bool(volumetric),
tile=tuple(int(x) for x in tile) if tile is not None else None,
rowsperstrip=(
@@ -246,7 +248,9 @@
cfg['photometric'] = self.photometric
if self.planarconfig is not None:
cfg['planarconfig'] = self.planarconfig
- if self.extrasamples is not None:
+ if self.extrasamples is False:
+ cfg['extrasamples'] = False
+ elif self.extrasamples is not None:
cfg['extrasamples'] = list(self.extrasamples)
if self.volumetric:
cfg['volumetric'] = self.volumetric
@@ -580,7 +584,7 @@
dimension_names: Sequence[str] | None = None,
zattrs: dict[str, Any] | None = None,
multiscales: bool | None = None,
- lock: threading.RLock | NullContext | None = None,
+ lock: contextlib.AbstractContextManager[Any] | None = None,
maxworkers: int | None = None,
buffersize: int | None = None,
read_only: bool | None = None,