Skip to content

Commit

Permalink
Simplify geotiff metadata writing
Browse files Browse the repository at this point in the history
  • Loading branch information
WeatherGod authored and tdrwenski committed Jul 1, 2024
1 parent f90f9e7 commit a6de94c
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 54 deletions.
95 changes: 41 additions & 54 deletions cdm/misc/src/main/java/ucar/nc2/geotiff/GeotiffWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}

/*
Expand Down
Binary file modified cdm/misc/src/test/data/ucar/nc2/geotiff/baseline_palette.tif
Binary file not shown.
65 changes: 65 additions & 0 deletions cdm/misc/src/test/java/ucar/nc2/geotiff/TestGeoTiffPalette.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer, Color> 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);
}
}
}

}

0 comments on commit a6de94c

Please sign in to comment.