| /* ==================================================================== |
| 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.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 java.util.Map; |
| import java.util.Objects; |
| import java.util.TreeMap; |
| import java.util.function.BiFunction; |
| |
| import org.apache.poi.sl.usermodel.AbstractColorStyle; |
| 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.PaintModifier; |
| 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 <a href="https://tips4java.wordpress.com/2009/07/05/hsl-color/">HSL code taken from Java Tips Weblog</a> |
| */ |
| 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 AbstractColorStyle(){ |
| @Override |
| public Color getColor() { |
| return new Color(color.getRed(), color.getGreen(), color.getBlue()); |
| } |
| @Override |
| public int getAlpha() { return (int)Math.round(color.getAlpha()*100000./255.); } |
| @Override |
| public int getHueOff() { return -1; } |
| @Override |
| public int getHueMod() { return -1; } |
| @Override |
| public int getSatOff() { return -1; } |
| @Override |
| public int getSatMod() { return -1; } |
| @Override |
| public int getLumOff() { return -1; } |
| @Override |
| public int getLumMod() { return -1; } |
| @Override |
| public int getShade() { return -1; } |
| @Override |
| public int getTint() { return -1; } |
| |
| |
| }; |
| } |
| |
| SimpleSolidPaint(ColorStyle color) { |
| if (color == null) { |
| throw new NullPointerException("Color needs to be specified"); |
| } |
| this.solidColor = color; |
| } |
| |
| @Override |
| public ColorStyle getSolidColor() { |
| return solidColor; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof SolidPaint)) { |
| return false; |
| } |
| return Objects.equals(getSolidColor(), ((SolidPaint) o).getSolidColor()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(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) { |
| return getPaint(graphics, paint, PaintModifier.NORM); |
| } |
| |
| public Paint getPaint(Graphics2D graphics, PaintStyle paint, PaintModifier modifier) { |
| if (modifier == PaintModifier.NONE) { |
| return null; |
| } |
| if (paint instanceof SolidPaint) { |
| return getSolidPaint((SolidPaint)paint, graphics, modifier); |
| } else if (paint instanceof GradientPaint) { |
| return getGradientPaint((GradientPaint)paint, graphics); |
| } else if (paint instanceof TexturePaint) { |
| return getTexturePaint((TexturePaint)paint, graphics); |
| } |
| return null; |
| } |
| |
| @SuppressWarnings({"WeakerAccess", "unused"}) |
| protected Paint getSolidPaint(SolidPaint fill, Graphics2D graphics, final PaintModifier modifier) { |
| final ColorStyle orig = fill.getSolidColor(); |
| ColorStyle cs = new AbstractColorStyle() { |
| @Override |
| public Color getColor() { |
| return orig.getColor(); |
| } |
| |
| @Override |
| public int getAlpha() { |
| return orig.getAlpha(); |
| } |
| |
| @Override |
| public int getHueOff() { |
| return orig.getHueOff(); |
| } |
| |
| @Override |
| public int getHueMod() { |
| return orig.getHueMod(); |
| } |
| |
| @Override |
| public int getSatOff() { |
| return orig.getSatOff(); |
| } |
| |
| @Override |
| public int getSatMod() { |
| return orig.getSatMod(); |
| } |
| |
| @Override |
| public int getLumOff() { |
| return orig.getLumOff(); |
| } |
| |
| @Override |
| public int getLumMod() { |
| return orig.getLumMod(); |
| } |
| |
| @Override |
| public int getShade() { |
| return scale(orig.getShade(), PaintModifier.DARKEN_LESS, PaintModifier.DARKEN); |
| } |
| |
| @Override |
| public int getTint() { |
| return scale(orig.getTint(), PaintModifier.LIGHTEN_LESS, PaintModifier.LIGHTEN); |
| } |
| |
| private int scale(int value, PaintModifier lessModifier, PaintModifier moreModifier) { |
| int delta = (modifier == lessModifier ? 20000 : (modifier == moreModifier ? 40000 : 0)); |
| return Math.min(100000, Math.max(0,value)+delta); |
| } |
| }; |
| |
| return applyColorTransform(cs); |
| } |
| |
| @SuppressWarnings("WeakerAccess") |
| 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."); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") |
| 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; |
| } |
| |
| return new java.awt.TexturePaint(image, textAnchor); |
| } |
| |
| /** |
| * Convert color transformations in {@link ColorStyle} to a {@link Color} instance |
| * |
| * @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> |
| * @see <a href="https://social.msdn.microsoft.com/Forums/office/en-US/040e0a1f-dbfe-4ce5-826b-38b4b6f6d3f7/saturation-modulation-satmod">saturation modulation (satMod)</a> |
| * @see <a href="http://stackoverflow.com/questions/6754127/office-open-xml-satmod-results-in-more-than-100-saturation">Office Open XML satMod results in more than 100% saturation</a> |
| */ |
| 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 <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.) |
| * |
| * @param hsl the hsl values |
| * @param hslPart the hsl part to modify [0..2] |
| * @param mod the modulation adjustment |
| * @param off the offset adjustment |
| */ |
| 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 shadePct = shade / 100000.; |
| |
| hsl[2] *= 1. - shadePct; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| // see 18.8.19 fgColor (Foreground Color) |
| double tintPct = tint / 100000.; |
| hsl[2] = hsl[2]*(1.-tintPct) + (100.-100.*(1.-tintPct)); |
| } |
| |
| @SuppressWarnings("WeakerAccess") |
| protected Paint createLinearGradientPaint(GradientPaint fill, Graphics2D graphics) { |
| // TODO: we need to find the two points for gradient - the problem is, which point at the outline |
| // do you take? My solution would be to apply the gradient rotation to the shape in reverse |
| // and then scan the shape for the largest possible horizontal distance |
| |
| double angle = fill.getGradientAngle(); |
| if (!fill.isRotatedWithShape()) { |
| angle -= shape.getRotation(); |
| } |
| |
| Rectangle2D anchor = DrawShape.getAnchor(graphics, shape); |
| |
| AffineTransform at = AffineTransform.getRotateInstance(Math.toRadians(angle), anchor.getCenterX(), anchor.getCenterY()); |
| |
| double diagonal = Math.sqrt(Math.pow(anchor.getWidth(),2) + Math.pow(anchor.getHeight(),2)); |
| final Point2D p1 = at.transform(new Point2D.Double(anchor.getCenterX() - diagonal / 2, anchor.getCenterY()), null); |
| final Point2D p2 = at.transform(new Point2D.Double(anchor.getMaxX(), anchor.getCenterY()), null); |
| |
| // snapToAnchor(p1, anchor); |
| // snapToAnchor(p2, anchor); |
| |
| // gradient paint on the same point throws an exception ... and doesn't make sense |
| return (p1.equals(p2)) ? null : safeFractions((f,c)->new LinearGradientPaint(p1,p2,f,c), fill); |
| } |
| |
| |
| @SuppressWarnings("WeakerAccess") |
| protected Paint createRadialGradientPaint(GradientPaint fill, Graphics2D graphics) { |
| Rectangle2D anchor = DrawShape.getAnchor(graphics, shape); |
| |
| final Point2D pCenter = new Point2D.Double(anchor.getCenterX(), anchor.getCenterY()); |
| |
| final float radius = (float)Math.max(anchor.getWidth(), anchor.getHeight()); |
| |
| return safeFractions((f,c)->new RadialGradientPaint(pCenter,radius,f,c), fill); |
| } |
| |
| @SuppressWarnings({"WeakerAccess", "unused"}) |
| protected Paint createPathGradientPaint(GradientPaint fill, Graphics2D graphics) { |
| // currently we ignore an eventually center setting |
| |
| return safeFractions(PathGradientPaint::new, fill); |
| } |
| |
| private Paint safeFractions(BiFunction<float[],Color[],Paint> init, GradientPaint fill) { |
| float[] fractions = fill.getGradientFractions(); |
| final ColorStyle[] styles = fill.getGradientColors(); |
| |
| // need to remap the fractions, because Java doesn't like repeating fraction values |
| Map<Float,Color> m = new TreeMap<>(); |
| for (int i = 0; i<fractions.length; i++) { |
| // if fc is null, use transparent color to get color of background |
| m.put(fractions[i], (styles[i] == null ? TRANSPARENT : applyColorTransform(styles[i]))); |
| } |
| |
| final Color[] colors = new Color[m.size()]; |
| if (fractions.length != m.size()) { |
| fractions = new float[m.size()]; |
| } |
| |
| int i=0; |
| for (Map.Entry<Float,Color> me : m.entrySet()) { |
| fractions[i] = me.getKey(); |
| colors[i] = me.getValue(); |
| i++; |
| } |
| |
| return init.apply(fractions, colors); |
| } |
| |
| /** |
| * 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 |
| |
| final double s; |
| |
| 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); |
| } |
| } |
| } |