/* * Copyright 2017 Laszlo Balazs-Csiki * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU * General Public License, version 3 as published by the Free * Software Foundation. * * Pixelitor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Pixelitor. If not, see <http://www.gnu.org/licenses/>. */ package pixelitor.filters.lookup; import com.jhlabs.image.PixelUtils; import pixelitor.filters.FilterWithParametrizedGUI; import pixelitor.filters.gui.IntChoiceParam; import pixelitor.filters.gui.IntChoiceParam.Value; import pixelitor.filters.gui.ParamSet; import pixelitor.filters.gui.RangeParam; import pixelitor.filters.gui.RangeWithColorsParam; import pixelitor.filters.gui.ShowOriginal; import pixelitor.filters.levels.RGBLookup; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ShortLookupTable; import static java.awt.Color.BLUE; import static java.awt.Color.CYAN; import static java.awt.Color.GREEN; import static java.awt.Color.MAGENTA; import static java.awt.Color.RED; import static java.awt.Color.YELLOW; /** * Color balance filter */ public class ColorBalance extends FilterWithParametrizedGUI { private static final int EVERYTHING = 0; private static final int SHADOWS = 1; private static final int MIDTONES = 2; private static final int HIGHLIGHTS = 4; private final IntChoiceParam affect = new IntChoiceParam("Affect", new Value[]{ new Value("Everything", EVERYTHING), new Value("Shadows", SHADOWS), new Value("Midtones", MIDTONES), new Value("Highlights", HIGHLIGHTS), }); private final RangeParam cyanRed = new RangeWithColorsParam(CYAN, RED, "Cyan-Red", -100, 0, 100); private final RangeParam magentaGreen = new RangeWithColorsParam(MAGENTA, GREEN, "Magenta-Green", -100, 0, 100); private final RangeParam yellowBlue = new RangeWithColorsParam(YELLOW, BLUE, "Yellow-Blue", -100, 0, 100); public ColorBalance() { super(ShowOriginal.YES); setParamSet(new ParamSet( affect, cyanRed, magentaGreen, yellowBlue )); } @Override public BufferedImage doTransform(BufferedImage src, BufferedImage dest) { float cr = cyanRed.getValueAsFloat(); float mg = magentaGreen.getValueAsFloat(); float yb = yellowBlue.getValueAsFloat(); if ((cr == 0) && (mg == 0) && (yb == 0)) { return src; } RGBLookup rgbLookup = new LookupHelper(cr, mg, yb, affect.getValue()) .getLookup(); BufferedImageOp filterOp = new FastLookupOp( (ShortLookupTable) rgbLookup.getLookupOp()); filterOp.filter(src, dest); return dest; } private static class LookupHelper { private final float cyanRed; private final float magentaGreen; private final float yellowBlue; private final int affect; private static final int LUT_TABLE_SIZE = 256; final short[] redMapping = new short[LUT_TABLE_SIZE]; final short[] greenMapping = new short[LUT_TABLE_SIZE]; final short[] blueMapping = new short[LUT_TABLE_SIZE]; public LookupHelper(float cyanRed, float magentaGreen, float yellowBlue, int affect) { this.cyanRed = cyanRed; this.magentaGreen = magentaGreen; this.yellowBlue = yellowBlue; this.affect = affect; } public RGBLookup getLookup() { if (affect == EVERYTHING) { setupMappingsForTotallyAffected(); } else { setupMappingsForPartiallyAffected(); } return new RGBLookup(redMapping, greenMapping, blueMapping); } private void setupMappingsForTotallyAffected() { for (short i = 0; i < LUT_TABLE_SIZE; i++) { short r = (short) (i + cyanRed - (magentaGreen / 2) - (yellowBlue / 2)); r = PixelUtils.clamp(r); redMapping[i] = r; short g = (short) (i + magentaGreen - (cyanRed / 2) - (yellowBlue / 2)); g = PixelUtils.clamp(g); greenMapping[i] = g; short b = (short) (i + yellowBlue - (magentaGreen / 2) - (cyanRed / 2)); b = PixelUtils.clamp(b); blueMapping[i] = b; } } private void setupMappingsForPartiallyAffected() { float[] affectFactor = calculateAffectFactor(affect); for (short i = 0; i < LUT_TABLE_SIZE; i++) { short r = (short) (i + affectFactor[i] * (cyanRed - (magentaGreen / 2) - (yellowBlue / 2))); r = PixelUtils.clamp(r); redMapping[i] = r; short g = (short) (i + affectFactor[i] * (magentaGreen - (cyanRed / 2) - (yellowBlue / 2))); g = PixelUtils.clamp(g); greenMapping[i] = g; short b = (short) (i + affectFactor[i] * (yellowBlue - (magentaGreen / 2) - (cyanRed / 2))); b = PixelUtils.clamp(b); blueMapping[i] = b; } } private static float[] calculateAffectFactor(int affect) { float[] affectFactor = new float[LUT_TABLE_SIZE]; for (int i = 0; i < LUT_TABLE_SIZE; i++) { switch (affect) { case SHADOWS: affectFactor[i] = 1.0f - (1.0f * i) / LUT_TABLE_SIZE; break; case HIGHLIGHTS: affectFactor[i] = (1.0f * i) / LUT_TABLE_SIZE; break; case MIDTONES: int halfSize = LUT_TABLE_SIZE / 2; if (i <= halfSize) { affectFactor[i] = (2.0f * i) / LUT_TABLE_SIZE; } else { affectFactor[i] = 2 * (1.0f - (1.0f * i) / LUT_TABLE_SIZE); } break; case EVERYTHING: // should not get here affectFactor[i] = 1.0f; break; } } return affectFactor; } } @Override public boolean supportsGray() { return false; } }