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.ImagingException;
026import org.apache.commons.imaging.common.ImageBuilder;
027import org.apache.commons.imaging.formats.tiff.photometricinterpreters.AbstractPhotometricInterpreter;
028
029/**
030 * Implements a custom photometric interpreter that can be supplied by applications in order to render Java images from real-valued TIFF data products. Most
031 * 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.
032 * 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
033 * 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
034 * supporting geospatial reference metadata for Geographic Information Systems). Because of the limits of the stock photometric interpreters, most
035 * floating-point TIFF files to not produce useful images.
036 * <p>
037 * This class allows an Apache Commons implementation to construct and specify a custom photometric interpreter when reading from a TIFF file. Applications may
038 * supply their own palette that maps real-valued data to specified colors.
039 * <p>
040 * This class provides two constructors:
041 * <ol>
042 * <li>A simple constructor to support gray scales</li>
043 * <li>A constructor to support a color palette (with potential interpolation)</li>
044 * </ol>
045 * <p>
046 * 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.
047 */
048public final class PhotometricInterpreterFloat extends AbstractPhotometricInterpreter {
049
050    ArrayList<PaletteEntry> rangePaletteEntries = new ArrayList<>();
051    ArrayList<PaletteEntry> singleValuePaletteEntries = new ArrayList<>();
052
053    float minFound = Float.POSITIVE_INFINITY;
054    float maxFound = Float.NEGATIVE_INFINITY;
055    int xMin;
056    int yMin;
057    int xMax;
058    int yMax;
059
060    double sumFound;
061    int nFound;
062
063    /**
064     * Constructs a photometric interpreter that will produce a gray scale linearly distributed across the RGB color space for values in the range valueBlack to
065     * 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
066     * result in proper numerical computations.
067     *
068     * @param valueBlack the value associated with the dark side of the gray scale
069     * @param valueWhite the value associated with the light side of the gray scale
070     */
071    public PhotometricInterpreterFloat(final float valueBlack, final float valueWhite) {
072        // The abstract base class requires that the following fields
073        // be set in the constructor:
074        // samplesPerPixel (int)
075        // bits per sample (array of type int[samplesPerPixel])
076        // predictor (int, not used by this class)
077        // width (int)
078        // height (int)
079        super(1, new int[] { 32 }, // bits per sample
080                0, // not used by this class
081                32, // pro forma width value
082                32 // pro format height value
083        );
084
085        if (valueWhite > valueBlack) {
086            final PaletteEntryForRange entry = new PaletteEntryForRange(valueBlack, valueWhite, Color.black, Color.white);
087            rangePaletteEntries.add(entry);
088        } else {
089            final PaletteEntryForRange entry = new PaletteEntryForRange(valueWhite, valueBlack, Color.white, Color.black);
090            rangePaletteEntries.add(entry);
091        }
092    }
093
094    /**
095     * Constructs a photometric interpreter that will use the specified palette to assign colors to floating-point values.
096     * <p>
097     * Although there is no prohibition against using palette entries with overlapping ranges, the behavior of such specifications is undefined and subject to
098     * 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
099     * 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}