This is an automated email from the ASF dual-hosted git repository. jiayu pushed a commit to branch feature/cog-writer in repository https://gitbox.apache.org/repos/asf/sedona.git
commit b31f4de563f996e6c1e7c0d06188aa56c2382fef Author: Jia Yu <[email protected]> AuthorDate: Thu Feb 19 02:11:46 2026 -0800 Add CogOptions builder for configurable COG generation - CogOptions: immutable builder with compression, compressionQuality, tileSize, resampling (Nearest/Bilinear/Bicubic), and overviewCount - CogWriter: new write(raster, CogOptions) primary method; existing overloads delegate to it; generateOverview accepts Interpolation param - RasterOutputs: new asCloudOptimizedGeoTiff(raster, CogOptions) overload - 13 new tests (25 total): builder validation, resampling modes, overviewCount=0/1, tileSize=512, RasterOutputs CogOptions path --- .../apache/sedona/common/raster/RasterOutputs.java | 16 ++ .../sedona/common/raster/cog/CogOptions.java | 243 +++++++++++++++++++++ .../apache/sedona/common/raster/cog/CogWriter.java | 106 ++++++--- .../sedona/common/raster/cog/CogWriterTest.java | 229 +++++++++++++++++++ 4 files changed, 564 insertions(+), 30 deletions(-) diff --git a/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java index 791e2d27e7..5a74fea38d 100644 --- a/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java +++ b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java @@ -36,6 +36,7 @@ import javax.imageio.ImageWriteParam; import javax.media.jai.InterpolationNearest; import javax.media.jai.JAI; import javax.media.jai.RenderedOp; +import org.apache.sedona.common.raster.cog.CogOptions; import org.apache.sedona.common.raster.cog.CogWriter; import org.apache.sedona.common.utils.RasterUtils; import org.geotools.api.coverage.grid.GridCoverageWriter; @@ -123,6 +124,21 @@ public class RasterOutputs { } } + /** + * Creates a Cloud Optimized GeoTIFF (COG) byte array with the given options. + * + * @param raster The input raster + * @param options COG generation options (compression, tileSize, resampling, overviewCount) + * @return COG file as byte array + */ + public static byte[] asCloudOptimizedGeoTiff(GridCoverage2D raster, CogOptions options) { + try { + return CogWriter.write(raster, options); + } catch (IOException e) { + throw new RuntimeException("Failed to write Cloud Optimized GeoTIFF", e); + } + } + /** * Creates a GeoTiff file with the provided raster. Primarily used for testing. * diff --git a/common/src/main/java/org/apache/sedona/common/raster/cog/CogOptions.java b/common/src/main/java/org/apache/sedona/common/raster/cog/CogOptions.java new file mode 100644 index 0000000000..d253be7428 --- /dev/null +++ b/common/src/main/java/org/apache/sedona/common/raster/cog/CogOptions.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sedona.common.raster.cog; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Options for Cloud Optimized GeoTIFF (COG) generation. + * + * <p>Use the {@link Builder} to construct instances: + * + * <pre>{@code + * CogOptions opts = CogOptions.builder() + * .compression("LZW") + * .compressionQuality(0.5) + * .tileSize(512) + * .resampling("Bilinear") + * .overviewCount(3) + * .build(); + * }</pre> + * + * <p>All fields are immutable once constructed. Validation is performed in {@link Builder#build()}. + */ +public final class CogOptions { + + /** Supported resampling algorithms for overview generation. */ + private static final List<String> VALID_RESAMPLING = + Arrays.asList("Nearest", "Bilinear", "Bicubic"); + + private final String compression; + private final double compressionQuality; + private final int tileSize; + private final String resampling; + private final int overviewCount; + + private CogOptions(Builder builder) { + this.compression = builder.compression; + this.compressionQuality = builder.compressionQuality; + this.tileSize = builder.tileSize; + this.resampling = builder.resampling; + this.overviewCount = builder.overviewCount; + } + + /** + * @return Compression type: "Deflate", "LZW", "JPEG", "PackBits" + */ + public String getCompression() { + return compression; + } + + /** + * @return Compression quality from 0.0 (max compression) to 1.0 (no compression) + */ + public double getCompressionQuality() { + return compressionQuality; + } + + /** + * @return Tile width and height in pixels (always a power of 2) + */ + public int getTileSize() { + return tileSize; + } + + /** + * @return Resampling algorithm for overview generation: "Nearest", "Bilinear", or "Bicubic" + */ + public String getResampling() { + return resampling; + } + + /** + * @return Number of overview levels. -1 means auto-compute based on image dimensions, 0 means no + * overviews. + */ + public int getOverviewCount() { + return overviewCount; + } + + /** + * @return A new builder initialized with default values + */ + public static Builder builder() { + return new Builder(); + } + + /** + * @return The default options (Deflate, quality 0.2, 256px tiles, Nearest, auto overviews) + */ + public static CogOptions defaults() { + return new Builder().build(); + } + + @Override + public String toString() { + return "CogOptions{" + + "compression='" + + compression + + '\'' + + ", compressionQuality=" + + compressionQuality + + ", tileSize=" + + tileSize + + ", resampling='" + + resampling + + '\'' + + ", overviewCount=" + + overviewCount + + '}'; + } + + /** Builder for {@link CogOptions}. */ + public static final class Builder { + private String compression = "Deflate"; + private double compressionQuality = 0.2; + private int tileSize = 256; + private String resampling = "Nearest"; + private int overviewCount = -1; + + private Builder() {} + + /** + * Set the compression type. Default: "Deflate". + * + * @param compression One of "Deflate", "LZW", "JPEG", "PackBits" + * @return this builder + */ + public Builder compression(String compression) { + this.compression = compression; + return this; + } + + /** + * Set the compression quality. Default: 0.2. + * + * @param compressionQuality Value from 0.0 (max compression) to 1.0 (no compression) + * @return this builder + */ + public Builder compressionQuality(double compressionQuality) { + this.compressionQuality = compressionQuality; + return this; + } + + /** + * Set the tile size for both width and height. Default: 256. + * + * @param tileSize Must be a positive power of 2 (e.g. 128, 256, 512, 1024) + * @return this builder + */ + public Builder tileSize(int tileSize) { + this.tileSize = tileSize; + return this; + } + + /** + * Set the resampling algorithm for overview generation. Default: "Nearest". + * + * @param resampling One of "Nearest", "Bilinear", "Bicubic" + * @return this builder + */ + public Builder resampling(String resampling) { + this.resampling = resampling; + return this; + } + + /** + * Set the number of overview levels. Default: -1 (auto-compute). + * + * @param overviewCount -1 for auto, 0 for no overviews, or a positive count + * @return this builder + */ + public Builder overviewCount(int overviewCount) { + this.overviewCount = overviewCount; + return this; + } + + /** + * Build and validate the options. + * + * @return A validated, immutable {@link CogOptions} instance + * @throws IllegalArgumentException if any option is invalid + */ + public CogOptions build() { + if (compression == null || compression.isEmpty()) { + throw new IllegalArgumentException("compression must not be null or empty"); + } + if (compressionQuality < 0 || compressionQuality > 1.0) { + throw new IllegalArgumentException( + "compressionQuality must be between 0.0 and 1.0, got: " + compressionQuality); + } + if (tileSize <= 0) { + throw new IllegalArgumentException("tileSize must be positive, got: " + tileSize); + } + if ((tileSize & (tileSize - 1)) != 0) { + throw new IllegalArgumentException("tileSize must be a power of 2, got: " + tileSize); + } + if (overviewCount < -1) { + throw new IllegalArgumentException( + "overviewCount must be -1 (auto), 0 (none), or positive, got: " + overviewCount); + } + + // Normalize resampling to title-case for matching + String normalized = normalizeResampling(resampling); + if (!VALID_RESAMPLING.contains(normalized)) { + throw new IllegalArgumentException( + "resampling must be one of " + VALID_RESAMPLING + ", got: '" + resampling + "'"); + } + this.resampling = normalized; + + return new CogOptions(this); + } + + /** + * Normalize the resampling string to title-case (first letter uppercase, rest lowercase) so + * callers can pass "nearest", "BILINEAR", etc. + */ + private static String normalizeResampling(String value) { + if (value == null || value.isEmpty()) { + return "Nearest"; + } + String lower = value.toLowerCase(Locale.ROOT); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); + } + } +} diff --git a/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java b/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java index 4adadedd43..4cbf19ea67 100644 --- a/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java +++ b/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.List; import javax.imageio.ImageWriteParam; import javax.media.jai.Interpolation; +import javax.media.jai.InterpolationBicubic; +import javax.media.jai.InterpolationBilinear; import javax.media.jai.InterpolationNearest; import org.geotools.api.coverage.grid.GridCoverageWriter; import org.geotools.api.parameter.GeneralParameterValue; @@ -65,49 +67,40 @@ public class CogWriter { private static final int MIN_OVERVIEW_SIZE = 2; /** - * Write a GridCoverage2D as a Cloud Optimized GeoTIFF byte array. + * Write a GridCoverage2D as a Cloud Optimized GeoTIFF byte array using the given options. * * @param raster The input raster - * @param compressionType Compression type: "Deflate", "LZW", "JPEG", "PackBits", or null for - * default (Deflate) - * @param compressionQuality Quality 0.0 (max compression) to 1.0 (no compression), or -1 for - * default - * @param tileSize Tile width and height in pixels + * @param options COG generation options (compression, tileSize, resampling, overviewCount) * @return COG file as byte array * @throws IOException if writing fails */ - public static byte[] write( - GridCoverage2D raster, String compressionType, double compressionQuality, int tileSize) - throws IOException { - - if (compressionType == null) { - compressionType = "Deflate"; - } - if (compressionQuality < 0) { - compressionQuality = 0.2; - } - if (compressionQuality > 1.0) { - throw new IllegalArgumentException( - "compressionQuality must be between 0.0 and 1.0, got: " + compressionQuality); - } - if (tileSize <= 0) { - throw new IllegalArgumentException("tileSize must be positive, got: " + tileSize); - } - if ((tileSize & (tileSize - 1)) != 0) { - throw new IllegalArgumentException("tileSize must be a power of 2, got: " + tileSize); - } + public static byte[] write(GridCoverage2D raster, CogOptions options) throws IOException { + String compressionType = options.getCompression(); + double compressionQuality = options.getCompressionQuality(); + int tileSize = options.getTileSize(); + String resampling = options.getResampling(); + int requestedOverviewCount = options.getOverviewCount(); RenderedImage image = raster.getRenderedImage(); int cols = image.getWidth(); int rows = image.getHeight(); // Step 1: Compute overview decimation factors - List<Integer> decimations = computeOverviewDecimations(cols, rows, tileSize); + List<Integer> decimations; + if (requestedOverviewCount == 0) { + decimations = new ArrayList<>(); + } else { + decimations = computeOverviewDecimations(cols, rows, tileSize); + if (requestedOverviewCount > 0 && requestedOverviewCount < decimations.size()) { + decimations = decimations.subList(0, requestedOverviewCount); + } + } // Step 2: Generate overview coverages + Interpolation interpolation = getInterpolation(resampling); List<GridCoverage2D> overviews = new ArrayList<>(); for (int decimation : decimations) { - GridCoverage2D overview = generateOverview(raster, decimation); + GridCoverage2D overview = generateOverview(raster, decimation, interpolation); overviews.add(overview); } @@ -128,6 +121,32 @@ public class CogWriter { return CogAssembler.assemble(parsedTiffs); } + /** + * Write a GridCoverage2D as a Cloud Optimized GeoTIFF byte array. + * + * @param raster The input raster + * @param compressionType Compression type: "Deflate", "LZW", "JPEG", "PackBits", or null for + * default (Deflate) + * @param compressionQuality Quality 0.0 (max compression) to 1.0 (no compression), or -1 for + * default + * @param tileSize Tile width and height in pixels + * @return COG file as byte array + * @throws IOException if writing fails + */ + public static byte[] write( + GridCoverage2D raster, String compressionType, double compressionQuality, int tileSize) + throws IOException { + + CogOptions.Builder builder = CogOptions.builder().tileSize(tileSize); + if (compressionType != null) { + builder.compression(compressionType); + } + if (compressionQuality >= 0) { + builder.compressionQuality(compressionQuality); + } + return write(raster, builder.build()); + } + /** * Write a GridCoverage2D as COG with default settings (Deflate compression, 256x256 tiles). * @@ -186,9 +205,11 @@ public class CogWriter { * * @param raster The full resolution raster * @param decimationFactor Factor to reduce by (2 = half size, 4 = quarter, etc.) + * @param interpolation The interpolation method to use for resampling * @return A new GridCoverage2D at reduced resolution */ - static GridCoverage2D generateOverview(GridCoverage2D raster, int decimationFactor) { + static GridCoverage2D generateOverview( + GridCoverage2D raster, int decimationFactor, Interpolation interpolation) { RenderedImage image = raster.getRenderedImage(); int newWidth = (int) Math.ceil((double) image.getWidth() / decimationFactor); int newHeight = (int) Math.ceil((double) image.getHeight() / decimationFactor); @@ -218,10 +239,35 @@ public class CogWriter { crs, null); - Interpolation interpolation = new InterpolationNearest(); return (GridCoverage2D) Operations.DEFAULT.resample(raster, null, gridGeometry, interpolation); } + /** + * Generate an overview using default nearest-neighbor interpolation. Kept for backward + * compatibility with tests. + */ + static GridCoverage2D generateOverview(GridCoverage2D raster, int decimationFactor) { + return generateOverview(raster, decimationFactor, new InterpolationNearest()); + } + + /** + * Map a resampling algorithm name to a JAI Interpolation instance. + * + * @param resampling One of "Nearest", "Bilinear", "Bicubic" + * @return The corresponding JAI Interpolation + */ + private static Interpolation getInterpolation(String resampling) { + switch (resampling) { + case "Bilinear": + return new InterpolationBilinear(); + case "Bicubic": + return new InterpolationBicubic(8); + case "Nearest": + default: + return new InterpolationNearest(); + } + } + /** * Write a GridCoverage2D as a tiled GeoTIFF byte array using GeoTools. * diff --git a/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java b/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java index dfbbd07d0e..7989456e6b 100644 --- a/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java +++ b/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java @@ -419,4 +419,233 @@ public class CogWriterTest { // The last IFD end should be well before the end of the file assertTrue("IFD region should be at start of file", lastIfdEnd < cogBytes.length / 2); } + + // --- CogOptions tests --- + + @Test + public void testCogOptionsDefaults() { + CogOptions opts = CogOptions.defaults(); + assertEquals("Deflate", opts.getCompression()); + assertEquals(0.2, opts.getCompressionQuality(), 0.001); + assertEquals(256, opts.getTileSize()); + assertEquals("Nearest", opts.getResampling()); + assertEquals(-1, opts.getOverviewCount()); + } + + @Test + public void testCogOptionsBuilder() { + CogOptions opts = + CogOptions.builder() + .compression("LZW") + .compressionQuality(0.8) + .tileSize(512) + .resampling("Bilinear") + .overviewCount(3) + .build(); + + assertEquals("LZW", opts.getCompression()); + assertEquals(0.8, opts.getCompressionQuality(), 0.001); + assertEquals(512, opts.getTileSize()); + assertEquals("Bilinear", opts.getResampling()); + assertEquals(3, opts.getOverviewCount()); + } + + @Test + public void testCogOptionsResamplingNormalization() { + // Case-insensitive resampling names + assertEquals("Nearest", CogOptions.builder().resampling("nearest").build().getResampling()); + assertEquals("Bilinear", CogOptions.builder().resampling("BILINEAR").build().getResampling()); + assertEquals("Bicubic", CogOptions.builder().resampling("bicubic").build().getResampling()); + } + + @Test + public void testCogOptionsInvalidResampling() { + try { + CogOptions.builder().resampling("Lanczos").build(); + fail("Expected IllegalArgumentException for invalid resampling"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("resampling")); + } + } + + @Test + public void testCogOptionsInvalidTileSize() { + try { + CogOptions.builder().tileSize(300).build(); + fail("Expected IllegalArgumentException for non-power-of-2 tileSize"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("power of 2")); + } + } + + @Test + public void testCogOptionsInvalidOverviewCount() { + try { + CogOptions.builder().overviewCount(-2).build(); + fail("Expected IllegalArgumentException for negative overviewCount"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("overviewCount")); + } + } + + @Test + public void testWriteWithCogOptions() throws IOException { + double[] bandValues = new double[512 * 512]; + for (int i = 0; i < bandValues.length; i++) { + bandValues[i] = (i * 7) % 256; + } + GridCoverage2D raster = + RasterConstructors.makeNonEmptyRaster( + 1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] {bandValues}); + + CogOptions opts = + CogOptions.builder().compression("LZW").compressionQuality(0.5).tileSize(256).build(); + + byte[] cogBytes = CogWriter.write(raster, opts); + assertNotNull(cogBytes); + assertTrue(cogBytes.length > 0); + + GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes); + assertEquals(512, readBack.getRenderedImage().getWidth()); + assertEquals(512, readBack.getRenderedImage().getHeight()); + + double[] originalValues = MapAlgebra.bandAsArray(raster, 1); + double[] readBackValues = MapAlgebra.bandAsArray(readBack, 1); + assertArrayEquals(originalValues, readBackValues, 0.01); + } + + @Test + public void testWriteWithBilinearResampling() throws IOException { + double[] bandValues = new double[512 * 512]; + for (int i = 0; i < bandValues.length; i++) { + bandValues[i] = (i * 3) % 256; + } + GridCoverage2D raster = + RasterConstructors.makeNonEmptyRaster( + 1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] {bandValues}); + + CogOptions opts = CogOptions.builder().resampling("Bilinear").build(); + byte[] cogBytes = CogWriter.write(raster, opts); + assertNotNull(cogBytes); + assertTrue(cogBytes.length > 0); + + // Must be valid TIFF and readable + GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes); + assertEquals(512, readBack.getRenderedImage().getWidth()); + } + + @Test + public void testWriteWithBicubicResampling() throws IOException { + double[] bandValues = new double[512 * 512]; + for (int i = 0; i < bandValues.length; i++) { + bandValues[i] = (i * 5) % 256; + } + GridCoverage2D raster = + RasterConstructors.makeNonEmptyRaster( + 1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] {bandValues}); + + CogOptions opts = CogOptions.builder().resampling("Bicubic").build(); + byte[] cogBytes = CogWriter.write(raster, opts); + assertNotNull(cogBytes); + + GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes); + assertEquals(512, readBack.getRenderedImage().getWidth()); + } + + @Test + public void testWriteWithOverviewCountZero() throws IOException { + double[] bandValues = new double[512 * 512]; + for (int i = 0; i < bandValues.length; i++) { + bandValues[i] = (i * 11) % 256; + } + GridCoverage2D raster = + RasterConstructors.makeNonEmptyRaster( + 1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] {bandValues}); + + CogOptions opts = CogOptions.builder().overviewCount(0).build(); + byte[] cogBytes = CogWriter.write(raster, opts); + assertNotNull(cogBytes); + + // With overviewCount=0, there should be exactly 1 IFD (no overviews) + ByteOrder byteOrder = (cogBytes[0] == 'I') ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + ByteBuffer buf = ByteBuffer.wrap(cogBytes).order(byteOrder); + int firstIfdOffset = buf.getInt(4); + int tagCount = buf.getShort(firstIfdOffset) & 0xFFFF; + int nextIfdOffset = buf.getInt(firstIfdOffset + 2 + tagCount * 12); + assertEquals("Should have no overview IFD when overviewCount=0", 0, nextIfdOffset); + + // Should still be readable + GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes); + assertEquals(512, readBack.getRenderedImage().getWidth()); + } + + @Test + public void testWriteWithSpecificOverviewCount() throws IOException { + // 1024x1024 with tileSize=256 would normally produce 2 overviews (2, 4) + double[] bandValues = new double[1024 * 1024]; + for (int i = 0; i < bandValues.length; i++) { + bandValues[i] = (i * 13) % 256; + } + GridCoverage2D raster = + RasterConstructors.makeNonEmptyRaster( + 1, "d", 1024, 1024, 0, 0, 1, -1, 0, 0, 4326, new double[][] {bandValues}); + + // Request only 1 overview + CogOptions opts = CogOptions.builder().overviewCount(1).build(); + byte[] cogBytes = CogWriter.write(raster, opts); + assertNotNull(cogBytes); + + // Count IFDs: should have exactly 2 (full-res + 1 overview) + ByteOrder byteOrder = (cogBytes[0] == 'I') ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + ByteBuffer buf = ByteBuffer.wrap(cogBytes).order(byteOrder); + + int ifdCount = 0; + int ifdOffset = buf.getInt(4); + while (ifdOffset != 0) { + ifdCount++; + int tagCount = buf.getShort(ifdOffset) & 0xFFFF; + ifdOffset = buf.getInt(ifdOffset + 2 + tagCount * 12); + } + assertEquals("Should have exactly 2 IFDs (full-res + 1 overview)", 2, ifdCount); + + GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes); + assertEquals(1024, readBack.getRenderedImage().getWidth()); + } + + @Test + public void testWriteWithTileSize512() throws IOException { + double[] bandValues = new double[1024 * 1024]; + for (int i = 0; i < bandValues.length; i++) { + bandValues[i] = (i * 17) % 256; + } + GridCoverage2D raster = + RasterConstructors.makeNonEmptyRaster( + 1, "d", 1024, 1024, 0, 0, 1, -1, 0, 0, 4326, new double[][] {bandValues}); + + CogOptions opts = CogOptions.builder().tileSize(512).build(); + byte[] cogBytes = CogWriter.write(raster, opts); + assertNotNull(cogBytes); + assertTrue(cogBytes.length > 0); + + GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes); + assertEquals(1024, readBack.getRenderedImage().getWidth()); + } + + @Test + public void testRasterOutputsWithCogOptions() throws IOException { + double[] bandValues = new double[256 * 256]; + for (int i = 0; i < bandValues.length; i++) { + bandValues[i] = i % 256; + } + GridCoverage2D raster = + RasterConstructors.makeNonEmptyRaster( + 1, "d", 256, 256, 0, 0, 1, -1, 0, 0, 4326, new double[][] {bandValues}); + + CogOptions opts = CogOptions.builder().compression("LZW").overviewCount(0).build(); + byte[] cogBytes = RasterOutputs.asCloudOptimizedGeoTiff(raster, opts); + assertNotNull(cogBytes); + + GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes); + assertEquals(256, readBack.getRenderedImage().getWidth()); + } }
