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,

Reply via email to