    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 */
    017
    018package org.apache.commons.imaging.common;
    019
    020import java.awt.color.ColorSpace;
    021import java.awt.image.BufferedImage;
    022import java.awt.image.ColorModel;
    023import java.awt.image.DataBuffer;
    024import java.awt.image.DataBufferInt;
    025import java.awt.image.DirectColorModel;
    026import java.awt.image.Raster;
    027import java.awt.image.RasterFormatException;
    028import java.awt.image.WritableRaster;
    029import java.util.Properties;
    030
    031/*
    032 * Development notes:
    033 * This class was introduced to the Apache Commons Imaging library in
    034 * order to improve performance in building images.  The setRGB method
    035 * provided by this class represents a substantial improvement in speed
    036 * compared to that of the BufferedImage class that was originally used
    037 * in Apache Sanselan.
    038 *   This increase is attained because ImageBuilder is a highly specialized
    039 * class that does not need to perform the general-purpose logic required
    040 * for BufferedImage.  If you need to modify this class to add new
    041 * image formats or functionality, keep in mind that some of its methods
    042 * are invoked literally millions of times when building an image.
    043 * Since even the introduction of something as small as a single conditional
    044 * inside of setRGB could result in a noticeable increase in the
    045 * time to read a file, changes should be made with care.
    046 *    During development, I experimented with inlining the setRGB logic
    047 * in some of the code that uses it. This approach did not significantly
    048 * improve performance, leading me to speculate that the Java JIT compiler
    049 * might have inlined the method at run time.  Further investigation
    050 * is required.
    051 */
    052
    053/**
    054 * A utility class primary intended for storing data obtained by reading image files.
    055 */
    056public final class ImageBuilder {
    057    private final int[] data;
    058    private final int width;
    059    private final int height;
    060    private final boolean hasAlpha;
    061    private final boolean isAlphaPremultiplied;
    062
    063    /**
    064     * Constructs an ImageBuilder instance.
    065     *
    066     * @param width    the width of the image to be built
    067     * @param height   the height of the image to be built
    068     * @param hasAlpha indicates whether the image has an alpha channel (the selection of alpha channel does not change the memory requirements for the
    069     *                 ImageBuilder or resulting BufferedImage.
    070     * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
    071     */
    072    public ImageBuilder(final int width, final int height, final boolean hasAlpha) {
    073        this(width, height, hasAlpha, false);
    074    }
    075
    076    /**
    077     * Constructs an ImageBuilder instance.
    078     *
    079     * @param width                the width of the image to be built
    080     * @param height               the height of the image to be built
    081     * @param hasAlpha             indicates whether the image has an alpha channel (the selection of alpha channel does not change the memory requirements for
    082     *                             the ImageBuilder or resulting BufferedImage.
    083     * @param isAlphaPremultiplied indicates whether alpha values are pre-multiplied; this setting is relevant only if alpha is true.
    084     * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
    085     */
    086    public ImageBuilder(final int width, final int height, final boolean hasAlpha, final boolean isAlphaPremultiplied) {
    087        checkDimensions(width, height);
    088        data = Allocator.intArray(width * height);
    089        this.width = width;
    090        this.height = height;
    091        this.hasAlpha = hasAlpha;
    092        this.isAlphaPremultiplied = isAlphaPremultiplied;
    093    }
    094
    095    /**
    096     * Performs a check on the specified sub-region to verify that it is within the constraints of the ImageBuilder bounds.
    097     *
    098     * @param x the X coordinate of the upper-left corner of the specified rectangular region
    099     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
    100     * @param w the width of the specified rectangular region
    101     * @param h the height of the specified rectangular region
    102     * @throws RasterFormatException if width or height are equal or less than zero, or if the subimage is outside raster (on x or y axis)
    103     */
    104    private void checkBounds(final int x, final int y, final int w, final int h) {
    105        if (w <= 0) {
    106            throw new RasterFormatException("negative or zero subimage width");
    107        }
    108        if (h <= 0) {
    109            throw new RasterFormatException("negative or zero subimage height");
    110        }
    111        if (x < 0 || x >= width) {
    112            throw new RasterFormatException("subimage x is outside raster");
    113        }
    114        if (x + w > width) {
    115            throw new RasterFormatException("subimage (x+width) is outside raster");
    116        }
    117        if (y < 0 || y >= height) {
    118            throw new RasterFormatException("subimage y is outside raster");
    119        }
    120        if (y + h > height) {
    121            throw new RasterFormatException("subimage (y+height) is outside raster");
    122        }
    123    }
    124
    125    /**
    126     * Checks for valid dimensions and throws {@link RasterFormatException} if the inputs are invalid.
    127     *
    128     * @param width  image width (must be greater than zero)
    129     * @param height image height (must be greater than zero)
    130     * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
    131     */
    132    private void checkDimensions(final int width, final int height) {
    133        if (width <= 0) {
    134            throw new RasterFormatException("zero or negative width value");
    135        }
    136        if (height <= 0) {
    137            throw new RasterFormatException("zero or negative height value");
    138        }
    139    }
    140
    141    /**
    142     * Create a BufferedImage using the data stored in the ImageBuilder.
    143     *
    144     * @return a valid BufferedImage.
    145     */
    146    public BufferedImage getBufferedImage() {
    147        return makeBufferedImage(data, width, height, hasAlpha);
    148    }
    149
    150    /**
    151     * Gets the height of the ImageBuilder pixel field
    152     *
    153     * @return a positive integer
    154     */
    155    public int getHeight() {
    156        return height;
    157    }
    158
    159    /**
    160     * Gets the RGB or ARGB value for the pixel at the position (x,y) within the image builder pixel field. For performance reasons no bounds checking is
    161     * applied.
    162     *
    163     * @param x the X coordinate of the pixel to be read
    164     * @param y the Y coordinate of the pixel to be read
    165     * @return the RGB or ARGB pixel value
    166     */
    167    public int getRgb(final int x, final int y) {
    168        final int rowOffset = y * width;
    169        return data[rowOffset + x];
    170    }
    171
    172    /**
    173     * Gets a subimage from the ImageBuilder using the specified parameters. If the parameters specify a rectangular region that is not entirely contained
    174     * within the bounds defined by the ImageBuilder, this method will throw a RasterFormatException. This runtime-exception behavior is consistent with the
    175     * behavior of the getSubimage method provided by BufferedImage.
    176     *
    177     * @param x the X coordinate of the upper-left corner of the specified rectangular region
    178     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
    179     * @param w the width of the specified rectangular region
    180     * @param h the height of the specified rectangular region
    181     * @return a BufferedImage that constructed from the data within the specified rectangular region
    182     * @throws RasterFormatException f the specified area is not contained within this ImageBuilder
    183     */
    184    public BufferedImage getSubimage(final int x, final int y, final int w, final int h) {
    185        checkBounds(x, y, w, h);
    186
    187        // Transcribe the data to an output image array
    188        final int[] argb = Allocator.intArray(w * h);
    189        int k = 0;
    190        for (int iRow = 0; iRow < h; iRow++) {
    191            final int dIndex = (iRow + y) * width + x;
    192            System.arraycopy(this.data, dIndex, argb, k, w);
    193            k += w;
    194
    195        }
    196
    197        return makeBufferedImage(argb, w, h, hasAlpha);
    198
    199    }
    200
    201    /**
    202     * Gets a subset of the ImageBuilder content using the specified parameters to indicate an area of interest. If the parameters specify a rectangular region
    203     * that is not entirely contained within the bounds defined by the ImageBuilder, this method will throw a RasterFormatException. This run- time exception is
    204     * consistent with the behavior of the getSubimage method provided by BufferedImage.
    205     *
    206     * @param x the X coordinate of the upper-left corner of the specified rectangular region
    207     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
    208     * @param w the width of the specified rectangular region
    209     * @param h the height of the specified rectangular region
    210     * @return a valid instance of the specified width and height.
    211     * @throws RasterFormatException if the specified area is not contained within this ImageBuilder
    212     */
    213    public ImageBuilder getSubset(final int x, final int y, final int w, final int h) {
    214        checkBounds(x, y, w, h);
    215        final ImageBuilder b = new ImageBuilder(w, h, hasAlpha, isAlphaPremultiplied);
    216        for (int i = 0; i < h; i++) {
    217            final int srcDex = (i + y) * width + x;
    218            final int outDex = i * w;
    219            System.arraycopy(data, srcDex, b.data, outDex, w);
    220        }
    221        return b;
    222    }
    223
    224    /**
    225     * Gets the width of the ImageBuilder pixel field
    226     *
    227     * @return a positive integer
    228     */
    229    public int getWidth() {
    230        return width;
    231    }
    232
    233    private BufferedImage makeBufferedImage(final int[] argb, final int w, final int h, final boolean useAlpha) {
    234        final ColorModel colorModel;
    235        final WritableRaster raster;
    236        final DataBufferInt buffer = new DataBufferInt(argb, w * h);
    237        if (useAlpha) {
    238            colorModel = new DirectColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000,
    239                    isAlphaPremultiplied, DataBuffer.TYPE_INT);
    240            raster = Raster.createPackedRaster(buffer, w, h, w, new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 }, null);
    241        } else {
    242            colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff);
    243            raster = Raster.createPackedRaster(buffer, w, h, w, new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff }, null);
    244        }
    245        return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties());
    246    }
    247
    248    /**
    249     * Sets the RGB or ARGB value for the pixel at position (x,y) within the image builder pixel field. For performance reasons, no bounds checking is applied.
    250     *
    251     * @param x    the X coordinate of the pixel to be set.
    252     * @param y    the Y coordinate of the pixel to be set.
    253     * @param argb the RGB or ARGB value to be stored.
    254     * @throws ArithmeticException      if the index computation overflows an int.
    255     * @throws IllegalArgumentException if the resulting index is illegal.
    256     */
    257    public void setRgb(final int x, final int y, final int argb) {
    258        // Throw ArithmeticException if the result overflows an int.
    259        final int rowOffset = Math.multiplyExact(y, width);
    260        // Throw ArithmeticException if the result overflows an int.
    261        final int index = Math.addExact(rowOffset, x);
    262        if (index > data.length) {
    263            throw new IllegalArgumentException("setRGB: Illegal array index.");
    264        }
    265        data[index] = argb;
    266    }
    267}