View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.imaging.common;
19  
20  import java.awt.color.ColorSpace;
21  import java.awt.image.BufferedImage;
22  import java.awt.image.ColorModel;
23  import java.awt.image.DataBuffer;
24  import java.awt.image.DataBufferInt;
25  import java.awt.image.DirectColorModel;
26  import java.awt.image.Raster;
27  import java.awt.image.RasterFormatException;
28  import java.awt.image.WritableRaster;
29  import java.util.Properties;
30  
31  /*
32   * Development notes:
33   * This class was introduced to the Apache Commons Imaging library in
34   * order to improve performance in building images.  The setRGB method
35   * provided by this class represents a substantial improvement in speed
36   * compared to that of the BufferedImage class that was originally used
37   * in Apache Sanselan.
38   *   This increase is attained because ImageBuilder is a highly specialized
39   * class that does not need to perform the general-purpose logic required
40   * for BufferedImage.  If you need to modify this class to add new
41   * image formats or functionality, keep in mind that some of its methods
42   * are invoked literally millions of times when building an image.
43   * Since even the introduction of something as small as a single conditional
44   * inside of setRGB could result in a noticeable increase in the
45   * time to read a file, changes should be made with care.
46   *    During development, I experimented with inlining the setRGB logic
47   * in some of the code that uses it. This approach did not significantly
48   * improve performance, leading me to speculate that the Java JIT compiler
49   * might have inlined the method at run time.  Further investigation
50   * is required.
51   */
52  
53  /**
54   * A utility class primary intended for storing data obtained by reading image files.
55   */
56  public class ImageBuilder {
57      private final int[] data;
58      private final int width;
59      private final int height;
60      private final boolean hasAlpha;
61      private final boolean isAlphaPremultiplied;
62  
63      /**
64       * Constructs an ImageBuilder instance.
65       *
66       * @param width    the width of the image to be built
67       * @param height   the height of the image to be built
68       * @param hasAlpha indicates whether the image has an alpha channel (the selection of alpha channel does not change the memory requirements for the
69       *                 ImageBuilder or resulting BufferedImage.
70       * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
71       */
72      public ImageBuilder(final int width, final int height, final boolean hasAlpha) {
73          checkDimensions(width, height);
74  
75          data = Allocator.intArray(width * height);
76          this.width = width;
77          this.height = height;
78          this.hasAlpha = hasAlpha;
79          this.isAlphaPremultiplied = false;
80      }
81  
82      /**
83       * Constructs an ImageBuilder instance.
84       *
85       * @param width                the width of the image to be built
86       * @param height               the height of the image to be built
87       * @param hasAlpha             indicates whether the image has an alpha channel (the selection of alpha channel does not change the memory requirements for
88       *                             the ImageBuilder or resulting BufferedImage.
89       * @param isAlphaPremultiplied indicates whether alpha values are pre-multiplied; this setting is relevant only if alpha is true.
90       * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
91       */
92      public ImageBuilder(final int width, final int height, final boolean hasAlpha, final boolean isAlphaPremultiplied) {
93          checkDimensions(width, height);
94          data = Allocator.intArray(width * height);
95          this.width = width;
96          this.height = height;
97          this.hasAlpha = hasAlpha;
98          this.isAlphaPremultiplied = isAlphaPremultiplied;
99      }
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 }