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