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        }