001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.imagery; 003 004import java.awt.Rectangle; 005import java.awt.RenderingHints; 006import java.awt.geom.Point2D; 007import java.awt.geom.Rectangle2D; 008import java.awt.image.BufferedImage; 009import java.awt.image.BufferedImageOp; 010import java.awt.image.ColorModel; 011import java.awt.image.DataBuffer; 012import java.awt.image.DataBufferByte; 013import java.awt.image.IndexColorModel; 014import java.util.Objects; 015import java.util.Optional; 016import java.util.function.Consumer; 017 018import org.openstreetmap.josm.tools.Logging; 019 020/** 021 * Colorful filter. 022 * @since 11914 (extracted from ColorfulImageProcessor) 023 */ 024public class ColorfulFilter implements BufferedImageOp { 025 private static final double LUMINOSITY_RED = .21d; 026 private static final double LUMINOSITY_GREEN = .72d; 027 private static final double LUMINOSITY_BLUE = .07d; 028 private final double colorfulness; 029 030 /** 031 * Create a new colorful filter. 032 * @param colorfulness The colorfulness as defined in the {@link ColorfulImageProcessor} class. 033 */ 034 ColorfulFilter(double colorfulness) { 035 this.colorfulness = colorfulness; 036 } 037 038 @Override 039 public BufferedImage filter(BufferedImage src, BufferedImage dst) { 040 if (src.getWidth() == 0 || src.getHeight() == 0) { 041 return src; 042 } 043 044 int type = src.getType(); 045 046 switch (type) { 047 case BufferedImage.TYPE_BYTE_INDEXED: 048 case BufferedImage.TYPE_3BYTE_BGR: 049 case BufferedImage.TYPE_4BYTE_ABGR: 050 case BufferedImage.TYPE_4BYTE_ABGR_PRE: 051 case BufferedImage.TYPE_INT_ARGB: 052 case BufferedImage.TYPE_INT_ARGB_PRE: 053 054 BufferedImage dest = Optional.ofNullable(dst).orElseGet(() -> createCompatibleDestImage(src, null)); 055 056 if (type == BufferedImage.TYPE_BYTE_INDEXED) { 057 try { 058 return filterIndexed(src, dest); 059 } catch (IllegalArgumentException ex) { 060 Logging.warn(ex); 061 break; 062 } 063 } 064 065 DataBuffer srcBuffer = src.getRaster().getDataBuffer(); 066 DataBuffer destBuffer = dest.getRaster().getDataBuffer(); 067 if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) { 068 Logging.trace("Images do not use DataBufferByte. Filtering RGB values instead."); 069 break; 070 } 071 072 if (type != dest.getType()) { 073 Logging.trace("Src / Dest differ in type (" + type + '/' + dest.getType() + "). Filtering RGB values instead."); 074 break; 075 } 076 077 int redOffset; 078 int greenOffset; 079 int blueOffset; 080 int alphaOffset = 0; 081 switch (type) { 082 case BufferedImage.TYPE_3BYTE_BGR: 083 blueOffset = 0; 084 greenOffset = 1; 085 redOffset = 2; 086 break; 087 case BufferedImage.TYPE_4BYTE_ABGR: 088 case BufferedImage.TYPE_4BYTE_ABGR_PRE: 089 blueOffset = 1; 090 greenOffset = 2; 091 redOffset = 3; 092 break; 093 case BufferedImage.TYPE_INT_ARGB: 094 case BufferedImage.TYPE_INT_ARGB_PRE: 095 redOffset = 0; 096 greenOffset = 1; 097 blueOffset = 2; 098 alphaOffset = 3; 099 break; 100 default: 101 return doFilterRGB(src); 102 } 103 104 doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset, 105 alphaOffset, src.getAlphaRaster() != null); 106 return dest; 107 } 108 109 return doFilterRGB(src); 110 } 111 112 /** 113 * Fast alternative for indexed images: We can change the palette here. 114 * @param src The source image 115 * @param dest The image to copy the source to 116 * @return The image. 117 */ 118 private BufferedImage filterIndexed(BufferedImage src, BufferedImage dest) { 119 Objects.requireNonNull(dest, "dst needs to be non null"); 120 if (src.getType() != BufferedImage.TYPE_BYTE_INDEXED) { 121 throw new IllegalArgumentException("Source must be of type TYPE_BYTE_INDEXED"); 122 } 123 if (dest.getType() != BufferedImage.TYPE_BYTE_INDEXED) { 124 throw new IllegalArgumentException("Destination must be of type TYPE_BYTE_INDEXED"); 125 } 126 if (!(src.getColorModel() instanceof IndexColorModel)) { 127 throw new IllegalArgumentException("Expecting an IndexColorModel for a image of type TYPE_BYTE_INDEXED"); 128 } 129 src.copyData(dest.getRaster()); 130 131 IndexColorModel model = (IndexColorModel) src.getColorModel(); 132 int size = model.getMapSize(); 133 byte[] red = getIndexColorModelData(size, model::getReds); 134 byte[] green = getIndexColorModelData(size, model::getGreens); 135 byte[] blue = getIndexColorModelData(size, model::getBlues); 136 byte[] alphas = getIndexColorModelData(size, model::getAlphas); 137 138 for (int i = 0; i < size; i++) { 139 int r = red[i] & 0xff; 140 int g = green[i] & 0xff; 141 int b = blue[i] & 0xff; 142 double luminosity = r * LUMINOSITY_RED + g * LUMINOSITY_GREEN + b * LUMINOSITY_BLUE; 143 red[i] = mix(r, luminosity); 144 green[i] = mix(g, luminosity); 145 blue[i] = mix(b, luminosity); 146 } 147 148 IndexColorModel dstModel = new IndexColorModel(model.getPixelSize(), model.getMapSize(), red, green, blue, alphas); 149 return new BufferedImage(dstModel, dest.getRaster(), dest.isAlphaPremultiplied(), null); 150 } 151 152 private static byte[] getIndexColorModelData(int size, Consumer<byte[]> consumer) { 153 byte[] data = new byte[size]; 154 consumer.accept(data); 155 return data; 156 } 157 158 private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset, 159 int alphaOffset, boolean hasAlpha) { 160 byte[] srcPixels = src.getData(); 161 byte[] destPixels = dest.getData(); 162 if (srcPixels.length != destPixels.length) { 163 Logging.trace("Cannot apply color filter: Source/Dest lengths differ."); 164 return; 165 } 166 int entries = hasAlpha ? 4 : 3; 167 for (int i = 0; i < srcPixels.length; i += entries) { 168 int r = srcPixels[i + redOffset] & 0xff; 169 int g = srcPixels[i + greenOffset] & 0xff; 170 int b = srcPixels[i + blueOffset] & 0xff; 171 double luminosity = r * LUMINOSITY_RED + g * LUMINOSITY_GREEN + b * LUMINOSITY_BLUE; 172 destPixels[i + redOffset] = mix(r, luminosity); 173 destPixels[i + greenOffset] = mix(g, luminosity); 174 destPixels[i + blueOffset] = mix(b, luminosity); 175 if (hasAlpha) { 176 destPixels[i + alphaOffset] = srcPixels[i + alphaOffset]; 177 } 178 } 179 } 180 181 private BufferedImage doFilterRGB(BufferedImage src) { 182 int w = src.getWidth(); 183 int h = src.getHeight(); 184 185 int[] arr = src.getRGB(0, 0, w, h, null, 0, w); 186 int argb, a, r, g, b; 187 double luminosity; 188 189 for (int i = 0; i < arr.length; i++) { 190 argb = arr[i]; 191 a = (argb >> 24) & 0xff; 192 r = (argb >> 16) & 0xff; 193 g = (argb >> 8) & 0xff; 194 b = argb & 0xff; 195 luminosity = r * LUMINOSITY_RED + g * LUMINOSITY_GREEN + b * LUMINOSITY_BLUE; 196 r = mixInt(r, luminosity); 197 g = mixInt(g, luminosity); 198 b = mixInt(b, luminosity); 199 argb = a; 200 argb = (argb << 8) + r; 201 argb = (argb << 8) + g; 202 argb = (argb << 8) + b; 203 arr[i] = argb; 204 } 205 206 BufferedImage dest = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); 207 dest.setRGB(0, 0, w, h, arr, 0, w); 208 return dest; 209 } 210 211 private int mixInt(int color, double luminosity) { 212 int val = (int) (colorfulness * color + (1 - colorfulness) * luminosity); 213 if (val < 0) { 214 return 0; 215 } else if (val > 0xff) { 216 return 0xff; 217 } else { 218 return val; 219 } 220 } 221 222 private byte mix(int color, double luminosity) { 223 return (byte) mixInt(color, luminosity); 224 } 225 226 @Override 227 public Rectangle2D getBounds2D(BufferedImage src) { 228 return new Rectangle(src.getWidth(), src.getHeight()); 229 } 230 231 @Override 232 public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { 233 return new BufferedImage(src.getWidth(), src.getHeight(), src.getType()); 234 } 235 236 @Override 237 public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { 238 return (Point2D) srcPt.clone(); 239 } 240 241 @Override 242 public RenderingHints getRenderingHints() { 243 return null; 244 } 245}