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 final 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 this(width, height, hasAlpha, false);
74 }
75
76 /**
77 * Constructs an ImageBuilder instance.
78 *
79 * @param width the width of the image to be built
80 * @param height the height of the image to be built
81 * @param hasAlpha indicates whether the image has an alpha channel (the selection of alpha channel does not change the memory requirements for
82 * the ImageBuilder or resulting BufferedImage.
83 * @param isAlphaPremultiplied indicates whether alpha values are pre-multiplied; this setting is relevant only if alpha is true.
84 * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero
85 */
86 public ImageBuilder(final int width, final int height, final boolean hasAlpha, final boolean isAlphaPremultiplied) {
87 checkDimensions(width, height);
88 data = Allocator.intArray(width * height);
89 this.width = width;
90 this.height = height;
91 this.hasAlpha = hasAlpha;
92 this.isAlphaPremultiplied = isAlphaPremultiplied;
93 }
94
95 /**
96 * Performs a check on the specified sub-region to verify that it is within the constraints of the ImageBuilder bounds.
97 *
98 * @param x the X coordinate of the upper-left corner of the specified rectangular region
99 * @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 }