From f424fce16e885d932bb0c9a02dbeb7d27e77b720 Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Wed, 26 Jul 2023 14:36:03 -0600 Subject: [PATCH] Remove redundant file size guards from rio-warp, add --dry-run (#2889) * Remove redundant file size guards from rio-warp, add --dry-run Resolves #2887 * Update change log --- CHANGES.txt | 8 ++++ rasterio/rio/warp.py | 83 +++++++++++++++++++++--------------------- tests/test_rio_warp.py | 47 +++++++++++++++++++++--- 3 files changed, 91 insertions(+), 47 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 000d7f64..ce8ca198 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,14 @@ Changes ======= +Next +---- + +- The output file size limits of rio-warp were made redundant by changes to the + GTiff driver in GDAL 2.1 and have been removed (#2889). A --dry-run option + has been added to the command. If used, the profile of the output dataset + will be printed and no warping will occur. + 1.3.8 (2023-06-26) ------------------ diff --git a/rasterio/rio/warp.py b/rasterio/rio/warp.py index f82d2a64..2fd86781 100644 --- a/rasterio/rio/warp.py +++ b/rasterio/rio/warp.py @@ -1,5 +1,6 @@ -"""$ rio warp""" +"""rio warp: CLI for reprojecting rasters.""" +import json import logging from math import ceil, floor import sys @@ -20,12 +21,6 @@ from rasterio.warp import ( logger = logging.getLogger(__name__) -# Improper usage of rio-warp can lead to accidental creation of -# extremely large datasets. We'll put a hard limit on the size of -# datasets and raise a usage error if the limits are exceeded. -MAX_OUTPUT_WIDTH = 100000 -MAX_OUTPUT_HEIGHT = 100000 - @click.command(short_help='Warp a raster dataset.') @options.files_inout_arg @@ -93,6 +88,11 @@ MAX_OUTPUT_HEIGHT = 100000 callback=_cb_key_val, help="GDAL warper and coordinate transformer options.", ) +@click.option( + "--dry-run", + is_flag=True, + help="Do not create an output file, but report on its expected size and other characteristics.", +) @click.pass_context def warp( ctx, @@ -114,6 +114,7 @@ def warp( creation_options, target_aligned_pixels, warper_options, + dry_run, ): """ Warp a raster dataset. @@ -127,23 +128,19 @@ def warp( \b $ rio warp input.tif output.tif --like template.tif - The output coordinate reference system may be either a PROJ.4 or - EPSG:nnnn string, + The destination's coordinate reference system may be an authority + name, PROJ4 string, JSON-encoded PROJ4, or WKT. \b --dst-crs EPSG:4326 --dst-crs '+proj=longlat +ellps=WGS84 +datum=WGS84' - - or a JSON text-encoded PROJ.4 object. - - \b --dst-crs '{"proj": "utm", "zone": 18, ...}' - If --dimensions are provided, --res and --bounds are not applicable and an - exception will be raised. - Resolution is calculated based on the relationship between the - raster bounds in the target coordinate system and the dimensions, - and may produce rectangular rather than square pixels. + If --dimensions are provided, --res and --bounds are not applicable + and an exception will be raised. Resolution is calculated based on + the relationship between the raster bounds in the target coordinate + system and the dimensions, and may produce rectangular rather than + square pixels. \b $ rio warp input.tif output.tif --dimensions 100 200 \\ @@ -354,15 +351,6 @@ def warp( # Update the dst nodata value out_kwargs.update(nodata=dst_nodata) - # When the bounds option is misused, extreme values of - # destination width and height may result. - if (dst_width < 0 or dst_height < 0 or - dst_width > MAX_OUTPUT_WIDTH or - dst_height > MAX_OUTPUT_HEIGHT): - raise click.BadParameter( - "Invalid output dimensions: {0}.".format( - (dst_width, dst_height))) - out_kwargs.update( crs=dst_crs, transform=dst_transform, @@ -386,18 +374,29 @@ def warp( out_kwargs.update(**creation_options) - with rasterio.open(output, 'w', **out_kwargs) as dst: - reproject( - source=rasterio.band(src, list(range(1, src.count + 1))), - destination=rasterio.band( - dst, list(range(1, src.count + 1))), - src_transform=src.transform, - src_crs=src.crs, - src_nodata=src_nodata, - dst_transform=out_kwargs['transform'], - dst_crs=out_kwargs['crs'], - dst_nodata=dst_nodata, - resampling=resampling, - num_threads=threads, - **warper_options - ) + if dry_run: + crs = out_kwargs.get("crs", None) + if crs: + epsg = src.crs.to_epsg() + if epsg: + out_kwargs['crs'] = 'EPSG:{}'.format(epsg) + else: + out_kwargs['crs'] = src.crs.to_string() + + click.echo("Output dataset profile:") + click.echo(json.dumps(dict(**out_kwargs), indent=2)) + else: + with rasterio.open(output, "w", **out_kwargs) as dst: + reproject( + source=rasterio.band(src, list(range(1, src.count + 1))), + destination=rasterio.band(dst, list(range(1, src.count + 1))), + src_transform=src.transform, + src_crs=src.crs, + src_nodata=src_nodata, + dst_transform=out_kwargs["transform"], + dst_crs=out_kwargs["crs"], + dst_nodata=dst_nodata, + resampling=resampling, + num_threads=threads, + **warper_options + ) diff --git a/tests/test_rio_warp.py b/tests/test_rio_warp.py index e24bbe81..d976e5fa 100644 --- a/tests/test_rio_warp.py +++ b/tests/test_rio_warp.py @@ -332,14 +332,25 @@ def test_warp_reproject_multi_bounds_fail(runner, tmpdir): def test_warp_reproject_bounds_crossup_fail(runner, tmpdir): - """Crossed-up bounds raises click.BadParameter.""" + """Crossed-up bounds raises RasterioIOError.""" srcname = 'tests/data/shade.tif' outputname = str(tmpdir.join('test.tif')) out_bounds = [-11850000, 4810000, -11849000, 4812000] - result = runner.invoke(main_group, [ - 'warp', srcname, outputname, '--dst-crs', 'EPSG:4326', '--res', 0.001, - '--bounds'] + out_bounds) - assert result.exit_code == 2 + result = runner.invoke( + main_group, + [ + "warp", + srcname, + outputname, + "--dst-crs", + "EPSG:4326", + "--res", + 0.001, + "--bounds", + ] + + out_bounds, + ) + assert result.exit_code == 1 def test_warp_reproject_src_bounds_res(runner, tmpdir): @@ -604,3 +615,29 @@ def test_coordinate_operation(runner, tmp_path, wotopt): with rasterio.open(outputname) as src: assert src.checksum(1) == 4705 + + +def test_dry_run(runner, tmpdir): + # See also warp_reproject_bounds_crossup_fail. + srcname = 'tests/data/shade.tif' + outputname = str(tmpdir.join('test.tif')) + out_bounds = [-11850000, 4810000, -11849000, 4812000] + result = runner.invoke( + main_group, + [ + "warp", + "--dry-run", + srcname, + outputname, + "--dst-crs", + "EPSG:4326", + "--res", + 0.001, + "--bounds", + ] + + out_bounds, + ) + + assert result.exit_code == 0 + assert '"width": 1000000' in result.output + assert '"height": 2000000' in result.output