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