1
2
3
4
5
6
7
8
9
10
11
12
13
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.PathIterator;
29 import java.awt.geom.Point2D;
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
54
55
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 float lineWidth_;
71 private Color fillColor_;
72 private Color strokeColor_;
73
74 private LineCap lineCap_;
75 private LineJoin lineJoin_;
76 private float miterLimit_;
77
78 private final List<Path2D> subPaths_;
79 private final Deque<SaveState> savedStates_;
80
81 static {
82
83
84
85 KNOWN_COLORS.put("black", Color.decode("#000000"));
86 KNOWN_COLORS.put("silver", Color.decode("#c0c0c0"));
87 KNOWN_COLORS.put("gray", Color.decode("#808080"));
88 KNOWN_COLORS.put("white", Color.decode("#ffffff"));
89 KNOWN_COLORS.put("maroon", Color.decode("#800000"));
90 KNOWN_COLORS.put("red", Color.decode("#ff0000"));
91 KNOWN_COLORS.put("purple", Color.decode("#800080"));
92 KNOWN_COLORS.put("fuchsia", Color.decode("#ff00ff"));
93 KNOWN_COLORS.put("green", Color.decode("#008000"));
94 KNOWN_COLORS.put("lime", Color.decode("#00ff00"));
95 KNOWN_COLORS.put("olive", Color.decode("#808000"));
96 KNOWN_COLORS.put("yellow", Color.decode("#ffff00"));
97 KNOWN_COLORS.put("navy", Color.decode("#000080"));
98 KNOWN_COLORS.put("blue", Color.decode("#0000ff"));
99 KNOWN_COLORS.put("teal", Color.decode("#008080"));
100 KNOWN_COLORS.put("aqua", Color.decode("#00ffff"));
101
102
103 KNOWN_COLORS.put("orange", Color.decode("#ffa500"));
104
105
106 KNOWN_COLORS.put("aliceblue", Color.decode("#f0f8ff"));
107 KNOWN_COLORS.put("antiquewhite", Color.decode("#faebd7"));
108 KNOWN_COLORS.put("aquamarine", Color.decode("#7fffd4"));
109 KNOWN_COLORS.put("azure", Color.decode("#f0ffff"));
110 KNOWN_COLORS.put("beige", Color.decode("#f5f5dc"));
111 KNOWN_COLORS.put("bisque", Color.decode("#ffe4c4"));
112 KNOWN_COLORS.put("blanchedalmond", Color.decode("#ffebcd"));
113 KNOWN_COLORS.put("blueviolet", Color.decode("#8a2be2"));
114 KNOWN_COLORS.put("brown", Color.decode("#a52a2a"));
115 KNOWN_COLORS.put("burlywood", Color.decode("#deb887"));
116 KNOWN_COLORS.put("cadetblue", Color.decode("#5f9ea0"));
117 KNOWN_COLORS.put("chartreuse", Color.decode("#7fff00"));
118 KNOWN_COLORS.put("chocolate", Color.decode("#d2691e"));
119 KNOWN_COLORS.put("coral", Color.decode("#ff7f50"));
120 KNOWN_COLORS.put("cornflowerblue", Color.decode("#6495ed"));
121 KNOWN_COLORS.put("cornsilk", Color.decode("#fff8dc"));
122 KNOWN_COLORS.put("crimson", Color.decode("#dc143c"));
123 KNOWN_COLORS.put("cyan", Color.decode("#00ffff"));
124 KNOWN_COLORS.put("darkblue", Color.decode("#00008b"));
125 KNOWN_COLORS.put("darkcyan", Color.decode("#008b8b"));
126 KNOWN_COLORS.put("darkgoldenrod", Color.decode("#b8860b"));
127 KNOWN_COLORS.put("darkgray", Color.decode("#a9a9a9"));
128 KNOWN_COLORS.put("darkgreen", Color.decode("#006400"));
129 KNOWN_COLORS.put("darkgrey", Color.decode("#a9a9a9"));
130 KNOWN_COLORS.put("darkkhaki", Color.decode("#bdb76b"));
131 KNOWN_COLORS.put("darkmagenta", Color.decode("#8b008b"));
132 KNOWN_COLORS.put("darkolivegreen", Color.decode("#556b2f"));
133 KNOWN_COLORS.put("darkorange", Color.decode("#ff8c00"));
134 KNOWN_COLORS.put("darkorchid", Color.decode("#9932cc"));
135 KNOWN_COLORS.put("darkred", Color.decode("#8b0000"));
136 KNOWN_COLORS.put("darksalmon", Color.decode("#e9967a"));
137 KNOWN_COLORS.put("darkseagreen", Color.decode("#8fbc8f"));
138 KNOWN_COLORS.put("darkslateblue", Color.decode("#483d8b"));
139 KNOWN_COLORS.put("darkslategray", Color.decode("#2f4f4f"));
140 KNOWN_COLORS.put("darkslategrey", Color.decode("#2f4f4f"));
141 KNOWN_COLORS.put("darkturquoise", Color.decode("#00ced1"));
142 KNOWN_COLORS.put("darkviolet", Color.decode("#9400d3"));
143 KNOWN_COLORS.put("deeppink", Color.decode("#ff1493"));
144 KNOWN_COLORS.put("deepskyblue", Color.decode("#00bfff"));
145 KNOWN_COLORS.put("dimgray", Color.decode("#696969"));
146 KNOWN_COLORS.put("dimgrey", Color.decode("#696969"));
147 KNOWN_COLORS.put("dodgerblue", Color.decode("#1e90ff"));
148 KNOWN_COLORS.put("firebrick", Color.decode("#b22222"));
149 KNOWN_COLORS.put("floralwhite", Color.decode("#fffaf0"));
150 KNOWN_COLORS.put("forestgreen", Color.decode("#228b22"));
151 KNOWN_COLORS.put("gainsboro", Color.decode("#dcdcdc"));
152 KNOWN_COLORS.put("ghostwhite", Color.decode("#f8f8ff"));
153 KNOWN_COLORS.put("gold", Color.decode("#ffd700"));
154 KNOWN_COLORS.put("goldenrod", Color.decode("#daa520"));
155 KNOWN_COLORS.put("greenyellow", Color.decode("#adff2f"));
156 KNOWN_COLORS.put("grey", Color.decode("#808080"));
157 KNOWN_COLORS.put("honeydew", Color.decode("#f0fff0"));
158 KNOWN_COLORS.put("hotpink", Color.decode("#ff69b4"));
159 KNOWN_COLORS.put("indianred", Color.decode("#cd5c5c"));
160 KNOWN_COLORS.put("indigo", Color.decode("#4b0082"));
161 KNOWN_COLORS.put("ivory", Color.decode("#fffff0"));
162 KNOWN_COLORS.put("khaki", Color.decode("#f0e68c"));
163 KNOWN_COLORS.put("lavender", Color.decode("#e6e6fa"));
164 KNOWN_COLORS.put("lavenderblush", Color.decode("#fff0f5"));
165 KNOWN_COLORS.put("lawngreen", Color.decode("#7cfc00"));
166 KNOWN_COLORS.put("lemonchiffon", Color.decode("#fffacd"));
167 KNOWN_COLORS.put("lightblue", Color.decode("#add8e6"));
168 KNOWN_COLORS.put("lightcoral", Color.decode("#f08080"));
169 KNOWN_COLORS.put("lightcyan", Color.decode("#e0ffff"));
170 KNOWN_COLORS.put("lightgoldenrodyellow", Color.decode("#fafad2"));
171 KNOWN_COLORS.put("lightgray", Color.decode("#d3d3d3"));
172 KNOWN_COLORS.put("lightgreen", Color.decode("#90ee90"));
173 KNOWN_COLORS.put("lightgrey", Color.decode("#d3d3d3"));
174 KNOWN_COLORS.put("lightpink", Color.decode("#ffb6c1"));
175 KNOWN_COLORS.put("lightsalmon", Color.decode("#ffa07a"));
176 KNOWN_COLORS.put("lightseagreen", Color.decode("#20b2aa"));
177 KNOWN_COLORS.put("lightskyblue", Color.decode("#87cefa"));
178 KNOWN_COLORS.put("lightslategray", Color.decode("#778899"));
179 KNOWN_COLORS.put("lightslategrey", Color.decode("#778899"));
180 KNOWN_COLORS.put("lightsteelblue", Color.decode("#b0c4de"));
181 KNOWN_COLORS.put("lightyellow", Color.decode("#ffffe0"));
182 KNOWN_COLORS.put("limegreen", Color.decode("#32cd32"));
183 KNOWN_COLORS.put("linen", Color.decode("#faf0e6"));
184 KNOWN_COLORS.put("magenta", Color.decode("#ff00ff"));
185 KNOWN_COLORS.put("mediumaquamarine", Color.decode("#66cdaa"));
186 KNOWN_COLORS.put("mediumblue", Color.decode("#0000cd"));
187 KNOWN_COLORS.put("mediumorchid", Color.decode("#ba55d3"));
188 KNOWN_COLORS.put("mediumpurple", Color.decode("#9370db"));
189 KNOWN_COLORS.put("mediumseagreen", Color.decode("#3cb371"));
190 KNOWN_COLORS.put("mediumslateblue", Color.decode("#7b68ee"));
191 KNOWN_COLORS.put("mediumspringgreen", Color.decode("#00fa9a"));
192 KNOWN_COLORS.put("mediumturquoise", Color.decode("#48d1cc"));
193 KNOWN_COLORS.put("mediumvioletred", Color.decode("#c71585"));
194 KNOWN_COLORS.put("midnightblue", Color.decode("#191970"));
195 KNOWN_COLORS.put("mintcream", Color.decode("#f5fffa"));
196 KNOWN_COLORS.put("mistyrose", Color.decode("#ffe4e1"));
197 KNOWN_COLORS.put("moccasin", Color.decode("#ffe4b5"));
198 KNOWN_COLORS.put("navajowhite", Color.decode("#ffdead"));
199 KNOWN_COLORS.put("oldlace", Color.decode("#fdf5e6"));
200 KNOWN_COLORS.put("olivedrab", Color.decode("#6b8e23"));
201 KNOWN_COLORS.put("orangered", Color.decode("#ff4500"));
202 KNOWN_COLORS.put("orchid", Color.decode("#da70d6"));
203 KNOWN_COLORS.put("palegoldenrod", Color.decode("#eee8aa"));
204 KNOWN_COLORS.put("palegreen", Color.decode("#98fb98"));
205 KNOWN_COLORS.put("paleturquoise", Color.decode("#afeeee"));
206 KNOWN_COLORS.put("palevioletred", Color.decode("#db7093"));
207 KNOWN_COLORS.put("papayawhip", Color.decode("#ffefd5"));
208 KNOWN_COLORS.put("peachpuff", Color.decode("#ffdab9"));
209 KNOWN_COLORS.put("peru", Color.decode("#cd853f"));
210 KNOWN_COLORS.put("pink", Color.decode("#ffc0cb"));
211 KNOWN_COLORS.put("plum", Color.decode("#dda0dd"));
212 KNOWN_COLORS.put("powderblue", Color.decode("#b0e0e6"));
213 KNOWN_COLORS.put("rosybrown", Color.decode("#bc8f8f"));
214 KNOWN_COLORS.put("royalblue", Color.decode("#4169e1"));
215 KNOWN_COLORS.put("saddlebrown", Color.decode("#8b4513"));
216 KNOWN_COLORS.put("salmon", Color.decode("#fa8072"));
217 KNOWN_COLORS.put("sandybrown", Color.decode("#f4a460"));
218 KNOWN_COLORS.put("seagreen", Color.decode("#2e8b57"));
219 KNOWN_COLORS.put("seashell", Color.decode("#fff5ee"));
220 KNOWN_COLORS.put("sienna", Color.decode("#a0522d"));
221 KNOWN_COLORS.put("skyblue", Color.decode("#87ceeb"));
222 KNOWN_COLORS.put("slateblue", Color.decode("#6a5acd"));
223 KNOWN_COLORS.put("slategray", Color.decode("#708090"));
224 KNOWN_COLORS.put("slategrey", Color.decode("#708090"));
225 KNOWN_COLORS.put("snow", Color.decode("#fffafa"));
226 KNOWN_COLORS.put("springgreen", Color.decode("#00ff7f"));
227 KNOWN_COLORS.put("steelblue", Color.decode("#4682b4"));
228 KNOWN_COLORS.put("tan", Color.decode("#d2b48c"));
229 KNOWN_COLORS.put("thistle", Color.decode("#d8bfd8"));
230 KNOWN_COLORS.put("tomato", Color.decode("#ff6347"));
231 KNOWN_COLORS.put("turquoise", Color.decode("#40e0d0"));
232 KNOWN_COLORS.put("violet", Color.decode("#ee82ee"));
233 KNOWN_COLORS.put("wheat", Color.decode("#f5deb3"));
234 KNOWN_COLORS.put("whitesmoke", Color.decode("#f5f5f5"));
235 KNOWN_COLORS.put("yellowgreen", Color.decode("#9acd32"));
236
237 KNOWN_COLORS.put("rebeccapurple", Color.decode("#663399"));
238 }
239
240 private static synchronized int nextId() {
241 return ID_GENERATOR_++;
242 }
243
244
245
246
247
248
249 public AwtRenderingBackend(final int imageWidth, final int imageHeight) {
250 id_ = nextId();
251 if (LOG.isDebugEnabled()) {
252 LOG.debug("[" + id_ + "] AwtRenderingBackend(" + imageWidth + ", " + imageHeight + ")");
253 }
254
255 image_ = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
256 graphics2D_ = image_.createGraphics();
257
258 graphics2D_.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
259 graphics2D_.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
260 graphics2D_.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
261
262
263
264
265 graphics2D_.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
266
267
268 fillColor_ = Color.black;
269 strokeColor_ = Color.black;
270 lineWidth_ = 1;
271 transformation_ = new AffineTransform();
272 updateGlobalAlpha(1f);
273 graphics2D_.setClip(null);
274
275 final Font font = new Font("SansSerif", Font.PLAIN, 10);
276 graphics2D_.setFont(font);
277
278 graphics2D_.setBackground(new Color(0f, 0f, 0f, 0f));
279 graphics2D_.setColor(Color.black);
280 graphics2D_.clearRect(0, 0, imageWidth, imageHeight);
281
282 lineCap_ = LineCap.BUTT;
283 lineJoin_ = LineJoin.MITER;
284 miterLimit_ = 10.0f;
285
286 subPaths_ = new ArrayList<>();
287 savedStates_ = new ArrayDeque<>();
288 }
289
290
291
292
293 @Override
294 public double getGlobalAlpha() {
295 return globalAlpha_;
296 }
297
298
299
300
301 @Override
302 public void setGlobalAlpha(final double globalAlpha) {
303 if (LOG.isDebugEnabled()) {
304 LOG.debug("[" + id_ + "] setGlobalAlpha(" + globalAlpha + ")");
305 }
306
307 if (globalAlpha >= 0 && globalAlpha <= 1) {
308 updateGlobalAlpha((float) globalAlpha);
309 }
310 }
311
312 private void updateGlobalAlpha(final float globalAlpha) {
313 globalAlpha_ = globalAlpha;
314 final AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, globalAlpha_);
315 graphics2D_.setComposite(composite);
316 }
317
318
319
320
321 @Override
322 public void beginPath() {
323 if (LOG.isDebugEnabled()) {
324 LOG.debug("[" + id_ + "] beginPath()");
325 }
326 subPaths_.clear();
327 }
328
329
330
331
332 @Override
333 public void ellipse(final double x, final double y,
334 final double radiusX, final double radiusY,
335 final double rotation, final double startAngle, final double endAngle,
336 final boolean anticlockwise) {
337 if (LOG.isDebugEnabled()) {
338 LOG.debug("[" + id_ + "] ellipse()");
339 }
340
341 if (startAngle == endAngle) {
342 return;
343 }
344
345 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
346 final AffineTransform transformation = new AffineTransform();
347 transformation.rotate(rotation, p.getX(), p.getY());
348
349 double startAngleDegree = Math.toDegrees(startAngle);
350 double endAngleDegree = Math.toDegrees(endAngle);
351
352 double extendAngle = endAngleDegree - startAngleDegree;
353 final Arc2D arc;
354 if (Math.abs(extendAngle) >= 360) {
355 arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
356 0, 360, Arc2D.OPEN);
357 }
358 else {
359 startAngleDegree = reverseAngle(startAngleDegree);
360 endAngleDegree = reverseAngle(endAngleDegree);
361
362 extendAngle = endAngleDegree - startAngleDegree;
363 if (!anticlockwise) {
364 extendAngle = -reverseAngle(extendAngle);
365 }
366
367 arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
368 startAngleDegree, extendAngle, Arc2D.OPEN);
369 }
370
371
372
373
374 final boolean hasCurrentPoint;
375 if (subPaths_.isEmpty()) {
376 final Path2D subPath = new Path2D.Double();
377 subPaths_.add(subPath);
378 hasCurrentPoint = false;
379 }
380 else {
381 hasCurrentPoint = subPaths_.get(subPaths_.size() - 1).getCurrentPoint() != null;
382 }
383 getCurrentSubPath().append(transformation.createTransformedShape(arc), hasCurrentPoint);
384 }
385
386
387
388
389 @Override
390 public void bezierCurveTo(final double cp1x, final double cp1y,
391 final double cp2x, final double cp2y, final double x, final double y) {
392 if (LOG.isDebugEnabled()) {
393 LOG.debug("[" + id_ + "] bezierCurveTo()");
394 }
395
396 final Point2D cp1 = transformation_.transform(new Point2D.Double(cp1x, cp1y), null);
397 final Point2D cp2 = transformation_.transform(new Point2D.Double(cp2x, cp2y), null);
398 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
399
400 getCurrentSubPath(cp1.getX(), cp1.getY())
401 .curveTo(cp1.getX(), cp1.getY(), cp2.getX(), cp2.getY(), p.getX(), p.getY());
402 }
403
404
405
406
407 @Override
408 public void arc(final double x, final double y, final double radius, final double startAngle,
409 final double endAngle, final boolean anticlockwise) {
410 if (LOG.isDebugEnabled()) {
411 LOG.debug("[" + id_ + "] arc()");
412 }
413
414 ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
415 }
416
417 private static double reverseAngle(final double degrees) {
418 return ((-degrees % 360) + 360) % 360;
419 }
420
421
422
423
424 @Override
425 public void clearRect(final double x, final double y, final double w, final double h) {
426 if (LOG.isDebugEnabled()) {
427 LOG.debug("[" + id_ + "] clearRect(" + x + ", " + y + ", " + w + ", " + h + ")");
428 }
429
430 final Composite savedComposite = graphics2D_.getComposite();
431 final Color savedColor = graphics2D_.getColor();
432
433 try {
434 graphics2D_.setComposite(AlphaComposite.Clear);
435 graphics2D_.fill(rectPath(x, y, w, h));
436 }
437 finally {
438 graphics2D_.setComposite(savedComposite);
439 graphics2D_.setColor(savedColor);
440 }
441 }
442
443
444
445
446 @Override
447 public void drawImage(final org.htmlunit.platform.image.ImageData imageData,
448 final int sx, final int sy, final Integer sWidth, final Integer sHeight,
449 final int dx, final int dy, final Integer dWidth, final Integer dHeight) throws IOException {
450 if (LOG.isDebugEnabled()) {
451 LOG.debug("[" + id_ + "] drawImage(" + sx + ", " + sy + ", " + sWidth + ", " + sHeight
452 + "," + dx + ", " + dy + ", " + dWidth + ", " + dHeight + ")");
453 }
454
455 try {
456 final ImageReader imageReader = ((ImageIOImageData) imageData).getImageReader();
457
458 if (imageReader.getNumImages(true) != 0) {
459 final BufferedImage img = imageReader.read(0);
460
461 final AffineTransform savedTransform = graphics2D_.getTransform();
462 try {
463 graphics2D_.setTransform(transformation_);
464 graphics2D_.setColor(fillColor_);
465
466 final int sx2;
467 if (sWidth == null) {
468 sx2 = sx + img.getWidth();
469 }
470 else {
471 sx2 = sx + sWidth;
472 }
473
474 final int sy2;
475 if (sHeight == null) {
476 sy2 = sy + img.getHeight();
477 }
478 else {
479 sy2 = sy + sHeight;
480 }
481
482 int dx1 = dx;
483 final int dx2;
484 if (dWidth == null) {
485 dx2 = dx + img.getWidth();
486 }
487 else {
488 if (dWidth < 0) {
489 dx1 = dx1 + dWidth;
490 dx2 = dx1 - dWidth;
491 }
492 else {
493 dx2 = dx1 + dWidth;
494 }
495 }
496
497 int dy1 = dy;
498 final int dy2;
499 if (dHeight == null) {
500 dy2 = dy + img.getHeight();
501 }
502 else {
503 if (dHeight < 0) {
504 dy1 = dy1 + dHeight;
505 dy2 = dy1 - dHeight;
506 }
507 else {
508 dy2 = dy1 + dHeight;
509 }
510 }
511
512 final Object done = new Object();
513 final ImageObserver imageObserver = (img1, flags, x, y, width, height) -> {
514
515 if ((flags & ImageObserver.ALLBITS) == ImageObserver.ALLBITS) {
516 return true;
517 }
518
519 if ((flags & ImageObserver.ABORT) == ImageObserver.ABORT
520 || (flags & ImageObserver.ERROR) == ImageObserver.ERROR) {
521 return true;
522 }
523
524 synchronized (done) {
525 done.notify();
526 }
527
528 return false;
529 };
530
531 synchronized (done) {
532 final boolean completelyLoaded =
533 graphics2D_.drawImage(img, dx1, dy1, dx2, dy2, sx, sy, sx2, sy2, imageObserver);
534 if (!completelyLoaded) {
535 while (true) {
536 try {
537 done.wait(4 * 1000);
538 break;
539 }
540 catch (final InterruptedException e) {
541 LOG.error("[" + id_ + "] AwtRenderingBackend interrupted "
542 + "while waiting for drawImage to finish.", e);
543
544
545 Thread.currentThread().interrupt();
546 }
547 }
548 }
549 }
550 }
551 finally {
552 graphics2D_.setTransform(savedTransform);
553 }
554 }
555 }
556 catch (final ClassCastException e) {
557 LOG.error("[" + id_ + "] drawImage(..) failed", e);
558 }
559 }
560
561
562
563
564 @Override
565 public String encodeToString(final String type) throws IOException {
566 String imageType = type;
567 if (imageType != null && imageType.startsWith("image/")) {
568 imageType = imageType.substring(6);
569 }
570 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
571 ImageIO.write(image_, imageType, bos);
572
573 final byte[] imageBytes = bos.toByteArray();
574 return new String(Base64.getEncoder().encode(imageBytes), StandardCharsets.US_ASCII);
575 }
576 }
577
578
579
580
581 @Override
582 public void fill(final RenderingBackend.WindingRule windingRule) {
583 if (LOG.isDebugEnabled()) {
584 LOG.debug("[" + id_ + "] fill("
585 + (windingRule == RenderingBackend.WindingRule.EVEN_ODD ? "evenOdd" : "nonZero") + ")");
586 }
587
588 final int awtRule = (windingRule == RenderingBackend.WindingRule.EVEN_ODD)
589 ? Path2D.WIND_EVEN_ODD
590 : Path2D.WIND_NON_ZERO;
591
592 graphics2D_.setColor(fillColor_);
593 for (final Path2D path2d : subPaths_) {
594 path2d.setWindingRule(awtRule);
595 graphics2D_.fill(path2d);
596 }
597 }
598
599
600
601
602 @Override
603 public void fillRect(final double x, final double y, final double w, final double h) {
604 if (LOG.isDebugEnabled()) {
605 LOG.debug("[" + id_ + "] fillRect(" + x + ", " + y + ", " + w + ", " + h + ")");
606 }
607
608 graphics2D_.setColor(fillColor_);
609 graphics2D_.fill(rectPath(x, y, w, h));
610 }
611
612
613
614
615 @Override
616 public void fillText(final String text, final double x, final double y) {
617 if (LOG.isDebugEnabled()) {
618 LOG.debug("[" + id_ + "] fillText('" + text + "', " + x + ", " + y + ")");
619 }
620
621 final AffineTransform savedTransform = graphics2D_.getTransform();
622 try {
623 graphics2D_.setTransform(transformation_);
624 graphics2D_.setColor(fillColor_);
625 graphics2D_.drawString(text, (float) x, (float) y);
626 }
627 finally {
628 graphics2D_.setTransform(savedTransform);
629 }
630 }
631
632
633
634
635 @Override
636 public byte[] getBytes(final int width, final int height, final int sx, final int sy) {
637 if (LOG.isDebugEnabled()) {
638 LOG.debug("[" + id_ + "] getBytes(" + width + ", " + height + ", " + sx + ", " + sy + ")");
639 }
640
641 final byte[] array = new byte[width * height * 4];
642 int index = 0;
643
644
645 for (int y = sy; y < sy + height; y++) {
646 for (int x = sx; x < sx + width; x++) {
647 if (x < 0 || x >= image_.getWidth() || y < 0 || y >= image_.getHeight()) {
648 array[index++] = (byte) 0;
649 array[index++] = (byte) 0;
650 array[index++] = (byte) 0;
651 array[index++] = (byte) 0;
652 }
653 else {
654 final int color = image_.getRGB(x, y);
655 array[index++] = (byte) ((color >> 16) & 0xFF);
656 array[index++] = (byte) ((color >> 8) & 0xFF);
657 array[index++] = (byte) (color & 0xFF);
658 array[index++] = (byte) ((color >>> 24) & 0xFF);
659 }
660 }
661 }
662
663 return array;
664 }
665
666
667
668
669 @Override
670 public void lineTo(final double x, final double y) {
671 if (LOG.isDebugEnabled()) {
672 LOG.debug("[" + id_ + "] lineTo(" + x + ", " + y + ")");
673 }
674
675 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
676 final Path2D subPath = getCurrentSubPath();
677 if (subPath.getCurrentPoint() == null) {
678 subPath.moveTo(p.getX(), p.getY());
679 }
680 else {
681 subPath.lineTo(p.getX(), p.getY());
682 }
683 }
684
685
686
687
688 @Override
689 public void moveTo(final double x, final double y) {
690 if (LOG.isDebugEnabled()) {
691 LOG.debug("[" + id_ + "] moveTo(" + x + ", " + y + ")");
692 }
693
694 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
695
696 if (!subPaths_.isEmpty()) {
697 final Path2D last = subPaths_.get(subPaths_.size() - 1);
698 if (!hasSegments(last)) {
699 last.reset();
700 last.moveTo(p.getX(), p.getY());
701 return;
702 }
703 }
704
705 final Path2D subPath = new Path2D.Double();
706 subPath.moveTo(p.getX(), p.getY());
707 subPaths_.add(subPath);
708 }
709
710
711
712
713 @Override
714 public void putImageData(final byte[] imageDataBytes,
715 final int imageDataWidth, final int imageDataHeight,
716 final int dx, final int dy, final int dirtyX, final int dirtyY,
717 final int dirtyWidth, final int dirtyHeight) {
718
719 if (LOG.isDebugEnabled()) {
720 LOG.debug("[" + id_ + "] putImageData()");
721 }
722
723 if (imageDataBytes == null || imageDataWidth <= 0 || imageDataHeight <= 0) {
724 return;
725 }
726
727
728 int sx = dirtyX;
729 int sy = dirtyY;
730 int sw = dirtyWidth;
731 int sh = dirtyHeight;
732 if (sw < 0) {
733 sx += sw;
734 sw = -sw;
735 }
736 if (sh < 0) {
737 sy += sh;
738 sh = -sh;
739 }
740 if (sw == 0 || sh == 0) {
741 return;
742 }
743
744
745 final int srcStartX = Math.max(0, sx);
746 final int srcStartY = Math.max(0, sy);
747 final int srcEndX = Math.min(imageDataWidth, sx + sw);
748 final int srcEndY = Math.min(imageDataHeight, sy + sh);
749 if (srcStartX >= srcEndX || srcStartY >= srcEndY) {
750 return;
751 }
752
753 final int srcWidth = srcEndX - srcStartX;
754 final int srcHeight = srcEndY - srcStartY;
755
756 final BufferedImage srcImage = new BufferedImage(srcWidth, srcHeight, BufferedImage.TYPE_INT_ARGB);
757 for (int row = 0; row < srcHeight; row++) {
758 for (int col = 0; col < srcWidth; col++) {
759 final int byteIdx = ((srcStartY + row) * imageDataWidth + (srcStartX + col)) * 4;
760 final int r = imageDataBytes[byteIdx] & 0xFF;
761 final int g = imageDataBytes[byteIdx + 1] & 0xFF;
762 final int b = imageDataBytes[byteIdx + 2] & 0xFF;
763 final int a = imageDataBytes[byteIdx + 3] & 0xFF;
764 srcImage.setRGB(col, row, (a << 24) | (r << 16) | (g << 8) | b);
765 }
766 }
767
768 final Composite savedComposite = graphics2D_.getComposite();
769 final AffineTransform savedTransform = graphics2D_.getTransform();
770 try {
771 graphics2D_.setComposite(AlphaComposite.Src);
772 graphics2D_.setTransform(new AffineTransform());
773 graphics2D_.drawImage(srcImage, dx + srcStartX, dy + srcStartY, null);
774 }
775 finally {
776 graphics2D_.setComposite(savedComposite);
777 graphics2D_.setTransform(savedTransform);
778 }
779 }
780
781
782
783
784 @Override
785 public void quadraticCurveTo(final double cpx, final double cpy,
786 final double x, final double y) {
787 if (LOG.isDebugEnabled()) {
788 LOG.debug("[" + id_ + "] quadraticCurveTo()");
789 }
790
791 final Point2D cp = transformation_.transform(new Point2D.Double(cpx, cpy), null);
792 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
793 getCurrentSubPath(cp.getX(), cp.getY()).quadTo(cp.getX(), cp.getY(), p.getX(), p.getY());
794 }
795
796
797
798
799 @Override
800 public void rect(final double x, final double y, final double w, final double h) {
801 if (LOG.isDebugEnabled()) {
802 LOG.debug("[" + id_ + "] rect(" + x + ", " + y + ", " + w + ", " + h + ")");
803 }
804
805 final Point2D p0 = transformation_.transform(new Point2D.Double(x, y), null);
806 subPaths_.add(rectPath(x, y, w, h));
807
808
809 final Path2D nextPath = new Path2D.Double();
810 nextPath.moveTo(p0.getX(), p0.getY());
811 subPaths_.add(nextPath);
812 }
813
814
815
816
817 @Override
818 public void setFillStyle(final String fillStyle) {
819 if (LOG.isDebugEnabled()) {
820 LOG.debug("[" + id_ + "] setFillStyle(" + fillStyle + ")");
821 }
822
823 final Color color = extractColor(fillStyle);
824 if (color != null) {
825 fillColor_ = color;
826 }
827 }
828
829
830
831
832 @Override
833 public void setStrokeStyle(final String strokeStyle) {
834 if (LOG.isDebugEnabled()) {
835 LOG.debug("[" + id_ + "] setStrokeStyle(" + strokeStyle + ")");
836 }
837
838 final Color color = extractColor(strokeStyle);
839 if (color != null) {
840 strokeColor_ = color;
841 }
842 }
843
844 private static Color extractColor(final String style) {
845 final String tmpStyle = style.replaceAll("\\s", "");
846
847 Color color = toAwtColor(StringUtils.findColorRGB(tmpStyle));
848 if (color == null) {
849 color = toAwtColor(StringUtils.findColorRGBA(tmpStyle));
850 }
851 if (color == null) {
852 color = toAwtColor(StringUtils.findColorHSL(tmpStyle));
853 }
854
855 if (color == null) {
856 if (tmpStyle.length() > 0 && tmpStyle.charAt(0) == '#') {
857 color = toAwtColor(StringUtils.asColorHexadecimal(tmpStyle));
858 }
859 else {
860 color = KNOWN_COLORS.get(tmpStyle.toLowerCase(Locale.ROOT));
861 }
862 }
863 return color;
864 }
865
866
867
868
869 @Override
870 public float getLineWidth() {
871 return lineWidth_;
872 }
873
874
875
876
877 @Override
878 public void restore() {
879 if (LOG.isDebugEnabled()) {
880 LOG.debug("[" + id_ + "] restore()");
881 }
882
883 if (savedStates_.isEmpty()) {
884 return;
885 }
886
887 savedStates_.pop().applyOn(this);
888 }
889
890
891
892
893 @Override
894 public void rotate(final double angle) {
895 if (LOG.isDebugEnabled()) {
896 LOG.debug("[" + id_ + "] rotate()");
897 }
898
899 transformation_.rotate(angle);
900 }
901
902
903
904
905 @Override
906 public void save() {
907 if (LOG.isDebugEnabled()) {
908 LOG.debug("[" + id_ + "] save()");
909 }
910
911 savedStates_.push(new SaveState(this));
912 }
913
914
915
916
917 @Override
918 public void setLineWidth(final float lineWidth) {
919 if (LOG.isDebugEnabled()) {
920 LOG.debug("[" + id_ + "] setLineWidth(" + lineWidth + ")");
921 }
922
923 lineWidth_ = lineWidth;
924 }
925
926
927
928
929 @Override
930 public void setTransform(final double m11, final double m12,
931 final double m21, final double m22, final double dx, final double dy) {
932 if (LOG.isDebugEnabled()) {
933 LOG.debug("[" + id_ + "] setTransform("
934 + m11 + ", " + m12 + ", " + m21 + ", " + m22 + ", " + dx + ", " + dy + ")");
935 }
936
937 transformation_ = new AffineTransform(m11, m12, m21, m22, dx, dy);
938 }
939
940
941
942
943 @Override
944 public void stroke() {
945 if (LOG.isDebugEnabled()) {
946 LOG.debug("[" + id_ + "] stroke()");
947 }
948
949 graphics2D_.setStroke(new BasicStroke(getLineWidth(),
950 toAwtLineCap(lineCap_), toAwtLineJoin(lineJoin_), miterLimit_));
951 graphics2D_.setColor(strokeColor_);
952
953 for (final Path2D path2d : subPaths_) {
954 if (hasSegments(path2d)) {
955 graphics2D_.draw(path2d);
956 }
957 }
958 }
959
960
961
962
963 @Override
964 public void strokeRect(final double x, final double y, final double w, final double h) {
965 if (LOG.isDebugEnabled()) {
966 LOG.debug("[" + id_ + "] strokeRect(" + x + ", " + y + ", " + w + ", " + h + ")");
967 }
968
969 graphics2D_.setStroke(new BasicStroke(getLineWidth(),
970 toAwtLineCap(lineCap_), toAwtLineJoin(lineJoin_), miterLimit_));
971 graphics2D_.setColor(strokeColor_);
972 graphics2D_.draw(rectPath(x, y, w, h));
973 }
974
975
976
977
978 @Override
979 public void transform(final double m11, final double m12,
980 final double m21, final double m22, final double dx, final double dy) {
981 if (LOG.isDebugEnabled()) {
982 LOG.debug("[" + id_ + "] transform()");
983 }
984
985 transformation_.concatenate(new AffineTransform(m11, m12, m21, m22, dx, dy));
986 }
987
988
989
990
991 @Override
992 public void translate(final double x, final double y) {
993 if (LOG.isDebugEnabled()) {
994 LOG.debug("[" + id_ + "] translate()");
995 }
996
997 transformation_.translate(x, y);
998 }
999
1000
1001
1002
1003 @Override
1004 public void clip(final RenderingBackend.WindingRule windingRule,
1005 final org.htmlunit.javascript.host.canvas.Path2D path) {
1006 if (LOG.isDebugEnabled()) {
1007 LOG.debug("[" + id_ + "] clip(" + windingRule + ", " + path + ")");
1008 }
1009
1010 final Path2D clipPath = new Path2D.Double();
1011
1012 if (path == null) {
1013 if (subPaths_.isEmpty()) {
1014 return;
1015 }
1016 for (final Path2D path2d : subPaths_) {
1017 if (hasSegments(path2d)) {
1018 clipPath.append(path2d, false);
1019 }
1020 }
1021 }
1022 else {
1023
1024 return;
1025 }
1026
1027 if (windingRule == WindingRule.NON_ZERO) {
1028 clipPath.setWindingRule(Path2D.WIND_NON_ZERO);
1029 }
1030 else {
1031 clipPath.setWindingRule(Path2D.WIND_EVEN_ODD);
1032 }
1033
1034 graphics2D_.clip(clipPath);
1035 }
1036
1037
1038
1039
1040 @Override
1041 public void closePath() {
1042 if (LOG.isDebugEnabled()) {
1043 LOG.debug("[" + id_ + "] closePath()");
1044 }
1045
1046 if (subPaths_.isEmpty()) {
1047 return;
1048 }
1049
1050 final Path2D current = subPaths_.get(subPaths_.size() - 1);
1051
1052
1053
1054 final double[] coords = new double[6];
1055 final PathIterator it = current.getPathIterator(null);
1056 if (it.isDone()) {
1057 return;
1058 }
1059 it.currentSegment(coords);
1060 final double firstX = coords[0];
1061 final double firstY = coords[1];
1062
1063 current.closePath();
1064
1065
1066
1067 final Path2D nextPath = new Path2D.Double();
1068 nextPath.moveTo(firstX, firstY);
1069 subPaths_.add(nextPath);
1070 }
1071
1072
1073
1074
1075 @Override
1076 public LineJoin getLineJoin() {
1077 if (LOG.isDebugEnabled()) {
1078 LOG.debug("[" + id_ + "] getLineJoin()");
1079 }
1080 return lineJoin_;
1081 }
1082
1083
1084
1085
1086 @Override
1087 public void setLineJoin(final LineJoin lineJoin) {
1088 if (LOG.isDebugEnabled()) {
1089 LOG.debug("[" + id_ + "] setLineJoin(" + lineJoin + ")");
1090 }
1091 lineJoin_ = lineJoin;
1092 }
1093
1094
1095
1096
1097 @Override
1098 public LineCap getLineCap() {
1099 if (LOG.isDebugEnabled()) {
1100 LOG.debug("[" + id_ + "] getLineCap()");
1101 }
1102 return lineCap_;
1103 }
1104
1105
1106
1107
1108 @Override
1109 public void setLineCap(final LineCap lineCap) {
1110 if (LOG.isDebugEnabled()) {
1111 LOG.debug("[" + id_ + "] setLineCap(" + lineCap + ")");
1112 }
1113 lineCap_ = lineCap;
1114 }
1115
1116 private Path2D getCurrentSubPath() {
1117 if (subPaths_.isEmpty()) {
1118 final Path2D subPath = new Path2D.Double();
1119 subPaths_.add(subPath);
1120 return subPath;
1121 }
1122 return subPaths_.get(subPaths_.size() - 1);
1123 }
1124
1125 private Path2D getCurrentSubPath(final double x, final double y) {
1126 if (subPaths_.isEmpty()) {
1127 final Path2D subPath = new Path2D.Double();
1128 subPaths_.add(subPath);
1129 subPath.moveTo(x, y);
1130 return subPath;
1131 }
1132
1133 final Path2D subPath = subPaths_.get(subPaths_.size() - 1);
1134 if (subPath.getCurrentPoint() == null) {
1135 subPath.moveTo(x, y);
1136 }
1137 return subPath;
1138 }
1139
1140 private Path2D rectPath(final double x, final double y, final double w, final double h) {
1141
1142
1143
1144 final Path2D rectPath = new Path2D.Double();
1145 final Point2D p0 = transformation_.transform(new Point2D.Double(x, y), null);
1146 final Point2D p1 = transformation_.transform(new Point2D.Double(x + w, y), null);
1147 final Point2D p2 = transformation_.transform(new Point2D.Double(x + w, y + h), null);
1148 final Point2D p3 = transformation_.transform(new Point2D.Double(x, y + h), null);
1149 rectPath.moveTo(p0.getX(), p0.getY());
1150 rectPath.lineTo(p1.getX(), p1.getY());
1151 rectPath.lineTo(p2.getX(), p2.getY());
1152 rectPath.lineTo(p3.getX(), p3.getY());
1153 rectPath.closePath();
1154
1155 return rectPath;
1156 }
1157
1158 private static boolean hasSegments(final Path2D path) {
1159 final PathIterator it = path.getPathIterator(null);
1160 while (!it.isDone()) {
1161 if (it.currentSegment(new double[6]) != PathIterator.SEG_MOVETO) {
1162 return true;
1163 }
1164 it.next();
1165 }
1166 return false;
1167 }
1168
1169 private static Color toAwtColor(final org.htmlunit.html.impl.Color color) {
1170 if (color == null) {
1171 return null;
1172 }
1173 return new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
1174 }
1175
1176 private static int toAwtLineJoin(final LineJoin lineJoin) {
1177 return switch (lineJoin) {
1178 case MITER -> BasicStroke.JOIN_MITER;
1179 case ROUND -> BasicStroke.JOIN_ROUND;
1180 case BEVEL -> BasicStroke.JOIN_BEVEL;
1181 };
1182 }
1183
1184 private static int toAwtLineCap(final LineCap lineCap) {
1185 return switch (lineCap) {
1186 case ROUND -> BasicStroke.CAP_ROUND;
1187 case SQUARE -> BasicStroke.CAP_SQUARE;
1188 case BUTT -> BasicStroke.CAP_BUTT;
1189 };
1190 }
1191
1192 private static final class SaveState {
1193 private final AffineTransform transformation_;
1194 private final float globalAlpha_;
1195 private final float lineWidth_;
1196 private final Color fillColor_;
1197 private final Color strokeColor_;
1198 private final Shape clip_;
1199
1200 private final LineCap lineCap_;
1201 private final LineJoin lineJoin_;
1202 private final float miterLimit_;
1203
1204 private final Font font_;
1205
1206 SaveState(final AwtRenderingBackend backend) {
1207 transformation_ = new AffineTransform(backend.transformation_);
1208 globalAlpha_ = backend.globalAlpha_;
1209 lineWidth_ = backend.lineWidth_;
1210 fillColor_ = backend.fillColor_;
1211 strokeColor_ = backend.strokeColor_;
1212
1213 lineCap_ = backend.lineCap_;
1214 lineJoin_ = backend.lineJoin_;
1215 miterLimit_ = backend.miterLimit_;
1216
1217
1218
1219 final Shape c = backend.graphics2D_.getClip();
1220 clip_ = (c == null) ? null : new Path2D.Double(c);
1221
1222
1223 font_ = backend.graphics2D_.getFont();
1224 }
1225
1226 void applyOn(final AwtRenderingBackend backend) {
1227 backend.transformation_ = new AffineTransform(transformation_);
1228 backend.lineWidth_ = lineWidth_;
1229 backend.fillColor_ = fillColor_;
1230 backend.strokeColor_ = strokeColor_;
1231
1232 backend.updateGlobalAlpha(globalAlpha_);
1233
1234 backend.lineCap_ = lineCap_;
1235 backend.lineJoin_ = lineJoin_;
1236 backend.miterLimit_ = miterLimit_;
1237
1238
1239
1240 backend.graphics2D_.setClip((clip_ == null) ? null : new Path2D.Double(clip_));
1241
1242 backend.graphics2D_.setFont(font_);
1243 }
1244 }
1245 }