001 /*
002 Copyright (c) 1996-2011, 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.webSvr.bgImg;
031
032 import java.awt.Transparency;
033 import java.awt.color.ColorSpace;
034 import java.awt.image.BufferedImage;
035 import java.awt.image.ColorModel;
036 import java.awt.image.ComponentColorModel;
037 import java.awt.image.DataBuffer;
038 import java.awt.image.WritableRaster;
039 import java.io.IOException;
040 import java.util.ArrayList;
041 import java.util.Arrays;
042 import java.util.Collections;
043 import java.util.List;
044 import java.util.Random;
045
046 import org.hd.d.pg2k.svrCore.ExhibitThumbnails;
047 import org.hd.d.pg2k.svrCore.Rnd;
048 import org.hd.d.pg2k.svrCore.MIME.AbstractImageHandler;
049 import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
050
051 /**
052 * Created by IntelliJ IDEA.
053 * User: Damon Hart-Davis
054 * Date: 29-Nov-2003
055 * Time: 18:05:45
056 */
057
058 /**Supporting constants and classes for dynamic background images.
059 */
060 public final class BgImageUtils
061 {
062 /**Maximum brightness (assuming 0 is black, 255 is white). */
063 static final int MAX_Y = 255;
064
065 /**Minimum brightness (assuming 0 is black, 255 is white).
066 * Should be less than MAX_Y by at least 0x33 (51)
067 * to (almost) force browsers with a 6x6x6 colour cube to show the
068 * image in at least two colours.
069 */
070 static final int MIN_Y = 204;
071
072 /**Type of background image that we dynamically generate.
073 * Since we want to be able to read text against this,
074 * and not consume too much bandwidth,
075 * both implying smoothly varying images,
076 * JPEG is probably the best format,
077 * especially as it is widely supported.
078 */
079 static final int DYN_MAP_IMG_TYPE = ExhibitMIME.ET_JPEG;
080
081 /**ExhibitMIME entry for the chosen output type. */
082 static final ExhibitMIME.ExhibitTypeParameters ETP =
083 ExhibitMIME.getParamsByType(DYN_MAP_IMG_TYPE);
084
085 /**URI of dynamic background-image JPEG; suffix should match MIME type. */
086 public static final String dynBgImgRRUL = "/_static/dynImg/bgImg.jpg";
087
088 /**We chose the background image dimensions to match that of a standard thumbnail. */
089 public static final int dynBgImgEdgePixels = ExhibitThumbnails.STD_STATIC_IMAGE_TN_LDIM_PX;
090
091 /**The maximum size of image produced (in bytes) is that of a standard thumbnail. */
092 public static final int dynBgImgMaxBytes = ExhibitThumbnails.STD_ABS_MAX_BYTES;
093
094 /**Functor/interface for dynamic background image generator.
095 * Generates a BufferedImage with the dimensions of a large thumbnail,
096 * suitable to use as a background image.
097 * <p>
098 * The simage will be relatively small (in bytes) and suitable to read
099 * black text again (will be fairly light colour).
100 */
101 public interface DynImageGen
102 {
103 /**Generate simple random monochrome BufferedImage with values uniformly distributed between max and min Y.
104 * This routine may alter the nature of the output based on the seed,
105 * which should ideally be distributed evenly across all possible
106 * values, like a hashCode.
107 */
108 public BufferedImage createImage(final int seed);
109 }
110
111 /**Simple monochrome random-pixel image.
112 * Each pixel is independently chosen.
113 */
114 public static final class SimpleUniformNoiseMonoImageGen implements DynImageGen
115 {
116 /**Generate simple random monochrome BufferedImage with values uniformly distributed between max and min Y.
117 * This routine may alter the nature of the output based on the seed,
118 * which should ideally be distributed evenly across all possible
119 * values, like a hashCode.
120 * <p>
121 * In this implementation, the lowest-order bit selects between
122 * "fast" and "good" random number sources.
123 */
124 public BufferedImage createImage(final int seed)
125 {
126 // Select between random-number generators based on the seed.
127 final Random rnd = ((seed & 1) == 0) ? Rnd.fastRnd : Rnd.goodRnd;
128
129 // Create a blank RGB (buffered) image.
130 final BufferedImage bi = new BufferedImage(dynBgImgEdgePixels,
131 dynBgImgEdgePixels,
132 BufferedImage.TYPE_BYTE_GRAY);
133
134 final int range = MAX_Y - MIN_Y + 1;
135 for(int y = dynBgImgEdgePixels; --y >= 0; )
136 {
137 for(int x = dynBgImgEdgePixels; --x >= 0; )
138 {
139 // Compute grey-scale value.
140 final int grey = rnd.nextInt(range) + MIN_Y;
141 // Convert to opaque rgb.
142 final int rgb = (grey << 16) | (grey << 8) | (grey) | 0xff000000;
143 bi.setRGB(x, y, rgb);
144 }
145 }
146
147 return(bi);
148 }
149 }
150
151 /**Simple monochrome uniform colour image.
152 * Each pixel is the same colour
153 * and will be suitable to read black/dark text against.
154 */
155 public static final class SimpleUniformMonoImageGen implements DynImageGen
156 {
157 /**Generate simple random monochrome BufferedImage with all pixels on colour between max and min Y.
158 * The colour is derived from the seed.
159 */
160 public BufferedImage createImage(final int seed)
161 {
162 // Create a blank RGB (buffered) image.
163 final BufferedImage bi = new BufferedImage(dynBgImgEdgePixels,
164 dynBgImgEdgePixels,
165 BufferedImage.TYPE_BYTE_GRAY);
166
167 final int range = MAX_Y - MIN_Y + 1;
168 // Compute grey-scale value.
169 final int grey = (new Random(seed)).nextInt(range) + MIN_Y;
170 // Convert to opaque rgb.
171 final int rgb = (grey << 16) | (grey << 8) | (grey) | 0xff000000;
172 // Set entire array to one value.
173 // TODO: must be more efficient way of doing this...
174 for(int y = dynBgImgEdgePixels; --y >= 0; )
175 {
176 for(int x = dynBgImgEdgePixels; --x >= 0; )
177 {
178 bi.setRGB(x, y, rgb);
179 }
180 }
181
182 return(bi);
183 }
184 }
185
186 /**Makes 2D sine-plus-noise colour image.
187 * This is slowly varying, and light,
188 * so should be easy to read dark text against.
189 * <p>
190 * This may revert to mono dependending on the seed.
191 */
192 public static final class Sine2DColourImageGen implements DynImageGen
193 {
194 /**Generate 2D sine-based colour with each colour distributed between max and min Y.
195 * The image parameters are mostly entirely derived from the seed.
196 */
197 public BufferedImage createImage(final int seed)
198 {
199 // Select between random-number generators based on the seed.
200 final Random pRnd = new Random(seed);
201
202 // Choose mono or colour.
203 // FIXME: Fix this so it works in colour...
204 final boolean isMono = true; // pRnd.nextBoolean();
205
206 // Extract basic metadata.
207 final ColorModel cm = isMono ?
208 new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
209 (new int[]{8}), false, true,
210 Transparency.OPAQUE,
211 DataBuffer.TYPE_BYTE) :
212 ColorModel.getRGBdefault();
213 final int width = dynBgImgEdgePixels;
214 final int height = dynBgImgEdgePixels;
215 final boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
216
217 // Construct BufferedImage and copy (raster) data in.
218 final WritableRaster raster = cm.createCompatibleWritableRaster(width, height);
219 final BufferedImage bi = new BufferedImage(cm, raster, isAlphaPremultiplied, null);
220
221 // Chromance (Y) range, and range of any one colour component.
222 final int range = MAX_Y - MIN_Y + 1;
223
224 // Chose the frequency in the x and y directions
225 // and a number of whole cycles per edge length.
226 // Make this a whole number of cycles so that
227 // the image will tile properly.
228 final int xFreq = getRndCycles(pRnd);
229 final int yFreq = getRndCycles(pRnd);
230
231 // Compute the sine values for the x and y dimensions.
232 final int xVals[] = new int[dynBgImgEdgePixels];
233 final int yVals[] = new int[dynBgImgEdgePixels];
234 final int rd2 = range / 2;
235 final int centre = MIN_Y + rd2;
236 final double f = (2.0 * Math.PI / dynBgImgEdgePixels);
237 for(int i = dynBgImgEdgePixels; --i >= 0; )
238 {
239 final double ixf = i * f;
240 xVals[i] = Math.max(MIN_Y, Math.min(MAX_Y,
241 centre + (int) Math.round(Math.sin(ixf * xFreq) * rd2)));
242 yVals[i] = Math.max(MIN_Y, Math.min(MAX_Y,
243 centre + (int) Math.round(Math.sin(ixf * yFreq) * rd2)));
244 }
245
246 for(int y = dynBgImgEdgePixels; --y >= 0; )
247 {
248 for(int x = dynBgImgEdgePixels; --x >= 0; )
249 {
250 // Compute least-significant (blue) value as noise.
251 final int blue = pRnd.nextInt(range) + MIN_Y;
252 // Convert to opaque rgb.
253 final int rgb = (xVals[x] << 16) | (yVals[y] << 8) | (blue) | 0xff000000;
254 bi.setRGB(x, y, rgb);
255 }
256 }
257
258 return(bi);
259 }
260
261 /**Chose a number of cycles for the background pattern across the image; strictly positive.
262 * The result is always (much) less than the number of pixels,
263 * and always at least 1.
264 *
265 * @param pRnd the pseudo-random (repeatable) source; never null
266 */
267 private static int getRndCycles(final Random pRnd)
268 {
269 return(1 + pRnd.nextInt(1 | pRnd.nextInt(dynBgImgEdgePixels / 16)));
270 }
271 }
272
273
274
275
276
277
278
279
280
281
282
283 /**Immutable List of instances all available basic DynImageGen; never null or empty. */
284 public static final List<DynImageGen> allDynGens =
285 Collections.unmodifiableList(new ArrayList<DynImageGen>(Arrays.asList(
286 new DynImageGen[]{
287 new SimpleUniformMonoImageGen(),
288 new SimpleUniformNoiseMonoImageGen(),
289 new Sine2DColourImageGen(),
290 })));
291
292
293 /**Convert BufferedImage to byte[]-encoded JPEG no larger than dynBgImgMaxBytes bytes.
294 */
295 static byte[] generateEncodedImage(final BufferedImage bi)
296 throws IOException
297 {
298 final AbstractImageHandler.ThumbnailParams tp = ETP.handler.getThumbnailParams();
299 if(tp == null) { throw new IllegalStateException("cannot get thumbnail parameters"); }
300 return(ETP.handler.makeSizeConstrainedEncodedImage(
301 tp.minQuality, tp.initialQualityHint, tp.maxQuality,
302 bi,
303 dynBgImgMaxBytes/4, dynBgImgMaxBytes/2, // Target limits.
304 1, dynBgImgMaxBytes, // Absolute limits...
305 dynBgImgMaxBytes/2)); // Target size...
306 }
307 }