View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
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.htmlunit.platform.canvas.rendering;
16  
17  import java.awt.AlphaComposite;
18  import java.awt.BasicStroke;
19  import java.awt.Color;
20  import java.awt.Composite;
21  import java.awt.Font;
22  import java.awt.Graphics2D;
23  import java.awt.RenderingHints;
24  import java.awt.Shape;
25  import java.awt.geom.AffineTransform;
26  import java.awt.geom.Arc2D;
27  import java.awt.geom.Path2D;
28  import java.awt.geom.Point2D;
29  import java.awt.geom.Rectangle2D;
30  import java.awt.image.BufferedImage;
31  import java.awt.image.ImageObserver;
32  import java.io.ByteArrayOutputStream;
33  import java.io.IOException;
34  import java.nio.charset.StandardCharsets;
35  import java.util.ArrayDeque;
36  import java.util.ArrayList;
37  import java.util.Base64;
38  import java.util.Deque;
39  import java.util.HashMap;
40  import java.util.List;
41  import java.util.Locale;
42  import java.util.Map;
43  
44  import javax.imageio.ImageIO;
45  import javax.imageio.ImageReader;
46  
47  import org.apache.commons.logging.Log;
48  import org.apache.commons.logging.LogFactory;
49  import org.htmlunit.platform.image.ImageIOImageData;
50  import org.htmlunit.util.StringUtils;
51  
52  /**
53   * The default implementation of {@link RenderingBackend}.
54   *
55   * @author Ronald Brill
56   */
57  public class AwtRenderingBackend implements RenderingBackend {
58  
59      private static final Log LOG = LogFactory.getLog(AwtRenderingBackend.class);
60      private static int ID_GENERATOR_;
61  
62      private static final Map<String, Color> KNOWN_COLORS = new HashMap<>();
63  
64      private final int id_;
65      private final BufferedImage image_;
66      private final Graphics2D graphics2D_;
67  
68      private AffineTransform transformation_;
69      private float globalAlpha_;
70      private int lineWidth_;
71      private Color fillColor_;
72      private Color strokeColor_;
73  
74      private final List<Path2D> subPaths_;
75      private final Deque<SaveState> savedStates_;
76  
77      static {
78          // see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
79  
80          // CSS Level 1
81          KNOWN_COLORS.put("black", Color.decode("#000000"));
82          KNOWN_COLORS.put("silver", Color.decode("#c0c0c0"));
83          KNOWN_COLORS.put("gray", Color.decode("#808080"));
84          KNOWN_COLORS.put("white", Color.decode("#ffffff"));
85          KNOWN_COLORS.put("maroon", Color.decode("#800000"));
86          KNOWN_COLORS.put("red", Color.decode("#ff0000"));
87          KNOWN_COLORS.put("purple", Color.decode("#800080"));
88          KNOWN_COLORS.put("fuchsia", Color.decode("#ff00ff"));
89          KNOWN_COLORS.put("green", Color.decode("#008000"));
90          KNOWN_COLORS.put("lime", Color.decode("#00ff00"));
91          KNOWN_COLORS.put("olive", Color.decode("#808000"));
92          KNOWN_COLORS.put("yellow", Color.decode("#ffff00"));
93          KNOWN_COLORS.put("navy", Color.decode("#000080"));
94          KNOWN_COLORS.put("blue", Color.decode("#0000ff"));
95          KNOWN_COLORS.put("teal", Color.decode("#008080"));
96          KNOWN_COLORS.put("aqua", Color.decode("#00ffff"));
97  
98          // CSS Level 2 (Revision 1)
99          KNOWN_COLORS.put("orange", Color.decode("#ffa500"));
100 
101         // CSS Color Module Level 3
102         KNOWN_COLORS.put("aliceblue", Color.decode("#f0f8ff"));
103         KNOWN_COLORS.put("antiquewhite", Color.decode("#faebd7"));
104         KNOWN_COLORS.put("aquamarine", Color.decode("#7fffd4"));
105         KNOWN_COLORS.put("azure", Color.decode("#f0ffff"));
106         KNOWN_COLORS.put("beige", Color.decode("#f5f5dc"));
107         KNOWN_COLORS.put("bisque", Color.decode("#ffe4c4"));
108         KNOWN_COLORS.put("blanchedalmond", Color.decode("#ffebcd"));
109         KNOWN_COLORS.put("blueviolet", Color.decode("#8a2be2"));
110         KNOWN_COLORS.put("brown", Color.decode("#a52a2a"));
111         KNOWN_COLORS.put("burlywood", Color.decode("#deb887"));
112         KNOWN_COLORS.put("cadetblue", Color.decode("#5f9ea0"));
113         KNOWN_COLORS.put("chartreuse", Color.decode("#7fff00"));
114         KNOWN_COLORS.put("chocolate", Color.decode("#d2691e"));
115         KNOWN_COLORS.put("coral", Color.decode("#ff7f50"));
116         KNOWN_COLORS.put("cornflowerblue", Color.decode("#6495ed"));
117         KNOWN_COLORS.put("cornsilk", Color.decode("#fff8dc"));
118         KNOWN_COLORS.put("crimson", Color.decode("#dc143c"));
119         KNOWN_COLORS.put("cyan", Color.decode("#00ffff")); // synonym of aqua
120         KNOWN_COLORS.put("darkblue", Color.decode("#00008b"));
121         KNOWN_COLORS.put("darkcyan", Color.decode("#008b8b"));
122         KNOWN_COLORS.put("darkgoldenrod", Color.decode("#b8860b"));
123         KNOWN_COLORS.put("darkgray", Color.decode("#a9a9a9"));
124         KNOWN_COLORS.put("darkgreen", Color.decode("#006400"));
125         KNOWN_COLORS.put("darkgrey", Color.decode("#a9a9a9"));
126         KNOWN_COLORS.put("darkkhaki", Color.decode("#bdb76b"));
127         KNOWN_COLORS.put("darkmagenta", Color.decode("#8b008b"));
128         KNOWN_COLORS.put("darkolivegreen", Color.decode("#556b2f"));
129         KNOWN_COLORS.put("darkorange", Color.decode("#ff8c00"));
130         KNOWN_COLORS.put("darkorchid", Color.decode("#9932cc"));
131         KNOWN_COLORS.put("darkred", Color.decode("#8b0000"));
132         KNOWN_COLORS.put("darksalmon", Color.decode("#e9967a"));
133         KNOWN_COLORS.put("darkseagreen", Color.decode("#8fbc8f"));
134         KNOWN_COLORS.put("darkslateblue", Color.decode("#483d8b"));
135         KNOWN_COLORS.put("darkslategray", Color.decode("#2f4f4f"));
136         KNOWN_COLORS.put("darkslategrey", Color.decode("#2f4f4f"));
137         KNOWN_COLORS.put("darkturquoise", Color.decode("#00ced1"));
138         KNOWN_COLORS.put("darkviolet", Color.decode("#9400d3"));
139         KNOWN_COLORS.put("deeppink", Color.decode("#ff1493"));
140         KNOWN_COLORS.put("deepskyblue", Color.decode("#00bfff"));
141         KNOWN_COLORS.put("dimgray", Color.decode("#696969"));
142         KNOWN_COLORS.put("dimgrey", Color.decode("#696969"));
143         KNOWN_COLORS.put("dodgerblue", Color.decode("#1e90ff"));
144         KNOWN_COLORS.put("firebrick", Color.decode("#b22222"));
145         KNOWN_COLORS.put("floralwhite", Color.decode("#fffaf0"));
146         KNOWN_COLORS.put("forestgreen", Color.decode("#228b22"));
147         KNOWN_COLORS.put("gainsboro", Color.decode("#dcdcdc"));
148         KNOWN_COLORS.put("ghostwhite", Color.decode("#f8f8ff"));
149         KNOWN_COLORS.put("gold", Color.decode("#ffd700"));
150         KNOWN_COLORS.put("goldenrod", Color.decode("#daa520"));
151         KNOWN_COLORS.put("greenyellow", Color.decode("#adff2f"));
152         KNOWN_COLORS.put("grey", Color.decode("#808080"));
153         KNOWN_COLORS.put("honeydew", Color.decode("#f0fff0"));
154         KNOWN_COLORS.put("hotpink", Color.decode("#ff69b4"));
155         KNOWN_COLORS.put("indianred", Color.decode("#cd5c5c"));
156         KNOWN_COLORS.put("indigo", Color.decode("#4b0082"));
157         KNOWN_COLORS.put("ivory", Color.decode("#fffff0"));
158         KNOWN_COLORS.put("khaki", Color.decode("#f0e68c"));
159         KNOWN_COLORS.put("lavender", Color.decode("#e6e6fa"));
160         KNOWN_COLORS.put("lavenderblush", Color.decode("#fff0f5"));
161         KNOWN_COLORS.put("lawngreen", Color.decode("#7cfc00"));
162         KNOWN_COLORS.put("lemonchiffon", Color.decode("#fffacd"));
163         KNOWN_COLORS.put("lightblue", Color.decode("#add8e6"));
164         KNOWN_COLORS.put("lightcoral", Color.decode("#f08080"));
165         KNOWN_COLORS.put("lightcyan", Color.decode("#e0ffff"));
166         KNOWN_COLORS.put("lightgoldenrodyellow", Color.decode("#fafad2"));
167         KNOWN_COLORS.put("lightgray", Color.decode("#d3d3d3"));
168         KNOWN_COLORS.put("lightgreen", Color.decode("#90ee90"));
169         KNOWN_COLORS.put("lightgrey", Color.decode("#d3d3d3"));
170         KNOWN_COLORS.put("lightpink", Color.decode("#ffb6c1"));
171         KNOWN_COLORS.put("lightsalmon", Color.decode("#ffa07a"));
172         KNOWN_COLORS.put("lightseagreen", Color.decode("#20b2aa"));
173         KNOWN_COLORS.put("lightskyblue", Color.decode("#87cefa"));
174         KNOWN_COLORS.put("lightslategray", Color.decode("#778899"));
175         KNOWN_COLORS.put("lightslategrey", Color.decode("#778899"));
176         KNOWN_COLORS.put("lightsteelblue", Color.decode("#b0c4de"));
177         KNOWN_COLORS.put("lightyellow", Color.decode("#ffffe0"));
178         KNOWN_COLORS.put("limegreen", Color.decode("#32cd32"));
179         KNOWN_COLORS.put("linen", Color.decode("#faf0e6"));
180         KNOWN_COLORS.put("magenta", Color.decode("#ff00ff")); // synonym of fuchsia
181         KNOWN_COLORS.put("mediumaquamarine", Color.decode("#66cdaa"));
182         KNOWN_COLORS.put("mediumblue", Color.decode("#0000cd"));
183         KNOWN_COLORS.put("mediumorchid", Color.decode("#ba55d3"));
184         KNOWN_COLORS.put("mediumpurple", Color.decode("#9370db"));
185         KNOWN_COLORS.put("mediumseagreen", Color.decode("#3cb371"));
186         KNOWN_COLORS.put("mediumslateblue", Color.decode("#7b68ee"));
187         KNOWN_COLORS.put("mediumspringgreen", Color.decode("#00fa9a"));
188         KNOWN_COLORS.put("mediumturquoise", Color.decode("#48d1cc"));
189         KNOWN_COLORS.put("mediumvioletred", Color.decode("#c71585"));
190         KNOWN_COLORS.put("midnightblue", Color.decode("#191970"));
191         KNOWN_COLORS.put("mintcream", Color.decode("#f5fffa"));
192         KNOWN_COLORS.put("mistyrose", Color.decode("#ffe4e1"));
193         KNOWN_COLORS.put("moccasin", Color.decode("#ffe4b5"));
194         KNOWN_COLORS.put("navajowhite", Color.decode("#ffdead"));
195         KNOWN_COLORS.put("oldlace", Color.decode("#fdf5e6"));
196         KNOWN_COLORS.put("olivedrab", Color.decode("#6b8e23"));
197         KNOWN_COLORS.put("orangered", Color.decode("#ff4500"));
198         KNOWN_COLORS.put("orchid", Color.decode("#da70d6"));
199         KNOWN_COLORS.put("palegoldenrod", Color.decode("#eee8aa"));
200         KNOWN_COLORS.put("palegreen", Color.decode("#98fb98"));
201         KNOWN_COLORS.put("paleturquoise", Color.decode("#afeeee"));
202         KNOWN_COLORS.put("palevioletred", Color.decode("#db7093"));
203         KNOWN_COLORS.put("papayawhip", Color.decode("#ffefd5"));
204         KNOWN_COLORS.put("peachpuff", Color.decode("#ffdab9"));
205         KNOWN_COLORS.put("peru", Color.decode("#cd853f"));
206         KNOWN_COLORS.put("pink", Color.decode("#ffc0cb"));
207         KNOWN_COLORS.put("plum", Color.decode("#dda0dd"));
208         KNOWN_COLORS.put("powderblue", Color.decode("#b0e0e6"));
209         KNOWN_COLORS.put("rosybrown", Color.decode("#bc8f8f"));
210         KNOWN_COLORS.put("royalblue", Color.decode("#4169e1"));
211         KNOWN_COLORS.put("saddlebrown", Color.decode("#8b4513"));
212         KNOWN_COLORS.put("salmon", Color.decode("#fa8072"));
213         KNOWN_COLORS.put("sandybrown", Color.decode("#f4a460"));
214         KNOWN_COLORS.put("seagreen", Color.decode("#2e8b57"));
215         KNOWN_COLORS.put("seashell", Color.decode("#fff5ee"));
216         KNOWN_COLORS.put("sienna", Color.decode("#a0522d"));
217         KNOWN_COLORS.put("skyblue", Color.decode("#87ceeb"));
218         KNOWN_COLORS.put("slateblue", Color.decode("#6a5acd"));
219         KNOWN_COLORS.put("slategray", Color.decode("#708090"));
220         KNOWN_COLORS.put("slategrey", Color.decode("#708090"));
221         KNOWN_COLORS.put("snow", Color.decode("#fffafa"));
222         KNOWN_COLORS.put("springgreen", Color.decode("#00ff7f"));
223         KNOWN_COLORS.put("steelblue", Color.decode("#4682b4"));
224         KNOWN_COLORS.put("tan", Color.decode("#d2b48c"));
225         KNOWN_COLORS.put("thistle", Color.decode("#d8bfd8"));
226         KNOWN_COLORS.put("tomato", Color.decode("#ff6347"));
227         KNOWN_COLORS.put("turquoise", Color.decode("#40e0d0"));
228         KNOWN_COLORS.put("violet", Color.decode("#ee82ee"));
229         KNOWN_COLORS.put("wheat", Color.decode("#f5deb3"));
230         KNOWN_COLORS.put("whitesmoke", Color.decode("#f5f5f5"));
231         KNOWN_COLORS.put("yellowgreen", Color.decode("#9acd32"));
232         // CSS Color Module Level 4
233         KNOWN_COLORS.put("rebeccapurple", Color.decode("#663399"));
234     }
235 
236     private static synchronized int nextId() {
237         return ID_GENERATOR_++;
238     }
239 
240     /**
241      * Constructor.
242      * @param imageWidth the width
243      * @param imageHeight the height
244      */
245     public AwtRenderingBackend(final int imageWidth, final int imageHeight) {
246         id_ = nextId();
247         if (LOG.isDebugEnabled()) {
248             LOG.debug("[" + id_ + "] AwtRenderingBackend(" + imageWidth + ", " + imageHeight + ")");
249         }
250 
251         image_ = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
252         graphics2D_ = image_.createGraphics();
253 
254         graphics2D_.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
255         graphics2D_.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
256         graphics2D_.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
257 
258         // reset
259         fillColor_ = Color.black;
260         strokeColor_ = Color.black;
261         lineWidth_ = 1;
262         transformation_ = new AffineTransform();
263         updateGlobalAlpha(1f);
264         graphics2D_.setClip(null);
265 
266         final Font font = new Font("SansSerif", Font.PLAIN, 10);
267         graphics2D_.setFont(font);
268 
269         graphics2D_.setBackground(new Color(0f, 0f, 0f, 0f));
270         graphics2D_.setColor(Color.black);
271         graphics2D_.clearRect(0, 0, imageWidth, imageHeight);
272 
273         subPaths_ = new ArrayList<>();
274         savedStates_ = new ArrayDeque<>();
275     }
276 
277     /**
278      * {@inheritDoc}
279      */
280     @Override
281     public double getGlobalAlpha() {
282         return globalAlpha_;
283     }
284 
285     /**
286      * {@inheritDoc}
287      */
288     @Override
289     public void setGlobalAlpha(final double globalAlpha) {
290         if (LOG.isDebugEnabled()) {
291             LOG.debug("[" + id_ + "] setGlobalAlpha(" + globalAlpha + ")");
292         }
293 
294         if (globalAlpha >= 0 && globalAlpha <= 1) {
295             updateGlobalAlpha((float) globalAlpha);
296         }
297     }
298 
299     private void updateGlobalAlpha(final float globalAlpha) {
300         globalAlpha_ = globalAlpha;
301         final AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, globalAlpha_);
302         graphics2D_.setComposite(composite);
303     }
304 
305     /**
306      * {@inheritDoc}
307      */
308     @Override
309     public void beginPath() {
310         if (LOG.isDebugEnabled()) {
311             LOG.debug("[" + id_ + "] beginPath()");
312         }
313         subPaths_.clear();
314     }
315 
316     /**
317      * {@inheritDoc}
318      */
319     @Override
320     public void ellipse(final double x, final double y,
321             final double radiusX, final double radiusY,
322             final double rotation, final double startAngle, final double endAngle,
323             final boolean anticlockwise) {
324         if (LOG.isDebugEnabled()) {
325             LOG.debug("[" + id_ + "] ellipse()");
326         }
327 
328         final Path2D subPath = getCurrentSubPath();
329         if (subPath != null) {
330             final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
331             final double startAngleDegree = 360 - (startAngle * 180 / Math.PI);
332             final double endAngleDegree = 360 - (endAngle * 180 / Math.PI);
333 
334             double extendAngle = startAngleDegree - endAngleDegree;
335             extendAngle = Math.min(360, Math.abs(extendAngle));
336             if (anticlockwise && extendAngle < 360) {
337                 extendAngle = extendAngle - 360;
338             }
339 
340             final AffineTransform transformation = new AffineTransform();
341             transformation.rotate(rotation, p.getX(), p.getY());
342             final Arc2D arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
343                                             startAngleDegree, extendAngle * -1, Arc2D.OPEN);
344             subPath.append(transformation.createTransformedShape(arc), false);
345         }
346     }
347 
348     /**
349      * {@inheritDoc}
350      */
351     @Override
352     public void bezierCurveTo(final double cp1x, final double cp1y,
353             final double cp2x, final double cp2y, final double x, final double y) {
354         if (LOG.isDebugEnabled()) {
355             LOG.debug("[" + id_ + "] bezierCurveTo()");
356         }
357 
358         final Path2D subPath = getCurrentSubPath();
359         if (subPath != null) {
360             final Point2D cp1 = transformation_.transform(new Point2D.Double(cp1x, cp1y), null);
361             final Point2D cp2 = transformation_.transform(new Point2D.Double(cp2x, cp2y), null);
362             final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
363             subPath.curveTo(cp1.getX(), cp1.getY(), cp2.getX(), cp2.getY(), p.getX(), p.getY());
364         }
365     }
366 
367     /**
368      * {@inheritDoc}
369      */
370     @Override
371     public void arc(final double x, final double y, final double radius, final double startAngle,
372             final double endAngle, final boolean anticlockwise) {
373         if (LOG.isDebugEnabled()) {
374             LOG.debug("[" + id_ + "] arc()");
375         }
376 
377         final Path2D subPath = getCurrentSubPath();
378         if (subPath != null) {
379             final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
380             final double startAngleDegree = 360 - (startAngle * 180 / Math.PI);
381             final double endAngleDegree = 360 - (endAngle * 180 / Math.PI);
382 
383             double extendAngle = startAngleDegree - endAngleDegree;
384             extendAngle = Math.min(360, Math.abs(extendAngle));
385             if (anticlockwise && extendAngle < 360) {
386                 extendAngle = extendAngle - 360;
387             }
388             final Arc2D arc = new Arc2D.Double(p.getX() - radius, p.getY() - radius, radius * 2, radius * 2,
389                                             startAngleDegree, extendAngle * -1, Arc2D.OPEN);
390             subPath.append(arc, false);
391         }
392     }
393 
394     /**
395      * {@inheritDoc}
396      */
397     @Override
398     public void clearRect(final double x, final double y, final double w, final double h) {
399         if (LOG.isDebugEnabled()) {
400             LOG.debug("[" + id_ + "] clearRect(" + x + ", " + y + ", " + w + ", " + h + ")");
401         }
402 
403         final Composite saved = graphics2D_.getComposite();
404 
405         graphics2D_.setColor(Color.BLACK);
406         graphics2D_.setComposite(AlphaComposite.Clear); // overpaint
407         final Rectangle2D rect = new Rectangle2D.Double(x, y, w, h);
408         graphics2D_.fill(transformation_.createTransformedShape(rect));
409 
410         graphics2D_.setComposite(saved);
411     }
412 
413     /**
414      * {@inheritDoc}
415      */
416     @Override
417     public void drawImage(final org.htmlunit.platform.image.ImageData imageData,
418             final int sx, final int sy, final Integer sWidth, final Integer sHeight,
419             final int dx, final int dy, final Integer dWidth, final Integer dHeight) throws IOException {
420         if (LOG.isDebugEnabled()) {
421             LOG.debug("[" + id_ + "] drawImage(" + sx + ", " + sy + ", " + sWidth + ", " + sHeight
422                     + "," + dx + ", " + dy + ", " + dWidth + ", " + dHeight + ")");
423         }
424 
425         try {
426             final ImageReader imageReader = ((ImageIOImageData) imageData).getImageReader();
427 
428             if (imageReader.getNumImages(true) != 0) {
429                 final BufferedImage img = imageReader.read(0);
430 
431                 final AffineTransform savedTransform = graphics2D_.getTransform();
432                 try {
433                     graphics2D_.setTransform(transformation_);
434                     graphics2D_.setColor(fillColor_);
435 
436                     final int sx2;
437                     if (sWidth == null) {
438                         sx2 = sx + img.getWidth();
439                     }
440                     else {
441                         sx2 = sx + sWidth;
442                     }
443 
444                     final int sy2;
445                     if (sHeight == null) {
446                         sy2 = sy + img.getHeight();
447                     }
448                     else {
449                         sy2 = sy + sHeight;
450                     }
451 
452                     int dx1 = dx;
453                     final int dx2;
454                     if (dWidth == null) {
455                         dx2 = dx + img.getWidth();
456                     }
457                     else {
458                         if (dWidth < 0) {
459                             dx1 = dx1 + dWidth;
460                             dx2 = dx1 - dWidth;
461                         }
462                         else {
463                             dx2 = dx1 + dWidth;
464                         }
465                     }
466 
467                     int dy1 = dy;
468                     final int dy2;
469                     if (dHeight == null) {
470                         dy2 = dy + img.getHeight();
471                     }
472                     else {
473                         if (dHeight < 0) {
474                             dy1 = dy1 + dHeight;
475                             dy2 = dy1 - dHeight;
476                         }
477                         else {
478                             dy2 = dy1 + dHeight;
479                         }
480                     }
481 
482                     final Object done = new Object();
483                     final ImageObserver imageObserver = (img1, flags, x, y, width, height) -> {
484 
485                         if ((flags & ImageObserver.ALLBITS) == ImageObserver.ALLBITS) {
486                             return true;
487                         }
488 
489                         if ((flags & ImageObserver.ABORT) == ImageObserver.ABORT
490                                 || (flags & ImageObserver.ERROR) == ImageObserver.ERROR) {
491                             return true;
492                         }
493 
494                         synchronized (done) {
495                             done.notify();
496                         }
497 
498                         return false;
499                     };
500 
501                     synchronized (done) {
502                         final boolean completelyLoaded =
503                                 graphics2D_.drawImage(img, dx1, dy1, dx2, dy2, sx, sy, sx2, sy2, imageObserver);
504                         if (!completelyLoaded) {
505                             while (true) {
506                                 try {
507                                     done.wait(4 * 1000); // max 4s
508                                     break;
509                                 }
510                                 catch (final InterruptedException e) {
511                                     LOG.error("[" + id_ + "] AwtRenderingBackend interrupted "
512                                             + "while waiting for drawImage to finish.", e);
513 
514                                     // restore interrupted status
515                                     Thread.currentThread().interrupt();
516                                 }
517                             }
518                         }
519                     }
520                 }
521                 finally {
522                     graphics2D_.setTransform(savedTransform);
523                 }
524             }
525         }
526         catch (final ClassCastException e) {
527             LOG.error("[" + id_ + "] drawImage(..) failed", e);
528         }
529     }
530 
531     /**
532      * {@inheritDoc}
533      */
534     @Override
535     public String encodeToString(final String type) throws IOException {
536         String imageType = type;
537         if (imageType != null && imageType.startsWith("image/")) {
538             imageType = imageType.substring(6);
539         }
540         try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
541             ImageIO.write(image_, imageType, bos);
542 
543             final byte[] imageBytes = bos.toByteArray();
544             return new String(Base64.getEncoder().encode(imageBytes), StandardCharsets.US_ASCII);
545         }
546     }
547 
548     /**
549      * {@inheritDoc}
550      */
551     @Override
552     public void fill() {
553         if (LOG.isDebugEnabled()) {
554             LOG.debug("[" + id_ + "] fill()");
555         }
556 
557         graphics2D_.setStroke(new BasicStroke(getLineWidth()));
558         graphics2D_.setColor(fillColor_);
559         for (final Path2D path2d : subPaths_) {
560             graphics2D_.fill(path2d);
561         }
562     }
563 
564     /**
565      * {@inheritDoc}
566      */
567     @Override
568     public void fillRect(final int x, final int y, final int w, final int h) {
569         if (LOG.isDebugEnabled()) {
570             LOG.debug("[" + id_ + "] fillRect(" + x + ", "  + y + ", "  + w + ", "  + h + ")");
571         }
572 
573         graphics2D_.setColor(fillColor_);
574         final Rectangle2D rect = new Rectangle2D.Double(x, y, w, h);
575         graphics2D_.fill(transformation_.createTransformedShape(rect));
576     }
577 
578     /**
579      * {@inheritDoc}
580      */
581     @Override
582     public void fillText(final String text, final double x, final double y) {
583         if (LOG.isDebugEnabled()) {
584             LOG.debug("[" + id_ + "] fillText('" + text + "', "  + x + ", "  + y + ")");
585         }
586 
587         final AffineTransform savedTransform = graphics2D_.getTransform();
588         try {
589             graphics2D_.setTransform(transformation_);
590             graphics2D_.setColor(fillColor_);
591             graphics2D_.drawString(text, (int) x, (int) y);
592         }
593         finally {
594             graphics2D_.setTransform(savedTransform);
595         }
596     }
597 
598     /**
599      * {@inheritDoc}
600      */
601     @Override
602     public byte[] getBytes(final int width, final int height, final int sx, final int sy) {
603         if (LOG.isDebugEnabled()) {
604             LOG.debug("[" + id_ + "] getBytes(" + width + ", " + height + ", " + sx + ", " + sy + ")");
605         }
606 
607         final byte[] array = new byte[width * height * 4];
608         int index = 0;
609         for (int x = sx; x < sx + width; x++) {
610             if (x < 0 || x >= image_.getWidth()) {
611                 array[index++] = (byte) 0;
612                 array[index++] = (byte) 0;
613                 array[index++] = (byte) 0;
614                 array[index++] = (byte) 0;
615             }
616             else {
617                 for (int y = sy; y < sy + height; y++) {
618                     if (y < 0 || y >= image_.getHeight()) {
619                         array[index++] = (byte) 0;
620                         array[index++] = (byte) 0;
621                         array[index++] = (byte) 0;
622                         array[index++] = (byte) 0;
623                     }
624                     else {
625                         final int color = image_.getRGB(x, y);
626                         array[index++] = (byte) ((color & 0xff0000) >> 16);
627                         array[index++] = (byte) ((color & 0xff00) >> 8);
628                         array[index++] = (byte) (color & 0xff);
629                         array[index++] = (byte) ((color & 0xff000000) >>> 24);
630                     }
631                 }
632             }
633         }
634         return array;
635     }
636 
637     /**
638      * {@inheritDoc}
639      */
640     @Override
641     public void lineTo(final double x, final double y) {
642         if (LOG.isDebugEnabled()) {
643             LOG.debug("[" + id_ + "] lineTo(" + x + ", " + y + ")");
644         }
645 
646         final Path2D subPath = getCurrentSubPath();
647         if (subPath != null) {
648             final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
649             subPath.lineTo(p.getX(), p.getY());
650         }
651     }
652 
653     /**
654      * {@inheritDoc}
655      */
656     @Override
657     public void moveTo(final double x, final double y) {
658         if (LOG.isDebugEnabled()) {
659             LOG.debug("[" + id_ + "] moveTo(" + x + ", " + y + ")");
660         }
661 
662         final Path2D subPath = new Path2D.Double();
663         final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
664         subPath.moveTo(p.getX(), p.getY());
665         subPaths_.add(subPath);
666     }
667 
668     /**
669      * {@inheritDoc}
670      */
671     @Override
672     public void putImageData(final byte[] imageDataBytes,
673             final int imageDataWidth, final int imageDataHeight,
674             final int dx, final int dy, final int dirtyX, final int dirtyY,
675             final int dirtyWidth, final int dirtyHeight) {
676 
677         if (LOG.isDebugEnabled()) {
678             LOG.debug("[" + id_ + "] putImageData()");
679         }
680 
681         final Color orgColor = graphics2D_.getColor();
682 
683         final int width = dx + imageDataWidth;
684         final int height = dy + imageDataHeight;
685         final int imageWidth = dirtyX + dirtyWidth;
686         final int imageHeight = dirtyY + dirtyHeight;
687 
688         int byteIdx = 0;
689         int imageX = 0;
690         int imageY = 0;
691         for (int insertY = dy; insertY < height; insertY++) {
692             for (int insertX = dx; insertX < width; insertX++) {
693                 if (0 <= insertX && insertX < image_.getWidth()
694                         && 0 <= insertY && insertY < image_.getHeight()
695                         && dirtyX <= imageX && imageX < imageWidth
696                         && dirtyY <= imageY && imageY < imageHeight) {
697                     final int r = imageDataBytes[byteIdx++] & 0xFF;
698                     final int g = imageDataBytes[byteIdx++] & 0xFF;
699                     final int b = imageDataBytes[byteIdx++] & 0xFF;
700                     final int a = imageDataBytes[byteIdx++] & 0xFF;
701                     final Color color = new Color(r, g, b, a);
702                     graphics2D_.setColor(color);
703                     graphics2D_.drawLine(insertX, insertY, insertX, insertY);
704                 }
705                 else {
706                     byteIdx += 4;
707                 }
708 
709                 imageX++;
710                 if (imageX == imageDataWidth) {
711                     imageX = 0;
712                     imageY++;
713                 }
714             }
715         }
716 
717         graphics2D_.setColor(orgColor);
718     }
719 
720     /**
721      * {@inheritDoc}
722      */
723     @Override
724     public void quadraticCurveTo(final double cpx, final double cpy,
725                     final double x, final double y) {
726         if (LOG.isDebugEnabled()) {
727             LOG.debug("[" + id_ + "] quadraticCurveTo()");
728         }
729 
730         final Path2D subPath = getCurrentSubPath();
731         if (subPath != null) {
732             final Point2D cp = transformation_.transform(new Point2D.Double(cpx, cpy), null);
733             final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
734             subPath.quadTo(cp.getX(), cp.getY(), p.getX(), p.getY());
735         }
736     }
737 
738     /**
739      * {@inheritDoc}
740      */
741     @Override
742     public void rect(final double x, final double y, final double w, final double h) {
743         if (LOG.isDebugEnabled()) {
744             LOG.debug("[" + id_ + "] rect()");
745         }
746 
747         final Path2D subPath = getCurrentSubPath();
748         if (subPath != null) {
749             final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
750             final Rectangle2D rect = new Rectangle2D.Double(p.getX(), p.getY(), w, h);
751             subPath.append(rect, false);
752         }
753     }
754 
755     /**
756      * {@inheritDoc}
757      */
758     @Override
759     public void setFillStyle(final String fillStyle) {
760         if (LOG.isDebugEnabled()) {
761             LOG.debug("[" + id_ + "] setFillStyle(" + fillStyle + ")");
762         }
763 
764         final Color color = extractColor(fillStyle);
765         if (color != null) {
766             fillColor_ = color;
767         }
768     }
769 
770     /**
771      * {@inheritDoc}
772      */
773     @Override
774     public void setStrokeStyle(final String strokeStyle) {
775         if (LOG.isDebugEnabled()) {
776             LOG.debug("[" + id_ + "] setStrokeStyle(" + strokeStyle + ")");
777         }
778 
779         final Color color = extractColor(strokeStyle);
780         if (color != null) {
781             strokeColor_ = color;
782         }
783     }
784 
785     private static Color extractColor(final String style) {
786         final String tmpStyle = style.replaceAll("\\s", "");
787 
788         Color color = toAwtColor(StringUtils.findColorRGB(tmpStyle));
789         if (color == null) {
790             color = toAwtColor(StringUtils.findColorRGBA(tmpStyle));
791         }
792         if (color == null) {
793             color = toAwtColor(StringUtils.findColorHSL(tmpStyle));
794         }
795 
796         if (color == null) {
797             if (tmpStyle.length() > 0 && tmpStyle.charAt(0) == '#') {
798                 color = toAwtColor(StringUtils.asColorHexadecimal(tmpStyle));
799             }
800             else {
801                 color = KNOWN_COLORS.get(tmpStyle.toLowerCase(Locale.ROOT));
802             }
803         }
804         return color;
805     }
806 
807     /**
808      * {@inheritDoc}
809      */
810     @Override
811     public int getLineWidth() {
812         return lineWidth_;
813     }
814 
815     /**
816      * {@inheritDoc}
817      */
818     @Override
819     public void restore() {
820         if (LOG.isDebugEnabled()) {
821             LOG.debug("[" + id_ + "] restore()");
822         }
823 
824         if (savedStates_.isEmpty()) {
825             return;
826         }
827 
828         savedStates_.pop().applyOn(this);
829     }
830 
831     /**
832      * {@inheritDoc}
833      */
834     @Override
835     public void rotate(final double angle) {
836         if (LOG.isDebugEnabled()) {
837             LOG.debug("[" + id_ + "] rotate()");
838         }
839 
840         transformation_.rotate(angle);
841     }
842 
843     /**
844      * {@inheritDoc}
845      */
846     @Override
847     public void save() {
848         if (LOG.isDebugEnabled()) {
849             LOG.debug("[" + id_ + "] save()");
850         }
851 
852         savedStates_.push(new SaveState(this));
853     }
854 
855     /**
856      * {@inheritDoc}
857      */
858     @Override
859     public void setLineWidth(final int lineWidth) {
860         if (LOG.isDebugEnabled()) {
861             LOG.debug("[" + id_ + "] setLineWidth(" + lineWidth + ")");
862         }
863 
864         lineWidth_ = lineWidth;
865     }
866 
867     /**
868      * {@inheritDoc}
869      */
870     @Override
871     public void setTransform(final double m11, final double m12,
872                     final double m21, final double m22, final double dx, final double dy) {
873         if (LOG.isDebugEnabled()) {
874             LOG.debug("[" + id_ + "] setTransform("
875                         + m11 + ", "  + m12 + ", "  + m21 + ", "  + m22 + ", "  + dx + ", "  + dy + ")");
876         }
877 
878         transformation_ = new AffineTransform(m11, m12, m21, m22, dx, dy);
879     }
880 
881     /**
882      * {@inheritDoc}
883      */
884     @Override
885     public void stroke() {
886         if (LOG.isDebugEnabled()) {
887             LOG.debug("[" + id_ + "] stroke()");
888         }
889 
890         graphics2D_.setStroke(new BasicStroke(getLineWidth()));
891         graphics2D_.setColor(strokeColor_);
892         for (final Path2D path2d : subPaths_) {
893             graphics2D_.draw(path2d);
894         }
895     }
896 
897     /**
898      * {@inheritDoc}
899      */
900     @Override
901     public void strokeRect(final int x, final int y, final int w, final int h) {
902         if (LOG.isDebugEnabled()) {
903             LOG.debug("[" + id_ + "] strokeRect(" + x + ", "  + y + ", "  + w + ", "  + h + ")");
904         }
905 
906         graphics2D_.setColor(strokeColor_);
907         final Rectangle2D rect = new Rectangle2D.Double(x, y, w, h);
908         graphics2D_.draw(transformation_.createTransformedShape(rect));
909     }
910 
911     /**
912      * {@inheritDoc}
913      */
914     @Override
915     public void transform(final double m11, final double m12,
916                     final double m21, final double m22, final double dx, final double dy) {
917         if (LOG.isDebugEnabled()) {
918             LOG.debug("[" + id_ + "] transform()");
919         }
920 
921         transformation_.concatenate(new AffineTransform(m11, m12, m21, m22, dx, dy));
922     }
923 
924     /**
925      * {@inheritDoc}
926      */
927     @Override
928     public void translate(final int x, final int y) {
929         if (LOG.isDebugEnabled()) {
930             LOG.debug("[" + id_ + "] translate()");
931         }
932 
933         transformation_.translate(x, y);
934     }
935 
936     /**
937      * {@inheritDoc}
938      */
939     @Override
940     public void clip(final RenderingBackend.WindingRule windingRule,
941             final org.htmlunit.javascript.host.canvas.Path2D path) {
942         if (LOG.isDebugEnabled()) {
943             LOG.debug("[" + id_ + "] clip(" + windingRule + ", " + path + ")");
944         }
945 
946         if (path == null && subPaths_.isEmpty()) {
947             graphics2D_.setClip(null);
948             return;
949         }
950 
951         final Path2D currentPath;
952         if (path == null) {
953             currentPath = subPaths_.get(subPaths_.size() - 1);
954         }
955         else {
956             // currentPath = path.getPath2D();
957             currentPath = null;
958         }
959         currentPath.closePath();
960 
961         if (windingRule == WindingRule.NON_ZERO) {
962             currentPath.setWindingRule(Path2D.WIND_NON_ZERO);
963         }
964         else {
965             currentPath.setWindingRule(Path2D.WIND_EVEN_ODD);
966         }
967 
968         graphics2D_.clip(currentPath);
969     }
970 
971     /**
972      * {@inheritDoc}
973      */
974     @Override
975     public void closePath() {
976         if (LOG.isDebugEnabled()) {
977             LOG.debug("[" + id_ + "] closePath()");
978         }
979 
980         if (subPaths_.isEmpty()) {
981             return;
982         }
983         subPaths_.get(subPaths_.size() - 1).closePath();
984     }
985 
986     private Path2D getCurrentSubPath() {
987         if (subPaths_.isEmpty()) {
988             final Path2D subPath = new Path2D.Double();
989             subPaths_.add(subPath);
990             return subPath;
991         }
992         return subPaths_.get(subPaths_.size() - 1);
993     }
994 
995     private static final class SaveState {
996         private final AffineTransform transformation_;
997         private final float globalAlpha_;
998         private final int lineWidth_;
999         private final Color fillColor_;
1000         private final Color strokeColor_;
1001         private final Shape clip_;
1002 
1003         SaveState(final AwtRenderingBackend backend) {
1004             transformation_ = backend.transformation_;
1005             globalAlpha_ = backend.globalAlpha_;
1006             lineWidth_ = backend.lineWidth_;
1007             fillColor_ = backend.fillColor_;
1008             strokeColor_ = backend.strokeColor_;
1009 
1010             clip_ = backend.graphics2D_.getClip();
1011         }
1012 
1013         void applyOn(final AwtRenderingBackend backend) {
1014             backend.transformation_ = transformation_;
1015             backend.globalAlpha_ = globalAlpha_;
1016             backend.lineWidth_ = lineWidth_;
1017             backend.fillColor_ = fillColor_;
1018             backend.strokeColor_ = strokeColor_;
1019 
1020             backend.graphics2D_.setClip(clip_);
1021         }
1022     }
1023 
1024     private static Color toAwtColor(final org.htmlunit.html.impl.Color color) {
1025         if (color == null) {
1026             return null;
1027         }
1028         return new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
1029     }
1030 }