001    /*
002    *
003    
004    Licensed under the Apache License, Version 2.0 (the "License");
005    you may not use this file except in compliance with the License.
006    You may obtain a copy of the License at
007    
008       http://www.apache.org/licenses/LICENSE-2.0
009    
010    Unless required by applicable law or agreed to in writing, software
011    distributed under the License is distributed on an "AS IS" BASIS,
012    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013    See the License for the specific language governing permissions and
014    limitations under the License.
015    */
016    
017    package railo.runtime.img.filter;import java.awt.Rectangle;
018    import java.awt.image.BufferedImage;
019    
020    import railo.runtime.engine.ThreadLocalPageContext;
021    import railo.runtime.exp.FunctionException;
022    import railo.runtime.exp.PageException;
023    import railo.runtime.img.ImageUtil;
024    import railo.runtime.type.KeyImpl;
025    import railo.runtime.type.List;
026    import railo.runtime.type.Struct;
027    
028    
029    /**
030     * A filter which quantizes an image to a set number of colors - useful for producing
031     * images which are to be encoded using an index color model. The filter can perform
032     * Floyd-Steinberg error-diffusion dithering if required. At present, the quantization
033     * is done using an octtree algorithm but I eventually hope to add more quantization
034     * methods such as median cut. Note: at present, the filter produces an image which
035     * uses the RGB color model (because the application it was written for required it).
036     * I hope to extend it to produce an IndexColorModel by request.
037     */
038    public class QuantizeFilter extends WholeImageFilter  implements DynFiltering {
039    
040            /**
041             * Floyd-Steinberg dithering matrix.
042             */
043            protected final static int[] matrix = {
044                     0, 0, 0,
045                     0, 0, 7,
046                     3, 5, 1,
047            };
048            private int sum = 3+5+7+1;
049    
050            private boolean dither;
051            private int numColors = 256;
052            private boolean serpentine = true;
053    
054            /**
055             * Set the number of colors to quantize to.
056             * @param numColors the number of colors. The default is 256.
057             */
058            public void setNumColors(int numColors) {
059                    this.numColors = Math.min(Math.max(numColors, 8), 256);
060            }
061    
062            /**
063             * Get the number of colors to quantize to.
064             * @return the number of colors.
065             */
066            public int getNumColors() {
067                    return numColors;
068            }
069    
070            /**
071             * Set whether to use dithering or not. If not, the image is posterized.
072             * @param dither true to use dithering
073             */
074            public void setDither(boolean dither) {
075                    this.dither = dither;
076            }
077    
078            /**
079             * Return the dithering setting
080             * @return the current setting
081             */
082            public boolean getDither() {
083                    return dither;
084            }
085    
086            /**
087             * Set whether to use a serpentine pattern for return or not. This can reduce 'avalanche' artifacts in the output.
088             * @param serpentine true to use serpentine pattern
089             */
090            public void setSerpentine(boolean serpentine) {
091                    this.serpentine = serpentine;
092            }
093            
094            /**
095             * Return the serpentine setting
096             * @return the current setting
097             */
098            public boolean getSerpentine() {
099                    return serpentine;
100            }
101            
102            public void quantize(int[] inPixels, int[] outPixels, int width, int height, int numColors, boolean dither, boolean serpentine) {
103                    int count = width*height;
104                    Quantizer quantizer = new OctTreeQuantizer();
105                    quantizer.setup(numColors);
106                    quantizer.addPixels(inPixels, 0, count);
107                    int[] table =  quantizer.buildColorTable();
108    
109                    if (!dither) {
110                            for (int i = 0; i < count; i++)
111                                    outPixels[i] = table[quantizer.getIndexForColor(inPixels[i])];
112                    } else {
113                            int index = 0;
114                            for (int y = 0; y < height; y++) {
115                                    boolean reverse = serpentine && (y & 1) == 1;
116                                    int direction;
117                                    if (reverse) {
118                                            index = y*width+width-1;
119                                            direction = -1;
120                                    } else {
121                                            index = y*width;
122                                            direction = 1;
123                                    }
124                                    for (int x = 0; x < width; x++) {
125                                            int rgb1 = inPixels[index];
126                                            int rgb2 = table[quantizer.getIndexForColor(rgb1)];
127    
128                                            outPixels[index] = rgb2;
129    
130                                            int r1 = (rgb1 >> 16) & 0xff;
131                                            int g1 = (rgb1 >> 8) & 0xff;
132                                            int b1 = rgb1 & 0xff;
133    
134                                            int r2 = (rgb2 >> 16) & 0xff;
135                                            int g2 = (rgb2 >> 8) & 0xff;
136                                            int b2 = rgb2 & 0xff;
137    
138                                            int er = r1-r2;
139                                            int eg = g1-g2;
140                                            int eb = b1-b2;
141    
142                                            for (int i = -1; i <= 1; i++) {
143                                                    int iy = i+y;
144                                                    if (0 <= iy && iy < height) {
145                                                            for (int j = -1; j <= 1; j++) {
146                                                                    int jx = j+x;
147                                                                    if (0 <= jx && jx < width) {
148                                                                            int w;
149                                                                            if (reverse)
150                                                                                    w = matrix[(i+1)*3-j+1];
151                                                                            else
152                                                                                    w = matrix[(i+1)*3+j+1];
153                                                                            if (w != 0) {
154                                                                                    int k = reverse ? index - j : index + j;
155                                                                                    rgb1 = inPixels[k];
156                                                                                    r1 = (rgb1 >> 16) & 0xff;
157                                                                                    g1 = (rgb1 >> 8) & 0xff;
158                                                                                    b1 = rgb1 & 0xff;
159                                                                                    r1 += er * w/sum;
160                                                                                    g1 += eg * w/sum;
161                                                                                    b1 += eb * w/sum;
162                                                                                    inPixels[k] = (PixelUtils.clamp(r1) << 16) | (PixelUtils.clamp(g1) << 8) | PixelUtils.clamp(b1);
163                                                                            }
164                                                                    }
165                                                            }
166                                                    }
167                                            }
168                                            index += direction;
169                                    }
170                            }
171                    }
172            }
173    
174            protected int[] filterPixels( int width, int height, int[] inPixels, Rectangle transformedSpace ) {
175                    int[] outPixels = new int[width*height];
176                    
177                    quantize(inPixels, outPixels, width, height, numColors, dither, serpentine);
178    
179                    return outPixels;
180            }
181    
182            public String toString() {
183                    return "Colors/Quantize...";
184            }
185            
186            public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src);
187                    Object o;
188                    if((o=parameters.removeEL(KeyImpl.init("Serpentine")))!=null)setSerpentine(ImageFilterUtil.toBooleanValue(o,"Serpentine"));
189                    if((o=parameters.removeEL(KeyImpl.init("NumColors")))!=null)setNumColors(ImageFilterUtil.toIntValue(o,"NumColors"));
190                    if((o=parameters.removeEL(KeyImpl.init("Dither")))!=null)setDither(ImageFilterUtil.toBooleanValue(o,"Dither"));
191    
192                    // check for arguments not supported
193                    if(parameters.size()>0) {
194                            throw new FunctionException(ThreadLocalPageContext.get(), "ImageFilter", 3, "parameters", "the parameter"+(parameters.size()>1?"s":"")+" ["+List.arrayToList(parameters.keysAsString(),", ")+"] "+(parameters.size()>1?"are":"is")+" not allowed, only the following parameters are supported [Serpentine, NumColors, Dither]");
195                    }
196    
197                    return filter(src, dst);
198            }
199    }