Repository: rasterio/rasterio (language: python)

Issue:
Relax assumption that tiled dataset blocks be multiples of 16
Here: https://github.com/rasterio/rasterio/blob/main/rasterio/_base.pyx#L1012. It's so for TIFFs, but not for other formats.


Discussion / hints:

Fix patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 459ad7bbf..79b494755 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -6,6 +6,11 @@ Next
 
 Deprecations:
 
+- The is_tiled property of a dataset will be removed in a future version and
+  a pending deprecation warning will be raised if it is used (#3015). This
+  warning may be changed to a deprecation warning at version 1.5.0. Please
+  consider copying the body of the property to your own project. It's just one
+  line.
 - The is_valid property of the CRS class is not useful and will be removed in
   version 2.0 (#2919).
 - The FilePath class has been supplanted by rasterio.open's new opener keyword
@@ -39,6 +44,9 @@ Bug fixes:
 
 Other changes:
 
+- The logic of is_tiled has been simplified to match GDAL's own internal logic
+  and do nothing more. It's now only a test that a raster's block width is not
+  equal to the raster width (#3015).
 - rasterize() better matches the behavior of Numpy array constructors. It no
   longer reduces the bit width of output, returning either float64 or int64
   arrays unless a data type is explicitly selected, and the fill parameter no
diff --git a/rasterio/_base.pyx b/rasterio/_base.pyx
index 4fcb7762b..d9bb05c20 100644
--- a/rasterio/_base.pyx
+++ b/rasterio/_base.pyx
@@ -1005,17 +1005,16 @@ cdef class DatasetBase:
 
     @property
     def is_tiled(self):
-        if len(self.block_shapes) == 0:
-            return False
-        else:
-            blockysize, blockxsize = self.block_shapes[0]
-            if blockxsize % 16 or blockysize % 16:
-                return False
-            # Perfectly square is a special case/
-            if blockxsize == blockysize == self.height == self.width:
-                return True
-            else:
-                return blockxsize < self.width or blockxsize > self.width
+        warnings.warn(
+            "is_tiled will be removed in a future version. "
+            "Please consider copying the body of this function "
+            "into your program or module.",
+            PendingDeprecationWarning
+        )
+        # It's rare but possible that a dataset's bands have different
+        # block structure. Therefore we check them all against the
+        # width of the dataset.
+        return self.block_shapes and all(self.width != w for _, w in self.block_shapes)
 
     @property
     def profile(self):
@@ -1026,15 +1025,16 @@ cdef class DatasetBase:
         """
         m = Profile(**self.meta)
 
-        if self.is_tiled:
+        if self.block_shapes:
             m.update(
                 blockxsize=self.block_shapes[0][1],
                 blockysize=self.block_shapes[0][0],
-                tiled=True)
-        elif len(self.block_shapes) > 0:
-            m.update(blockysize=self.block_shapes[0][0], tiled=False)
-        else:
-            m.update(tiled=False)
+            )
+
+        # It's rare but possible that a dataset's bands have different
+        # block structure. Therefore we check them all against the
+        # width of the dataset.
+        m.update(tiled=self.block_shapes and all(self.width != w for _, w in self.block_shapes))
 
         if self.compression:
             m['compress'] = self.compression.name
diff --git a/rasterio/_io.pyx b/rasterio/_io.pyx
index 4ed465352..e6a12ed8c 100644
--- a/rasterio/_io.pyx
+++ b/rasterio/_io.pyx
@@ -1459,8 +1459,6 @@ cdef class DatasetWriterBase(DatasetReaderBase):
                 # ignore if only one provided
                 kwargs.pop("blockxsize", None)
                 kwargs.pop("blockysize", None)
-            elif int(blockxsize) % 16 or int(blockysize) % 16:
-                raise RasterBlockError("The height and width of dataset blocks must be multiples of 16")
             kwargs["tiled"] = "TRUE"
 
         if mode in ('w', 'w+'):
@@ -1496,6 +1494,13 @@ cdef class DatasetWriterBase(DatasetReaderBase):
                     GDALCreate(drv, fname, width, height, count, gdal_dtype, options)
                 )
 
+            except CPLE_AppDefinedError as exc:
+                if "Bad value" in str(exc):
+                    raise RasterBlockError(
+                        "The height and width of TIFF dataset blocks must be multiples of 16"
+                    )
+                else:
+                    raise RasterioIOError(str(exc))
             except CPLE_BaseError as exc:
                 raise RasterioIOError(str(exc))
 


Test patch:
diff --git a/tests/test_blocks.py b/tests/test_blocks.py
index e4592012e..612e9fabd 100644
--- a/tests/test_blocks.py
+++ b/tests/test_blocks.py
@@ -191,14 +191,14 @@ def test_block_window_tiff(path_rgb_byte_tif):
             assert src.block_window(1, i, j) == w
 
 
-@pytest.mark.parametrize("blocksize", [16, 32, 256, 1024])
+@pytest.mark.parametrize("blocksize", [32, 256, 1024])
 def test_block_windows_bigger_blocksize(tmpdir, blocksize):
     """Ensure that block sizes greater than raster size are ok"""
     tempfile = str(tmpdir.join("test.tif"))
     profile = default_gtiff_profile.copy()
     profile.update(height=16, width=16, count=1, blockxsize=blocksize, blockysize=blocksize)
     with rasterio.open(tempfile, "w", **profile) as dst:
-        assert dst.is_tiled
+        assert all((blocksize, blocksize) == (h, w) for (h, w) in dst.block_shapes)
         for ij, window in dst.block_windows():
             dst.write(np.ones((1, 1), dtype="uint8"), 1, window=window)
 
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index 9e404dbf7..27b03b29d 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -89,9 +89,10 @@ def test_creation_untiled_blockysize(tmp_path, blockysize):
         pass
 
     with rasterio.open(tmpfile) as dataset:
-        assert not dataset.is_tiled
+        blockysize, blockxsize = dataset.block_shapes[0]
+        assert blockxsize == dataset.width
         assert dataset.profile["blockysize"] == min(blockysize, 61)
-        assert dataset.block_shapes[0][0] == min(blockysize, 61)
+        assert blockysize == min(blockysize, 61)
 
 
 @pytest.mark.parametrize(
diff --git a/tests/test_profile.py b/tests/test_profile.py
index aea337626..29c26f84a 100644
--- a/tests/test_profile.py
+++ b/tests/test_profile.py
@@ -81,19 +81,19 @@ def test_dataset_profile_property_tiled(data):
 def test_dataset_profile_property_untiled(data, path_rgb_byte_tif):
     """An untiled dataset's profile has block y sizes"""
     with rasterio.open(path_rgb_byte_tif) as src:
-        assert 'blockxsize' not in src.profile
-        assert src.profile['blockysize'] == 3
-        assert src.profile['tiled'] is False
+        assert src.profile["blockxsize"] == 791
+        assert src.profile["blockysize"] == 3
+        assert src.profile["tiled"] is False
 
 
 def test_dataset_convert_untiled_to_tiled(tmp_path, path_rgb_byte_tif):
     with rasterio.open(path_rgb_byte_tif) as src:
-        assert 'blockxsize' not in src.profile
-        assert src.profile['blockysize'] == 3
-        assert src.profile['tiled'] is False
+        assert src.profile["blockxsize"] == 791
+        assert src.profile["blockysize"] == 3
+        assert src.profile["tiled"] is False
 
         dst_profile = src.profile
-        dst_profile.update(tiled=True)
+        dst_profile.update(tiled=True, blockxsize=256, blockysize=256)
         with rasterio.open(tmp_path / 'test_tiled.tif', 'w+', **dst_profile) as dst_ds:
             assert dst_ds.profile['tiled'] is True
             assert dst_ds.profile['blockysize'] == 256
diff --git a/tests/test_rio_convert.py b/tests/test_rio_convert.py
index 3e77f685a..17f3e725b 100644
--- a/tests/test_rio_convert.py
+++ b/tests/test_rio_convert.py
@@ -15,7 +15,8 @@ def test_tiled(tmpdir, runner):
     )
     assert result.exit_code == 0
     with rasterio.open(outputname) as src:
-        assert not src.is_tiled
+        blockysize, blockxsize = src.block_shapes[0]
+        assert blockxsize == src.width
 
 
 def test_format(tmpdir, runner):