diff --git a/app/api/tiler.py b/app/api/tiler.py index eade40c4..58484ab2 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -246,26 +246,26 @@ def get_elevation_tiles(elevation, url, x, y, z, tilesize, nodata, resampling, p tile = np.full((tilesize * 3, tilesize * 3), nodata, dtype=elevation.dtype) with COGReader(url) as src: try: - left, _ = src.tile(x - 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata, + left, _discard_ = src.tile(x - 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata, resampling_method=resampling, padding=padding) tile[tilesize:tilesize * 2, 0:tilesize] = left except TileOutsideBounds: pass try: - right, _ = src.tile(x + 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata, + right, _discard_ = src.tile(x + 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata, resampling_method=resampling, padding=padding) tile[tilesize:tilesize * 2, tilesize * 2:tilesize * 3] = right except TileOutsideBounds: pass try: - bottom, _ = src.tile(x, y + 1, z, indexes=1, tilesize=tilesize, nodata=nodata, + bottom, _discard_ = src.tile(x, y + 1, z, indexes=1, tilesize=tilesize, nodata=nodata, resampling_method=resampling, padding=padding) tile[tilesize * 2:tilesize * 3, tilesize:tilesize * 2] = bottom except TileOutsideBounds: pass try: - top, _ = src.tile(x, y - 1, z, indexes=1, tilesize=tilesize, nodata=nodata, + top, _discard_ = src.tile(x, y - 1, z, indexes=1, tilesize=tilesize, nodata=nodata, resampling_method=resampling, padding=padding) tile[0:tilesize, tilesize:tilesize * 2] = top except TileOutsideBounds: @@ -314,7 +314,7 @@ class Tiles(TaskNestedView): if hillshade == '' or hillshade == '0': hillshade = None try: - expr, _ = lookup_formula(formula, bands) + expr, _discard_ = lookup_formula(formula, bands) except ValueError as e: raise exceptions.ValidationError(str(e)) @@ -442,7 +442,7 @@ class Tiles(TaskNestedView): if intensity is not None: rgb = tile.post_process(in_range=(rescale_arr,)) if colormap: - rgb, _ = apply_cmap(rgb.data, colormap.get(color_map)) + rgb, _discard_ = apply_cmap(rgb.data, colormap.get(color_map)) if rgb.data.shape[0] != 3: raise exceptions.ValidationError( _("Cannot process tile: intensity image provided, but no RGB data was computed.")) @@ -486,21 +486,23 @@ class Export(TaskNestedView): if bands == '': bands = None if rescale == '': rescale = None if epsg == '': epsg = None + if color_map == '': color_map = None + if hillshade == '': hillshade = None expr = None - if not export_format in ['gtiff', 'gtiff-rgb', 'jpg', 'png']: - raise exceptions.ValidationError(_("Unsupported format: %(format)") % {'format': export_format}) + if not export_format in ['gtiff', 'gtiff-rgb', 'jpg', 'png', 'kmz']: + raise exceptions.ValidationError(_("Unsupported format: %(value)s") % {'value': export_format}) if epsg is not None: try: epsg = int(epsg) except ValueError: - raise exception.ValidationError(_("Invalid EPSG code: %(epsg)") % {'epsg': epsg}) + raise exceptions.ValidationError(_("Invalid EPSG code: %(value)s") % {'value': epsg}) if formula and bands: try: - expr, _ = lookup_formula(formula, bands) + expr, _discard_ = lookup_formula(formula, bands) except ValueError as e: raise exceptions.ValidationError(str(e)) @@ -516,7 +518,7 @@ class Export(TaskNestedView): try: rescale = list(map(float, rescale.split(","))) except ValueError: - raise exception.ValidationError(_("Invalid rescale value: %(value)") % {'value': rescale}) + raise exceptions.ValidationError(_("Invalid rescale value: %(value)s") % {'value': rescale}) if hillshade is not None: try: @@ -524,7 +526,7 @@ class Export(TaskNestedView): if hillshade < 0: raise Exception("Hillshade must be > 0") except: - raise exception.ValidationError(_("Invalid hillshade value: %(value)") % {'value': hillshade}) + raise exceptions.ValidationError(_("Invalid hillshade value: %(value)s") % {'value': hillshade}) url = get_raster_path(task, asset_type) @@ -549,5 +551,7 @@ class Export(TaskNestedView): rescale=rescale, color_map=color_map, hillshade=hillshade, - dem=asset_type in ['dsm', 'dtm']).task_id + dem=asset_type in ['dsm', 'dtm'], + name=task.name, + asset_type=asset_type).task_id return Response({'celery_task_id': celery_task_id, 'filename': filename}) diff --git a/app/raster_utils.py b/app/raster_utils.py index 55a4aed2..b558cdd9 100644 --- a/app/raster_utils.py +++ b/app/raster_utils.py @@ -1,6 +1,9 @@ # Export a raster index after applying a band expression import rasterio import re +import logging +import os +import subprocess import numpy as np import numexpr as ne from rasterio.enums import ColorInterp @@ -10,7 +13,6 @@ from rio_tiler.errors import InvalidColorMapName from app.api.hsvblend import hsv_blend from app.api.hillshade import LightSource from rasterio.warp import calculate_default_transform, reproject, Resampling -import logging logger = logging.getLogger('app.logger') @@ -21,7 +23,8 @@ def extension_for_export_format(export_format): 'gtiff': 'tif', 'gtiff-rgb': 'tif', 'jpg': 'jpg', - 'png': 'png' + 'png': 'png', + 'kmz': 'kmz' } return extensions.get(export_format, 'tif') @@ -33,6 +36,8 @@ def export_raster(input, output, **opts): color_map = opts.get('color_map') hillshade = opts.get('hillshade') dem = opts.get('dem') + name = opts.get('name', 'raster') # KMZ specific + asset_type = opts.get('asset_type', 'raster') # KMZ specific with rasterio.open(input) as src: profile = src.meta.copy() @@ -43,6 +48,18 @@ def export_raster(input, output, **opts): with_alpha = True rgb = False indexes = src.indexes + output_raster = output + jpg_background = 255 # white + + # KMZ is special, we just export it as jpg with EPSG:4326 + # and then call GDAL to tile/package it + kmz = export_format == "kmz" + if kmz: + export_format = "jpg" + epsg = 4326 + path_base, _ = os.path.splitext(output) + output_raster = path_base + ".jpg" + jpg_background = 0 # black if export_format == "jpg": driver = "JPEG" @@ -95,7 +112,7 @@ def export_raster(input, output, **opts): if not skip_rescale and rescale is not None: arr = linear_rescale(arr, in_range=rescale) if not skip_alpha and not with_alpha: - arr[mask==0] = 255 # Set white background + arr[mask==0] = jpg_background if not skip_type and rgb and arr.dtype != np.uint8: arr = arr.astype(np.uint8) @@ -173,7 +190,7 @@ def export_raster(input, output, **opts): # Make sure this is float32 arr = arr.astype(np.float32) - with rasterio.open(output, 'w', **profile) as dst: + with rasterio.open(output_raster, 'w', **profile) as dst: # Apply colormap? if rgb and cmap is not None: rgb_data, _ = apply_cmap(process(arr, skip_alpha=True), cmap) @@ -190,7 +207,7 @@ def export_raster(input, output, **opts): write_band(process(arr)[0], dst, 1) elif dem: # Apply hillshading, colormaps to elevation - with rasterio.open(output, 'w', **profile) as dst: + with rasterio.open(output_raster, 'w', **profile) as dst: arr = src.read() intensity = None @@ -221,7 +238,7 @@ def export_raster(input, output, **opts): write_band(process(arr)[0], dst, 1) else: # Copy bands as-is - with rasterio.open(output, 'w', **profile) as dst: + with rasterio.open(output_raster, 'w', **profile) as dst: band_num = 1 for idx in indexes: ci = src.colorinterp[idx - 1] @@ -234,3 +251,8 @@ def export_raster(input, output, **opts): else: write_band(process(arr), dst, band_num) band_num += 1 + + if kmz: + subprocess.check_output(["gdal_translate", "-of", "KMLSUPEROVERLAY", + "-co", "Name={}".format(name), + "-co", "FORMAT=JPEG", output_raster, output]) diff --git a/app/security.py b/app/security.py index 3f369b8f..d24ce829 100644 --- a/app/security.py +++ b/app/security.py @@ -1,4 +1,5 @@ from django.core.exceptions import SuspiciousFileOperation +from shlex import _find_unsafe import os def path_traversal_check(unsafe_path, known_safe_path): @@ -9,4 +10,16 @@ def path_traversal_check(unsafe_path, known_safe_path): raise SuspiciousFileOperation("{} is not safe".format(unsafe_path)) # Passes the check - return unsafe_path \ No newline at end of file + return unsafe_path + + +def double_quote(s): + """Return a shell-escaped version of the string *s*.""" + if not s: + return '""' + if _find_unsafe(s) is None: + return s + + # use double quotes, and prefix double quotes with a \ + # the string $"b is then quoted as "$\"b" + return '"' + s.replace('"', '\\\"') + '"' \ No newline at end of file diff --git a/app/static/app/js/components/ExportAssetPanel.jsx b/app/static/app/js/components/ExportAssetPanel.jsx index 48618d11..6371971c 100644 --- a/app/static/app/js/components/ExportAssetPanel.jsx +++ b/app/static/app/js/components/ExportAssetPanel.jsx @@ -9,7 +9,7 @@ import Workers from '../classes/Workers'; export default class ExportAssetPanel extends React.Component { static defaultProps = { - exportFormats: ["gtiff-rgb", "gtiff", "jpg", "png"], + exportFormats: ["gtiff-rgb", "gtiff", "jpg", "png", "kmz"], asset: "", exportParams: {}, task: null, @@ -45,6 +45,10 @@ export default class ExportAssetPanel extends React.Component { 'png': { label: _("PNG (RGB)"), icon: "fas fa-palette" + }, + 'kmz': { + label: _("KMZ (RGB)"), + icon: "fa fa-globe" } };