1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.imaging.formats.tiff.photometricinterpreters.floatingpoint;
18
19 import java.awt.Color;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.Comparator;
23 import java.util.List;
24
25 import org.apache.commons.imaging.ImagingException;
26 import org.apache.commons.imaging.common.ImageBuilder;
27 import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
28
29 /**
30 * Implements a custom photometric interpreter that can be supplied by applications in order to render Java images from real-valued TIFF data products. Most
31 * TIFF files include a specification for a "photometric interpreter" that implements logic for transforming the raw data in a TIFF file to a rendered image.
32 * But the TIFF standard does not include a specification for a photometric interpreter that can be used for rendering floating-point data. TIFF files are
33 * sometimes used to specify non-image data as a floating-point raster. This approach is particularly common in GeoTIFF files (TIFF files that contain tags for
34 * supporting geospatial reference metadata for Geographic Information Systems). Because of the limits of the stock photometric interpreters, most
35 * floating-point TIFF files to not produce useful images.
36 * <p>
37 * This class allows an Apache Commons implementation to construct and specify a custom photometric interpreter when reading from a TIFF file. Applications may
38 * supply their own palette that maps real-valued data to specified colors.
39 * <p>
40 * This class provides two constructors:
41 * <ol>
42 * <li>A simple constructor to support gray scales</li>
43 * <li>A constructor to support a color palette (with potential interpolation)</li>
44 * </ol>
45 * <p>
46 * To use this class, an application must access the TIFF file using the low-level, TIFF-specific API provided by the Apache Commons Imaging library.
47 */
48 public class PhotometricInterpreterFloat extends PhotometricInterpreter {
49
50 ArrayList<PaletteEntry> rangePaletteEntries = new ArrayList<>();
51 ArrayList<PaletteEntry> singleValuePaletteEntries = new ArrayList<>();
52
53 float minFound = Float.POSITIVE_INFINITY;
54 float maxFound = Float.NEGATIVE_INFINITY;
55 int xMin;
56 int yMin;
57 int xMax;
58 int yMax;
59
60 double sumFound;
61 int nFound;
62
63 /**
64 * Constructs a photometric interpreter that will produce a gray scale linearly distributed across the RGB color space for values in the range valueBlack to
65 * valueWhite. Note that the two values may be given in either ascending order or descending order, but they must not be equal. Infinite values will not
66 * result in proper numerical computations.
67 *
68 * @param valueBlack the value associated with the dark side of the gray scale
69 * @param valueWhite the value associated with the light side of the gray scale
70 */
71 public PhotometricInterpreterFloat(final float valueBlack, final float valueWhite) {
72 // The abstract base class requires that the following fields
73 // be set in the constructor:
74 // samplesPerPixel (int)
75 // bits per sample (array of type int[samplesPerPixel])
76 // predictor (int, not used by this class)
77 // width (int)
78 // height (int)
79 super(1, new int[] { 32 }, // bits per sample
80 0, // not used by this class
81 32, // pro forma width value
82 32 // pro format height value
83 );
84
85 if (valueWhite > valueBlack) {
86 final PaletteEntryForRange entry = new PaletteEntryForRange(valueBlack, valueWhite, Color.black, Color.white);
87 rangePaletteEntries.add(entry);
88 } else {
89 final PaletteEntryForRange entry = new PaletteEntryForRange(valueWhite, valueBlack, Color.white, Color.black);
90 rangePaletteEntries.add(entry);
91 }
92 }
93
94 /**
95 * Constructs a photometric interpreter that will use the specified palette to assign colors to floating-point values.
96 * <p>
97 * Although there is no prohibition against using palette entries with overlapping ranges, the behavior of such specifications is undefined and subject to
98 * change in the future. Therefore, it is not recommended. The exception in the use of single-value palette entries which may be used to override the
99 * specifications for ranges.
100 *
101 * @param paletteEntries a valid, non-empty list of palette entries
102 */
103 public PhotometricInterpreterFloat(final List<PaletteEntry> paletteEntries) {
104 // The abstract base class requires that the following fields
105 // be set in the constructor:
106 // samplesPerPixel (int)
107 // bits per sample (array of type int[samplesPerPixel])
108 // predictor (int, not used by this class)
109 // width (int)
110 // height (int)
111 super(1, new int[] { 32 }, // bits per sample
112 0, // not used by this class
113 32, // pro forma width value
114 32 // pro format height value
115 );
116
117 if (paletteEntries == null || paletteEntries.isEmpty()) {
118 throw new IllegalArgumentException("Palette entries list must be non-null and non-empty");
119 }
120
121 for (final PaletteEntry entry : paletteEntries) {
122 if (entry.coversSingleEntry()) {
123 singleValuePaletteEntries.add(entry);
124 } else {
125 rangePaletteEntries.add(entry);
126 }
127 }
128
129 final Comparator<PaletteEntry> comparator = (o1, o2) -> {
130 if (o1.getLowerBound() == o2.getLowerBound()) {
131 return Double.compare(o1.getUpperBound(), o2.getUpperBound());
132 }
133 return Double.compare(o1.getLowerBound(), o2.getLowerBound());
134 };
135
136 rangePaletteEntries.sort(comparator);
137 singleValuePaletteEntries.sort(comparator);
138 }
139
140 /**
141 * Gets the maximum value found while rendering the image
142 *
143 * @return if data was processed, a valid value; otherwise, Negative Infinity
144 */
145 public float getMaxFound() {
146 return maxFound;
147 }
148
149 /**
150 * Gets the coordinates (x,y) at which the maximum value was identified during processing
151 *
152 * @return a valid array of length 2.
153 */
154 public int[] getMaxXY() {
155 return new int[] { xMax, yMax };
156 }
157
158 /**
159 * Gets the mean of the values found while processing
160 *
161 * @return if data was processed, a valid mean value; otherwise, a zero.
162 */
163 public float getMeanFound() {
164 if (nFound == 0) {
165 return 0;
166 }
167 return (float) (sumFound / nFound);
168 }
169
170 /**
171 * Gets the minimum value found while rendering the image
172 *
173 * @return if data was processed, a valid value; otherwise, Positive Infinity
174 */
175 public float getMinFound() {
176 return minFound;
177 }
178
179 /**
180 * Gets the coordinates (x,y) at which the minimum value was identified during processing
181 *
182 * @return a valid array of length 2.
183 */
184 public int[] getMinXY() {
185 return new int[] { xMin, yMin };
186 }
187
188 @Override
189 public void interpretPixel(final ImageBuilder imageBuilder, final int[] samples, final int x, final int y) throws ImagingException, IOException {
190
191 final float f = Float.intBitsToFloat(samples[0]);
192 // in the event of NaN, do not store entry in the image builder.
193
194 // only the single bound palette entries support NaN
195 for (final PaletteEntry entry : singleValuePaletteEntries) {
196 if (entry.isCovered(f)) {
197 final int p = entry.getArgb(f);
198 imageBuilder.setRgb(x, y, p);
199 return;
200 }
201 }
202
203 if (Float.isNaN(f)) {
204 // if logic reaches here, there is no definition
205 // for a NaN.
206 return;
207 }
208 if (f < minFound) {
209 minFound = f;
210 xMin = x;
211 yMin = y;
212 }
213 if (f > maxFound) {
214 maxFound = f;
215 xMax = x;
216 yMax = y;
217 }
218 nFound++;
219 sumFound += f;
220
221 for (final PaletteEntry entry : singleValuePaletteEntries) {
222 if (entry.isCovered(f)) {
223 final int p = entry.getArgb(f);
224 imageBuilder.setRgb(x, y, p);
225 return;
226 }
227 }
228
229 for (final PaletteEntry entry : rangePaletteEntries) {
230 if (entry.isCovered(f)) {
231 final int p = entry.getArgb(f);
232 imageBuilder.setRgb(x, y, p);
233 break;
234 }
235 }
236 }
237
238 /**
239 * Provides a method for mapping a pixel value to an integer (ARGB) value. This method is not defined for the standard photometric interpreters and is
240 * provided as a convenience to applications that are processing data outside the standard TIFF image-reading modules.
241 *
242 * @param f the floating point value to be mapped to an ARGB value
243 * @return a valid ARGB value, or zero if no palette specification covers the input value.
244 */
245 public int mapValueToArgb(final float f) {
246
247 // The single-value palette entries can accept a Float.NaN as
248 // a target while the range-of-values entries cannot. So
249 // check the single-values before testing for Float.isNaN()
250 // because NaN may have special treatment.
251 for (final PaletteEntry entry : singleValuePaletteEntries) {
252 if (entry.isCovered(f)) {
253 return entry.getArgb(f);
254 }
255 }
256
257 if (Float.isNaN(f)) {
258 // if logic reaches here, there is no definition
259 // for a NaN.
260 return 0;
261 }
262
263 for (final PaletteEntry entry : rangePaletteEntries) {
264 if (entry.isCovered(f)) {
265 return entry.getArgb(f);
266 }
267 }
268 return 0;
269 }
270
271 }