blob: 16562b82a06b0e38f3adddb14fed59762390f955 [file] [log] [blame]
/* ====================================================================
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 static org.apache.poi.sl.usermodel.PaintStyle.TRANSPARENT_PAINT;
import java.awt.*;
import java.awt.MultipleGradientPaint.ColorSpaceType;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.geom.*;
import java.io.IOException;
import java.io.InputStream;
import org.apache.poi.sl.usermodel.*;
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.util.POILogFactory;
import org.apache.poi.util.POILogger;
/**
* This class handles color transformations
*
* @see HSL code taken from <a href="https://tips4java.wordpress.com/2009/07/05/hsl-color/">Java Tips Weblog</a>
*/
public class DrawPaint {
// HSL code is public domain - see https://tips4java.wordpress.com/contact-us/
private final static POILogger LOG = POILogFactory.getLogger(DrawPaint.class);
protected PlaceableShape shape;
public DrawPaint(PlaceableShape shape) {
this.shape = shape;
}
public static SolidPaint createSolidPaint(final Color color) {
return new SolidPaint() {
public ColorStyle getSolidColor() {
return new ColorStyle(){
public Color getColor() { return color; }
public int getAlpha() { return -1; }
public int getLumOff() { return -1; }
public int getLumMod() { return -1; }
public int getShade() { return -1; }
public int getTint() { return -1; }
};
}
};
}
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 TRANSPARENT_PAINT.getSolidColor().getColor();
assert(graphics != null);
ImageRenderer renderer = (ImageRenderer)graphics.getRenderingHint(Drawable.IMAGE_RENDERER);
if (renderer == null) renderer = new ImageRenderer();
try {
renderer.loadImage(fill.getImageData(), fill.getContentType());
} catch (IOException e) {
LOG.log(POILogger.ERROR, "Can't load image data - using transparent color", e);
return TRANSPARENT_PAINT.getSolidColor().getColor();
}
int alpha = fill.getAlpha();
if (alpha != -1) {
renderer.setAlpha(alpha/100000.f);
}
Dimension dim = renderer.getDimension();
Rectangle2D textAnchor = new Rectangle2D.Double(0, 0, dim.getWidth(), dim.getHeight());
Paint paint = new java.awt.TexturePaint(renderer.getImage(), textAnchor);
return paint;
}
/**
* Convert color transformations in {@link ColorStyle} to a {@link Color} instance
*/
public static Color applyColorTransform(ColorStyle color){
Color result = color.getColor();
if (result == null || color.getAlpha() == 100) {
return TRANSPARENT_PAINT.getSolidColor().getColor();
}
result = applyAlpha(result, color);
result = applyLuminance(result, color);
result = applyShade(result, color);
result = applyTint(result, color);
return result;
}
protected static Color applyAlpha(Color c, ColorStyle fc) {
int alpha = c.getAlpha();
return (alpha == 255) ? c : new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
}
/**
* Apply lumMod / lumOff adjustments
*
* @param c the color to modify
* @param lumMod luminance modulation in the range [0..100000]
* @param lumOff luminance offset in the range [0..100000]
* @return modified color
*
* @see <a href="https://msdn.microsoft.com/en-us/library/dd560821%28v=office.12%29.aspx">Using Office Open XML to Customize Document Formatting in the 2007 Office System</a>
*/
protected static Color applyLuminance(Color c, ColorStyle fc) {
int lumMod = fc.getLumMod();
if (lumMod == -1) lumMod = 100000;
int lumOff = fc.getLumOff();
if (lumOff == -1) lumOff = 0;
if (lumMod == 100000 && lumOff == 0) return c;
// 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 <a:lumOff> tag appears after the <a:lumMod> 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.)
//
// For a shade, the equation is luminance * %tint.
//
// For a tint, the equation is luminance * %tint + (1-%tint).
// (Note that 1-%tint is equal to the lumOff value in DrawingML.)
double fLumOff = lumOff / 100000d;
double fLumMod = lumMod / 100000d;
double hsl[] = RGB2HSL(c);
hsl[2] = hsl[2]*fLumMod+fLumOff;
Color c2 = HSL2RGB(hsl[0], hsl[1], hsl[2], c.getAlpha()/255d);
return c2;
}
/**
* This algorithm returns result different from PowerPoint.
* TODO: revisit and improve
*/
protected static Color applyShade(Color c, ColorStyle fc) {
int shade = fc.getShade();
if (shade == -1) return c;
float fshade = shade / 100000.f;
float red = c.getRed() * fshade;
float green = c.getGreen() * fshade;
float blue = c.getGreen() * fshade;
return new Color(Math.round(red), Math.round(green), Math.round(blue), c.getAlpha());
}
/**
* This algorithm returns result different from PowerPoint.
* TODO: revisit and improve
*/
protected static Color applyTint(Color c, ColorStyle fc) {
int tint = fc.getTint();
if (tint == -1) return c;
float ftint = tint / 100000.f;
float red = ftint * c.getRed() + (1.f - ftint) * 255.f;
float green = ftint * c.getGreen() + (1.f - ftint) * 255.f;
float blue = ftint * c.getBlue() + (1.f - ftint) * 255.f;
return new Color(Math.round(red), Math.round(green), Math.round(blue), c.getAlpha());
}
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);
float[] fractions = fill.getGradientFractions();
Color[] colors = new Color[fractions.length];
int i = 0;
for (ColorStyle fc : fill.getGradientColors()) {
colors[i++] = 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
*
* @returns the RGB Color object
*/
private static Color HSL2RGB(double h, double s, double l, double alpha) {
if (s <0.0f || s > 100.0f) {
String message = "Color parameter outside of expected range - Saturation";
throw new IllegalArgumentException( message );
}
if (l <0.0f || l > 100.0f) {
String message = "Color parameter outside of expected range - Luminance";
throw new IllegalArgumentException( message );
}
if (alpha <0.0f || alpha > 1.0f) {
String message = "Color parameter outside of expected range - 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};
}
}