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;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.gui.layer.ImageProcessor;
016
017/**
018 * Adds or removes the colorfulness of the image.
019 *
020 * @author Michael Zangl
021 * @since 10547
022 */
023public class ColorfulImageProcessor implements ImageProcessor {
024    private ColorfulFilter op;
025    private double colorfulness = 1;
026
027    /**
028     * Gets the colorfulness value.
029     * @return The value
030     */
031    public double getColorfulness() {
032        return colorfulness;
033    }
034
035    /**
036     * Sets the colorfulness value. Clamps it to 0+
037     * @param colorfulness The value
038     */
039    public void setColorfulness(double colorfulness) {
040        if (colorfulness < 0) {
041            this.colorfulness = 0;
042        } else {
043            this.colorfulness = colorfulness;
044        }
045
046        if (this.colorfulness < .95 || this.colorfulness > 1.05) {
047            op = new ColorfulFilter(this.colorfulness);
048        } else {
049            op = null;
050        }
051    }
052
053    @Override
054    public BufferedImage process(BufferedImage image) {
055        if (op != null) {
056            return op.filter(image, null);
057        } else {
058            return image;
059        }
060    }
061
062    @Override
063    public String toString() {
064        return "ColorfulImageProcessor [colorfulness=" + colorfulness + ']';
065    }
066
067    static class ColorfulFilter implements BufferedImageOp {
068        private final double colorfulness;
069
070        /**
071         * Create a new colorful filter.
072         * @param colorfulness The colorfulness as defined in the {@link ColorfulImageProcessor} class.
073         */
074        ColorfulFilter(double colorfulness) {
075            this.colorfulness = colorfulness;
076        }
077
078        @Override
079        public BufferedImage filter(BufferedImage src, BufferedImage dst) {
080            if (src.getWidth() == 0 || src.getHeight() == 0) {
081                return src;
082            }
083
084            BufferedImage dest = dst;
085            if (dest == null) {
086                dest = createCompatibleDestImage(src, null);
087            }
088            DataBuffer srcBuffer = src.getRaster().getDataBuffer();
089            DataBuffer destBuffer = dest.getRaster().getDataBuffer();
090            if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) {
091                Main.trace("Cannot apply color filter: Images do not use DataBufferByte.");
092                return src;
093            }
094
095            int type = src.getType();
096            if (type != dest.getType()) {
097                Main.trace("Cannot apply color filter: Src / Dest differ in type (" + type + '/' + dest.getType() + ')');
098                return src;
099            }
100            int redOffset;
101            int greenOffset;
102            int blueOffset;
103            int alphaOffset = 0;
104            switch (type) {
105            case BufferedImage.TYPE_3BYTE_BGR:
106                blueOffset = 0;
107                greenOffset = 1;
108                redOffset = 2;
109                break;
110            case BufferedImage.TYPE_4BYTE_ABGR:
111            case BufferedImage.TYPE_4BYTE_ABGR_PRE:
112                blueOffset = 1;
113                greenOffset = 2;
114                redOffset = 3;
115                break;
116            case BufferedImage.TYPE_INT_ARGB:
117            case BufferedImage.TYPE_INT_ARGB_PRE:
118                redOffset = 0;
119                greenOffset = 1;
120                blueOffset = 2;
121                alphaOffset = 3;
122                break;
123            default:
124                Main.trace("Cannot apply color filter: Source image is of wrong type (" + type + ").");
125                return src;
126            }
127            doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset,
128                    alphaOffset, src.getAlphaRaster() != null);
129            return dest;
130        }
131
132        private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset,
133                int alphaOffset, boolean hasAlpha) {
134            byte[] srcPixels = src.getData();
135            byte[] destPixels = dest.getData();
136            if (srcPixels.length != destPixels.length) {
137                Main.trace("Cannot apply color filter: Source/Dest lengths differ.");
138                return;
139            }
140            int entries = hasAlpha ? 4 : 3;
141            for (int i = 0; i < srcPixels.length; i += entries) {
142                int r = srcPixels[i + redOffset] & 0xff;
143                int g = srcPixels[i + greenOffset] & 0xff;
144                int b = srcPixels[i + blueOffset] & 0xff;
145                double luminosity = r * .21d + g * .72d + b * .07d;
146                destPixels[i + redOffset] = mix(r, luminosity);
147                destPixels[i + greenOffset] = mix(g, luminosity);
148                destPixels[i + blueOffset] = mix(b, luminosity);
149                if (hasAlpha) {
150                    destPixels[i + alphaOffset] = srcPixels[i + alphaOffset];
151                }
152            }
153        }
154
155        private byte mix(int color, double luminosity) {
156            int val = (int) (colorfulness * color + (1 - colorfulness) * luminosity);
157            if (val < 0) {
158                return 0;
159            } else if (val > 0xff) {
160                return (byte) 0xff;
161            } else {
162                return (byte) val;
163            }
164        }
165
166        @Override
167        public Rectangle2D getBounds2D(BufferedImage src) {
168            return new Rectangle(src.getWidth(), src.getHeight());
169        }
170
171        @Override
172        public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
173            return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
174        }
175
176        @Override
177        public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
178            return (Point2D) srcPt.clone();
179        }
180
181        @Override
182        public RenderingHints getRenderingHints() {
183            return null;
184        }
185    }
186}