    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}