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 }