001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.tiff.photometricinterpreters.floatingpoint;
018
019import java.awt.Color;
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Comparator;
023import java.util.List;
024
025import org.apache.commons.imaging.ImageReadException;
026import org.apache.commons.imaging.common.ImageBuilder;
027import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreter;
028
029/**
030 * Implements a custom photometric interpreter that can be supplied by
031 * applications in order to render Java images from real-valued TIFF data
032 * products. Most TIFF files include a specification for a "photometric
033 * interpreter" that implements logic for transforming the raw data in a TIFF
034 * file to a rendered image. But the TIFF standard does not include a
035 * specification for a photometric interpreter that can be used for rendering
036 * floating-point data. TIFF files are sometimes used to specify non-image data
037 * as a floating-point raster. This approach is particularly common in GeoTIFF
038 * files (TIFF files that contain tags for supporting geospatial reference
039 * metadata for Geographic Information Systems). Because of the limits of the
040 * stock photometric interpreters, most floating-point TIFF files to not produce
041 * useful images.
042 * <p>
043 * This class allows an Apache Commons implementation to construct and specify a
044 * custom photometric interpreter when reading from a TIFF file. Applications
045 * may supply their own palette that maps real-valued data to specified colors.
046 * <p>
047 * This class provides two constructors:
048 * <ol>
049 * <li>A simple constructor to support gray scales</li>
050 * <li>A constructor to support a color palette (with potential
051 * interpolation)</li>
052 * </ol>
053 * <p>
054 * To use this class, an application must access the TIFF file using the
055 * low-level, TIFF-specific API provided by the Apache Commons Imaging library.
056 *
057 */
058public class PhotometricInterpreterFloat extends PhotometricInterpreter {
059
060    ArrayList<PaletteEntry> rangePaletteEntries = new ArrayList<>();
061    ArrayList<PaletteEntry> singleValuePaletteEntries = new ArrayList<>();
062
063    float minFound = Float.POSITIVE_INFINITY;
064    float maxFound = Float.NEGATIVE_INFINITY;
065    int xMin;
066    int yMin;
067    int xMax;
068    int yMax;
069
070    double sumFound;
071    int nFound;
072
073    /**
074     * Constructs a photometric interpreter that will produce a gray scale
075     * linearly distributed across the RGB color space for values in the range
076     * valueBlack to valueWhite. Note that the two values may be given in either
077     * ascending order or descending order, but they must not be equal. Infinite
078     * values will not result in proper numerical computations.
079     *
080     * @param valueBlack the value associated with the dark side of the gray
081     * scale
082     * @param valueWhite the value associated with the light side of the gray
083     * scale
084     */
085    public PhotometricInterpreterFloat(
086        final float valueBlack, final float valueWhite) {
087        // The abstract base class requires that the following fields
088        // be set in the constructor:
089        //     samplesPerPixel (int)
090        //     bits per sample (array of type int[samplesPerPixel])
091        //     predictor (int, not used by this class)
092        //     width (int)
093        //     height (int)
094        super(
095            1,
096            new int[]{32}, // bits per sample
097            0, // not used by this class
098            32, // pro forma width value
099            32 // pro format height value
100        );
101
102
103        if (valueWhite > valueBlack) {
104            final PaletteEntryForRange entry
105                = new PaletteEntryForRange(valueBlack, valueWhite, Color.black, Color.white);
106            rangePaletteEntries.add(entry);
107        } else {
108            final PaletteEntryForRange entry
109                = new PaletteEntryForRange(valueWhite, valueBlack, Color.white, Color.black);
110            rangePaletteEntries.add(entry);
111        }
112    }
113
114    /**
115     * Constructs a photometric interpreter that will use the specified palette
116     * to assign colors to floating-point values.
117     * <p>
118     * Although there is no prohibition against using palette entries with overlapping ranges,
119     * the behavior of such specifications is undefined and subject to change in the future.
120     * Therefore, it is not recommended.  The exception in in the use of single-value
121     * palette entries which may be used to override the specifications for ranges.
122     *
123     * @param paletteEntries a valid, non-empty list of palette entries
124     */
125    public PhotometricInterpreterFloat(final List<PaletteEntry> paletteEntries) {
126        // The abstract base class requires that the following fields
127        // be set in the constructor:
128        //     samplesPerPixel (int)
129        //     bits per sample (array of type int[samplesPerPixel])
130        //     predictor (int, not used by this class)
131        //     width (int)
132        //     height (int)
133        super(
134            1,
135            new int[]{32}, // bits per sample
136            0, // not used by this class
137            32, // pro forma width value
138            32 // pro format height value
139        );
140
141        if (paletteEntries == null || paletteEntries.isEmpty()) {
142            throw new IllegalArgumentException(
143                "Palette entries list must be non-null and non-empty");
144        }
145
146        for (final PaletteEntry entry : paletteEntries) {
147            if (entry.coversSingleEntry()) {
148                singleValuePaletteEntries.add(entry);
149            } else {
150                rangePaletteEntries.add(entry);
151            }
152        }
153
154        final Comparator<PaletteEntry> comparator = (o1, o2) -> {
155            if (o1.getLowerBound() == o2.getLowerBound()) {
156                return Double.compare(o1.getUpperBound(), o2.getUpperBound());
157            }
158            return Double.compare(o1.getLowerBound(), o2.getLowerBound());
159        };
160
161        rangePaletteEntries.sort(comparator);
162        singleValuePaletteEntries.sort(comparator);
163    }
164
165    @Override
166    public void interpretPixel(
167        final ImageBuilder imageBuilder,
168        final int[] samples, final int x, final int y)
169        throws ImageReadException, IOException {
170
171        final float f = Float.intBitsToFloat(samples[0]);
172        // in the event of NaN, do not store entry in the image builder.
173
174        // only the single bound palette entries support NaN
175        for (final PaletteEntry entry : singleValuePaletteEntries) {
176            if (entry.isCovered(f)) {
177                final int p = entry.getARGB(f);
178                imageBuilder.setRGB(x, y, p);
179                return;
180            }
181        }
182
183        if (Float.isNaN(f)) {
184            // if logic reaches here, there is no definition
185            // for a NaN.
186            return;
187        }
188        if (f < minFound) {
189            minFound = f;
190            xMin = x;
191            yMin = y;
192        }
193        if (f > maxFound) {
194            maxFound = f;
195            xMax = x;
196            yMax = y;
197        }
198        nFound++;
199        sumFound += f;
200
201        for (final PaletteEntry entry : singleValuePaletteEntries) {
202            if (entry.isCovered(f)) {
203                final int p = entry.getARGB(f);
204                imageBuilder.setRGB(x, y, p);
205                return;
206            }
207        }
208
209        for (final PaletteEntry entry : rangePaletteEntries) {
210            if (entry.isCovered(f)) {
211                final int p = entry.getARGB(f);
212                imageBuilder.setRGB(x, y, p);
213                break;
214            }
215        }
216    }
217
218    /**
219     * Gets the minimum value found while rendering the image
220     *
221     * @return if data was processed, a valid value; otherwise, Positive
222     * Infinity
223     */
224    public float getMinFound() {
225        return minFound;
226    }
227
228    /**
229     * Gets the coordinates (x,y) at which the maximum value was identified
230     * during processing
231     *
232     * @return a valid array of length 2.
233     */
234    public int[] getMaxXY() {
235        return new int[]{xMax, yMax};
236    }
237
238    /**
239     * Gets the maximum value found while rendering the image
240     *
241     * @return if data was processed, a valid value; otherwise, Negative
242     * Infinity
243     */
244    public float getMaxFound() {
245        return maxFound;
246    }
247
248    /**
249     * Gets the coordinates (x,y) at which the minimum value was identified
250     * during processing
251     *
252     * @return a valid array of length 2.
253     */
254    public int[] getMinXY() {
255        return new int[]{xMin, yMin};
256    }
257
258    /**
259     * Get the mean of the values found while processing
260     *
261     * @return if data was processed, a valid mean value; otherwise, a zero.
262     */
263    public float getMeanFound() {
264        if (nFound == 0) {
265            return 0;
266        }
267        return (float) (sumFound / nFound);
268    }
269
270    /**
271     * Provides a method for mapping a pixel value to an integer (ARGB) value.
272     * This method is not defined for the standard photometric interpreters and
273     * is provided as a convenience to applications that are processing data
274     * outside the standard TIFF image-reading modules.
275     *
276     * @param f the floating point value to be mapped to an ARGB value
277     * @return a valid ARGB value, or zero if no palette specification covers
278     * the input value.
279     */
280    public int mapValueToARGB(final float f) {
281
282        // The single-value palette entries can accept a Float.NaN as
283        // a target while the range-of-values entries cannot.  So
284        // check the single-values before testing for Float.isNaN()
285        // because NaN may have special treatment.
286        for (final PaletteEntry entry : singleValuePaletteEntries) {
287            if (entry.isCovered(f)) {
288                return entry.getARGB(f);
289            }
290        }
291
292        if (Float.isNaN(f)) {
293            // if logic reaches here, there is no definition
294            // for a NaN.
295            return 0;
296        }
297
298        for (final PaletteEntry entry : rangePaletteEntries) {
299            if (entry.isCovered(f)) {
300                return entry.getARGB(f);
301            }
302        }
303        return 0;
304    }
305
306}