    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}