@@ -65,8 +65,9 @@ public void close() throws IOException {
6565 /**
6666 * Write GridDatatype data to the geotiff file.
6767 *
68- * This is for backwards-compatibility. Assumes dtype is FLOAT if greyScale is false, and assumes
69- * dtype is BYTE if greyScale is true.
68+ * Greyscale mode will auto-normalize the data from 1 to 255 and save as unsigned bytes, with 0's used
69+ * for missing data. A color table can be applied if specified via `setColorTable()`.
70+ * Non-greyscale mode will save the data as floats, encoding missing data as the data minimum minus one.
7071 *
7172 * @param dataset grid in contained in this dataset
7273 * @param grid data is in this grid
@@ -75,21 +76,39 @@ public void close() throws IOException {
7576 * @throws IOException on i/o error
7677 */
7778 public void writeGrid (GridDataset dataset , GridDatatype grid , Array data , boolean greyScale ) throws IOException {
78- writeGrid (dataset , grid , data , greyScale , greyScale ? DataType .BYTE : DataType .FLOAT );
79+ writeGrid (dataset , grid , data , greyScale , greyScale ? DataType .UBYTE : DataType .FLOAT );
7980 }
8081
8182 /**
8283 * Write GridDatatype data to the geotiff file.
8384 *
85+ * Greyscale mode will auto-normalize the data from 1 to 255 and save as unsigned bytes, with 0's used
86+ * for missing data.
87+ * Non-greyscale mode with a floating point dtype will save the data as floats, encoding missing data
88+ * as the data's minimum minus one. Any other dtype will save the data coerced to the specified dtype.
89+ *
90+ * A color table can be applied if specified via `setColorTable()` and the dtype is UBYTE.
91+ *
8492 * @param dataset grid in contained in this dataset
8593 * @param grid data is in this grid
8694 * @param data 2D array in YX order
8795 * @param greyScale if true, write greyScale image, else dataSample.
88- * @param dtype DataType for the output.
96+ * @param dtype DataType for the output. See other writeGrid() documentation for more details.
8997 * @throws IOException on i/o error
98+ * @throws IllegalArgumentException if above assumptions not valid
9099 */
91100 public void writeGrid (GridDataset dataset , GridDatatype grid , Array data , boolean greyScale , DataType dtype )
92- throws IOException {
101+ throws IOException , IllegalArgumentException {
102+ // This check has to be *before* resolving the dtype so that we are only
103+ // checking explicitly specified data types.
104+ if (greyScale && dtype != DataType .UBYTE ) {
105+ throw new IllegalArgumentException ("When greyScale is true, dtype must be UBYTE" );
106+ }
107+
108+ if (colorTable != null && colorTable .length > 0 && dtype != DataType .UBYTE ) {
109+ throw new IllegalArgumentException ("When using the color table, dtype must be UBYTE" );
110+ }
111+
93112 GridCoordSystem gcs = grid .getCoordinateSystem ();
94113
95114 if (!gcs .isRegularSpatial ()) {
@@ -136,8 +155,9 @@ public void writeGrid(GridDataset dataset, GridDatatype grid, Array data, boolea
136155 * <li>be equally spaced
137156 * </ol>
138157 *
139- * This is for backwards-compatibility. Assumes dtype is FLOAT if greyScale is false, and assumes
140- * dtype is BYTE if greyScale is true.
158+ * Greyscale mode will auto-normalize the data from 1 to 255 and save as unsigned bytes, with 0's used
159+ * for missing data. A color table can be applied if specified via `setColorTable()`.
160+ * Non-greyscale mode will save the data as floats, encoding missing data as the data minimum minus one.
141161 *
142162 * @param grid original grid
143163 * @param data 2D array in YX order
@@ -148,12 +168,12 @@ public void writeGrid(GridDataset dataset, GridDatatype grid, Array data, boolea
148168 * @param yInc increment y coord
149169 * @param imageNumber used to write multiple images
150170 * @throws IOException on i/o error
151- * @throws IllegalArgumentException if above assumptions not valid *
171+ * @throws IllegalArgumentException if above assumptions not valid
152172 */
153173 void writeGrid (GridDatatype grid , Array data , boolean greyScale , double xStart , double yStart , double xInc ,
154174 double yInc , int imageNumber ) throws IOException {
155175 writeGrid (grid , data , greyScale , xStart , yStart , xInc , yInc , imageNumber ,
156- greyScale ? DataType .BYTE : DataType .FLOAT );
176+ greyScale ? DataType .UBYTE : DataType .FLOAT );
157177 }
158178
159179 /**
@@ -165,6 +185,13 @@ void writeGrid(GridDatatype grid, Array data, boolean greyScale, double xStart,
165185 * <li>be equally spaced
166186 * </ol>
167187 *
188+ * Greyscale mode will auto-normalize the data from 1 to 255 and save as unsigned bytes, with 0's used
189+ * for missing data.
190+ * Non-greyscale mode with a floating point dtype will save the data as floats, encoding missing data
191+ * as the data's minimum minus one. Any other dtype will save the data coerced to the specified dtype.
192+ *
193+ * A color table can be applied if specified via `setColorTable()` and the dtype is UBYTE.
194+ *
168195 * @param grid original grid
169196 * @param data 2D array in YX order
170197 * @param greyScale if true, normalize the data before writing, otherwise, only handle missing data.
@@ -175,31 +202,28 @@ void writeGrid(GridDatatype grid, Array data, boolean greyScale, double xStart,
175202 * @param imageNumber used to write multiple images
176203 * @param dtype if greyScale is false, then save the data in the given data type.
177204 * Currently, this is a bit hobbled in order to avoid back-compatibility breaks.
178- * If greyScale is true and this is not BYTE, then an exception is thrown.
205+ * If dtype is DOUBLE, is is currenly downcasted to FLOAT.
206+ * When dtype is floating point, missing data is encoded as the data's minimum minus one.
179207 * If null, then use the datatype of the given array.
180208 * @throws IOException on i/o error
181- * @throws IllegalArgumentException if above assumptions not valid *
209+ * @throws IllegalArgumentException if above assumptions not valid
182210 */
183211 void writeGrid (GridDatatype grid , Array data , boolean greyScale , double xStart , double yStart , double xInc ,
184- double yInc , int imageNumber , DataType dtype ) throws IOException {
185-
186- int nextStart ;
187- GridCoordSystem gcs = grid .getCoordinateSystem ();
212+ double yInc , int imageNumber , DataType dtype ) throws IOException , IllegalArgumentException {
188213
189214 // This check has to be *before* resolving the dtype so that we are only
190215 // checking explicitly specified data types.
191- if (greyScale && dtype != DataType .BYTE ) {
192- throw new IllegalArgumentException ("When greyScale is true, dtype must be BYTE or null " );
216+ if (greyScale && dtype != DataType .UBYTE ) {
217+ throw new IllegalArgumentException ("When greyScale is true, dtype must be UBYTE " );
193218 }
194219
195- if (dtype == null ) {
196- dtype = data .getDataType ();
197- // Need to cap at single precision floats because that's what gets written for floating points
198- if (dtype == DataType .DOUBLE ) {
199- dtype = DataType .FLOAT ;
200- }
220+ if (colorTable != null && colorTable .length > 0 && dtype != DataType .UBYTE ) {
221+ throw new IllegalArgumentException ("When using the color table, dtype must be UBYTE" );
201222 }
202223
224+ int nextStart ;
225+ GridCoordSystem gcs = grid .getCoordinateSystem ();
226+
203227 // get rid of this when all projections are implemented
204228 if (!gcs .isLatLon () && !(gcs .getProjection () instanceof LambertConformal )
205229 && !(gcs .getProjection () instanceof Stereographic ) && !(gcs .getProjection () instanceof Mercator )
@@ -209,6 +233,14 @@ void writeGrid(GridDatatype grid, Array data, boolean greyScale, double xStart,
209233 throw new IllegalArgumentException ("Unsupported projection = " + gcs .getProjection ().getClass ().getName ());
210234 }
211235
236+ if (dtype == null ) {
237+ dtype = data .getDataType ();
238+ // Need to cap at single precision floats because that's what gets written for floating points
239+ if (dtype == DataType .DOUBLE ) {
240+ dtype = DataType .FLOAT ;
241+ }
242+ }
243+
212244 // write the data first
213245 MAMath .MinMax dataMinMax = grid .getMinMaxSkipMissingData (data );
214246 if (greyScale ) {
@@ -235,6 +267,20 @@ private void writeMetadata(boolean greyScale, double xStart, double yStart, doub
235267 int width , int imageNumber , int nextStart , MAMath .MinMax dataMinMax , Projection proj , DataType dtype )
236268 throws IOException {
237269
270+ if (dtype == null ) {
271+ throw new IllegalArgumentException ("dtype can't be null in writeMetadata()" );
272+ }
273+
274+ if (greyScale && dtype != DataType .UBYTE ) {
275+ throw new IllegalArgumentException ("When greyScale is true, dtype must be UBYTE" );
276+ }
277+
278+ if (colorTable != null && colorTable .length > 0 && dtype != DataType .UBYTE ) {
279+ throw new IllegalArgumentException ("When using the color table, the dtype must be UBYTE" );
280+ }
281+
282+ int elemSize = dtype .getSize ();
283+
238284 geotiff .addTag (new IFDEntry (Tag .ImageWidth , FieldType .SHORT ).setValue (width ));
239285 geotiff .addTag (new IFDEntry (Tag .ImageLength , FieldType .SHORT ).setValue (height ));
240286
@@ -257,18 +303,6 @@ private void writeMetadata(boolean greyScale, double xStart, double yStart, doub
257303 * geotiff.addTag( new IFDEntry(Tag.StripOffsets, FieldType.LONG).setValue(nextStart));
258304 */
259305
260- if (dtype == null ) {
261- throw new IllegalArgumentException ("dtype can't be null in writeMetadata()" );
262- }
263-
264- // This check has to be *before* resolving the dtype so that we are only
265- // checking explicitly specified data types.
266- if (greyScale && dtype != DataType .BYTE ) {
267- throw new IllegalArgumentException ("When greyScale is true, dtype must be BYTE" );
268- }
269-
270- int elemSize = dtype .getSize ();
271-
272306 int [] soffset = new int [height ];
273307 int [] sbytecount = new int [height ];
274308 if (imageNumber == 1 ) {
@@ -301,15 +335,16 @@ private void writeMetadata(boolean greyScale, double xStart, double yStart, doub
301335 geotiff .addTag (new IFDEntry (Tag .PhotometricInterpretation , FieldType .SHORT ).setValue (1 ));
302336 } else {
303337 if (colorTable != null && colorTable .length > 0 ) {
338+ // standard tags for Palette-color images ( see TIFF spec, section 5)
304339 geotiff .addTag (new IFDEntry (Tag .PhotometricInterpretation , FieldType .SHORT ).setValue (3 ));
305340 geotiff .addTag (new IFDEntry (Tag .ColorMap , FieldType .SHORT , colorTable .length ).setValue (colorTable ));
306341 } else {
307- geotiff .addTag (new IFDEntry (Tag .PhotometricInterpretation , FieldType .SHORT ).setValue (1 )); // black is zero :
308- // not used?
342+ geotiff .addTag (new IFDEntry (Tag .PhotometricInterpretation , FieldType .SHORT ).setValue (1 )); // black is zero
309343 }
310- // standard tags for SampleFormat ( see TIFF spec, section 19)
344+
311345 geotiff .addTag (new IFDEntry (Tag .BitsPerSample , FieldType .SHORT ).setValue (elemSize * 8 ));
312346
347+ // standard tags for SampleFormat ( see TIFF spec, section 19)
313348 if (dtype .isIntegral ()) {
314349 geotiff .addTag (new IFDEntry (Tag .SampleFormat , FieldType .SHORT ).setValue (dtype .isUnsigned () ? 1 : 2 )); // UINT or
315350 // INT
@@ -408,8 +443,8 @@ public int[] getColorTable() {
408443 * be floored/ceilinged to the [0, 255] range. The color table is also assumed to be for pixel values
409444 * between 0 and 255.
410445 *
411- * In order for the color table to be properly included in the geotiff, the "greyScale" mode must be false,
412- * and the output data type must be byte or integer .
446+ * In order for the color table to be properly included in the geotiff, the output data type must be unsigned bytes.
447+ * This works even for greyscale mode .
413448 */
414449 public void setColorTable (Map <Integer , Color > colorMap ) {
415450 setColorTable (colorMap , new Color (0 , 0 , 0 ));
@@ -424,18 +459,17 @@ public void setColorTable(Map<Integer, Color> colorMap) {
424459 * For these RGB triplets, 0 is minimum intensity, 255 is maximum intensity. Values outside that range will
425460 * be floored/ceilinged to the [0, 255] range. The color table is also assumed to be for pixel values
426461 * between 0 and 255.
427- * In order for the color table to be properly included in the geotiff, the "greyScale" mode must be false,
428- * and the output data type must be byte or integer .
462+ * In order for the color table to be properly included in the geotiff, the output data type must be unsigned bytes.
463+ * This works even for greyscale mode .
429464 */
430465 public void setColorTable (Map <Integer , Color > colorMap , Color defaultRGB ) {
431466 if (colorMap == null ) {
432467 colorTable = null ;
433468 return ;
434469 }
435470
436- // FIXME: This isn't quite right because this assumes that the data being written is
437- // unsigned bytes, but the tiff spec says that it should be sized to the width of
438- // the data type, but I would need to know the data type, which this writer doesn't know.
471+ // TIFF spec allows for 4 or 8 bits per sample (making for 16 or 256 entries).
472+ // Since we don't support saving data as 4 bits per sample, we'll force it to 256.
439473 colorTable = new int [3 * 256 ];
440474 for (int i = 0 ; i < 256 ; i ++) {
441475 // Scale it up to [0, 65535], which is needed by the ColorMap tag.
@@ -475,7 +509,7 @@ public static HashMap<Integer, Color> createColorMap(int[] flag_values, String[]
475509 *
476510 * @param data input data array (of any data type)
477511 * @param isUnsigned coerce to unsigned bytes
478- * @return integer data array
512+ * @return byte data array
479513 */
480514 static ArrayByte coerceByte (Array data , boolean isUnsigned ) {
481515 ArrayByte array = (ArrayByte ) Array .factory (isUnsigned ? DataType .UBYTE : DataType .BYTE , data .getShape ());
@@ -495,15 +529,15 @@ static ArrayByte coerceByte(Array data, boolean isUnsigned) {
495529 *
496530 * @param data input data array (of any data type)
497531 * @param isUnsigned coerce to unsigned integers
498- * @return integer data array
532+ * @return short integer data array
499533 */
500534 static ArrayShort coerceShort (Array data , boolean isUnsigned ) {
501535 ArrayShort array = (ArrayShort ) Array .factory (isUnsigned ? DataType .USHORT : DataType .SHORT , data .getShape ());
502536 IndexIterator dataIter = data .getIndexIterator ();
503537 IndexIterator resultIter = array .getIndexIterator ();
504538
505539 while (dataIter .hasNext ()) {
506- resultIter .setIntNext (dataIter .getIntNext ());
540+ resultIter .setShortNext (dataIter .getShortNext ());
507541 }
508542
509543 return array ;
@@ -516,7 +550,7 @@ static ArrayShort coerceShort(Array data, boolean isUnsigned) {
516550 *
517551 * @param data input data array (of any data type)
518552 * @param isUnsigned coerce to unsigned integers
519- * @return integer data array
553+ * @return 32-bit integer data array
520554 */
521555 static ArrayInt coerceInt (Array data , boolean isUnsigned ) {
522556 ArrayInt array = (ArrayInt ) Array .factory (isUnsigned ? DataType .UINT : DataType .INT , data .getShape ());
@@ -543,7 +577,7 @@ static ArrayFloat coerceFloat(Array data) {
543577 IndexIterator resultIter = array .getIndexIterator ();
544578
545579 while (dataIter .hasNext ()) {
546- resultIter .setIntNext (dataIter .getIntNext ());
580+ resultIter .setFloatNext (dataIter .getFloatNext ());
547581 }
548582
549583 return array ;
@@ -574,7 +608,7 @@ private ArrayFloat replaceMissingValues(IsMissingEvaluator grid, Array data, MAM
574608 }
575609
576610 /**
577- * Replace missing values with 0; scale other values between 1 and 255, return a byte data array.
611+ * Replace missing values with 0; scale other values between 1 and 255, return a ubyte data array.
578612 *
579613 * @param grid GridDatatype
580614 * @param data input data array
@@ -820,29 +854,52 @@ private double geoShiftGetXstart(Array lon, double inc) {
820854 /**
821855 * Write GridCoverage data to the geotiff file.
822856 *
857+ * Greyscale mode will auto-normalize the data from 1 to 255 and save as unsigned bytes, with 0's used
858+ * for missing data. A color table can be applied if specified via `setColorTable()`.
859+ * Non-greyscale mode will save the data as floats, encoding missing data as the data minimum minus one.
860+ *
823861 * @param array GeoReferencedArray array in YX order
824862 * @param greyScale if true, write greyScale image, else dataSample.
825863 * @throws IOException on i/o error
826864 */
827865 public void writeGrid (GeoReferencedArray array , boolean greyScale ) throws IOException {
828- writeGrid (array , greyScale , greyScale ? DataType .BYTE : DataType .FLOAT );
866+ writeGrid (array , greyScale , greyScale ? DataType .UBYTE : DataType .FLOAT );
829867 }
830868
831869 /**
832870 * Write GridCoverage data to the geotiff file.
833871 *
872+ * Greyscale mode will auto-normalize the data from 1 to 255 and save as unsigned bytes, with 0's used
873+ * for missing data.
874+ * Non-greyscale mode with a floating point dtype will save the data as floats, encoding missing data
875+ * as the data's minimum minus one. Any other dtype will save the data coerced to the specified dtype.
876+ *
877+ * A color table can be applied if specified via `setColorTable()` and the dtype is UBYTE.
878+ *
834879 * @param array GeoReferencedArray array in YX order
835880 * @param greyScale if true, write greyScale image, else dataSample.
836881 * @param dtype if greyScale is false, then save the data in the given data type.
837882 * Currently, this is a bit hobbled in order to avoid back-compatibility breaks.
838- * If greyScale is true and this is not BYTE, then an exception is thrown.
883+ * If greyScale is true and this is not UBYTE, then an exception is thrown.
884+ * If dtype is DOUBLE, it downcasted to FLOAT instead.
885+ * If using the colorTable and this is not UBYTE, then an exception is thrown.
839886 * If null, then use the datatype of the given array.
840887 * @throws IOException on i/o error
841888 * @throws IllegalArgumentException if data isn't regular or if contradicting the greyScale argument.
842889 */
843890 public void writeGrid (GeoReferencedArray array , boolean greyScale , DataType dtype )
844891 throws IOException , IllegalArgumentException {
845892
893+ // This check has to be *before* resolving the dtype so that we are only
894+ // checking explicitly specified data types.
895+ if (greyScale && dtype != DataType .UBYTE ) {
896+ throw new IllegalArgumentException ("When greyScale is true, dtype must be UBYTE" );
897+ }
898+
899+ if (colorTable != null && colorTable .length > 0 && dtype != DataType .UBYTE ) {
900+ throw new IllegalArgumentException ("When using the colorTable, the dtype must be UBYTE" );
901+ }
902+
846903 CoverageCoordSys gcs = array .getCoordSysForData ();
847904 if (!gcs .isRegularSpatial ())
848905 throw new IllegalArgumentException ("Must have 1D x and y axes for " + array .getCoverageName ());
@@ -866,12 +923,6 @@ public void writeGrid(GeoReferencedArray array, boolean greyScale, DataType dtyp
866923 yStart = yaxis .getCoordEdgeLast () * scaler ;
867924 }
868925
869- // This check has to be *before* resolving the dtype so that we are only
870- // checking explicitly specified data types.
871- if (greyScale && dtype != DataType .BYTE ) {
872- throw new IllegalArgumentException ("When greyScale is true, dtype must be BYTE or null" );
873- }
874-
875926 if (dtype == null ) {
876927 dtype = data .getDataType ();
877928 // Need to cap at single precision floats because that's what gets written for floating points
0 commit comments