001 /*
002 Copyright (c) 1996-2012, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.svrCore;
031
032 import java.awt.image.BufferedImage;
033 import java.awt.image.ColorModel;
034 import java.awt.image.DataBuffer;
035 import java.awt.image.IndexColorModel;
036 import java.awt.image.RenderedImage;
037 import java.awt.image.WritableRaster;
038 import java.util.HashSet;
039 import java.util.Hashtable;
040 import java.util.Set;
041 import java.util.TreeSet;
042
043 import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
044 import org.hd.d.pg2k.svrCore.MIME.Quantize;
045
046 /**
047 * Created by IntelliJ IDEA.
048 * User: Damon Hart-Davis
049 * Date: 30-Aug-2003
050 * Time: 23:46:46
051 */
052
053 /**Basic image-handling utilities.
054 */
055 public final class ImageUtils
056 {
057 /**Prevent instantiation. */
058 private ImageUtils() { }
059
060
061 /**Count distinct colours in 24-bit RGB colour space.
062 * Counts the number of distinct colours in the sRGB colour model
063 * in the given image, ignoring alpha, with each the depth of each
064 * band being 8 bits, for a total of 24 bits for each pixel.
065 * <p>
066 * If the source image is not in RGB format then
067 * colour conversion will take place.
068 * <p>
069 * Is slow but tries to be reasonably memory efficient.
070 * <p>
071 * The input image is not altered.
072 */
073 public static int countDistinct24BitRGBColours(final BufferedImage im)
074 {
075 final int height = im.getHeight();
076 final int width = im.getWidth();
077 final Set<Integer> distinctColours = new HashSet<Integer>();
078
079 for(int y = height; --y >= 0; )
080 {
081 for(int x = width; --x >= 0; )
082 {
083 distinctColours.add(new Integer(im.getRGB(x, y) & 0xffffff));
084 }
085 }
086
087 return(distinctColours.size());
088 }
089
090
091 /**Method to convert a RenderedImage to a BufferedImage.
092 * Thanks to <a href="http://www.magelang.com/faq/view.jsp?EID=114602">Jim Moore (06 Apr 2003)</a>
093 * for his routine to "do it right"! Very slightly adjusted by DHD.
094 *
095 * @param ri the image to be converted
096 * (if already a BufferedImage then it is returned intact);
097 * must not be null
098 *
099 * @return the original if already a BufferedImage,
100 * else a copy of the data
101 */
102 public static BufferedImage convertRenderedImageToBufferedImage(final RenderedImage ri)
103 {
104 // Return as-is if already a BufferedImage.
105 if(ri instanceof BufferedImage)
106 { return (BufferedImage) ri; }
107
108 // Extract basic metadata.
109 final ColorModel cm = ri.getColorModel();
110 final int width = ri.getWidth();
111 final int height = ri.getHeight();
112 final boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
113
114 // Extract properties if any.
115 final String[] keys = ri.getPropertyNames();
116 Hashtable<String, Object> properties = null;
117 if(keys != null)
118 {
119 properties = new Hashtable<String, Object>(keys.length * 2 + 1);
120 for(int i = 0; i < keys.length; i++)
121 { properties.put(keys[i], ri.getProperty(keys[i])); }
122 }
123
124 // Construct BufferedImage and copy (raster) data in.
125 final WritableRaster raster = cm.createCompatibleWritableRaster(width, height);
126 final BufferedImage result = new BufferedImage(cm, raster, isAlphaPremultiplied, properties);
127 ri.copyData(raster);
128
129 return(result);
130 }
131
132 /**Create colour reduced image; never null
133 * A new image is created that is potentially colour-reduced from the
134 * original; no new colours should be introduced unless during the
135 * conversion to or from the RGB colour space in which reduction is done.
136 * The R, G and B quantities are each 8 bits.
137 * <p>
138 * By default the new image is returned in the same ColorModel as
139 * the original, but forcing the result to an indexed
140 * (palette-based) model such as used by GIF can requested,
141 * in which case the palette size is capped appropriately,
142 * and the bits-per-pixel may be chosen to be 1, 2, 4, or 8
143 * depending on the palette size.
144 * <p>
145 * Any input image that can be converted to RGB should be acceptable.
146 * <p>
147 * If no colour reduction is achieved and no ColorModel change is requested
148 * then the original image is returned;
149 * in no case is the original image altered.
150 * <p>
151 * This may return a single-colour image if the input image is too tricky
152 * and the maximum number of colours it can return is too small,
153 * but that will be avoided if possible.
154 *
155 * @param in the original image; must not be null
156 * @param maxColours maximum number of distinct colours allowed in the
157 * result image (as measured in a 24-bit (8,8,8) sRGB colour space);
158 * must be at least 2 and will be capped to 256 if an indexed model
159 * is forced for the result
160 * @param forceByteIndexModel if true, force the result to be a
161 * byte-indexed palette-based color model (maximum 256 colours)
162 * that may be (a) suitable for formats such as GIF and
163 * (b) more compressable
164 *
165 * @return image same dimensions as the input image,
166 * possibly with fewer colours; never null though might be single colour
167 */
168 public static BufferedImage makeColourReducedBufferedImage(
169 final BufferedImage in,
170 int maxColours,
171 final boolean forceByteIndexModel)
172 {
173 //System.out.println("makeColourReducedBufferedImage("+maxColours+"): START *****************");
174
175 if(in == null)
176 { throw new IllegalArgumentException("input image cannot be null"); }
177
178 if(maxColours < 2)
179 { throw new IllegalArgumentException("at least two colours must be allowed in the result"); }
180
181 if(forceByteIndexModel && (maxColours > 256))
182 { maxColours = 256; }
183
184 // Extract copy of pixels from source image in TYPE_INT_ARGB format.
185 // Set scansize to be width to efficiently pack the result.
186 final int width = in.getWidth();
187 final int height = in.getHeight();
188 final ColorModel cm = ImageUtils.extractColorModelOrRGB(in);
189 //final boolean isAlphaPremultiplied = in.isAlphaPremultiplied();
190
191 // If the input already has a byte-indexed palette,
192 // and maxColours is no smaller than the palette already in the image,
193 // then return the original.
194 if((cm instanceof IndexColorModel) &&
195 (((IndexColorModel) cm).getMapSize() <= maxColours) &&
196 (cm.getTransferType() == DataBuffer.TYPE_BYTE))
197 {
198 //System.out.println("makeColourReducedBufferedImage(): input no more colours than max and already byte indexed: returning original");
199 return(in); // Input already a byte-indexed colour model.
200 }
201
202 // Extract RGB pixels from source image.
203 final int pixels[] = in.getRGB(0, 0, width, height, null, 0, width);
204
205 //System.out.print("makeColourReducedBufferedImage(): input pixels = ");
206 // for(int i = Math.min(20, pixels.length); --i >= 0; ) { System.out.print(Integer.toHexString(pixels[i]) + ", "); }
207 //System.out.println();
208
209 // Count the colours in the original image (ignore any alpha).
210 final Set<Integer> originalDistinctColours = new TreeSet<Integer>();
211 for(int i = pixels.length; --i >= 0; )
212 { originalDistinctColours.add(new Integer(pixels[i] & 0xffffff)); }
213 //System.out.println("makeColourReducedBufferedImage(): original colour count: " + originalDistinctColours.size());
214
215 // Now do the colour reduction...
216 // Note that each RGB pixel value is replaced with a palete *index*.
217 final int[] palette = Quantize.quantizeImage(
218 new int[][]{pixels}, maxColours);
219 //System.out.println("makeColourReducedBufferedImage(): new colour count: " + palette.length);
220
221 // If we did not reduce the colours, consider returning the original.
222 final int origCols = originalDistinctColours.size();
223 if(palette.length >= origCols)
224 {
225 // If did not try to force an indexed output,
226 // then return the original.
227 if(!forceByteIndexModel)
228 {
229 //System.out.println("makeColourReducedBufferedImage(): did not manage to reduce number of colors: returning original");
230 return(in); // Didn't reduce the image at all.
231 }
232
233 // If the input already had a byte-indexed palette,
234 // and the new palette is no smaller than the old
235 // (ie there were no redundant/duplicate colours in the original)
236 // then return the original.
237 if((cm instanceof IndexColorModel) &&
238 (((IndexColorModel) cm).getMapSize() <= palette.length) &&
239 (cm.getTransferType() == DataBuffer.TYPE_BYTE))
240 {
241 //System.out.println("makeColourReducedBufferedImage(): did not manage to reduce number of colors and already byte indexed: returning original");
242 return(in); // Input already a byte-indexed colour model.
243 }
244
245 // Need to generate byte-indexed form, so fall through...
246 }
247
248 //System.out.print("makeColourReducedBufferedImage(): palette = ");
249 // for(int i = palette.length; --i >= 0; ) { System.out.print(Integer.toHexString(palette[i]) + ", "); }
250 //System.out.println();
251 //System.out.print("makeColourReducedBufferedImage(): pixels = ");
252 // for(int i = Math.min(20, pixels.length); --i >= 0; ) { System.out.print(Integer.toHexString(pixels[i]) + ", "); }
253 //System.out.println();
254
255
256 // Replace palette index values with RGB values.
257 // Force all pixels to be opaque before setting them in the image.
258 for(int i = pixels.length; --i >= 0; )
259 { pixels[i] = palette[pixels[i]] | 0xff000000; }
260
261 // Handle creation of a new byte-indexed colour map specially,
262 // though we don't really need to.
263 BufferedImage result;
264 if(forceByteIndexModel)
265 {
266 // Force all palette entries to be opaque.
267 for(int i = palette.length; --i >= 0; )
268 { palette[i] |= 0xff000000; }
269
270 final IndexColorModel outputColourModel =
271 new IndexColorModel(computeIndexBitsForByteIndexColorMap(palette.length),
272 palette.length,
273 palette,
274 0, // Start at offset zero.
275 false, // No alpha.
276 -1, // No transparent colour.
277 DataBuffer.TYPE_BYTE); // 8-bit palette
278
279 result = new BufferedImage(width, height,
280 BufferedImage.TYPE_BYTE_INDEXED,
281 outputColourModel);
282 }
283 else
284 {
285 // Coerce data into original colour model.
286 // Discard any properties of the original.
287 final ColorModel outputColourModel = cm;
288
289 // Handle other images in default way.
290 final WritableRaster raster =
291 outputColourModel.createCompatibleWritableRaster(width, height);
292 result = new BufferedImage(outputColourModel, raster, false, null);
293 }
294
295 // Actually store the pixels...
296 result.setRGB(0, 0, width, height, pixels, 0, width);
297
298 //System.err.println("makeColourReducedBufferedImage(): palette.length = " + palette.length + ", number of colours in result = " + countDistinct24BitRGBColours(result));
299
300 return(result);
301 }
302
303 /**Compute the number of bits to store the pixel of an indexed image given the size of the palette; strictly positive power of two up to 8.
304 * The default behaviour is to return 8, for a byte-indexed palette,
305 * but this may return 4 if the palette has no more than 16 entries,
306 * 2 if the palette has no more than 4 entries, or
307 * 1 if the palette has no more than 2 entries,
308 * ie widths that will pack neatly into a byte.
309 *
310 * @return number of pits needed to store palette,
311 * as submultiple of 8
312 */
313 private static int computeIndexBitsForByteIndexColorMap(final int paletteSize)
314 {
315 if((paletteSize < 0) || (paletteSize > 256))
316 { throw new IllegalArgumentException(); }
317
318 if(paletteSize <= 2) { return(1); }
319 if(paletteSize <= 4) { return(2); }
320 if(paletteSize <= 16) { return(4); }
321
322 // Default is to use a whole byte for each pixel.
323 return(8);
324 }
325
326 /**Return true-colour ARGB format image if src is indexed, else return original.
327 * The returned image should be easy to draw on.
328 * <p>
329 * The src image is not altered.
330 *
331 * @param src input image, is not altered; must not be null
332 * @return non-indexed image
333 */
334 public static BufferedImage convertToTrueColourARGB(final BufferedImage src,
335 final boolean forceCopy)
336 {
337 if(src == null)
338 { throw new IllegalArgumentException(); }
339
340 // If image is indexed, expand to full true-colour format.
341 // Else if copy is forced, do it anyway to get a separate copy.
342 if(forceCopy ||
343 (src.getColorModel() instanceof IndexColorModel))
344 {
345 final int width = src.getWidth();
346 final int height = src.getHeight();
347
348 final BufferedImage rgbBi = new BufferedImage(
349 width, height, BufferedImage.TYPE_INT_ARGB);
350 rgbBi.setRGB(0, 0, width, height,
351 src.getRGB(0, 0, width, height, null, 0, width),
352 0, width);
353
354 return(rgbBi);
355 }
356
357 // Return original.
358 return(src);
359 }
360
361 /**Extract the ColorModel of the inpuit image, or return the default RGB model; never null.
362 * This is needed because BufferedImage.getColorModel() can return null.
363 *
364 * @param imageIn input image from which to extract the ColorModel;
365 * must not be null
366 * @return a non-null ColorModel
367 */
368 public static ColorModel extractColorModelOrRGB(final BufferedImage imageIn)
369 {
370 // Extract just the map fragment that we need.
371 // Coerce data into original colour model.
372 // Discard any properties of the original.
373 final ColorModel rcm = imageIn.getColorModel();
374
375 // If the source image does not have a colour model,
376 // create an appropriate default one (RGB).
377 final ColorModel cm = (rcm != null) ?
378 rcm : ColorModel.getRGBdefault();
379
380 return(cm);
381 }
382
383 /**Returns true if exhibit might be used as own thumbnail (in an HTML page for example).
384 * If true then we don't count downloads of this exhibit
385 * (and may adjust computation of "goodness" to take account of this).
386 * <p>
387 * An exhibit is potentially inlinable if:
388 * <ul>
389 * <li>It is small enough (smaller than the maximum size of a thumbnail).
390 * <li>It is a type which can typically be inlined, eg a GIF image.
391 * </ul>
392 *
393 * @param esa the exhibit details; never null
394 *
395 * @return true if the exhibit is potentially inlinable
396 */
397 public static boolean canBeOwnThumbnail(final ExhibitStaticAttr esa)
398 {
399 return((esa.length <= ExhibitThumbnails.STD_ABS_MAX_BYTES) &&
400 canInlineInHTMLPageSimple((ExhibitMIME.getInputFileType(esa.getCharSequence()))));
401 }
402
403 /**Returns true if the given MIME-type can always be inlined in an HTML page.
404 * This only tests for very common and widely-supported image types.
405 * <p>
406 * This does not dynamically test the capabilities of the user agent
407 * that content is to be shown to.
408 * <p>
409 * If the exhibitType argument is null, this returns false.
410 */
411 public static boolean canInlineInHTMLPageSimple(final ExhibitMIME.ExhibitTypeParameters exhibitType)
412 {
413 if(exhibitType == null) { return(false); }
414 switch(exhibitType.type)
415 {
416 case ExhibitMIME.ET_JPEG:
417 case ExhibitMIME.ET_GIF:
418 case ExhibitMIME.ET_PNG: // Most HTML browsers will accept PNG now.
419 { return(true); }
420 }
421 return(false);
422 }
423 }