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 final 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 this(width, height, hasAlpha, false); 074 } 075 076 /** 077 * Constructs an ImageBuilder instance. 078 * 079 * @param width the width of the image to be built 080 * @param height the height of the image to be built 081 * @param hasAlpha indicates whether the image has an alpha channel (the selection of alpha channel does not change the memory requirements for 082 * the ImageBuilder or resulting BufferedImage. 083 * @param isAlphaPremultiplied indicates whether alpha values are pre-multiplied; this setting is relevant only if alpha is true. 084 * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero 085 */ 086 public ImageBuilder(final int width, final int height, final boolean hasAlpha, final boolean isAlphaPremultiplied) { 087 checkDimensions(width, height); 088 data = Allocator.intArray(width * height); 089 this.width = width; 090 this.height = height; 091 this.hasAlpha = hasAlpha; 092 this.isAlphaPremultiplied = isAlphaPremultiplied; 093 } 094 095 /** 096 * Performs a check on the specified sub-region to verify that it is within the constraints of the ImageBuilder bounds. 097 * 098 * @param x the X coordinate of the upper-left corner of the specified rectangular region 099 * @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}