Repository: rasterio/rasterio

Issue:
GDAL GetMetadataItem('BLOCK_SIZE_%d_%d' % (c, r), 'TIFF')
I have been using GDAL/QGIS (carry over from my remote sensing days) for deep learning in digital pathology.  We primarily deal with JPEG tiles in TIFF pyramid images.  I have since found rasterio which is so much more convenient.  However, one feature I need is to access jpeg length of each block (tiffinfo -s), which is a good indicator of empty or nearly empty space. In gdal, it is

GetRasterBand(1).GetMetadataItem('BLOCK_SIZE_%d_%d' % (c, r), 'TIFF')

However, it doesn't show up in tags(), tags(1), or tags(ns='TIFF')

I'm using version 0.31.0


Discussion:
@ruitaodong Could you try `tags(1, ns='TIFF')`? If that doesn't work could you link me to a small JPEG-in-TIFF image I can use to debug?

There have been some changes to Rasterio's `tags` method since 0.31 (we're at 1.0a9 now, getting close to 1.0) and an upgrade is also worth trying.
Sorry that I closed it by mistake
[1M01.zip](https://github.com/mapbox/rasterio/files/1137058/1M01.zip)
>>> rasterio.open('1M01.tif').tags(1, 'TIFF')
{}

What's interesting is that with goal

from osgeo import gdal
slide = gdal.Open('1M01.tif')
slide.GetRasterBand(1).GetMetadataItem('BLOCK_SIZE_%d_%d' % (0, 0), 'TIFF')
#returns '4135', but
slide.GetRasterBand(1).GetMetadata_Dict('TIFF')
#returns {}


@ruitaodong that *is* interesting. Rasterio is using the same GDAL function as `GetMetadata_Dict()`. This may be a GDAL bug.
Upstream bug report: https://trac.osgeo.org/gdal/ticket/6961. As a workaround, we could call `GetMetadataItem()` to get TIFF metadata when `ns='TIFF'`.
@sgillies, good catch.  Thanks for looking into it and thanks for rasterio. 
@ruitaodong Even Rouault pointed out in the GDAL tracker that dumping all the block sizes (possibly hundreds or thousands) into a dict might not be user friendly. I agree with that. I feel like we should be able do better than this

`slide.GetRasterBand(1).GetMetadataItem('BLOCK_SIZE_%d_%d' % (0, 0), 'TIFF')`

Perhaps a new dataset method like

`def block(self, bidx, i, j)`

could return something like

`{'window': Window(...), 'size': 8*256*256}`

Such a method would also serve users who want to get the window (indexes) of a single block.
I think that makes sense.  I know that my use case is very specific.  However, it is a little weird as BLOCK_SIZE is not actually per band.  It is compressed jpeg size (minus huffman table size) for the whole tile (in ycrcb in my case).   
BLOCK_SIZE is in the general case a per band property. In fact it depends on the TIFF PlanarConfig tag value (in GDAL INTERLEAVE metadata item). If PlanarConfig=Separate (INTERLEAVE=BAND), then you have indeed one block for each band. If PlanarConfig=Contig (INTERLEAVE=PIXEL), then the block is shared by all bands. So in the general case, it is safer to ask at the band level.

Resolving patch:
diff --git a/rasterio/errors.py b/rasterio/errors.py
--- a/rasterio/errors.py
+++ b/rasterio/errors.py
@@ -68,3 +68,7 @@ class WindowEvaluationError(ValueError):
 
 class RasterioDeprecationWarning(UserWarning):
     """Rasterio module deprecations"""
+
+
+class RasterBlockError(RasterioError):
+    """Raised when raster block access fails"""
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -298,7 +298,8 @@ def run(self):
     readme = f.read()
 
 # Runtime requirements.
-inst_reqs = ['affine', 'attrs', 'cligj', 'numpy', 'snuggs>=1.4.1', 'click-plugins']
+inst_reqs = [
+    'affine', 'attrs', 'cligj', 'numpy', 'snuggs>=1.4.1', 'click-plugins']
 
 if sys.version_info < (3, 4):
     inst_reqs.append('enum34')


Test patch:
diff --git a/tests/test_blocks.py b/tests/test_blocks.py
--- a/tests/test_blocks.py
+++ b/tests/test_blocks.py
@@ -8,10 +8,12 @@
 import unittest
 
 import numpy as np
+from packaging.version import parse
 import pytest
 
 import rasterio
 from rasterio import windows
+from rasterio.errors import RasterBlockError
 
 
 class WindowTest(unittest.TestCase):
@@ -61,6 +63,7 @@ def test_window_index():
     arr = np.ones((20, 20))
     assert arr[idx].shape == (4, 11)
 
+
 class RasterBlocksTest(unittest.TestCase):
 
     def test_blocks(self):
@@ -80,7 +83,8 @@ def test_blocks(self):
             self.assertEqual(second, windows.Window.from_ranges((3, 6), (0, 791)))
             (j, i), last = list(itr)[~0]
             self.assertEqual((j, i), (239, 0))
-            self.assertEqual(last, windows.Window.from_ranges((717, 718), (0, 791)))
+            self.assertEqual(
+                last, windows.Window.from_ranges((717, 718), (0, 791)))
 
     def test_block_coverage(self):
         with rasterio.open('tests/data/RGB.byte.tif') as s:
@@ -89,6 +93,7 @@ def test_block_coverage(self):
                 sum((w[0][1] - w[0][0]) * (w[1][1] - w[1][0])
                     for ji, w in s.block_windows(1)))
 
+
 class WindowReadTest(unittest.TestCase):
     def test_read_window(self):
         with rasterio.open('tests/data/RGB.byte.tif') as s:
@@ -100,6 +105,7 @@ def test_read_window(self):
                 first_block.shape,
                 rasterio.window_shape(first_window))
 
+
 class WindowWriteTest(unittest.TestCase):
 
     def setUp(self):
@@ -161,3 +167,32 @@ def test_block_windows_filtered_none(path_rgb_byte_tif):
         itr = ((ij, win) for ij, win in src.block_windows() if filter_func(win))
         with pytest.raises(StopIteration):
             next(itr)
+
+
+@pytest.mark.skipif(
+    parse(rasterio.__gdal_version__) < parse('2.0.0'),
+    reason="TIFF block size access requires GDAL 2.0")
+def test_block_size_tiff(path_rgb_byte_tif):
+    """Without compression a TIFF's blocks are all the same size"""
+    with rasterio.open(path_rgb_byte_tif) as src:
+        block_windows = list(src.block_windows())
+        sizes = [src.block_size(1, i, j) for (i, j), w in block_windows]
+        assert sizes.count(2373) == 1
+        assert sizes.count(7119) == len(block_windows) - 1
+
+
+@pytest.mark.skipif(
+    parse(rasterio.__gdal_version__) < parse('2.0.0'),
+    reason="TIFF block size access requires GDAL 2.0")
+def test_block_size_exception():
+    """A JPEG has no TIFF metadata and no API for block size"""
+    with pytest.raises(RasterBlockError):
+        with rasterio.open('tests/data/389225main_sw_1965_1024.jpg') as src:
+            src.block_size(1, 0, 0)
+
+
+def test_block_window_tiff(path_rgb_byte_tif):
+    """Block window accessors are consistent"""
+    with rasterio.open(path_rgb_byte_tif) as src:
+        for (i, j), w in src.block_windows():
+            assert src.block_window(1, i, j) == w