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}