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}