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 }