diff --git a/autotest/gcore/cog.py b/autotest/gcore/cog.py index 8056e244caed..4b6e4b6a6aba 100755 --- a/autotest/gcore/cog.py +++ b/autotest/gcore/cog.py @@ -18,6 +18,7 @@ import gdaltest import pytest +import webserver from test_py_scripts import samples_path from osgeo import gdal, osr @@ -1990,3 +1991,300 @@ def test_cog_preserve_ALPHA_PREMULTIPLIED_on_copy(tmp_vsimem): ds.GetRasterBand(4).GetMetadataItem("ALPHA", "IMAGE_STRUCTURE") == "PREMULTIPLIED" ) + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with gdal.quiet_errors(): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=TILE", "BLOCKSIZE=32"], + ) + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 21212, + 21053, + 21349, + ] + + _check_cog(out_filename) + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_band(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with gdal.quiet_errors(): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=BAND", "BLOCKSIZE=32"], + ) + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "BAND" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 21212, + 21053, + 21349, + ] + + _check_cog(out_filename) + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_with_mask(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with gdal.quiet_errors(): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Translate( + "", "data/stefan_full_rgba.tif", options="-f MEM -b 1 -b 2 -b 3 -mask 4" + ), + options=["INTERLEAVE=TILE", "BLOCKSIZE=32"], + ) + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 12603, + 58561, + 36064, + ] + assert ds.GetRasterBand(1).GetMaskBand().Checksum() == 22499 + + _check_cog(out_filename) + + # Check that the tiles are in the expected order in the file + last_offset = 0 + for y in range(2): + for x in range(2): + for band in range(3): + offset = int( + ds.GetRasterBand(band + 1).GetMetadataItem( + f"BLOCK_OFFSET_{x}_{y}", "TIFF" + ) + ) + assert offset > last_offset + last_offset = offset + offset = int( + ds.GetRasterBand(1) + .GetMaskBand() + .GetMetadataItem(f"BLOCK_OFFSET_{x}_{y}", "TIFF") + ) + assert offset > last_offset + last_offset = offset + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_with_mask_and_ovr(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + out2_filename = str(tmp_vsimem / "out2.tif") + + ds = gdal.Translate( + out_filename, + "data/stefan_full_rgba.tif", + options="-b 1 -b 2 -b 3 -mask 4 -outsize 1024 0", + ) + ds.BuildOverviews("NEAR", [2]) + ds.Close() + + ds = gdal.Open(out_filename) + expected_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + expected_md += [ds.GetRasterBand(1).GetMaskBand().Checksum()] + expected_ovr_md = [ + ds.GetRasterBand(band + 1).GetOverview(0).Checksum() for band in range(3) + ] + expected_ovr_md += [ds.GetRasterBand(1).GetMaskBand().GetOverview(0).Checksum()] + + gdal.GetDriverByName("COG").CreateCopy( + out2_filename, + ds, + options=["INTERLEAVE=TILE", "OVERVIEW_RESAMPLING=NEAREST"], + ) + + ds = gdal.Open(out2_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + assert ds.GetMetadataItem("LAYOUT", "IMAGE_STRUCTURE") == "COG" + + _check_cog(out2_filename) + + got_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + got_md += [ds.GetRasterBand(1).GetMaskBand().Checksum()] + assert got_md == expected_md + got_ovr_md = [ + ds.GetRasterBand(band + 1).GetOverview(0).Checksum() for band in range(3) + ] + got_ovr_md += [ds.GetRasterBand(1).GetMaskBand().GetOverview(0).Checksum()] + assert got_ovr_md == expected_ovr_md + + +############################################################################### +# Check that our reading of a COG with /vsicurl is efficient + + +@pytest.mark.require_curl() +@pytest.mark.skipif( + not check_libtiff_internal_or_at_least(4, 0, 11), + reason="libtiff >= 4.0.11 required", +) +@pytest.mark.parametrize("INTERLEAVE", ["BAND", "TILE"]) +def test_cog_interleave_tile_or_band_vsicurl(tmp_vsimem, INTERLEAVE): + + gdal.VSICurlClearCache() + + webserver_process = None + webserver_port = 0 + + (webserver_process, webserver_port) = webserver.launch( + handler=webserver.DispatcherHttpHandler + ) + if webserver_port == 0: + pytest.skip() + + in_filename = str(tmp_vsimem / "in.tif") + cog_filename = str(tmp_vsimem / "cog.tif") + + ds = gdal.Translate( + in_filename, + "data/stefan_full_rgba.tif", + options="-b 1 -b 2 -b 3 -mask 4 -outsize 1024 0", + ) + ds.BuildOverviews("NEAR", [2]) + ds.Close() + + src_ds = gdal.Open(in_filename) + gdal.GetDriverByName("COG").CreateCopy( + cog_filename, + src_ds, + options=["INTERLEAVE=" + INTERLEAVE, "OVERVIEW_RESAMPLING=NEAREST"], + ) + + def extract(offset, size): + f = gdal.VSIFOpenL(cog_filename, "rb") + gdal.VSIFSeekL(f, offset, 0) + data = gdal.VSIFReadL(size, 1, f) + gdal.VSIFCloseL(f) + return data + + try: + filesize = gdal.VSIStatL(cog_filename).size + + handler = webserver.SequentialHandler() + handler.add("HEAD", "/cog.tif", 200, {"Content-Length": "%d" % filesize}) + handler.add( + "GET", + "/cog.tif", + 206, + {"Content-Length": "16384"}, + extract(0, 16384), + expected_headers={"Range": "bytes=0-16383"}, + ) + with webserver.install_http_handler(handler): + ds = gdal.Open("/vsicurl/http://localhost:%d/cog.tif" % webserver_port) + + handler = webserver.SequentialHandler() + + def method(request): + # sys.stderr.write('%s\n' % str(request.headers)) + + if request.headers["Range"].startswith("bytes="): + rng = request.headers["Range"][len("bytes=") :] + assert len(rng.split("-")) == 2 + start = int(rng.split("-")[0]) + end = int(rng.split("-")[1]) + + request.protocol_version = "HTTP/1.1" + request.send_response(206) + request.send_header("Content-type", "application/octet-stream") + request.send_header( + "Content-Range", "bytes %d-%d/%d" % (start, end, filesize) + ) + request.send_header("Content-Length", end - start + 1) + request.send_header("Connection", "close") + request.end_headers() + + request.wfile.write(extract(start, end - start + 1)) + + handler.add("GET", "/cog.tif", custom_method=method) + with webserver.install_http_handler(handler): + ret = ds.ReadRaster() + assert ret == src_ds.ReadRaster() + + finally: + webserver.server_stop(webserver_process, webserver_port) + + gdal.VSICurlClearCache() + + +############################################################################### + + +@pytest.mark.require_creation_option("COG", "JPEG") +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_jpeg(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + gdal.GetDriverByName("GTiff").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=BAND", "COMPRESS=JPEG"], + ) + with gdal.Open(out_filename) as ds: + expected_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=TILE", "COMPRESS=JPEG"], + ) + with gdal.Open(out_filename) as ds: + got_md = [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] + + assert got_md == expected_md + + +############################################################################### + + +@pytest.mark.require_creation_option("COG", "WEBP") +@gdaltest.enable_exceptions() +def test_cog_write_interleave_tile_webp_error(tmp_vsimem): + out_filename = str(tmp_vsimem / "out.tif") + + with pytest.raises( + Exception, match="COMPRESS=WEBP only supported for INTERLEAVE=PIXEL" + ): + gdal.GetDriverByName("COG").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=["INTERLEAVE=TILE", "COMPRESS=WEBP"], + ) diff --git a/autotest/gcore/test_driver_metadata.py b/autotest/gcore/test_driver_metadata.py index d36628be4055..b8907c47e938 100644 --- a/autotest/gcore/test_driver_metadata.py +++ b/autotest/gcore/test_driver_metadata.py @@ -130,6 +130,7 @@ + diff --git a/autotest/gcore/tiff_write.py b/autotest/gcore/tiff_write.py index 25f58a827f9c..c589fdf6b200 100755 --- a/autotest/gcore/tiff_write.py +++ b/autotest/gcore/tiff_write.py @@ -11988,3 +11988,55 @@ def test_tiff_write_warn_ignore_predictor_option(tmp_vsimem): out_filename, 1, 1, options=["PREDICTOR=2"] ) assert "PREDICTOR option is ignored" in gdal.GetLastErrorMsg() + + +############################################################################### +# + + +@gdaltest.enable_exceptions() +@pytest.mark.parametrize("COPY_SRC_OVERVIEWS", ["YES", "NO"]) +def test_tiff_write_interleave_tile(tmp_vsimem, COPY_SRC_OVERVIEWS): + out_filename = str(tmp_vsimem / "out.tif") + with pytest.raises( + Exception, match="INTERLEAVE=TILE is only supported for CreateCopy" + ): + gdal.GetDriverByName("GTiff").Create( + out_filename, 1, 1, 2, options=["INTERLEAVE=TILE"] + ) + + ds = gdal.GetDriverByName("GTiff").CreateCopy( + out_filename, + gdal.Open("data/rgbsmall.tif"), + options=[ + "INTERLEAVE=TILE", + "TILED=YES", + "BLOCKXSIZE=32", + "BLOCKYSIZE=32", + "COPY_SRC_OVERVIEWS=" + COPY_SRC_OVERVIEWS, + ], + ) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + ds.Close() + + ds = gdal.Open(out_filename) + assert ds.GetMetadataItem("INTERLEAVE", "IMAGE_STRUCTURE") == "TILE" + + assert [ds.GetRasterBand(band + 1).Checksum() for band in range(3)] == [ + 21212, + 21053, + 21349, + ] + + # Check that the tiles are in the expected order in the file + last_offset = 0 + for y in range(2): + for x in range(2): + for band in range(3): + offset = int( + ds.GetRasterBand(band + 1).GetMetadataItem( + f"BLOCK_OFFSET_{x}_{y}", "TIFF" + ) + ) + assert offset > last_offset + last_offset = offset diff --git a/doc/source/drivers/raster/cog.rst b/doc/source/drivers/raster/cog.rst index c46bac1f5703..d9a6f06fca1b 100644 --- a/doc/source/drivers/raster/cog.rst +++ b/doc/source/drivers/raster/cog.rst @@ -43,6 +43,105 @@ General creation options Sets the tile width and height in pixels. Must be divisible by 16. +- .. co:: INTERLEAVE + :choices: BAND, PIXEL, TILE + :since: 3.11 + + Set the interleaving to use + + * ``PIXEL``: for each spatial block, one TIFF tile/strip gathering values for + all bands is used . This matches the ``contiguous`` planar configuration in + TIFF terminology. + This is also known as a ``BIP (Band Interleaved Per Pixel)`` organization. + Such method is the default, and may be slightly less + efficient than BAND interleaving for some purposes, but some applications + only support pixel interleaved TIFF files. On the other hand, image-based + compression methods may perform better using PIXEL interleaving. For JPEG + PHOTOMETRIC=YCbCr, pixel interleaving is required. It is also required for + WebP compression. + + Prior to GDAL 3.11, INTERLEAVE=PIXEL was the only possible interleaving + output by the COG driver. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_new_strip_or_tile() + end_for + end_for + end_for + end_for + + + * ``BAND``: for each spatial block, one TIFF tile/strip is used for each band. + This matches the contiguous ``separate`` configuration in TIFF terminology. + This is also known as a ``BSQ (Band SeQuential)`` organization. + + In addition to that, when using CreateCopy(), data for the first band is + written first, followed by data for the second band, etc. + The pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + + + * ``TILE`` (added in 3.11, only for CreateCopy()): this is a sort of + compromise between PIXEL and BAND, using the ``separate`` configuration, + but where data for a same spatial block is written for all bands, before + the data of the next spatial block. + When the block height is 1, this is also known as a + ``BIL (Band Interleaved per Line)`` organization. + + Such a layout may be useful for writing hyperspectral datasets (several + hundred of bands), to get a compromise between efficient spatial query, + and partial band selection. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for band in 0 ... numberBands -1: + start_new_strip_or_tile() + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + end_for + + + Starting with GDAL 3.5, when copying from a source dataset with multiple bands + which advertises a INTERLEAVE metadata item, if the INTERLEAVE creation option + is not specified, the source dataset INTERLEAVE will be automatically taken + into account, unless the COMPRESS creation option is specified. + - .. co:: COMPRESS :choices: NONE, LZW, JPEG, DEFLATE, ZSTD, WEBP, LERC, LERC_DEFLATE, LERC_ZSTD, LZMA :default: LZW @@ -56,7 +155,7 @@ General creation options files (seen as UInt16 bands with NBITS=12). For the COG driver, JPEG compression for 3 or 4-band images automatically selects the PHOTOMETRIC=YCBCR colorspace with a 4:2:2 subsampling of the Y,Cb,Cr - components. + components with the default INTERLEAVE=PIXEL. For a input dataset (single-band or 3-band), plus an alpha band, the alpha band will be converted as a 1-bit DEFLATE compressed mask. @@ -65,6 +164,10 @@ General creation options * ``ZSTD`` is available when using internal libtiff and if GDAL built against libzstd >=1.0, or if built against external libtiff with zstd support. + * ``WEBP`` is available when using internal libtiff and if GDAL built against + libwebp, or if built against external libtiff with WebP support. + It can only be used with the default INTERLEAVE=PIXEL. + * ``LERC`` is available when using internal libtiff. * ``LERC_ZSTD`` is available when ``LERC`` and ``ZSTD`` are available. @@ -559,11 +662,18 @@ line). mask.TileByteCounts[i], but none of them actually need to be read) * trailer of mask data (4 bytes) + This is only written if INTERLEAVE=PIXEL. + +- ``INTERLEAVE=BAND`` or ``INTERLEAVE=TILE``: (GDAL >= 3.11) + Reflects the value of the INTERLEAVE creation option. + Omission implies INTERLEAVE=PIXEL. + + .. note:: The content of the header ghost area can be retrieved by getting the ``GDAL_STRUCTURAL_METADATA`` metadata item of the ``TIFF`` metadata domain - on the datasett object (with GetMetadataItem()) + on the dataset object (with GetMetadataItem()) .. _cog.tile_data_leader_trailer: @@ -576,7 +686,8 @@ that follows it. This leader is *ghost* in the sense that the TileOffsets[] array does not point to it, but points to the real payload. Hence the offset of the leader is TileOffsets[i]-4. -An optimized reader seeing the ``BLOCK_LEADER=SIZE_AS_UINT4`` metadata item will thus look for TileOffset[i] +For INTERLEAVE=PIXEL or INTERLEAVE=TILE, an optimized reader seeing the +``BLOCK_LEADER=SIZE_AS_UINT4`` metadata item will thus look for TileOffset[i] and TileOffset[i+1] to deduce it must fetch the data starting at offset=TileOffset[i] - 4 and of size=TileOffset[i+1]-TileOffset[i]+4. It then checks the 4 first bytes to see if the size in this leader marker is @@ -587,6 +698,9 @@ MASK_INTERLEAVED_WITH_IMAGERY=YES, then the tile size indicated in the leader will be < TileOffset[i+1]-TileOffset[i] since the data for the mask will follow the imagery data (see MASK_INTERLEAVED_WITH_IMAGERY=YES) +For INTERLEAVE=BAND, the above paragraph applies but the successor of tile i +is not tile i+1, but tile i+nTilesPerBand. + Each tile data is immediately followed by a trailer, consisting of the repetition of the last 4 bytes of the payload of the tile data. The size of this trailer is *not* included in the TileByteCounts[] array. The purpose of this trailer is forces @@ -613,3 +727,9 @@ See Also - If your source dataset is an internally tiled geotiff with the desired georeferencing and compression, using `cogger `__ (possibly along with gdaladdo to create overviews) will be much faster than the COG driver. + + +.. below is an allow-list for spelling checker. + +.. spelling:word-list:: + nTilesPerBand diff --git a/doc/source/drivers/raster/gtiff.rst b/doc/source/drivers/raster/gtiff.rst index f6d4f722c42a..cdc14a6e3634 100644 --- a/doc/source/drivers/raster/gtiff.rst +++ b/doc/source/drivers/raster/gtiff.rst @@ -420,13 +420,95 @@ This driver supports the following creation options: RPC information is available. - .. co:: INTERLEAVE - :choices: BAND, PIXEL + :choices: BAND, PIXEL, TILE + + Set the interleaving to use + + * ``PIXEL``: for each spatial block, one TIFF tile/strip gathering values for + all bands is used . This matches the ``contiguous`` planar configuration in + TIFF terminology. + This is also known as a ``BIP (Band Interleaved Per Pixel)`` organization. + Such method is the default, and may be slightly less + efficient than BAND interleaving for some purposes, but some applications + only support pixel interleaved TIFF files. On the other hand, image-based + compression methods may perform better using PIXEL interleaving. For JPEG + PHOTOMETRIC=YCbCr, pixel interleaving is required. It is also required for + WebP compression. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_new_strip_or_tile() + end_for + end_for + end_for + end_for + + + * ``BAND``: for each spatial block, one TIFF tile/strip is used for each band. + This matches the contiguous ``separate`` configuration in TIFF terminology. + This is also known as a ``BSQ (Band SeQuential)`` organization. + + In addition to that, when using CreateCopy(), data for the first band is + written first, followed by data for the second band, etc. + The pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + start_new_strip_or_tile() + for band in 0 ... numberBands -1: + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + + + * ``TILE`` (added in 3.11, only for CreateCopy()): this is a sort of + compromise between PIXEL and BAND, using the ``separate`` configuration, + but where data for a same spatial block is written for all bands, before + the data of the next spatial block. + When the block height is 1, this is also known as a + ``BIL (Band Interleaved per Line)`` organization. + + Such a layout may be useful for writing hyperspectral datasets (several + hundred of bands), to get a compromise between efficient spatial query, + and partial band selection. + + Assuming a pixel[band][y][x] indexed array, when using CreateCopy(), + the pseudo code for the file disposition is: + + :: + + for y in 0 ... numberOfBlocksInHeight - 1: + for x in 0 ... numberOfTilesInWidth - 1: + for band in 0 ... numberBands -1: + start_new_strip_or_tile() + for j in 0 ... blockHeight - 1: + for i in 0 ... blockWidth -1: + write(pixel[band][y*blockHeight+j][x*blockWidth+i]) + end_for + end_for + end_new_strip_or_tile() + end_for + end_for + end_for + - By default TIFF files with pixel - interleaving (PLANARCONFIG_CONTIG in TIFF terminology) are created. - These are slightly less efficient than BAND interleaving for some - purposes, but some applications only support pixel interleaved TIFF - files. Starting with GDAL 3.5, when copying from a source dataset with multiple bands which advertises a INTERLEAVE metadata item, if the INTERLEAVE creation option is not specified, the source dataset INTERLEAVE will be automatically taken diff --git a/frmts/gtiff/cogdriver.cpp b/frmts/gtiff/cogdriver.cpp index ac56ca0d2845..366955c7fb8a 100644 --- a/frmts/gtiff/cogdriver.cpp +++ b/frmts/gtiff/cogdriver.cpp @@ -763,6 +763,21 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, return nullptr; } + const CPLString osCompress = CSLFetchNameValueDef( + papszOptions, "COMPRESS", gbHasLZW ? "LZW" : "NONE"); + + const char *pszInterleave = + CSLFetchNameValueDef(papszOptions, "INTERLEAVE", "PIXEL"); + if (EQUAL(osCompress, "WEBP")) + { + if (!EQUAL(pszInterleave, "PIXEL")) + { + CPLError(CE_Failure, CPLE_NotSupported, + "COMPRESS=WEBP only supported for INTERLEAVE=PIXEL"); + return nullptr; + } + } + CPLConfigOptionSetter oSetterReportDirtyBlockFlushing( "GDAL_REPORT_DIRTY_BLOCK_FLUSHING", "NO", true); @@ -877,9 +892,7 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, } } - CPLString osCompress = CSLFetchNameValueDef(papszOptions, "COMPRESS", - gbHasLZW ? "LZW" : "NONE"); - if (EQUAL(osCompress, "JPEG") && + if (EQUAL(osCompress, "JPEG") && EQUAL(pszInterleave, "PIXEL") && (poCurDS->GetRasterCount() == 2 || poCurDS->GetRasterCount() == 4) && poCurDS->GetRasterBand(poCurDS->GetRasterCount()) ->GetColorInterpretation() == GCI_AlphaBand) @@ -1198,7 +1211,7 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, if (EQUAL(osCompress, "JPEG")) { aosOptions.SetNameValue("JPEG_QUALITY", pszQuality); - if (nBands == 3) + if (nBands == 3 && EQUAL(pszInterleave, "PIXEL")) aosOptions.SetNameValue("PHOTOMETRIC", "YCBCR"); } else if (EQUAL(osCompress, "WEBP")) @@ -1316,7 +1329,8 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, } std::unique_ptr poPhotometricSetter; - if (nBands == 3 && EQUAL(pszOverviewCompress, "JPEG")) + if (nBands == 3 && EQUAL(pszOverviewCompress, "JPEG") && + EQUAL(pszInterleave, "PIXEL")) { poPhotometricSetter.reset( new CPLConfigOptionSetter("PHOTOMETRIC_OVERVIEW", "YCBCR", true)); @@ -1347,6 +1361,8 @@ GDALDataset *GDALCOGCreator::Create(const char *pszFilename, aosOptions.AddNameValue("SRC_MDD", *papszSrcMDDIter); CSLDestroy(papszSrcMDD); + aosOptions.SetNameValue("INTERLEAVE", pszInterleave); + CPLDebug("COG", "Generating final product: start"); auto poRet = poGTiffDrv->CreateCopy(pszFilename, poCurDS, false, aosOptions.List(), @@ -1523,6 +1539,11 @@ void GDALCOGDriver::InitializeCreationOptionList() "(16)'/>" " " " " "