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