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}