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        }