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 || src.getType() == BufferedImage.TYPE_CUSTOM) { 041 return src; 042 } 043 044 BufferedImage dest = Optional.ofNullable(dst).orElseGet(() -> createCompatibleDestImage(src, null)); 045 int type = src.getType(); 046 047 if (type == BufferedImage.TYPE_BYTE_INDEXED) { 048 return filterIndexed(src, dest); 049 } 050 051 DataBuffer srcBuffer = src.getRaster().getDataBuffer(); 052 DataBuffer destBuffer = dest.getRaster().getDataBuffer(); 053 if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) { 054 Logging.trace("Cannot apply color filter: Images do not use DataBufferByte."); 055 return src; 056 } 057 058 if (type != dest.getType()) { 059 Logging.trace("Cannot apply color filter: Src / Dest differ in type (" + type + '/' + dest.getType() + ')'); 060 return src; 061 } 062 int redOffset; 063 int greenOffset; 064 int blueOffset; 065 int alphaOffset = 0; 066 switch (type) { 067 case BufferedImage.TYPE_3BYTE_BGR: 068 blueOffset = 0; 069 greenOffset = 1; 070 redOffset = 2; 071 break; 072 case BufferedImage.TYPE_4BYTE_ABGR: 073 case BufferedImage.TYPE_4BYTE_ABGR_PRE: 074 blueOffset = 1; 075 greenOffset = 2; 076 redOffset = 3; 077 break; 078 case BufferedImage.TYPE_INT_ARGB: 079 case BufferedImage.TYPE_INT_ARGB_PRE: 080 redOffset = 0; 081 greenOffset = 1; 082 blueOffset = 2; 083 alphaOffset = 3; 084 break; 085 default: 086 Logging.trace("Cannot apply color filter: Source image is of wrong type (" + type + ")."); 087 return src; 088 } 089 doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset, 090 alphaOffset, src.getAlphaRaster() != null); 091 return dest; 092 } 093 094 /** 095 * Fast alternative for indexed images: We can change the palette here. 096 * @param src The source image 097 * @param dest The image to copy the source to 098 * @return The image. 099 */ 100 private BufferedImage filterIndexed(BufferedImage src, BufferedImage dest) { 101 Objects.requireNonNull(dest, "dst needs to be non null"); 102 if (src.getType() != BufferedImage.TYPE_BYTE_INDEXED) { 103 throw new IllegalArgumentException("Source must be of type TYPE_BYTE_INDEXED"); 104 } 105 if (dest.getType() != BufferedImage.TYPE_BYTE_INDEXED) { 106 throw new IllegalArgumentException("Destination must be of type TYPE_BYTE_INDEXED"); 107 } 108 if (!(src.getColorModel() instanceof IndexColorModel)) { 109 throw new IllegalArgumentException("Expecting an IndexColorModel for a image of type TYPE_BYTE_INDEXED"); 110 } 111 src.copyData(dest.getRaster()); 112 113 IndexColorModel model = (IndexColorModel) src.getColorModel(); 114 int size = model.getMapSize(); 115 byte[] red = getIndexColorModelData(size, model::getReds); 116 byte[] green = getIndexColorModelData(size, model::getGreens); 117 byte[] blue = getIndexColorModelData(size, model::getBlues); 118 byte[] alphas = getIndexColorModelData(size, model::getAlphas); 119 120 for (int i = 0; i < size; i++) { 121 int r = red[i] & 0xff; 122 int g = green[i] & 0xff; 123 int b = blue[i] & 0xff; 124 double luminosity = r * LUMINOSITY_RED + g * LUMINOSITY_GREEN + b * LUMINOSITY_BLUE; 125 red[i] = mix(r, luminosity); 126 green[i] = mix(g, luminosity); 127 blue[i] = mix(b, luminosity); 128 } 129 130 IndexColorModel dstModel = new IndexColorModel(model.getPixelSize(), model.getMapSize(), red, green, blue, alphas); 131 return new BufferedImage(dstModel, dest.getRaster(), dest.isAlphaPremultiplied(), null); 132 } 133 134 private static byte[] getIndexColorModelData(int size, Consumer<byte[]> consumer) { 135 byte[] data = new byte[size]; 136 consumer.accept(data); 137 return data; 138 } 139 140 private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset, 141 int alphaOffset, boolean hasAlpha) { 142 byte[] srcPixels = src.getData(); 143 byte[] destPixels = dest.getData(); 144 if (srcPixels.length != destPixels.length) { 145 Logging.trace("Cannot apply color filter: Source/Dest lengths differ."); 146 return; 147 } 148 int entries = hasAlpha ? 4 : 3; 149 for (int i = 0; i < srcPixels.length; i += entries) { 150 int r = srcPixels[i + redOffset] & 0xff; 151 int g = srcPixels[i + greenOffset] & 0xff; 152 int b = srcPixels[i + blueOffset] & 0xff; 153 double luminosity = r * LUMINOSITY_RED + g * LUMINOSITY_GREEN + b * LUMINOSITY_BLUE; 154 destPixels[i + redOffset] = mix(r, luminosity); 155 destPixels[i + greenOffset] = mix(g, luminosity); 156 destPixels[i + blueOffset] = mix(b, luminosity); 157 if (hasAlpha) { 158 destPixels[i + alphaOffset] = srcPixels[i + alphaOffset]; 159 } 160 } 161 } 162 163 private byte mix(int color, double luminosity) { 164 int val = (int) (colorfulness * color + (1 - colorfulness) * luminosity); 165 if (val < 0) { 166 return 0; 167 } else if (val > 0xff) { 168 return (byte) 0xff; 169 } else { 170 return (byte) val; 171 } 172 } 173 174 @Override 175 public Rectangle2D getBounds2D(BufferedImage src) { 176 return new Rectangle(src.getWidth(), src.getHeight()); 177 } 178 179 @Override 180 public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { 181 return new BufferedImage(src.getWidth(), src.getHeight(), src.getType()); 182 } 183 184 @Override 185 public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { 186 return (Point2D) srcPt.clone(); 187 } 188 189 @Override 190 public RenderingHints getRenderingHints() { 191 return null; 192 } 193}