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 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        checkDimensions(width, height);
074
075        data = Allocator.intArray(width * height);
076        this.width = width;
077        this.height = height;
078        this.hasAlpha = hasAlpha;
079        this.isAlphaPremultiplied = false;
080    }
081
082    /**
083     * Constructs an ImageBuilder instance.
084     *
085     * @param width                the width of the image to be built
086     * @param height               the height of the image to be built
087     * @param hasAlpha             indicates whether the image has an alpha channel (the selection of alpha channel does not change the memory requirements for
088     *                             the ImageBuilder or resulting BufferedImage.
089     * @param isAlphaPremultiplied indicates whether alpha values are pre-multiplied; this setting is relevant only if alpha is true.
090     * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
091     */
092    public ImageBuilder(final int width, final int height, final boolean hasAlpha, final boolean isAlphaPremultiplied) {
093        checkDimensions(width, height);
094        data = Allocator.intArray(width * height);
095        this.width = width;
096        this.height = height;
097        this.hasAlpha = hasAlpha;
098        this.isAlphaPremultiplied = isAlphaPremultiplied;
099    }
100
101    /**
102     * Performs a check on the specified sub-region to verify that it is within the constraints of the ImageBuilder bounds.
103     *
104     * @param x the X coordinate of the upper-left corner of the specified rectangular region
105     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
106     * @param w the width of the specified rectangular region
107     * @param h the height of the specified rectangular region
108     * @throws RasterFormatException if width or height are equal or less than zero, or if the subimage is outside raster (on x or y axis)
109     */
110    private void checkBounds(final int x, final int y, final int w, final int h) {
111        if (w <= 0) {
112            throw new RasterFormatException("negative or zero subimage width");
113        }
114        if (h <= 0) {
115            throw new RasterFormatException("negative or zero subimage height");
116        }
117        if (x < 0 || x >= width) {
118            throw new RasterFormatException("subimage x is outside raster");
119        }
120        if (x + w > width) {
121            throw new RasterFormatException("subimage (x+width) is outside raster");
122        }
123        if (y < 0 || y >= height) {
124            throw new RasterFormatException("subimage y is outside raster");
125        }
126        if (y + h > height) {
127            throw new RasterFormatException("subimage (y+height) is outside raster");
128        }
129    }
130
131    /**
132     * Checks for valid dimensions and throws {@link RasterFormatException} if the inputs are invalid.
133     *
134     * @param width  image width (must be greater than zero)
135     * @param height image height (must be greater than zero)
136     * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
137     */
138    private void checkDimensions(final int width, final int height) {
139        if (width <= 0) {
140            throw new RasterFormatException("zero or negative width value");
141        }
142        if (height <= 0) {
143            throw new RasterFormatException("zero or negative height value");
144        }
145    }
146
147    /**
148     * Create a BufferedImage using the data stored in the ImageBuilder.
149     *
150     * @return a valid BufferedImage.
151     */
152    public BufferedImage getBufferedImage() {
153        return makeBufferedImage(data, width, height, hasAlpha);
154    }
155
156    /**
157     * Gets the height of the ImageBuilder pixel field
158     *
159     * @return a positive integer
160     */
161    public int getHeight() {
162        return height;
163    }
164
165    /**
166     * 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
167     * applied.
168     *
169     * @param x the X coordinate of the pixel to be read
170     * @param y the Y coordinate of the pixel to be read
171     * @return the RGB or ARGB pixel value
172     */
173    public int getRgb(final int x, final int y) {
174        final int rowOffset = y * width;
175        return data[rowOffset + x];
176    }
177
178    /**
179     * Gets a subimage from the ImageBuilder using the specified parameters. If the parameters specify a rectangular region that is not entirely contained
180     * within the bounds defined by the ImageBuilder, this method will throw a RasterFormatException. This runtime-exception behavior is consistent with the
181     * behavior of the getSubimage method provided by BufferedImage.
182     *
183     * @param x the X coordinate of the upper-left corner of the specified rectangular region
184     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
185     * @param w the width of the specified rectangular region
186     * @param h the height of the specified rectangular region
187     * @return a BufferedImage that constructed from the data within the specified rectangular region
188     * @throws RasterFormatException f the specified area is not contained within this ImageBuilder
189     */
190    public BufferedImage getSubimage(final int x, final int y, final int w, final int h) {
191        checkBounds(x, y, w, h);
192
193        // Transcribe the data to an output image array
194        final int[] argb = Allocator.intArray(w * h);
195        int k = 0;
196        for (int iRow = 0; iRow < h; iRow++) {
197            final int dIndex = (iRow + y) * width + x;
198            System.arraycopy(this.data, dIndex, argb, k, w);
199            k += w;
200
201        }
202
203        return makeBufferedImage(argb, w, h, hasAlpha);
204
205    }
206
207    /**
208     * Gets a subset of the ImageBuilder content using the specified parameters to indicate an area of interest. If the parameters specify a rectangular region
209     * that is not entirely contained within the bounds defined by the ImageBuilder, this method will throw a RasterFormatException. This run- time exception is
210     * consistent with the behavior of the getSubimage method provided by BufferedImage.
211     *
212     * @param x the X coordinate of the upper-left corner of the specified rectangular region
213     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
214     * @param w the width of the specified rectangular region
215     * @param h the height of the specified rectangular region
216     * @return a valid instance of the specified width and height.
217     * @throws RasterFormatException if the specified area is not contained within this ImageBuilder
218     */
219    public ImageBuilder getSubset(final int x, final int y, final int w, final int h) {
220        checkBounds(x, y, w, h);
221        final ImageBuilder b = new ImageBuilder(w, h, hasAlpha, isAlphaPremultiplied);
222        for (int i = 0; i < h; i++) {
223            final int srcDex = (i + y) * width + x;
224            final int outDex = i * w;
225            System.arraycopy(data, srcDex, b.data, outDex, w);
226        }
227        return b;
228    }
229
230    /**
231     * Gets the width of the ImageBuilder pixel field
232     *
233     * @return a positive integer
234     */
235    public int getWidth() {
236        return width;
237    }
238
239    private BufferedImage makeBufferedImage(final int[] argb, final int w, final int h, final boolean useAlpha) {
240        ColorModel colorModel;
241        WritableRaster raster;
242        final DataBufferInt buffer = new DataBufferInt(argb, w * h);
243        if (useAlpha) {
244            colorModel = new DirectColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000,
245                    isAlphaPremultiplied, DataBuffer.TYPE_INT);
246            raster = Raster.createPackedRaster(buffer, w, h, w, new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 }, null);
247        } else {
248            colorModel = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff);
249            raster = Raster.createPackedRaster(buffer, w, h, w, new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff }, null);
250        }
251        return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties());
252    }
253
254    /**
255     * 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.
256     *
257     * @param x    the X coordinate of the pixel to be set.
258     * @param y    the Y coordinate of the pixel to be set.
259     * @param argb the RGB or ARGB value to be stored.
260     * @throws ArithmeticException      if the index computation overflows an int.
261     * @throws IllegalArgumentException if the resulting index is illegal.
262     */
263    public void setRgb(final int x, final int y, final int argb) {
264        // Throw ArithmeticException if the result overflows an int.
265        final int rowOffset = Math.multiplyExact(y, width);
266        // Throw ArithmeticException if the result overflows an int.
267        final int index = Math.addExact(rowOffset, x);
268        if (index > data.length) {
269            throw new IllegalArgumentException("setRGB: Illegal array index.");
270        }
271        data[index] = argb;
272    }
273}