You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DrawPaint.java 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.sl.draw;
  16. import static org.apache.poi.sl.draw.geom.ArcToCommand.convertOoxml2AwtAngle;
  17. import java.awt.Color;
  18. import java.awt.Graphics2D;
  19. import java.awt.LinearGradientPaint;
  20. import java.awt.MultipleGradientPaint.ColorSpaceType;
  21. import java.awt.MultipleGradientPaint.CycleMethod;
  22. import java.awt.Paint;
  23. import java.awt.RadialGradientPaint;
  24. import java.awt.Shape;
  25. import java.awt.geom.AffineTransform;
  26. import java.awt.geom.Dimension2D;
  27. import java.awt.geom.Point2D;
  28. import java.awt.geom.Rectangle2D;
  29. import java.awt.image.BufferedImage;
  30. import java.awt.image.DataBuffer;
  31. import java.awt.image.IndexColorModel;
  32. import java.awt.image.WritableRaster;
  33. import java.io.IOException;
  34. import java.io.InputStream;
  35. import java.util.Collection;
  36. import java.util.Iterator;
  37. import java.util.List;
  38. import java.util.Map;
  39. import java.util.Objects;
  40. import java.util.TreeMap;
  41. import java.util.function.BiFunction;
  42. import java.util.stream.Stream;
  43. import org.apache.logging.log4j.LogManager;
  44. import org.apache.logging.log4j.Logger;
  45. import org.apache.poi.sl.usermodel.AbstractColorStyle;
  46. import org.apache.poi.sl.usermodel.ColorStyle;
  47. import org.apache.poi.sl.usermodel.Insets2D;
  48. import org.apache.poi.sl.usermodel.PaintStyle;
  49. import org.apache.poi.sl.usermodel.PaintStyle.FlipMode;
  50. import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint;
  51. import org.apache.poi.sl.usermodel.PaintStyle.PaintModifier;
  52. import org.apache.poi.sl.usermodel.PaintStyle.SolidPaint;
  53. import org.apache.poi.sl.usermodel.PaintStyle.TexturePaint;
  54. import org.apache.poi.sl.usermodel.PlaceableShape;
  55. import org.apache.poi.util.Dimension2DDouble;
  56. /**
  57. * This class handles color transformations.
  58. *
  59. * @see <a href="https://tips4java.wordpress.com/2009/07/05/hsl-color/">HSL code taken from Java Tips Weblog</a>
  60. */
  61. public class DrawPaint {
  62. // HSL code is public domain - see https://tips4java.wordpress.com/contact-us/
  63. // HSL code is public domain - see https://tips4java.wordpress.com/contact-us/
  64. private static final Logger LOG = LogManager.getLogger(DrawPaint.class);
  65. private static final Color TRANSPARENT = new Color(1f,1f,1f,0f);
  66. protected PlaceableShape<?,?> shape;
  67. public DrawPaint(PlaceableShape<?,?> shape) {
  68. this.shape = shape;
  69. }
  70. private static class SimpleSolidPaint implements SolidPaint {
  71. private final ColorStyle solidColor;
  72. SimpleSolidPaint(final Color color) {
  73. if (color == null) {
  74. throw new NullPointerException("Color needs to be specified");
  75. }
  76. this.solidColor = new AbstractColorStyle(){
  77. @Override
  78. public Color getColor() {
  79. return new Color(color.getRed(), color.getGreen(), color.getBlue());
  80. }
  81. @Override
  82. public int getAlpha() { return (int)Math.round(color.getAlpha()*100000./255.); }
  83. @Override
  84. public int getHueOff() { return -1; }
  85. @Override
  86. public int getHueMod() { return -1; }
  87. @Override
  88. public int getSatOff() { return -1; }
  89. @Override
  90. public int getSatMod() { return -1; }
  91. @Override
  92. public int getLumOff() { return -1; }
  93. @Override
  94. public int getLumMod() { return -1; }
  95. @Override
  96. public int getShade() { return -1; }
  97. @Override
  98. public int getTint() { return -1; }
  99. };
  100. }
  101. SimpleSolidPaint(ColorStyle color) {
  102. if (color == null) {
  103. throw new NullPointerException("Color needs to be specified");
  104. }
  105. this.solidColor = color;
  106. }
  107. @Override
  108. public ColorStyle getSolidColor() {
  109. return solidColor;
  110. }
  111. @Override
  112. public boolean equals(Object o) {
  113. if (this == o) {
  114. return true;
  115. }
  116. if (!(o instanceof SolidPaint)) {
  117. return false;
  118. }
  119. return Objects.equals(getSolidColor(), ((SolidPaint) o).getSolidColor());
  120. }
  121. @Override
  122. public int hashCode() {
  123. return Objects.hash(solidColor);
  124. }
  125. }
  126. public static SolidPaint createSolidPaint(final Color color) {
  127. return (color == null) ? null : new SimpleSolidPaint(color);
  128. }
  129. public static SolidPaint createSolidPaint(final ColorStyle color) {
  130. return (color == null) ? null : new SimpleSolidPaint(color);
  131. }
  132. public Paint getPaint(Graphics2D graphics, PaintStyle paint) {
  133. return getPaint(graphics, paint, PaintModifier.NORM);
  134. }
  135. public Paint getPaint(Graphics2D graphics, PaintStyle paint, PaintModifier modifier) {
  136. if (modifier == PaintModifier.NONE) {
  137. return TRANSPARENT;
  138. }
  139. if (paint instanceof SolidPaint) {
  140. return getSolidPaint((SolidPaint)paint, graphics, modifier);
  141. } else if (paint instanceof GradientPaint) {
  142. return getGradientPaint((GradientPaint)paint, graphics);
  143. } else if (paint instanceof TexturePaint) {
  144. return getTexturePaint((TexturePaint)paint, graphics);
  145. }
  146. return TRANSPARENT;
  147. }
  148. @SuppressWarnings({"WeakerAccess", "unused"})
  149. protected Paint getSolidPaint(SolidPaint fill, Graphics2D graphics, final PaintModifier modifier) {
  150. final ColorStyle orig = fill.getSolidColor();
  151. ColorStyle cs = new AbstractColorStyle() {
  152. @Override
  153. public Color getColor() {
  154. return orig.getColor();
  155. }
  156. @Override
  157. public int getAlpha() {
  158. return orig.getAlpha();
  159. }
  160. @Override
  161. public int getHueOff() {
  162. return orig.getHueOff();
  163. }
  164. @Override
  165. public int getHueMod() {
  166. return orig.getHueMod();
  167. }
  168. @Override
  169. public int getSatOff() {
  170. return orig.getSatOff();
  171. }
  172. @Override
  173. public int getSatMod() {
  174. return orig.getSatMod();
  175. }
  176. @Override
  177. public int getLumOff() {
  178. return orig.getLumOff();
  179. }
  180. @Override
  181. public int getLumMod() {
  182. return orig.getLumMod();
  183. }
  184. @Override
  185. public int getShade() {
  186. return scale(orig.getShade(), PaintModifier.DARKEN_LESS, PaintModifier.DARKEN);
  187. }
  188. @Override
  189. public int getTint() {
  190. return scale(orig.getTint(), PaintModifier.LIGHTEN_LESS, PaintModifier.LIGHTEN);
  191. }
  192. private int scale(int value, PaintModifier lessModifier, PaintModifier moreModifier) {
  193. if (value == -1) {
  194. return -1;
  195. }
  196. int delta = (modifier == lessModifier ? 20000 : (modifier == moreModifier ? 40000 : 0));
  197. return Math.min(100000, Math.max(0,value)+delta);
  198. }
  199. };
  200. return applyColorTransform(cs);
  201. }
  202. @SuppressWarnings("WeakerAccess")
  203. protected Paint getGradientPaint(GradientPaint fill, Graphics2D graphics) {
  204. switch (fill.getGradientType()) {
  205. case linear:
  206. return createLinearGradientPaint(fill, graphics);
  207. case rectangular:
  208. // TODO: implement rectangular gradient fill
  209. case circular:
  210. return createRadialGradientPaint(fill, graphics);
  211. case shape:
  212. return createPathGradientPaint(fill, graphics);
  213. default:
  214. throw new UnsupportedOperationException("gradient fill of type "+fill+" not supported.");
  215. }
  216. }
  217. @SuppressWarnings("WeakerAccess")
  218. protected Paint getTexturePaint(TexturePaint fill, Graphics2D graphics) {
  219. assert(graphics != null);
  220. final String contentType = fill.getContentType();
  221. if (contentType == null || contentType.isEmpty()) {
  222. return TRANSPARENT;
  223. }
  224. ImageRenderer renderer = DrawPictureShape.getImageRenderer(graphics, contentType);
  225. // TODO: handle tile settings, currently the pattern is always streched 100% in height/width
  226. Rectangle2D textAnchor = shape.getAnchor();
  227. try (InputStream is = fill.getImageData()) {
  228. if (is == null) {
  229. return TRANSPARENT;
  230. }
  231. Boolean cacheImage = (Boolean)graphics.getRenderingHint(Drawable.CACHE_IMAGE_SOURCE);
  232. renderer.setCacheInput(cacheImage != null && cacheImage);
  233. renderer.loadImage(is, contentType);
  234. int alpha = fill.getAlpha();
  235. if (0 <= alpha && alpha < 100000) {
  236. renderer.setAlpha(alpha/100000.f);
  237. }
  238. Dimension2D imgDim = renderer.getDimension();
  239. if ("image/x-wmf".contains(contentType)) {
  240. // don't rely on wmf dimensions, use dimension of anchor
  241. // TODO: check pixels vs. points for image dimension
  242. imgDim = new Dimension2DDouble(textAnchor.getWidth(), textAnchor.getHeight());
  243. }
  244. BufferedImage image = renderer.getImage(imgDim);
  245. if(image == null) {
  246. LOG.atError().log("Can't load image data");
  247. return TRANSPARENT;
  248. }
  249. double flipX = 1, flipY = 1;
  250. final FlipMode flip = fill.getFlipMode();
  251. if (flip != null && flip != FlipMode.NONE) {
  252. final int width = image.getWidth(), height = image.getHeight();
  253. switch (flip) {
  254. case X:
  255. flipX = 2;
  256. break;
  257. case Y:
  258. flipY = 2;
  259. break;
  260. case XY:
  261. flipX = 2;
  262. flipY = 2;
  263. break;
  264. }
  265. final BufferedImage img = new BufferedImage((int)(width*flipX), (int)(height*flipY), BufferedImage.TYPE_INT_ARGB);
  266. Graphics2D g = img.createGraphics();
  267. g.drawImage(image, 0, 0, null);
  268. switch (flip) {
  269. case X:
  270. g.drawImage(image, 2*width, 0, -width, height, null);
  271. break;
  272. case Y:
  273. g.drawImage(image, 0, 2*height, width, -height, null);
  274. break;
  275. case XY:
  276. g.drawImage(image, 2*width, 0, -width, height, null);
  277. g.drawImage(image, 0, 2*height, width, -height, null);
  278. g.drawImage(image, 2*width, 2*height, -width, -height, null);
  279. break;
  280. }
  281. g.dispose();
  282. image = img;
  283. }
  284. image = colorizePattern(fill, image);
  285. Shape s = (Shape)graphics.getRenderingHint(Drawable.GRADIENT_SHAPE);
  286. // TODO: check why original bitmaps scale/behave differently to vector based images
  287. return new DrawTexturePaint(renderer, image, s, fill, flipX, flipY, renderer instanceof BitmapImageRenderer);
  288. } catch (IOException e) {
  289. LOG.atError().withThrowable(e).log("Can't load image data - using transparent color");
  290. return TRANSPARENT;
  291. }
  292. }
  293. /**
  294. * In case a duotone element is specified, handle image as pattern and replace its color values
  295. * with the corresponding percentile / linear value between fore- and background color
  296. *
  297. * @return the original image if no duotone was found, otherwise the colorized pattern
  298. */
  299. private static BufferedImage colorizePattern(TexturePaint fill, BufferedImage pattern) {
  300. final List<ColorStyle> duoTone = fill.getDuoTone();
  301. if (duoTone == null || duoTone.size() != 2) {
  302. return pattern;
  303. }
  304. // the pattern image is actually a gray scale image, so we simply take the first color component
  305. // as an index into our gradient samples
  306. final int redBits = pattern.getSampleModel().getSampleSize(0);
  307. final int blendBits = Math.max(Math.min(redBits, 8), 1);
  308. final int blendShades = 1 << blendBits;
  309. // Currently ImageIO converts 16-bit images to 8-bit internally, so it's unlikely to get a blendRatio != 1
  310. final double blendRatio = blendShades / (double)(1 << Math.max(redBits,1));
  311. final int[] gradSample = linearBlendedColors(duoTone, blendShades);
  312. final IndexColorModel icm = new IndexColorModel(blendBits, blendShades, gradSample, 0, true, -1, DataBuffer.TYPE_BYTE);
  313. final BufferedImage patIdx = new BufferedImage(pattern.getWidth(), pattern.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, icm);
  314. final WritableRaster rasterRGBA = pattern.getRaster();
  315. final WritableRaster rasterIdx = patIdx.getRaster();
  316. final int[] redSample = new int[pattern.getWidth()];
  317. for (int y=0; y<pattern.getHeight(); y++) {
  318. rasterRGBA.getSamples(0, y, redSample.length, 1, 0, redSample);
  319. scaleShades(redSample, blendRatio);
  320. rasterIdx.setSamples(0, y, redSample.length, 1, 0, redSample);
  321. }
  322. return patIdx;
  323. }
  324. private static void scaleShades(int[] samples, double ratio) {
  325. if (ratio != 1) {
  326. for (int x=0; x<samples.length; x++) {
  327. samples[x] = (int)Math.rint(samples[x] * ratio);
  328. }
  329. }
  330. }
  331. private static int[] linearBlendedColors(List<ColorStyle> duoTone, final int blendShades) {
  332. Color[] colors = duoTone.stream().map(DrawPaint::applyColorTransform).toArray(Color[]::new);
  333. float[] fractions = { 0, 1 };
  334. // create lookup list of blended colors of back- and foreground
  335. BufferedImage gradBI = new BufferedImage(blendShades, 1, BufferedImage.TYPE_INT_ARGB);
  336. Graphics2D gradG = gradBI.createGraphics();
  337. gradG.setPaint(new LinearGradientPaint(0,0, blendShades,0, fractions, colors));
  338. gradG.fillRect(0,0, blendShades,1);
  339. gradG.dispose();
  340. return gradBI.getRGB(0, 0, blendShades, 1, null, 0, blendShades);
  341. }
  342. /**
  343. * Convert color transformations in {@link ColorStyle} to a {@link Color} instance
  344. *
  345. * @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>
  346. * @see <a href="https://social.msdn.microsoft.com/Forums/office/en-US/040e0a1f-dbfe-4ce5-826b-38b4b6f6d3f7/saturation-modulation-satmod">saturation modulation (satMod)</a>
  347. */
  348. public static Color applyColorTransform(ColorStyle color){
  349. // TODO: The colors don't match 100% the results of Powerpoint, maybe because we still
  350. // operate in sRGB and not scRGB ... work in progress ...
  351. if (color == null || color.getColor() == null) {
  352. return TRANSPARENT;
  353. }
  354. Color result = color.getColor();
  355. final double alpha = getAlpha(result, color);
  356. final double[] scRGB = RGB2SCRGB(result);
  357. applyShade(scRGB, color);
  358. applyTint(scRGB, color);
  359. result = SCRGB2RGB(scRGB);
  360. // values are in the range [0..100] (usually ...)
  361. double[] hsl = RGB2HSL(result);
  362. applyHslModOff(hsl, 0, color.getHueMod(), color.getHueOff());
  363. applyHslModOff(hsl, 1, color.getSatMod(), color.getSatOff());
  364. applyHslModOff(hsl, 2, color.getLumMod(), color.getLumOff());
  365. result = HSL2RGB(hsl[0], hsl[1], hsl[2], alpha);
  366. return result;
  367. }
  368. private static double getAlpha(Color c, ColorStyle fc) {
  369. double alpha = c.getAlpha()/255d;
  370. int fcAlpha = fc.getAlpha();
  371. if (fcAlpha != -1) {
  372. alpha *= fcAlpha/100000d;
  373. }
  374. return Math.min(1, Math.max(0, alpha));
  375. }
  376. /**
  377. * Apply the modulation and offset adjustments to the given HSL part
  378. *
  379. * Example for lumMod/lumOff:
  380. * The lumMod value is the percent luminance. A lumMod value of "60000",
  381. * is 60% of the luminance of the original color.
  382. * When the color is a shade of the original theme color, the lumMod
  383. * attribute is the only one of the tags shown here that appears.
  384. * The <a:lumOff> tag appears after the <a:lumMod> tag when the color is a
  385. * tint of the original. The lumOff value always equals 1-lumMod, which is used in the tint calculation
  386. *
  387. * Despite having different ways to display the tint and shade percentages,
  388. * all of the programs use the same method to calculate the resulting color.
  389. * Convert the original RGB value to HSL ... and then adjust the luminance (L)
  390. * with one of the following equations before converting the HSL value back to RGB.
  391. * (The % tint in the following equations refers to the tint, themetint, themeshade,
  392. * or lumMod values, as applicable.)
  393. *
  394. * @param hsl the hsl values
  395. * @param hslPart the hsl part to modify [0..2]
  396. * @param mod the modulation adjustment
  397. * @param off the offset adjustment
  398. */
  399. private static void applyHslModOff(double[] hsl, int hslPart, int mod, int off) {
  400. if (mod != -1) {
  401. hsl[hslPart] *= mod / 100_000d;
  402. }
  403. if (off != -1) {
  404. hsl[hslPart] += off / 1000d;
  405. }
  406. }
  407. /**
  408. * Apply the shade
  409. *
  410. * For a shade, the equation is luminance * %tint.
  411. */
  412. private static void applyShade(double[] scRGB, ColorStyle fc) {
  413. int shade = fc.getShade();
  414. if (shade == -1) {
  415. return;
  416. }
  417. final double shadePct = shade / 100_000.;
  418. for (int i=0; i<3; i++) {
  419. scRGB[i] = Math.max(0, Math.min(1, scRGB[i]*shadePct));
  420. }
  421. }
  422. /**
  423. * Apply the tint
  424. */
  425. private static void applyTint(double[] scRGB, ColorStyle fc) {
  426. int tint = fc.getTint();
  427. if (tint == -1 || tint == 0) {
  428. return;
  429. }
  430. // see 18.8.19 fgColor (Foreground Color)
  431. double tintPct = tint / 100_000.;
  432. for (int i=0; i<3; i++) {
  433. scRGB[i] = 1 - (1 - scRGB[i]) * tintPct;
  434. }
  435. }
  436. @SuppressWarnings("WeakerAccess")
  437. protected Paint createLinearGradientPaint(GradientPaint fill, Graphics2D graphics) {
  438. // TODO: we need to find the two points for gradient - the problem is, which point at the outline
  439. // do you take? My solution would be to apply the gradient rotation to the shape in reverse
  440. // and then scan the shape for the largest possible horizontal distance
  441. double angle = fill.getGradientAngle();
  442. if (!fill.isRotatedWithShape()) {
  443. angle -= shape.getRotation();
  444. }
  445. Rectangle2D anchor = DrawShape.getAnchor(graphics, shape);
  446. if (anchor == null) {
  447. return TRANSPARENT;
  448. }
  449. angle = convertOoxml2AwtAngle(-angle, anchor.getWidth(), anchor.getHeight());
  450. AffineTransform at = AffineTransform.getRotateInstance(Math.toRadians(angle), anchor.getCenterX(), anchor.getCenterY());
  451. double diagonal = Math.sqrt(Math.pow(anchor.getWidth(),2) + Math.pow(anchor.getHeight(),2));
  452. final Point2D p1 = at.transform(new Point2D.Double(anchor.getCenterX() - diagonal / 2, anchor.getCenterY()), null);
  453. final Point2D p2 = at.transform(new Point2D.Double(anchor.getMaxX(), anchor.getCenterY()), null);
  454. // snapToAnchor(p1, anchor);
  455. // snapToAnchor(p2, anchor);
  456. // gradient paint on the same point throws an exception ... and doesn't make sense
  457. // also having less than two fractions will not work
  458. return (p1.equals(p2) || fill.getGradientFractions().length < 2) ?
  459. null :
  460. safeFractions((f,c)->new LinearGradientPaint(p1,p2,f,c), fill);
  461. }
  462. @SuppressWarnings("WeakerAccess")
  463. protected Paint createRadialGradientPaint(GradientPaint fill, Graphics2D graphics) {
  464. Rectangle2D anchor = DrawShape.getAnchor(graphics, shape);
  465. if (anchor == null) {
  466. return TRANSPARENT;
  467. }
  468. Insets2D insets = fill.getFillToInsets();
  469. if (insets == null) {
  470. insets = new Insets2D(0,0,0,0);
  471. }
  472. // TODO: handle negative width/height
  473. final Point2D pCenter = new Point2D.Double(
  474. anchor.getCenterX(), anchor.getCenterY()
  475. );
  476. final Point2D pFocus = new Point2D.Double(
  477. getCenterVal(anchor.getMinX(), anchor.getMaxX(), insets.left, insets.right),
  478. getCenterVal(anchor.getMinY(), anchor.getMaxY(), insets.top, insets.bottom)
  479. );
  480. final float radius = (float)Math.max(anchor.getWidth(), anchor.getHeight());
  481. final AffineTransform at = new AffineTransform();
  482. at.translate(pFocus.getX(), pFocus.getY());
  483. at.scale(
  484. getScale(anchor.getMinX(), anchor.getMaxX(), insets.left, insets.right),
  485. getScale(anchor.getMinY(), anchor.getMaxY(), insets.top, insets.bottom)
  486. );
  487. at.translate(-pFocus.getX(), -pFocus.getY());
  488. return safeFractions((f,c)->new RadialGradientPaint(pCenter, radius, pFocus, f, c, CycleMethod.NO_CYCLE, ColorSpaceType.SRGB, at), fill);
  489. }
  490. private static double getScale(double absMin, double absMax, double relMin, double relMax) {
  491. double absDelta = absMax-absMin;
  492. double absStart = absMin+absDelta*relMin;
  493. double absStop = (relMin+relMax <= 1) ? absMax-absDelta*relMax : absMax+absDelta*relMax;
  494. return (absDelta == 0) ? 1 : (absStop-absStart)/absDelta;
  495. }
  496. private static double getCenterVal(double absMin, double absMax, double relMin, double relMax) {
  497. double absDelta = absMax-absMin;
  498. double absStart = absMin+absDelta*relMin;
  499. double absStop = (relMin+relMax <= 1) ? absMax-absDelta*relMax : absMax+absDelta*relMax;
  500. return absStart+(absStop-absStart)/2.;
  501. }
  502. @SuppressWarnings({"WeakerAccess", "unused"})
  503. protected Paint createPathGradientPaint(GradientPaint fill, Graphics2D graphics) {
  504. // currently we ignore an eventually center setting
  505. return safeFractions(PathGradientPaint::new, fill);
  506. }
  507. private Paint safeFractions(BiFunction<float[],Color[],Paint> init, GradientPaint fill) {
  508. // if style is null, use transparent color to get color of background
  509. final Iterator<Color> styles = Stream.of(fill.getGradientColors())
  510. .map(s -> s == null ? TRANSPARENT : applyColorTransform(s))
  511. .iterator();
  512. // need to remap the fractions, because Java doesn't like repeating fraction values
  513. Map<Float,Color> m = new TreeMap<>();
  514. for (float fraction : fill.getGradientFractions()) {
  515. float gradientFraction = fraction;
  516. // Multiple gradient stops at the same location
  517. // can lead to failure when creating AWT gradient, especially
  518. // if there are only two stops and they are both on the exact
  519. // same location.
  520. // (The example of (only) 2 stops at exactly the same location will cause:
  521. // java.lang.IllegalArgumentException: User must specify at least 2 colors).
  522. //
  523. // To fix this we nudge the stop a teeny tiny bit.
  524. if (m.containsKey(gradientFraction)) {
  525. gradientFraction += (gradientFraction == 1.0 ? -1.0 : 1.0) * 0.00000005;
  526. }
  527. m.put(gradientFraction, styles.next());
  528. }
  529. return init.apply(toArray(m.keySet()), m.values().toArray(new Color[0]));
  530. }
  531. private static float[] toArray(Collection<Float> floatList) {
  532. int[] idx = { 0 };
  533. float[] ret = new float[floatList.size()];
  534. floatList.forEach(f -> ret[idx[0]++] = f);
  535. return ret;
  536. }
  537. /**
  538. * Convert HSL values to a RGB Color.
  539. *
  540. * @param h Hue is specified as degrees in the range 0 - 360.
  541. * @param s Saturation is specified as a percentage in the range 1 - 100.
  542. * @param l Luminance is specified as a percentage in the range 1 - 100.
  543. * @param alpha the alpha value between 0 - 1
  544. *
  545. * @return the RGB Color object
  546. */
  547. public static Color HSL2RGB(double h, double s, double l, double alpha) {
  548. // we clamp the values, as it possible to come up with more than 100% sat/lum
  549. // (see links in applyColorTransform() for more info)
  550. s = Math.max(0, Math.min(100, s));
  551. l = Math.max(0, Math.min(100, l));
  552. if (alpha <0.0f || alpha > 1.0f) {
  553. String message = "Color parameter outside of expected range - Alpha: " + alpha;
  554. throw new IllegalArgumentException( message );
  555. }
  556. // Formula needs all values between 0 - 1.
  557. h = h % 360.0f;
  558. h /= 360f;
  559. s /= 100f;
  560. l /= 100f;
  561. double q = (l < 0.5d)
  562. ? l * (1d + s)
  563. : (l + s) - (s * l);
  564. double p = 2d * l - q;
  565. double r = Math.max(0, HUE2RGB(p, q, h + (1.0d / 3.0d)));
  566. double g = Math.max(0, HUE2RGB(p, q, h));
  567. double b = Math.max(0, HUE2RGB(p, q, h - (1.0d / 3.0d)));
  568. r = Math.min(r, 1.0d);
  569. g = Math.min(g, 1.0d);
  570. b = Math.min(b, 1.0d);
  571. return new Color((float)r, (float)g, (float)b, (float)alpha);
  572. }
  573. private static double HUE2RGB(double p, double q, double h) {
  574. if (h < 0d) {
  575. h += 1d;
  576. }
  577. if (h > 1d) {
  578. h -= 1d;
  579. }
  580. if (6d * h < 1d) {
  581. return p + ((q - p) * 6d * h);
  582. }
  583. if (2d * h < 1d) {
  584. return q;
  585. }
  586. if (3d * h < 2d) {
  587. return p + ( (q - p) * 6d * ((2.0d / 3.0d) - h) );
  588. }
  589. return p;
  590. }
  591. /**
  592. * Convert a RGB Color to it corresponding HSL values.
  593. *
  594. * @return an array containing the 3 HSL values.
  595. */
  596. public static double[] RGB2HSL(Color color) {
  597. // Get RGB values in the range 0 - 1
  598. float[] rgb = color.getRGBColorComponents( null );
  599. double r = rgb[0];
  600. double g = rgb[1];
  601. double b = rgb[2];
  602. // Minimum and Maximum RGB values are used in the HSL calculations
  603. double min = Math.min(r, Math.min(g, b));
  604. double max = Math.max(r, Math.max(g, b));
  605. // Calculate the Hue
  606. double h = 0;
  607. if (max == min) {
  608. h = 0;
  609. } else if (max == r) {
  610. h = ((60d * (g - b) / (max - min)) + 360d) % 360d;
  611. } else if (max == g) {
  612. h = (60d * (b - r) / (max - min)) + 120d;
  613. } else if (max == b) {
  614. h = (60d * (r - g) / (max - min)) + 240d;
  615. }
  616. // Calculate the Luminance
  617. double l = (max + min) / 2d;
  618. // Calculate the Saturation
  619. final double s;
  620. if (max == min) {
  621. s = 0;
  622. } else if (l <= .5d) {
  623. s = (max - min) / (max + min);
  624. } else {
  625. s = (max - min) / (2d - max - min);
  626. }
  627. return new double[] {h, s * 100, l * 100};
  628. }
  629. /**
  630. * Convert sRGB Color to scRGB [0..1] (0:red,1:green,2:blue).
  631. * Alpha needs to be handled separately.
  632. *
  633. * @see <a href="https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Color.cs,1048">.Net implementation sRgbToScRgb</a>
  634. */
  635. public static double[] RGB2SCRGB(Color color) {
  636. float[] rgb = color.getColorComponents(null);
  637. double[] scRGB = new double[3];
  638. for (int i=0; i<3; i++) {
  639. if (rgb[i] < 0) {
  640. scRGB[i] = 0;
  641. } else if (rgb[i] <= 0.04045) {
  642. scRGB[i] = rgb[i] / 12.92;
  643. } else if (rgb[i] <= 1) {
  644. scRGB[i] = Math.pow((rgb[i] + 0.055) / 1.055, 2.4);
  645. } else {
  646. scRGB[i] = 1;
  647. }
  648. }
  649. return scRGB;
  650. }
  651. /**
  652. * Convert scRGB [0..1] components (0:red,1:green,2:blue) to sRGB Color.
  653. * Alpha needs to be handled separately.
  654. *
  655. * @see <a href="https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Color.cs,1075">.Net implementation ScRgbTosRgb</a>
  656. */
  657. public static Color SCRGB2RGB(double... scRGB) {
  658. final double[] rgb = new double[3];
  659. for (int i=0; i<3; i++) {
  660. if (scRGB[i] < 0) {
  661. rgb[i] = 0;
  662. } else if (scRGB[i] <= 0.0031308) {
  663. rgb[i] = scRGB[i] * 12.92;
  664. } else if (scRGB[i] < 1) {
  665. rgb[i] = 1.055 * Math.pow(scRGB[i], 1.0 / 2.4) - 0.055;
  666. } else {
  667. rgb[i] = 1;
  668. }
  669. }
  670. return new Color((float)rgb[0],(float)rgb[1],(float)rgb[2]);
  671. }
  672. static void fillPaintWorkaround(Graphics2D graphics, Shape shape) {
  673. // the ibm jdk has a rendering/JIT bug, which throws an AIOOBE in
  674. // TexturePaintContext$Int.setRaster(TexturePaintContext.java:476)
  675. // this usually doesn't happen while debugging, because JIT doesn't jump in then.
  676. try {
  677. graphics.fill(shape);
  678. } catch (ArrayIndexOutOfBoundsException e) {
  679. LOG.atWarn().withThrowable(e).log("IBM JDK failed with TexturePaintContext AIOOBE - try adding the following to the VM parameter:\n" +
  680. "-Xjit:exclude={sun/java2d/pipe/AlphaPaintPipe.renderPathTile(Ljava/lang/Object;[BIIIIII)V} and " +
  681. "search for 'JIT Problem Determination for IBM SDK using -Xjit' (http://www-01.ibm.com/support/docview.wss?uid=swg21294023) " +
  682. "for how to add/determine further excludes");
  683. }
  684. }
  685. }