Repository: cogeotiff/rio-cogeo

Issue:
add `rio cogeo info`
I really like https://github.com/blacha/cogeotiff#cogeotiff-info cli by @blacha 👏 

Discussion:

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 4cba75c..c4e20c1 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,8 @@
+2.0a3 (TBD)
+------------------
+- add `info` CLI (#134)
+
+
 2.0a2 (2020-05-20)
 ------------------
 - add `--config` CLI option to pass additional GDAL Configuration options (#135)
diff --git a/README.md b/README.md
index 931f606..a82e093 100644
--- a/README.md
+++ b/README.md
@@ -45,14 +45,16 @@ $ rio cogeo --help
   Rasterio cogeo subcommands.
 
   Options:
-    --help  Show this message and exit.
+    --version  Show the version and exit.
+    --help     Show this message and exit.
 
   Commands:
     create    Create COGEO
+    info      Lists information about a raster dataset.
     validate  Validate COGEO
 ```
 
-- Create a Cloud Optimized Geotiff.
+### Create
 
 ```
 $ rio cogeo create --help
@@ -89,7 +91,7 @@ $ rio cogeo create --help
     --help                           Show this message and exit.
 ```
 
-- Check if a Cloud Optimized Geotiff is valid.
+### Validate
 
 ```
 $ rio cogeo validate --help
@@ -104,6 +106,22 @@ Options:
 
 The `strict` options will treat warnings (e.g missing overviews) as errors.
 
+
+### Info
+(extented version or `rio info`).
+
+```
+$ rio cogeo info --help
+Usage: rio cogeo info [OPTIONS] INPUT
+
+  Dataset info.
+
+Options:
+  --json  Print as JSON.
+  --help  Show this message and exit.
+```
+
+
 ### Examples
 
 ```bash
@@ -115,6 +133,39 @@ $ rio cogeo validate mydataset_jpeg.tif
 
 # Create a COGEO with JPEG profile and the first 3 bands of the data and add internal mask
 $ rio cogeo create mydataset.tif mydataset_jpeg.tif -b 1,2,3 --add-mask --cog-profile jpeg
+
+# List Raster info
+$ rio cogeo info mydataset_jpeg.tif
+Driver: GTiff
+File: mydataset_jpeg.tif
+Compression: DEFLATE
+ColorSpace: None
+
+Profile
+    Width:            10980
+    Height:           10980
+    Bands:            1
+    Tiled:            True
+    Dtype:            uint16
+    NoData:           0.0
+    Alpha Band:       False
+    Internal Mask:    False
+    Interleave:       BAND
+    Colormap:         False
+
+Geo
+    Crs:              EPSG:32634
+    Origin:           (699960.0, 3600000.0)
+    Resolution:       (10.0, -10.0)
+    BoundingBox:      (699960.0, 3490200.0, 809760.0, 3600000.0)
+
+IFD
+    Id      Size           BlockSize     Decimation           
+    0       10980x10980    1024x1024     0
+    1       5490x5490      128x128       2
+    2       2745x2745      128x128       4
+    3       1373x1373      128x128       8
+    4       687x687        128x128       16
 ```
 
 ## Default COGEO profiles
diff --git a/rio_cogeo/cogeo.py b/rio_cogeo/cogeo.py
index 292341f..c247ab5 100644
--- a/rio_cogeo/cogeo.py
+++ b/rio_cogeo/cogeo.py
@@ -6,6 +6,7 @@ import sys
 import tempfile
 import warnings
 from contextlib import ExitStack, contextmanager
+from typing import Any, Dict
 
 import click
 import mercantile
@@ -304,7 +305,7 @@ def cog_translate(
                 copy(tmp_dst, dst_path, copy_src_overviews=True, **dst_kwargs)
 
 
-def cog_validate(src_path, strict=False):
+def cog_validate(src_path: str, strict: bool = False, quiet: bool = False):
     """
     Validate Cloud Optimized Geotiff.
 
@@ -314,6 +315,8 @@ def cog_validate(src_path, strict=False):
         A dataset path or URL. Will be opened in "r" mode.
     strict: bool
         Treat warnings as errors
+    quiet: bool
+        Remove standard outputs
 
     This script is the rasterio equivalent of
     https://svn.osgeo.org/gdal/trunk/gdal/swig/python/samples/validate_cloud_optimized_geotiff.py
@@ -321,7 +324,7 @@ def cog_validate(src_path, strict=False):
     """
     errors = []
     warnings = []
-    details = {}
+    details: Dict[str, Any] = {}
 
     if not GDALVersion.runtime().at_least("2.2"):
         raise Exception("GDAL 2.2 or above required")
@@ -449,20 +452,18 @@ def cog_validate(src_path, strict=False):
                     if not ovr_dst.is_tiled:
                         errors.append("Overview of index {} is not tiled".format(ix))
 
-    if warnings:
+    if warnings and not quiet:
         click.secho("The following warnings were found:", fg="yellow", err=True)
         for w in warnings:
             click.echo("- " + w, err=True)
         click.echo(err=True)
 
-    if errors:
+    if errors and not quiet:
         click.secho("The following errors were found:", fg="red", err=True)
         for e in errors:
             click.echo("- " + e, err=True)
 
-        return False
-
-    if warnings and strict:
+    if errors or (warnings and strict):
         return False
 
     return True
diff --git a/rio_cogeo/scripts/cli.py b/rio_cogeo/scripts/cli.py
index 7fdc336..c40979b 100644
--- a/rio_cogeo/scripts/cli.py
+++ b/rio_cogeo/scripts/cli.py
@@ -1,13 +1,16 @@
 """Rio_cogeo.scripts.cli."""
 
+import json
 import os
 import warnings
 
 import click
 import numpy
+import rasterio
 from rasterio.enums import Resampling as ResamplingEnums
 from rasterio.rio import options
 
+from rio_cogeo import utils
 from rio_cogeo import version as cogeo_version
 from rio_cogeo.cogeo import cog_translate, cog_validate
 from rio_cogeo.profiles import cog_profiles
@@ -254,3 +257,114 @@ def validate(input, strict):
         click.echo("{} is a valid cloud optimized GeoTIFF".format(input))
     else:
         click.echo("{} is NOT a valid cloud optimized GeoTIFF".format(input))
+
+
+@cogeo.command(short_help="Lists information about a raster dataset.")
+@options.file_in_arg
+@click.option(
+    "--json", "to_json", default=False, is_flag=True, help="Print as JSON.",
+)
+def info(input, to_json):
+    """Dataset info."""
+    with rasterio.open(input) as src_dst:
+        _info = {
+            "Path": input,
+            "Driver": src_dst.driver,
+            "Compression": src_dst.compression.value if src_dst.compression else None,
+            "ColorSpace": src_dst.photometric.value if src_dst.photometric else None,
+        }
+
+        try:
+            colormap = src_dst.colormap(1)
+        except ValueError:
+            colormap = None
+
+        profile = {
+            "Bands": src_dst.count,
+            "Width": src_dst.width,
+            "Height": src_dst.height,
+            "Count": src_dst.count,
+            "Tiled": src_dst.is_tiled,
+            "Dtype": src_dst.dtypes[0],
+            "Interleave": src_dst.interleaving.value,
+            "Alpha Band": utils.has_alpha_band(src_dst),
+            "Internal Mask": utils.has_mask_band(src_dst),
+            "Nodata": src_dst.nodata,
+            "ColorMap": colormap is not None,
+        }
+        geo = {
+            "CRS": f"EPSG:{src_dst.crs.to_epsg()}",
+            "BoundingBox": tuple(src_dst.bounds),
+            "Origin": (src_dst.transform.c, src_dst.transform.f),
+            "Resolution": (src_dst.transform.a, src_dst.transform.e),
+        }
+
+        ifd_raw = [
+            {
+                "Level": 0,
+                "Width": src_dst.width,
+                "Height": src_dst.height,
+                "Blocksize": src_dst.block_shapes[0],
+                "Decimation": 0,
+            }
+        ]
+        overviews = src_dst.overviews(1)
+
+    ifd_ovr = []
+    for ix, decim in enumerate(overviews):
+        with rasterio.open(input, OVERVIEW_LEVEL=ix) as ovr_dst:
+            ifd_ovr.append(
+                {
+                    "Level": ix + 1,
+                    "Width": ovr_dst.width,
+                    "Height": ovr_dst.height,
+                    "Blocksize": ovr_dst.block_shapes[0],
+                    "Decimation": decim,
+                }
+            )
+
+    ifds = ifd_raw + ifd_ovr
+
+    if to_json:
+        output = _info.copy()
+        output["Profile"] = profile
+        output["GEO"] = geo
+        output["IFD"] = ifds
+        click.echo(json.dumps(output))
+    else:
+        sep = 25
+        click.echo(
+            f"""{click.style('Driver:', bold=True)} {_info['Driver']}
+{click.style('File:', bold=True)} {_info['Path']}
+{click.style('Compression:', bold=True)} {_info['Compression']}
+{click.style('ColorSpace:', bold=True)} {_info['ColorSpace']}
+
+{click.style('Profile', bold=True)}
+    {click.style("Width:", bold=True):<{sep}} {profile['Width']}
+    {click.style("Height:", bold=True):<{sep}} {profile['Height']}
+    {click.style("Bands:", bold=True):<{sep}} {profile['Count']}
+    {click.style("Tiled:", bold=True):<{sep}} {profile['Tiled']}
+    {click.style("Dtype:", bold=True):<{sep}} {profile['Dtype']}
+    {click.style("NoData:", bold=True):<{sep}} {profile['Nodata']}
+    {click.style("Alpha Band:", bold=True):<{sep}} {profile['Alpha Band']}
+    {click.style("Internal Mask:", bold=True):<{sep}} {profile['Internal Mask']}
+    {click.style("Interleave:", bold=True):<{sep}} {profile['Interleave']}
+    {click.style("ColorMap:", bold=True):<{sep}} {profile['ColorMap']}
+
+{click.style('Geo', bold=True)}
+    {click.style("Crs:", bold=True):<{sep}} {geo['CRS']}
+    {click.style("Origin:", bold=True):<{sep}} {geo['Origin']}
+    {click.style("Resolution:", bold=True):<{sep}} {geo['Resolution']}
+    {click.style("BoundingBox:", bold=True):<{sep}} {geo['BoundingBox']}"""
+        )
+
+        click.echo(
+            f"""
+{click.style('IFD', bold=True)}
+    {click.style('Id', underline=True, bold=True):<20}{click.style('Size', underline=True, bold=True):<27}{click.style('BlockSize', underline=True, bold=True):<26}{click.style('Decimation', underline=True, bold=True):<33}"""
+        )
+
+        for ifd in ifds:
+            wh = f"{ifd['Width']}x{ifd['Height']}"
+            bl = f"{ifd['Blocksize'][1]}x{ifd['Blocksize'][0]}"
+            click.echo(f"""    {ifd['Level']:<8}{wh:<15}{bl:<14}{ifd['Decimation']}""")
diff --git a/setup.py b/setup.py
index 899078b..6faae5f 100644
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,7 @@ if sys.version_info >= (3, 6):
 
 setup(
     name="rio-cogeo",
-    version="2.0a2",
+    version="2.0a3",
     python_requires=">=3",
     description=u"CloudOptimized GeoTIFF (COGEO) creation plugin for rasterio",
     long_description=readme,


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 0528ed6..7d64962 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -485,3 +485,15 @@ def test_cogeo_validUpercaseProfile(monkeypatch, runner):
         assert result.exit_code == 0
         with rasterio.open("output.tif") as src:
             assert src.compression.value == "DEFLATE"
+
+
+def test_cogeo_info(runner):
+    """Should work as expected."""
+    with runner.isolated_filesystem():
+        result = runner.invoke(cogeo, ["info", raster_path_rgb])
+        assert not result.exception
+        assert result.exit_code == 0
+
+        result = runner.invoke(cogeo, ["info", raster_invalid_cog, "--json"])
+        assert not result.exception
+        assert result.exit_code == 0
diff --git a/tests/test_validate.py b/tests/test_validate.py
index e1b1c7a..791fb04 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -37,7 +37,7 @@ def test_cog_validate_valid(monkeypatch):
     assert cog_validate(raster_rgb)
 
     # not tiled, no overview
-    assert not cog_validate(raster_big)
+    assert not cog_validate(raster_big, quiet=True)
 
     # external overview
     assert not cog_validate(raster_external)