All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.poi.sl.draw.DrawPaint Maven / Gradle / Ivy

There is a newer version: 5.2.5
Show newest version
/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
==================================================================== */

package org.apache.poi.sl.draw;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.MultipleGradientPaint.ColorSpaceType;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

import org.apache.poi.sl.usermodel.ColorStyle;
import org.apache.poi.sl.usermodel.PaintStyle;
import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint;
import org.apache.poi.sl.usermodel.PaintStyle.SolidPaint;
import org.apache.poi.sl.usermodel.PaintStyle.TexturePaint;
import org.apache.poi.sl.usermodel.PlaceableShape;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;


/**
 * This class handles color transformations.
 * 
 * @see HSL code taken from Java Tips Weblog
 */
public class DrawPaint {
    // HSL code is public domain - see https://tips4java.wordpress.com/contact-us/
    
    private static final POILogger LOG = POILogFactory.getLogger(DrawPaint.class);

    private static final Color TRANSPARENT = new Color(1f,1f,1f,0f);
    
    protected PlaceableShape shape;
    
    public DrawPaint(PlaceableShape shape) {
        this.shape = shape;
    }

    private static class SimpleSolidPaint implements SolidPaint {
        private final ColorStyle solidColor;

        SimpleSolidPaint(final Color color) {
            if (color == null) {
                throw new NullPointerException("Color needs to be specified");
            }
            this.solidColor = new ColorStyle(){
                    public Color getColor() {
                        return new Color(color.getRed(), color.getGreen(), color.getBlue());
                    }
                    public int getAlpha() { return (int)Math.round(color.getAlpha()*100000./255.); }
                    public int getHueOff() { return -1; }
                    public int getHueMod() { return -1; }
                    public int getSatOff() { return -1; }
                    public int getSatMod() { return -1; }
                    public int getLumOff() { return -1; }
                    public int getLumMod() { return -1; }
                    public int getShade() { return -1; }
                    public int getTint() { return -1; }
                };
        }

        SimpleSolidPaint(ColorStyle color) {
            if (color == null) {
                throw new NullPointerException("Color needs to be specified");
            }
            this.solidColor = color;
        }
        
        public ColorStyle getSolidColor() {
            return solidColor;
        }
    }
    
    public static SolidPaint createSolidPaint(final Color color) {
        return (color == null) ? null : new SimpleSolidPaint(color);
    }
    
    public static SolidPaint createSolidPaint(final ColorStyle color) {
        return (color == null) ? null : new SimpleSolidPaint(color);
    }
    
    public Paint getPaint(Graphics2D graphics, PaintStyle paint) {
        if (paint instanceof SolidPaint) {
            return getSolidPaint((SolidPaint)paint, graphics);
        } else if (paint instanceof GradientPaint) {
            return getGradientPaint((GradientPaint)paint, graphics);
        } else if (paint instanceof TexturePaint) {
            return getTexturePaint((TexturePaint)paint, graphics);
        }
        return null;
    }
    
    protected Paint getSolidPaint(SolidPaint fill, Graphics2D graphics) {
        return applyColorTransform(fill.getSolidColor());
    }

    protected Paint getGradientPaint(GradientPaint fill, Graphics2D graphics) {
        switch (fill.getGradientType()) {
        case linear:
            return createLinearGradientPaint(fill, graphics);
        case circular:
            return createRadialGradientPaint(fill, graphics);
        case shape:
            return createPathGradientPaint(fill, graphics);
        default:
            throw new UnsupportedOperationException("gradient fill of type "+fill+" not supported.");
        }
    }

    protected Paint getTexturePaint(TexturePaint fill, Graphics2D graphics) {
        InputStream is = fill.getImageData();
        if (is == null) return null;
        assert(graphics != null);
        
        ImageRenderer renderer = DrawPictureShape.getImageRenderer(graphics, fill.getContentType());

        try {
            try {
                renderer.loadImage(is, fill.getContentType());
            } finally {
                is.close();
            }
        } catch (IOException e) {
            LOG.log(POILogger.ERROR, "Can't load image data - using transparent color", e);
            return null;
        }

        int alpha = fill.getAlpha();
        if (0 <= alpha && alpha < 100000) {
            renderer.setAlpha(alpha/100000.f);
        }
        
        Rectangle2D textAnchor = shape.getAnchor();
        BufferedImage image;
        if ("image/x-wmf".equals(fill.getContentType())) {
            // don't rely on wmf dimensions, use dimension of anchor
            // TODO: check pixels vs. points for image dimension 
            image = renderer.getImage(new Dimension((int)textAnchor.getWidth(), (int)textAnchor.getHeight()));
        } else {
            image = renderer.getImage();
        }

        if(image == null) {
            LOG.log(POILogger.ERROR, "Can't load image data");
            return null;
        }
        Paint paint = new java.awt.TexturePaint(image, textAnchor);

        return paint;
    }
    
    /**
     * Convert color transformations in {@link ColorStyle} to a {@link Color} instance
     * 
     * @see Using Office Open XML to Customize Document Formatting in the 2007 Office System
     * @see saturation modulation (satMod)
     * @see Office Open XML satMod results in more than 100% saturation
     */
    public static Color applyColorTransform(ColorStyle color){
        // TODO: The colors don't match 100% the results of Powerpoint, maybe because we still
        // operate in sRGB and not scRGB ... work in progress ...
        if (color == null || color.getColor() == null) {
            return TRANSPARENT;
        }
        
        Color result = color.getColor();

        double alpha = getAlpha(result, color);
        double hsl[] = RGB2HSL(result); // values are in the range [0..100] (usually ...)
        applyHslModOff(hsl, 0, color.getHueMod(), color.getHueOff());
        applyHslModOff(hsl, 1, color.getSatMod(), color.getSatOff());
        applyHslModOff(hsl, 2, color.getLumMod(), color.getLumOff());
        applyShade(hsl, color);
        applyTint(hsl, color);

        result = HSL2RGB(hsl[0], hsl[1], hsl[2], alpha);
        
        return result;
    }

    private static double getAlpha(Color c, ColorStyle fc) {
        double alpha = c.getAlpha()/255d;
        int fcAlpha = fc.getAlpha();
        if (fcAlpha != -1) {
            alpha *= fcAlpha/100000d;
        }
        return Math.min(1, Math.max(0, alpha));
    }
    
    /**
     * Apply the modulation and offset adjustments to the given HSL part
     * 
     * Example for lumMod/lumOff:
     * The lumMod value is the percent luminance. A lumMod value of "60000",
     * is 60% of the luminance of the original color.
     * When the color is a shade of the original theme color, the lumMod
     * attribute is the only one of the tags shown here that appears.
     * The  tag appears after the  tag when the color is a
     * tint of the original. The lumOff value always equals 1-lumMod, which is used in the tint calculation
     * 
     * Despite having different ways to display the tint and shade percentages,
     * all of the programs use the same method to calculate the resulting color.
     * Convert the original RGB value to HSL ... and then adjust the luminance (L)
     * with one of the following equations before converting the HSL value back to RGB.
     * (The % tint in the following equations refers to the tint, themetint, themeshade,
     * or lumMod values, as applicable.)
     * 
     * @param hsl the hsl values
     * @param hslPart the hsl part to modify [0..2]
     * @param mod the modulation adjustment
     * @param off the offset adjustment
     * @return the modified hsl value
     * 
     */
    private static void applyHslModOff(double hsl[], int hslPart, int mod, int off) {
        if (mod == -1) mod = 100000;
        if (off == -1) off = 0;
        if (!(mod == 100000 && off == 0)) {
            double fOff = off / 1000d;
            double fMod = mod / 100000d;
            hsl[hslPart] = hsl[hslPart]*fMod+fOff;
        }
    }
    
    /**
     * Apply the shade
     * 
     * For a shade, the equation is luminance * %tint.
     */
    private static void applyShade(double hsl[], ColorStyle fc) {
        int shade = fc.getShade();
        if (shade == -1) return;
        
        double fshade = shade / 100000.d;
        
        hsl[2] *= fshade;
    }

    /**
     * Apply the tint
     * 
     * For a tint, the equation is luminance * %tint + (1-%tint).
     * (Note that 1-%tint is equal to the lumOff value in DrawingML.)
     */
    private static void applyTint(double hsl[], ColorStyle fc) {
        int tint = fc.getTint();
        if (tint == -1) return;
        
        double ftint = tint / 100000.f;

        hsl[2] = hsl[2] * ftint + (100 - ftint*100.);
    }
    

    protected Paint createLinearGradientPaint(GradientPaint fill, Graphics2D graphics) {
        double angle = fill.getGradientAngle();
        Rectangle2D anchor = DrawShape.getAnchor(graphics, shape);

        AffineTransform at = AffineTransform.getRotateInstance(
            Math.toRadians(angle),
            anchor.getX() + anchor.getWidth() / 2,
            anchor.getY() + anchor.getHeight() / 2);

        double diagonal = Math.sqrt(anchor.getHeight() * anchor.getHeight() + anchor.getWidth() * anchor.getWidth());
        Point2D p1 = new Point2D.Double(anchor.getX() + anchor.getWidth() / 2 - diagonal / 2,
                anchor.getY() + anchor.getHeight() / 2);
        p1 = at.transform(p1, null);

        Point2D p2 = new Point2D.Double(anchor.getX() + anchor.getWidth(), anchor.getY() + anchor.getHeight() / 2);
        p2 = at.transform(p2, null);
        
        snapToAnchor(p1, anchor);
        snapToAnchor(p2, anchor);

        if (p1.equals(p2)) {
            // gradient paint on the same point throws an exception ... and doesn't make sense
            return null;
        }

        float[] fractions = fill.getGradientFractions();
        Color[] colors = new Color[fractions.length];
        
        int i = 0;
        for (ColorStyle fc : fill.getGradientColors()) {
            // if fc is null, use transparent color to get color of background
            colors[i++] = (fc == null) ? TRANSPARENT : applyColorTransform(fc);
        }

        AffineTransform grAt  = new AffineTransform();
        if(fill.isRotatedWithShape()) {
            double rotation = shape.getRotation();
            if (rotation != 0.) {
                double centerX = anchor.getX() + anchor.getWidth() / 2;
                double centerY = anchor.getY() + anchor.getHeight() / 2;

                grAt.translate(centerX, centerY);
                grAt.rotate(Math.toRadians(-rotation));
                grAt.translate(-centerX, -centerY);
            }
        }

        return new LinearGradientPaint
            (p1, p2, fractions, colors, CycleMethod.NO_CYCLE, ColorSpaceType.SRGB, grAt);
    }

    protected Paint createRadialGradientPaint(GradientPaint fill, Graphics2D graphics) {
        Rectangle2D anchor = DrawShape.getAnchor(graphics, shape);

        Point2D pCenter = new Point2D.Double(anchor.getX() + anchor.getWidth()/2,
                anchor.getY() + anchor.getHeight()/2);

        float radius = (float)Math.max(anchor.getWidth(), anchor.getHeight());

        float[] fractions = fill.getGradientFractions();
        Color[] colors = new Color[fractions.length];

        int i=0;
        for (ColorStyle fc : fill.getGradientColors()) {
            colors[i++] = applyColorTransform(fc);
        }

        return new RadialGradientPaint(pCenter, radius, fractions, colors);
    }

    protected Paint createPathGradientPaint(GradientPaint fill, Graphics2D graphics) {
        // currently we ignore an eventually center setting
        
        float[] fractions = fill.getGradientFractions();
        Color[] colors = new Color[fractions.length];

        int i=0;
        for (ColorStyle fc : fill.getGradientColors()) {
            colors[i++] = applyColorTransform(fc);
        }

        return new PathGradientPaint(colors, fractions);
    }
    
    protected void snapToAnchor(Point2D p, Rectangle2D anchor) {
        if (p.getX() < anchor.getX()) {
            p.setLocation(anchor.getX(), p.getY());
        } else if (p.getX() > (anchor.getX() + anchor.getWidth())) {
            p.setLocation(anchor.getX() + anchor.getWidth(), p.getY());
        }

        if (p.getY() < anchor.getY()) {
            p.setLocation(p.getX(), anchor.getY());
        } else if (p.getY() > (anchor.getY() + anchor.getHeight())) {
            p.setLocation(p.getX(), anchor.getY() + anchor.getHeight());
        }
    }

    /**
     *  Convert HSL values to a RGB Color.
     *
     *  @param h Hue is specified as degrees in the range 0 - 360.
     *  @param s Saturation is specified as a percentage in the range 1 - 100.
     *  @param l Luminance is specified as a percentage in the range 1 - 100.
     *  @param alpha  the alpha value between 0 - 1
     *
     *  @return the RGB Color object
     */
    public static Color HSL2RGB(double h, double s, double l, double alpha) {
        // we clamp the values, as it possible to come up with more than 100% sat/lum
        // (see links in applyColorTransform() for more info)
        s = Math.max(0, Math.min(100, s));
        l = Math.max(0, Math.min(100, l));

        if (alpha <0.0f || alpha > 1.0f) {
            String message = "Color parameter outside of expected range - Alpha: " + alpha;
            throw new IllegalArgumentException( message );
        }

        //  Formula needs all values between 0 - 1.

        h = h % 360.0f;
        h /= 360f;
        s /= 100f;
        l /= 100f;

        double q = (l < 0.5d)
            ? l * (1d + s)
            : (l + s) - (s * l);

        double p = 2d * l - q;

        double r = Math.max(0, HUE2RGB(p, q, h + (1.0d / 3.0d)));
        double g = Math.max(0, HUE2RGB(p, q, h));
        double b = Math.max(0, HUE2RGB(p, q, h - (1.0d / 3.0d)));

        r = Math.min(r, 1.0d);
        g = Math.min(g, 1.0d);
        b = Math.min(b, 1.0d);

        return new Color((float)r, (float)g, (float)b, (float)alpha);
    }

    private static double HUE2RGB(double p, double q, double h) {
        if (h < 0d) h += 1d;

        if (h > 1d) h -= 1d;

        if (6d * h < 1d) {
            return p + ((q - p) * 6d * h);
        }

        if (2d * h < 1d) {
            return q;
        }

        if (3d * h < 2d) {
            return p + ( (q - p) * 6d * ((2.0d / 3.0d) - h) );
        }

        return p;
    }


    /**
     *  Convert a RGB Color to it corresponding HSL values.
     *
     *  @return an array containing the 3 HSL values.
     */
    private static double[] RGB2HSL(Color color)
    {
        //  Get RGB values in the range 0 - 1

        float[] rgb = color.getRGBColorComponents( null );
        double r = rgb[0];
        double g = rgb[1];
        double b = rgb[2];

        //  Minimum and Maximum RGB values are used in the HSL calculations

        double min = Math.min(r, Math.min(g, b));
        double max = Math.max(r, Math.max(g, b));

        //  Calculate the Hue

        double h = 0;

        if (max == min) {
            h = 0;
        } else if (max == r) {
            h = ((60d * (g - b) / (max - min)) + 360d) % 360d;
        } else if (max == g) {
            h = (60d * (b - r) / (max - min)) + 120d;
        } else if (max == b) {
            h = (60d * (r - g) / (max - min)) + 240d;
        }

        //  Calculate the Luminance

        double l = (max + min) / 2d;

        //  Calculate the Saturation

        double s = 0;

        if (max == min) {
            s = 0;
        } else if (l <= .5d) {
            s = (max - min) / (max + min);
        } else {
            s = (max - min) / (2d - max - min);
        }

        return new double[] {h, s * 100, l * 100};
    }
    
    /**
     * Convert sRGB float component [0..1] from sRGB to linear RGB [0..100000]
     * 
     * @see Color#getRGBColorComponents(float[])
     */
    public static int srgb2lin(float sRGB) {
        // scRGB has a linear gamma of 1.0, scale the AWT-Color which is in sRGB to linear RGB
        // see https://en.wikipedia.org/wiki/SRGB (the reverse transformation)
        if (sRGB <= 0.04045d) {
            return (int)Math.rint(100000d * sRGB / 12.92d);
        } else {
            return (int)Math.rint(100000d * Math.pow((sRGB + 0.055d) / 1.055d, 2.4d));
        }
    }
    
    /**
     * Convert linear RGB [0..100000] to sRGB float component [0..1]
     * 
     * @see Color#getRGBColorComponents(float[])
     */
    public static float lin2srgb(int linRGB) {
        // color in percentage is in linear RGB color space, i.e. needs to be gamma corrected for AWT color
        // see https://en.wikipedia.org/wiki/SRGB (The forward transformation)
        if (linRGB <= 0.0031308d) {
            return (float)(linRGB / 100000d * 12.92d);
        } else {
            return (float)(1.055d * Math.pow(linRGB / 100000d, 1.0d/2.4d) - 0.055d);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy