aboutsummaryrefslogtreecommitdiffstats
path: root/src/java/org/apache/poi/sl/draw/DrawPaint.java
blob: e78dd2ee140dfd91b5a2e158ba746ad1690d3174 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
/* ====================================================================
   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.draw.geom.ArcToCommand.convertOoxml2AwtAngle;

import java.awt.Color;
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.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.stream.Stream;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.sl.usermodel.AbstractColorStyle;
import org.apache.poi.sl.usermodel.ColorStyle;
import org.apache.poi.sl.usermodel.Insets2D;
import org.apache.poi.sl.usermodel.PaintStyle;
import org.apache.poi.sl.usermodel.PaintStyle.FlipMode;
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.Dimension2DDouble;


/**
 * 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/

    // HSL code is public domain - see https://tips4java.wordpress.com/contact-us/

    private static final Logger LOG = LogManager.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 TRANSPARENT;
        }
        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 TRANSPARENT;
    }

    @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) {
                if (value == -1) {
                    return -1;
                }
                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 rectangular:
            // TODO: implement rectangular gradient fill
        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) {
        assert(graphics != null);

        final String contentType = fill.getContentType();
        if (contentType == null || contentType.isEmpty()) {
            return TRANSPARENT;
        }

        ImageRenderer renderer = DrawPictureShape.getImageRenderer(graphics, contentType);

        // TODO: handle tile settings, currently the pattern is always streched 100% in height/width
        Rectangle2D textAnchor = shape.getAnchor();

        try (InputStream is = fill.getImageData()) {
            if (is == null) {
                return TRANSPARENT;
            }

            renderer.loadImage(is, contentType);

            int alpha = fill.getAlpha();
            if (0 <= alpha && alpha < 100000) {
                renderer.setAlpha(alpha/100000.f);
            }

            Dimension2D imgDim = renderer.getDimension();
            if ("image/x-wmf".contains(contentType)) {
                // don't rely on wmf dimensions, use dimension of anchor
                // TODO: check pixels vs. points for image dimension
                imgDim = new Dimension2DDouble(textAnchor.getWidth(), textAnchor.getHeight());
            }

            BufferedImage image = renderer.getImage(imgDim);
            if(image == null) {
                LOG.atError().log("Can't load image data");
                return TRANSPARENT;
            }

            double flipX = 1, flipY = 1;
            final FlipMode flip = fill.getFlipMode();
            if (flip != null && flip != FlipMode.NONE) {
                final int width = image.getWidth(), height = image.getHeight();
                switch (flip) {
                    case X:
                        flipX = 2;
                        break;
                    case Y:
                        flipY = 2;
                        break;
                    case XY:
                        flipX = 2;
                        flipY = 2;
                        break;
                }

                final BufferedImage img = new BufferedImage((int)(width*flipX), (int)(height*flipY), BufferedImage.TYPE_INT_ARGB);
                Graphics2D g = img.createGraphics();
                g.drawImage(image, 0, 0, null);

                switch (flip) {
                    case X:
                        g.drawImage(image, 2*width, 0, -width, height, null);
                        break;
                    case Y:
                        g.drawImage(image, 0, 2*height, width, -height, null);
                        break;
                    case XY:
                        g.drawImage(image, 2*width, 0, -width, height, null);
                        g.drawImage(image, 0, 2*height, width, -height, null);
                        g.drawImage(image, 2*width, 2*height, -width, -height, null);
                        break;
                }

                g.dispose();
                image = img;
            }

            image = colorizePattern(fill, image);

            Shape s = (Shape)graphics.getRenderingHint(Drawable.GRADIENT_SHAPE);

            // TODO: check why original bitmaps scale/behave differently to vector based images
            return new DrawTexturePaint(image, s, fill, flipX, flipY, renderer instanceof BitmapImageRenderer);
        } catch (IOException e) {
            LOG.atError().withThrowable(e).log("Can't load image data - using transparent color");
            return TRANSPARENT;
        }
    }

    /**
     * In case a duotone element is specified, handle image as pattern and replace its color values
     * with the corresponding percentile / linear value between fore- and background color
     *
     * @return the original image if no duotone was found, otherwise the colorized pattern
     */
    private static BufferedImage colorizePattern(TexturePaint fill, BufferedImage pattern) {
        final List<ColorStyle> duoTone = fill.getDuoTone();
        if (duoTone == null || duoTone.size() != 2) {
            return pattern;
        }

        // the pattern image is actually a gray scale image, so we simply take the first color component
        // as an index into our gradient samples
        final int redBits = pattern.getSampleModel().getSampleSize(0);
        final int blendBits = Math.max(Math.min(redBits, 8), 1);
        final int blendShades = 1 << blendBits;
        // Currently ImageIO converts 16-bit images to 8-bit internally, so it's unlikely to get a blendRatio != 1
        final double blendRatio = blendShades / (double)(1 << Math.max(redBits,1));
        final int[] gradSample = linearBlendedColors(duoTone, blendShades);

        final IndexColorModel icm = new IndexColorModel(blendBits, blendShades, gradSample, 0, true, -1, DataBuffer.TYPE_BYTE);
        final BufferedImage patIdx = new BufferedImage(pattern.getWidth(), pattern.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, icm);

        final WritableRaster rasterRGBA = pattern.getRaster();
        final WritableRaster rasterIdx = patIdx.getRaster();

        final int[] redSample = new int[pattern.getWidth()];
        for (int y=0; y<pattern.getHeight(); y++) {
            rasterRGBA.getSamples(0, y, redSample.length, 1, 0, redSample);
            scaleShades(redSample, blendRatio);
            rasterIdx.setSamples(0, y, redSample.length, 1, 0, redSample);
        }

        return patIdx;
    }

    private static void scaleShades(int[] samples, double ratio) {
        if (ratio != 1) {
            for (int x=0; x<samples.length; x++) {
                samples[x] = (int)Math.rint(samples[x] * ratio);
            }
        }
    }

    private static int[] linearBlendedColors(List<ColorStyle> duoTone, final int blendShades) {
        Color[] colors = duoTone.stream().map(DrawPaint::applyColorTransform).toArray(Color[]::new);
        float[] fractions = { 0, 1 };

        // create lookup list of blended colors of back- and foreground
        BufferedImage gradBI = new BufferedImage(blendShades, 1, BufferedImage.TYPE_INT_ARGB);
        Graphics2D gradG = gradBI.createGraphics();
        gradG.setPaint(new LinearGradientPaint(0,0, blendShades,0, fractions, colors));
        gradG.fillRect(0,0, blendShades,1);
        gradG.dispose();

        return gradBI.getRGB(0, 0, blendShades, 1, null, 0, blendShades);
    }


    /**
     * 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();

        final double alpha = getAlpha(result, color);

        final double[] scRGB = RGB2SCRGB(result);
        applyShade(scRGB, color);
        applyTint(scRGB, color);
        result = SCRGB2RGB(scRGB);

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

        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) {
            hsl[hslPart] *= mod / 100_000d;
        }
        if (off != -1) {
            hsl[hslPart] += off / 1000d;
        }
    }

    /**
     * Apply the shade
     *
     * For a shade, the equation is luminance * %tint.
     */
    private static void applyShade(double[] scRGB, ColorStyle fc) {
        int shade = fc.getShade();
        if (shade == -1) {
            return;
        }

        final double shadePct = shade / 100_000.;
        for (int i=0; i<3; i++) {
            scRGB[i] = Math.max(0, Math.min(1, scRGB[i]*shadePct));
        }
    }

    /**
     * Apply the tint
     */
    private static void applyTint(double[] scRGB, ColorStyle fc) {
        int tint = fc.getTint();
        if (tint == -1 || tint == 0) {
            return;
        }

        // see 18.8.19 fgColor (Foreground Color)
        double tintPct = tint / 100_000.;

        for (int i=0; i<3; i++) {
            scRGB[i] =  1 - (1 - scRGB[i]) * 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);
        if (anchor == null) {
            return TRANSPARENT;
        }

        angle = convertOoxml2AwtAngle(-angle, anchor.getWidth(), anchor.getHeight());

        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
        // also having less than two fractions will not work
        return (p1.equals(p2) || fill.getGradientFractions().length < 2) ?
                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);
        if (anchor == null) {
            return TRANSPARENT;
        }

        Insets2D insets = fill.getFillToInsets();
        if (insets == null) {
            insets = new Insets2D(0,0,0,0);
        }

        // TODO: handle negative width/height
        final Point2D pCenter = new Point2D.Double(
            anchor.getCenterX(), anchor.getCenterY()
        );

        final Point2D pFocus = new Point2D.Double(
            getCenterVal(anchor.getMinX(), anchor.getMaxX(), insets.left, insets.right),
            getCenterVal(anchor.getMinY(), anchor.getMaxY(), insets.top, insets.bottom)
        );

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

        final AffineTransform at = new AffineTransform();
        at.translate(pFocus.getX(), pFocus.getY());
        at.scale(
            getScale(anchor.getMinX(), anchor.getMaxX(), insets.left, insets.right),
            getScale(anchor.getMinY(), anchor.getMaxY(), insets.top, insets.bottom)
        );
        at.translate(-pFocus.getX(), -pFocus.getY());

        return safeFractions((f,c)->new RadialGradientPaint(pCenter, radius, pFocus, f, c, CycleMethod.NO_CYCLE, ColorSpaceType.SRGB, at), fill);
    }

    private static double getScale(double absMin, double absMax, double relMin, double relMax) {
        double absDelta = absMax-absMin;
        double absStart = absMin+absDelta*relMin;
        double absStop = (relMin+relMax <= 1) ? absMax-absDelta*relMax : absMax+absDelta*relMax;
        return (absDelta == 0) ? 1 : (absStop-absStart)/absDelta;
    }

    private static double getCenterVal(double absMin, double absMax, double relMin, double relMax) {
        double absDelta = absMax-absMin;
        double absStart = absMin+absDelta*relMin;
        double absStop = (relMin+relMax <= 1) ? absMax-absDelta*relMax : absMax+absDelta*relMax;
        return absStart+(absStop-absStart)/2.;
    }

    @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) {
        // if style is null, use transparent color to get color of background
        final Iterator<Color> styles = Stream.of(fill.getGradientColors())
            .map(s -> s == null ? TRANSPARENT : applyColorTransform(s))
            .iterator();

        // need to remap the fractions, because Java doesn't like repeating fraction values
        Map<Float,Color> m = new TreeMap<>();
        for (float fraction : fill.getGradientFractions()) {
            m.put(fraction, styles.next());
        }

        return init.apply(toArray(m.keySet()), m.values().toArray(new Color[0]));
    }

    private static float[] toArray(Collection<Float> floatList) {
        int[] idx = { 0 };
        float[] ret = new float[floatList.size()];
        floatList.forEach(f -> ret[idx[0]++] = f);
        return ret;
    }

    /**
     *  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.
     */
    public 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 Color to scRGB [0..1] (0:red,1:green,2:blue).
     * Alpha needs to be handled separately.
     *
     * @see <a href="https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Color.cs,1048">.Net implementation sRgbToScRgb</a>
     */
    public static double[] RGB2SCRGB(Color color) {
        float[] rgb = color.getColorComponents(null);
        double[] scRGB = new double[3];
        for (int i=0; i<3; i++) {
            if (rgb[i] < 0) {
                scRGB[i] = 0;
            } else if (rgb[i] <= 0.04045) {
                scRGB[i] = rgb[i] / 12.92;
            } else if (rgb[i] <= 1) {
                scRGB[i] = Math.pow((rgb[i] + 0.055) / 1.055, 2.4);
            } else {
                scRGB[i] = 1;
            }
        }
        return scRGB;
    }

    /**
     * Convert scRGB [0..1] components (0:red,1:green,2:blue) to sRGB Color.
     * Alpha needs to be handled separately.
     *
     * @see <a href="https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Color.cs,1075">.Net implementation ScRgbTosRgb</a>
     */
    public static Color SCRGB2RGB(double... scRGB) {
        final double[] rgb = new double[3];
        for (int i=0; i<3; i++) {
            if (scRGB[i] < 0) {
                rgb[i] = 0;
            } else if (scRGB[i] <= 0.0031308) {
                rgb[i] = scRGB[i] * 12.92;
            } else if (scRGB[i] < 1) {
                rgb[i] = 1.055 * Math.pow(scRGB[i], 1.0 / 2.4) - 0.055;
            } else {
                rgb[i] = 1;
            }
        }
        return new Color((float)rgb[0],(float)rgb[1],(float)rgb[2]);
    }

    static void fillPaintWorkaround(Graphics2D graphics, Shape shape) {
        // the ibm jdk has a rendering/JIT bug, which throws an AIOOBE in
        // TexturePaintContext$Int.setRaster(TexturePaintContext.java:476)
        // this usually doesn't happen while debugging, because JIT doesn't jump in then.
        try {
            graphics.fill(shape);
        } catch (ArrayIndexOutOfBoundsException e) {
            LOG.atWarn().withThrowable(e).log("IBM JDK failed with TexturePaintContext AIOOBE - try adding the following to the VM parameter:\n" +
                "-Xjit:exclude={sun/java2d/pipe/AlphaPaintPipe.renderPathTile(Ljava/lang/Object;[BIIIIII)V} and " +
                "search for 'JIT Problem Determination for IBM SDK using -Xjit' (http://www-01.ibm.com/support/docview.wss?uid=swg21294023) " +
                "for how to add/determine further excludes");
        }
    }
}