From a6de94c48cff4f70e0b8d3497852b5d0218c1814 Mon Sep 17 00:00:00 2001 From: Benjamin Root Date: Mon, 13 May 2024 10:51:21 -0400 Subject: [PATCH] Simplify geotiff metadata writing --- .../java/ucar/nc2/geotiff/GeotiffWriter.java | 95 ++++++++---------- .../ucar/nc2/geotiff/baseline_palette.tif | Bin 12915 -> 12967 bytes .../ucar/nc2/geotiff/TestGeoTiffPalette.java | 65 ++++++++++++ 3 files changed, 106 insertions(+), 54 deletions(-) diff --git a/cdm/misc/src/main/java/ucar/nc2/geotiff/GeotiffWriter.java b/cdm/misc/src/main/java/ucar/nc2/geotiff/GeotiffWriter.java index 5c611a61ef..193d586a0c 100644 --- a/cdm/misc/src/main/java/ucar/nc2/geotiff/GeotiffWriter.java +++ b/cdm/misc/src/main/java/ucar/nc2/geotiff/GeotiffWriter.java @@ -323,63 +323,50 @@ private void writeMetadata(boolean greyScale, double xStart, double yStart, doub geotiff.addTag(new IFDEntry(Tag.Software, FieldType.ASCII).setValue("nc2geotiff")); geotiff.addTag(new IFDEntry(Tag.PlanarConfiguration, FieldType.SHORT).setValue(1)); - if (greyScale) { - // standard tags for Greyscale images ( see TIFF spec, section 4) - geotiff.addTag(new IFDEntry(Tag.BitsPerSample, FieldType.SHORT).setValue(8)); // 8 bits per sample - geotiff.addTag(new IFDEntry(Tag.SamplesPerPixel, FieldType.SHORT).setValue(1)); - - geotiff.addTag(new IFDEntry(Tag.XResolution, FieldType.RATIONAL).setValue(1, 1)); - geotiff.addTag(new IFDEntry(Tag.YResolution, FieldType.RATIONAL).setValue(1, 1)); - geotiff.addTag(new IFDEntry(Tag.ResolutionUnit, FieldType.SHORT).setValue(1)); - // black is zero (value used in GeotiffWriter) - geotiff.addTag(new IFDEntry(Tag.PhotometricInterpretation, FieldType.SHORT).setValue(1)); + // standard tags for Greyscale images ( see TIFF spec, section 4) + geotiff.addTag(new IFDEntry(Tag.BitsPerSample, FieldType.SHORT).setValue(elemSize * 8)); + geotiff.addTag(new IFDEntry(Tag.SamplesPerPixel, FieldType.SHORT).setValue(1)); + + geotiff.addTag(new IFDEntry(Tag.XResolution, FieldType.RATIONAL).setValue(1, 1)); + geotiff.addTag(new IFDEntry(Tag.YResolution, FieldType.RATIONAL).setValue(1, 1)); + geotiff.addTag(new IFDEntry(Tag.ResolutionUnit, FieldType.SHORT).setValue(1)); + + if (colorTable != null && colorTable.length > 0) { + // standard tags for Palette-color images ( see TIFF spec, section 5) + geotiff.addTag(new IFDEntry(Tag.PhotometricInterpretation, FieldType.SHORT).setValue(3)); + geotiff.addTag(new IFDEntry(Tag.ColorMap, FieldType.SHORT, colorTable.length).setValue(colorTable)); } else { - if (colorTable != null && colorTable.length > 0) { - // standard tags for Palette-color images ( see TIFF spec, section 5) - geotiff.addTag(new IFDEntry(Tag.PhotometricInterpretation, FieldType.SHORT).setValue(3)); - geotiff.addTag(new IFDEntry(Tag.ColorMap, FieldType.SHORT, colorTable.length).setValue(colorTable)); + geotiff.addTag(new IFDEntry(Tag.PhotometricInterpretation, FieldType.SHORT).setValue(1)); // black is zero + } + + // standard tags for SampleFormat ( see TIFF spec, section 19) + if (dtype.isIntegral() && !greyScale) { + geotiff.addTag(new IFDEntry(Tag.SampleFormat, FieldType.SHORT).setValue(dtype.isUnsigned() ? 1 : 2)); // UINT or + // INT + int min = (int) (dataMinMax.min); + int max = (int) (dataMinMax.max); + FieldType ftype; + DataType sdtype = dtype.withSignedness(DataType.Signedness.SIGNED); + if (sdtype == DataType.BYTE) { + ftype = dtype.isUnsigned() ? FieldType.BYTE : FieldType.SBYTE; + } else if (sdtype == DataType.SHORT) { + ftype = dtype.isUnsigned() ? FieldType.SHORT : FieldType.SSHORT; + } else if (sdtype == DataType.INT) { + // A geotiff LONG/SLONG is really a 4-byte regular integer + ftype = dtype.isUnsigned() ? FieldType.LONG : FieldType.SLONG; } else { - geotiff.addTag(new IFDEntry(Tag.PhotometricInterpretation, FieldType.SHORT).setValue(1)); // black is zero - } - - geotiff.addTag(new IFDEntry(Tag.BitsPerSample, FieldType.SHORT).setValue(elemSize * 8)); - - // standard tags for SampleFormat ( see TIFF spec, section 19) - if (dtype.isIntegral()) { - geotiff.addTag(new IFDEntry(Tag.SampleFormat, FieldType.SHORT).setValue(dtype.isUnsigned() ? 1 : 2)); // UINT or - // INT - } else if (dtype.isFloatingPoint()) { - geotiff.addTag(new IFDEntry(Tag.SampleFormat, FieldType.SHORT).setValue(3)); // IEEE Floating Point Type - } else { - throw new IllegalArgumentException("Unsupported data type for geotiff: " + dtype); - } - geotiff.addTag(new IFDEntry(Tag.SamplesPerPixel, FieldType.SHORT).setValue(1)); - - if (dtype.isFloatingPoint()) { - float min = (float) (dataMinMax.min); - float max = (float) (dataMinMax.max); - geotiff.addTag(new IFDEntry(Tag.SMinSampleValue, FieldType.FLOAT).setValue(min)); - geotiff.addTag(new IFDEntry(Tag.SMaxSampleValue, FieldType.FLOAT).setValue(max)); - geotiff.addTag(new IFDEntry(Tag.GDALNoData, FieldType.ASCII).setValue(String.valueOf(min - 1.f))); - } else if (dtype.isIntegral()) { - int min = (int) (dataMinMax.min); - int max = (int) (dataMinMax.max); - FieldType ftype; - DataType sdtype = dtype.withSignedness(DataType.Signedness.SIGNED); - if (sdtype == DataType.BYTE) { - ftype = dtype.isUnsigned() ? FieldType.BYTE : FieldType.SBYTE; - } else if (sdtype == DataType.SHORT) { - ftype = dtype.isUnsigned() ? FieldType.SHORT : FieldType.SSHORT; - } else if (sdtype == DataType.INT) { - // A geotiff LONG/SLONG is really a 4-byte regular integer - ftype = dtype.isUnsigned() ? FieldType.LONG : FieldType.SLONG; - } else { - throw new IllegalArgumentException("Unsupported dtype: " + dtype); - } - geotiff.addTag(new IFDEntry(Tag.SMinSampleValue, ftype).setValue(min)); - geotiff.addTag(new IFDEntry(Tag.SMaxSampleValue, ftype).setValue(max)); - // No GDALNoData tag is set as it is ambiguous what would be appropriate here. + throw new IllegalArgumentException("Unsupported dtype: " + dtype); } + geotiff.addTag(new IFDEntry(Tag.SMinSampleValue, ftype).setValue(min)); + geotiff.addTag(new IFDEntry(Tag.SMaxSampleValue, ftype).setValue(max)); + // No GDALNoData tag is set as it is ambiguous what would be appropriate here. + } else if (dtype.isFloatingPoint()) { + geotiff.addTag(new IFDEntry(Tag.SampleFormat, FieldType.SHORT).setValue(3)); // IEEE Floating Point Type + float min = (float) (dataMinMax.min); + float max = (float) (dataMinMax.max); + geotiff.addTag(new IFDEntry(Tag.SMinSampleValue, FieldType.FLOAT).setValue(min)); + geotiff.addTag(new IFDEntry(Tag.SMaxSampleValue, FieldType.FLOAT).setValue(max)); + geotiff.addTag(new IFDEntry(Tag.GDALNoData, FieldType.ASCII).setValue(String.valueOf(min - 1.f))); } /* diff --git a/cdm/misc/src/test/data/ucar/nc2/geotiff/baseline_palette.tif b/cdm/misc/src/test/data/ucar/nc2/geotiff/baseline_palette.tif index bcf76d7b9c58bab5031f42e81bf7584e96da4fe7..e156b0cdf23b0aba54d65c4903804a248d7d6f4f 100644 GIT binary patch delta 158 zcmeyIvOINzq&lP2WGVGv#<0nG>iX)>7^N6k85kHC85p$77^UItIz|}=X0SL=hEZek z0(C{+Hbw`K7)V%q!sK)6`i2u)nizP1Ts8&}6ow9Hgzt4glK>8MFWZ delta 105 zcmZ3U`Z;BTq&lPcWGVGvMx)7j>iW!w7-c3OQCH**Vsv0&W&j}u?U>1*)b$NxTACPm wfLt~P2E&r}^1A~!H`$h&Ppc-}{J7;r{h6?lMHf=?A05P~0ssI20 diff --git a/cdm/misc/src/test/java/ucar/nc2/geotiff/TestGeoTiffPalette.java b/cdm/misc/src/test/java/ucar/nc2/geotiff/TestGeoTiffPalette.java index 5492e139d8..c61b67b9e8 100644 --- a/cdm/misc/src/test/java/ucar/nc2/geotiff/TestGeoTiffPalette.java +++ b/cdm/misc/src/test/java/ucar/nc2/geotiff/TestGeoTiffPalette.java @@ -200,4 +200,69 @@ public void testWritePalette() throws IOException { Assert.assertTrue(FileUtils.contentEquals(file1, file2)); } } + + @Test + public void testWritePaletteGreyscale() throws IOException { + // The "greyscale" mode is a misnomer. It really just means that it performs an + // certain normalization on the data that can be represented as 1-255, which is + // typically take as a greyscale, but there is no reason why a color table can't + // be applied to it as well. This test exercises this. + // Note that this test does not check the data, only the tags. + String gridOut = tempFolder.newFile().getAbsolutePath(); + String baseline = "src/test/data/ucar/nc2/geotiff/baseline_palette.tif"; + logger.info("****geotiff palette write {}", gridOut); + + HashMap colorMap = + GeotiffWriter.createColorMap(new int[] {1, 2, 3, 4}, new String[] {"#00AAff", "#151412", "#DE01aB", "#100ABB"}); + int[] colorTable; + + Array dtArray; + try (GridDataset dataset = GridDataset.open("src/test/data/ucar/nc2/geotiff/categorical.nc")) { + final GeoGrid grid = dataset.findGridByName("drought"); + assert grid != null; + final GridCoordSystem gcs = grid.getCoordinateSystem(); + assert gcs != null; + int rtindex = -1; + int tindex = -1; + CoordinateAxis1D timeAxis = gcs.getTimeAxis1D(); + assert timeAxis != null; + tindex = (int) timeAxis.getSize() - 1; // last one + dtArray = grid.readDataSlice(rtindex, -1, tindex, 0, -1, -1); + + try (GeotiffWriter writer = new GeotiffWriter(gridOut)) { + writer.setColorTable(colorMap, Color.black); + writer.writeGrid(dataset, grid, dtArray, true, DataType.UBYTE); + colorTable = writer.getColorTable(); + } + + // read it back in to check the tags + try (GeoTiff geotiff = new GeoTiff(gridOut)) { + geotiff.read(); + logger.debug("{}", geotiff.showInfo()); + + IFDEntry photoTag = geotiff.findTag(Tag.PhotometricInterpretation); + Assert.assertNotNull(photoTag); + Assert.assertEquals(1, photoTag.count); + Assert.assertEquals(3, photoTag.value[0]); + + IFDEntry colorTableTag = geotiff.findTag(Tag.ColorMap); + Assert.assertNotNull(colorTableTag); + Assert.assertEquals(3 * 256, colorTableTag.count); + Assert.assertArrayEquals(colorTable, colorTableTag.value); + + // For backwards-compatibility, the min/max/nodata tags are not recorded. + // There is no reason why this has to be the case. + IFDEntry sMinTag = geotiff.findTag(Tag.SMinSampleValue); + Assert.assertNull(sMinTag); + IFDEntry sMaxTag = geotiff.findTag(Tag.SMaxSampleValue); + Assert.assertNull(sMaxTag); + + // When doing a color paletted geotiff, no assumption is made + // about the NoData value and is therefore not encoded. + IFDEntry noDataTag = geotiff.findTag(Tag.GDALNoData); + Assert.assertNull(noDataTag); + } + } + } + }