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.ByteBuffer;
35 import java.nio.charset.StandardCharsets;
36 import java.util.ArrayDeque;
37 import java.util.ArrayList;
38 import java.util.Base64;
39 import java.util.Deque;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.Map;
44
45 import javax.imageio.ImageIO;
46 import javax.imageio.ImageReader;
47
48 import org.apache.commons.logging.Log;
49 import org.apache.commons.logging.LogFactory;
50 import org.htmlunit.platform.image.ImageIOImageData;
51 import org.htmlunit.util.StringUtils;
52
53
54
55
56
57
58 public class AwtRenderingBackend implements RenderingBackend {
59
60 private static final Log LOG = LogFactory.getLog(AwtRenderingBackend.class);
61 private static int ID_GENERATOR_;
62
63 private static final Map<String, Color> KNOWN_COLORS = new HashMap<>();
64
65 private final int id_;
66 private final BufferedImage image_;
67 private final Graphics2D graphics2D_;
68
69 private AffineTransform transformation_;
70 private float globalAlpha_;
71 private float lineWidth_;
72 private Color fillColor_;
73 private Color strokeColor_;
74
75 private LineCap lineCap_;
76 private LineJoin lineJoin_;
77 private float miterLimit_;
78
79 private final List<Path2D> subPaths_;
80 private final Deque<SaveState> savedStates_;
81
82 static {
83
84
85
86 KNOWN_COLORS.put("black", Color.decode("#000000"));
87 KNOWN_COLORS.put("silver", Color.decode("#c0c0c0"));
88 KNOWN_COLORS.put("gray", Color.decode("#808080"));
89 KNOWN_COLORS.put("white", Color.decode("#ffffff"));
90 KNOWN_COLORS.put("maroon", Color.decode("#800000"));
91 KNOWN_COLORS.put("red", Color.decode("#ff0000"));
92 KNOWN_COLORS.put("purple", Color.decode("#800080"));
93 KNOWN_COLORS.put("fuchsia", Color.decode("#ff00ff"));
94 KNOWN_COLORS.put("green", Color.decode("#008000"));
95 KNOWN_COLORS.put("lime", Color.decode("#00ff00"));
96 KNOWN_COLORS.put("olive", Color.decode("#808000"));
97 KNOWN_COLORS.put("yellow", Color.decode("#ffff00"));
98 KNOWN_COLORS.put("navy", Color.decode("#000080"));
99 KNOWN_COLORS.put("blue", Color.decode("#0000ff"));
100 KNOWN_COLORS.put("teal", Color.decode("#008080"));
101 KNOWN_COLORS.put("aqua", Color.decode("#00ffff"));
102
103
104 KNOWN_COLORS.put("orange", Color.decode("#ffa500"));
105
106
107 KNOWN_COLORS.put("aliceblue", Color.decode("#f0f8ff"));
108 KNOWN_COLORS.put("antiquewhite", Color.decode("#faebd7"));
109 KNOWN_COLORS.put("aquamarine", Color.decode("#7fffd4"));
110 KNOWN_COLORS.put("azure", Color.decode("#f0ffff"));
111 KNOWN_COLORS.put("beige", Color.decode("#f5f5dc"));
112 KNOWN_COLORS.put("bisque", Color.decode("#ffe4c4"));
113 KNOWN_COLORS.put("blanchedalmond", Color.decode("#ffebcd"));
114 KNOWN_COLORS.put("blueviolet", Color.decode("#8a2be2"));
115 KNOWN_COLORS.put("brown", Color.decode("#a52a2a"));
116 KNOWN_COLORS.put("burlywood", Color.decode("#deb887"));
117 KNOWN_COLORS.put("cadetblue", Color.decode("#5f9ea0"));
118 KNOWN_COLORS.put("chartreuse", Color.decode("#7fff00"));
119 KNOWN_COLORS.put("chocolate", Color.decode("#d2691e"));
120 KNOWN_COLORS.put("coral", Color.decode("#ff7f50"));
121 KNOWN_COLORS.put("cornflowerblue", Color.decode("#6495ed"));
122 KNOWN_COLORS.put("cornsilk", Color.decode("#fff8dc"));
123 KNOWN_COLORS.put("crimson", Color.decode("#dc143c"));
124 KNOWN_COLORS.put("cyan", Color.decode("#00ffff"));
125 KNOWN_COLORS.put("darkblue", Color.decode("#00008b"));
126 KNOWN_COLORS.put("darkcyan", Color.decode("#008b8b"));
127 KNOWN_COLORS.put("darkgoldenrod", Color.decode("#b8860b"));
128 KNOWN_COLORS.put("darkgray", Color.decode("#a9a9a9"));
129 KNOWN_COLORS.put("darkgreen", Color.decode("#006400"));
130 KNOWN_COLORS.put("darkgrey", Color.decode("#a9a9a9"));
131 KNOWN_COLORS.put("darkkhaki", Color.decode("#bdb76b"));
132 KNOWN_COLORS.put("darkmagenta", Color.decode("#8b008b"));
133 KNOWN_COLORS.put("darkolivegreen", Color.decode("#556b2f"));
134 KNOWN_COLORS.put("darkorange", Color.decode("#ff8c00"));
135 KNOWN_COLORS.put("darkorchid", Color.decode("#9932cc"));
136 KNOWN_COLORS.put("darkred", Color.decode("#8b0000"));
137 KNOWN_COLORS.put("darksalmon", Color.decode("#e9967a"));
138 KNOWN_COLORS.put("darkseagreen", Color.decode("#8fbc8f"));
139 KNOWN_COLORS.put("darkslateblue", Color.decode("#483d8b"));
140 KNOWN_COLORS.put("darkslategray", Color.decode("#2f4f4f"));
141 KNOWN_COLORS.put("darkslategrey", Color.decode("#2f4f4f"));
142 KNOWN_COLORS.put("darkturquoise", Color.decode("#00ced1"));
143 KNOWN_COLORS.put("darkviolet", Color.decode("#9400d3"));
144 KNOWN_COLORS.put("deeppink", Color.decode("#ff1493"));
145 KNOWN_COLORS.put("deepskyblue", Color.decode("#00bfff"));
146 KNOWN_COLORS.put("dimgray", Color.decode("#696969"));
147 KNOWN_COLORS.put("dimgrey", Color.decode("#696969"));
148 KNOWN_COLORS.put("dodgerblue", Color.decode("#1e90ff"));
149 KNOWN_COLORS.put("firebrick", Color.decode("#b22222"));
150 KNOWN_COLORS.put("floralwhite", Color.decode("#fffaf0"));
151 KNOWN_COLORS.put("forestgreen", Color.decode("#228b22"));
152 KNOWN_COLORS.put("gainsboro", Color.decode("#dcdcdc"));
153 KNOWN_COLORS.put("ghostwhite", Color.decode("#f8f8ff"));
154 KNOWN_COLORS.put("gold", Color.decode("#ffd700"));
155 KNOWN_COLORS.put("goldenrod", Color.decode("#daa520"));
156 KNOWN_COLORS.put("greenyellow", Color.decode("#adff2f"));
157 KNOWN_COLORS.put("grey", Color.decode("#808080"));
158 KNOWN_COLORS.put("honeydew", Color.decode("#f0fff0"));
159 KNOWN_COLORS.put("hotpink", Color.decode("#ff69b4"));
160 KNOWN_COLORS.put("indianred", Color.decode("#cd5c5c"));
161 KNOWN_COLORS.put("indigo", Color.decode("#4b0082"));
162 KNOWN_COLORS.put("ivory", Color.decode("#fffff0"));
163 KNOWN_COLORS.put("khaki", Color.decode("#f0e68c"));
164 KNOWN_COLORS.put("lavender", Color.decode("#e6e6fa"));
165 KNOWN_COLORS.put("lavenderblush", Color.decode("#fff0f5"));
166 KNOWN_COLORS.put("lawngreen", Color.decode("#7cfc00"));
167 KNOWN_COLORS.put("lemonchiffon", Color.decode("#fffacd"));
168 KNOWN_COLORS.put("lightblue", Color.decode("#add8e6"));
169 KNOWN_COLORS.put("lightcoral", Color.decode("#f08080"));
170 KNOWN_COLORS.put("lightcyan", Color.decode("#e0ffff"));
171 KNOWN_COLORS.put("lightgoldenrodyellow", Color.decode("#fafad2"));
172 KNOWN_COLORS.put("lightgray", Color.decode("#d3d3d3"));
173 KNOWN_COLORS.put("lightgreen", Color.decode("#90ee90"));
174 KNOWN_COLORS.put("lightgrey", Color.decode("#d3d3d3"));
175 KNOWN_COLORS.put("lightpink", Color.decode("#ffb6c1"));
176 KNOWN_COLORS.put("lightsalmon", Color.decode("#ffa07a"));
177 KNOWN_COLORS.put("lightseagreen", Color.decode("#20b2aa"));
178 KNOWN_COLORS.put("lightskyblue", Color.decode("#87cefa"));
179 KNOWN_COLORS.put("lightslategray", Color.decode("#778899"));
180 KNOWN_COLORS.put("lightslategrey", Color.decode("#778899"));
181 KNOWN_COLORS.put("lightsteelblue", Color.decode("#b0c4de"));
182 KNOWN_COLORS.put("lightyellow", Color.decode("#ffffe0"));
183 KNOWN_COLORS.put("limegreen", Color.decode("#32cd32"));
184 KNOWN_COLORS.put("linen", Color.decode("#faf0e6"));
185 KNOWN_COLORS.put("magenta", Color.decode("#ff00ff"));
186 KNOWN_COLORS.put("mediumaquamarine", Color.decode("#66cdaa"));
187 KNOWN_COLORS.put("mediumblue", Color.decode("#0000cd"));
188 KNOWN_COLORS.put("mediumorchid", Color.decode("#ba55d3"));
189 KNOWN_COLORS.put("mediumpurple", Color.decode("#9370db"));
190 KNOWN_COLORS.put("mediumseagreen", Color.decode("#3cb371"));
191 KNOWN_COLORS.put("mediumslateblue", Color.decode("#7b68ee"));
192 KNOWN_COLORS.put("mediumspringgreen", Color.decode("#00fa9a"));
193 KNOWN_COLORS.put("mediumturquoise", Color.decode("#48d1cc"));
194 KNOWN_COLORS.put("mediumvioletred", Color.decode("#c71585"));
195 KNOWN_COLORS.put("midnightblue", Color.decode("#191970"));
196 KNOWN_COLORS.put("mintcream", Color.decode("#f5fffa"));
197 KNOWN_COLORS.put("mistyrose", Color.decode("#ffe4e1"));
198 KNOWN_COLORS.put("moccasin", Color.decode("#ffe4b5"));
199 KNOWN_COLORS.put("navajowhite", Color.decode("#ffdead"));
200 KNOWN_COLORS.put("oldlace", Color.decode("#fdf5e6"));
201 KNOWN_COLORS.put("olivedrab", Color.decode("#6b8e23"));
202 KNOWN_COLORS.put("orangered", Color.decode("#ff4500"));
203 KNOWN_COLORS.put("orchid", Color.decode("#da70d6"));
204 KNOWN_COLORS.put("palegoldenrod", Color.decode("#eee8aa"));
205 KNOWN_COLORS.put("palegreen", Color.decode("#98fb98"));
206 KNOWN_COLORS.put("paleturquoise", Color.decode("#afeeee"));
207 KNOWN_COLORS.put("palevioletred", Color.decode("#db7093"));
208 KNOWN_COLORS.put("papayawhip", Color.decode("#ffefd5"));
209 KNOWN_COLORS.put("peachpuff", Color.decode("#ffdab9"));
210 KNOWN_COLORS.put("peru", Color.decode("#cd853f"));
211 KNOWN_COLORS.put("pink", Color.decode("#ffc0cb"));
212 KNOWN_COLORS.put("plum", Color.decode("#dda0dd"));
213 KNOWN_COLORS.put("powderblue", Color.decode("#b0e0e6"));
214 KNOWN_COLORS.put("rosybrown", Color.decode("#bc8f8f"));
215 KNOWN_COLORS.put("royalblue", Color.decode("#4169e1"));
216 KNOWN_COLORS.put("saddlebrown", Color.decode("#8b4513"));
217 KNOWN_COLORS.put("salmon", Color.decode("#fa8072"));
218 KNOWN_COLORS.put("sandybrown", Color.decode("#f4a460"));
219 KNOWN_COLORS.put("seagreen", Color.decode("#2e8b57"));
220 KNOWN_COLORS.put("seashell", Color.decode("#fff5ee"));
221 KNOWN_COLORS.put("sienna", Color.decode("#a0522d"));
222 KNOWN_COLORS.put("skyblue", Color.decode("#87ceeb"));
223 KNOWN_COLORS.put("slateblue", Color.decode("#6a5acd"));
224 KNOWN_COLORS.put("slategray", Color.decode("#708090"));
225 KNOWN_COLORS.put("slategrey", Color.decode("#708090"));
226 KNOWN_COLORS.put("snow", Color.decode("#fffafa"));
227 KNOWN_COLORS.put("springgreen", Color.decode("#00ff7f"));
228 KNOWN_COLORS.put("steelblue", Color.decode("#4682b4"));
229 KNOWN_COLORS.put("tan", Color.decode("#d2b48c"));
230 KNOWN_COLORS.put("thistle", Color.decode("#d8bfd8"));
231 KNOWN_COLORS.put("tomato", Color.decode("#ff6347"));
232 KNOWN_COLORS.put("turquoise", Color.decode("#40e0d0"));
233 KNOWN_COLORS.put("violet", Color.decode("#ee82ee"));
234 KNOWN_COLORS.put("wheat", Color.decode("#f5deb3"));
235 KNOWN_COLORS.put("whitesmoke", Color.decode("#f5f5f5"));
236 KNOWN_COLORS.put("yellowgreen", Color.decode("#9acd32"));
237
238 KNOWN_COLORS.put("rebeccapurple", Color.decode("#663399"));
239 }
240
241 private static synchronized int nextId() {
242 return ID_GENERATOR_++;
243 }
244
245
246
247
248
249
250 public AwtRenderingBackend(final int imageWidth, final int imageHeight) {
251 id_ = nextId();
252 if (LOG.isDebugEnabled()) {
253 LOG.debug("[" + id_ + "] AwtRenderingBackend(" + imageWidth + ", " + imageHeight + ")");
254 }
255
256 image_ = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
257 graphics2D_ = image_.createGraphics();
258
259 graphics2D_.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
260 graphics2D_.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
261 graphics2D_.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
262
263
264
265
266 graphics2D_.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
267
268
269 fillColor_ = Color.black;
270 strokeColor_ = Color.black;
271 lineWidth_ = 1;
272 transformation_ = new AffineTransform();
273 updateGlobalAlpha(1f);
274 graphics2D_.setClip(null);
275
276 final Font font = new Font("SansSerif", Font.PLAIN, 10);
277 graphics2D_.setFont(font);
278
279 graphics2D_.setBackground(new Color(0f, 0f, 0f, 0f));
280 graphics2D_.setColor(Color.black);
281 graphics2D_.clearRect(0, 0, imageWidth, imageHeight);
282
283 lineCap_ = LineCap.BUTT;
284 lineJoin_ = LineJoin.MITER;
285 miterLimit_ = 10.0f;
286
287 subPaths_ = new ArrayList<>();
288 savedStates_ = new ArrayDeque<>();
289 }
290
291
292
293
294 @Override
295 public double getGlobalAlpha() {
296 return globalAlpha_;
297 }
298
299
300
301
302 @Override
303 public void setGlobalAlpha(final double globalAlpha) {
304 if (LOG.isDebugEnabled()) {
305 LOG.debug("[" + id_ + "] setGlobalAlpha(" + globalAlpha + ")");
306 }
307
308 if (globalAlpha >= 0 && globalAlpha <= 1) {
309 updateGlobalAlpha((float) globalAlpha);
310 }
311 }
312
313 private void updateGlobalAlpha(final float globalAlpha) {
314 globalAlpha_ = globalAlpha;
315 final AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, globalAlpha_);
316 graphics2D_.setComposite(composite);
317 }
318
319
320
321
322 @Override
323 public void beginPath() {
324 if (LOG.isDebugEnabled()) {
325 LOG.debug("[" + id_ + "] beginPath()");
326 }
327 subPaths_.clear();
328 }
329
330
331
332
333 @Override
334 public void ellipse(final double x, final double y,
335 final double radiusX, final double radiusY,
336 final double rotation, final double startAngle, final double endAngle,
337 final boolean anticlockwise) {
338 if (LOG.isDebugEnabled()) {
339 LOG.debug("[" + id_ + "] ellipse()");
340 }
341
342 if (startAngle == endAngle) {
343 return;
344 }
345
346 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
347 final AffineTransform transformation = new AffineTransform();
348 transformation.rotate(rotation, p.getX(), p.getY());
349
350 double startAngleDegree = Math.toDegrees(startAngle);
351 double endAngleDegree = Math.toDegrees(endAngle);
352
353 double extendAngle = endAngleDegree - startAngleDegree;
354 final Arc2D arc;
355 if (Math.abs(extendAngle) >= 360) {
356 arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
357 0, 360, Arc2D.OPEN);
358 }
359 else {
360 startAngleDegree = reverseAngle(startAngleDegree);
361 endAngleDegree = reverseAngle(endAngleDegree);
362
363 extendAngle = endAngleDegree - startAngleDegree;
364 if (!anticlockwise) {
365 extendAngle = -reverseAngle(extendAngle);
366 }
367
368 arc = new Arc2D.Double(p.getX() - radiusX, p.getY() - radiusY, radiusX * 2, radiusY * 2,
369 startAngleDegree, extendAngle, Arc2D.OPEN);
370 }
371
372
373
374
375 final boolean hasCurrentPoint;
376 if (subPaths_.isEmpty()) {
377 final Path2D subPath = new Path2D.Double();
378 subPaths_.add(subPath);
379 hasCurrentPoint = false;
380 }
381 else {
382 hasCurrentPoint = subPaths_.get(subPaths_.size() - 1).getCurrentPoint() != null;
383 }
384 getCurrentSubPath().append(transformation.createTransformedShape(arc), hasCurrentPoint);
385 }
386
387
388
389
390 @Override
391 public void bezierCurveTo(final double cp1x, final double cp1y,
392 final double cp2x, final double cp2y, final double x, final double y) {
393 if (LOG.isDebugEnabled()) {
394 LOG.debug("[" + id_ + "] bezierCurveTo()");
395 }
396
397 final Point2D cp1 = transformation_.transform(new Point2D.Double(cp1x, cp1y), null);
398 final Point2D cp2 = transformation_.transform(new Point2D.Double(cp2x, cp2y), null);
399 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
400
401 getCurrentSubPath(cp1.getX(), cp1.getY())
402 .curveTo(cp1.getX(), cp1.getY(), cp2.getX(), cp2.getY(), p.getX(), p.getY());
403 }
404
405
406
407
408 @Override
409 public void arc(final double x, final double y, final double radius, final double startAngle,
410 final double endAngle, final boolean anticlockwise) {
411 if (LOG.isDebugEnabled()) {
412 LOG.debug("[" + id_ + "] arc()");
413 }
414
415 ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
416 }
417
418 private static double reverseAngle(final double degrees) {
419 return ((-degrees % 360) + 360) % 360;
420 }
421
422
423
424
425 @Override
426 public void clearRect(final double x, final double y, final double w, final double h) {
427 if (LOG.isDebugEnabled()) {
428 LOG.debug("[" + id_ + "] clearRect(" + x + ", " + y + ", " + w + ", " + h + ")");
429 }
430
431 final Composite savedComposite = graphics2D_.getComposite();
432 final Color savedColor = graphics2D_.getColor();
433
434 try {
435 graphics2D_.setComposite(AlphaComposite.Clear);
436 graphics2D_.fill(rectPath(x, y, w, h));
437 }
438 finally {
439 graphics2D_.setComposite(savedComposite);
440 graphics2D_.setColor(savedColor);
441 }
442 }
443
444
445
446
447 @Override
448 public void drawImage(final org.htmlunit.platform.image.ImageData imageData,
449 final int sx, final int sy, final Integer sWidth, final Integer sHeight,
450 final int dx, final int dy, final Integer dWidth, final Integer dHeight) throws IOException {
451 if (LOG.isDebugEnabled()) {
452 LOG.debug("[" + id_ + "] drawImage(" + sx + ", " + sy + ", " + sWidth + ", " + sHeight
453 + "," + dx + ", " + dy + ", " + dWidth + ", " + dHeight + ")");
454 }
455
456 try {
457 final ImageReader imageReader = ((ImageIOImageData) imageData).getImageReader();
458
459 if (imageReader.getNumImages(true) != 0) {
460 final BufferedImage img = imageReader.read(0);
461
462 final AffineTransform savedTransform = graphics2D_.getTransform();
463 try {
464 graphics2D_.setTransform(transformation_);
465 graphics2D_.setColor(fillColor_);
466
467 final int sx2;
468 if (sWidth == null) {
469 sx2 = sx + img.getWidth();
470 }
471 else {
472 sx2 = sx + sWidth;
473 }
474
475 final int sy2;
476 if (sHeight == null) {
477 sy2 = sy + img.getHeight();
478 }
479 else {
480 sy2 = sy + sHeight;
481 }
482
483 int dx1 = dx;
484 final int dx2;
485 if (dWidth == null) {
486 dx2 = dx + img.getWidth();
487 }
488 else {
489 if (dWidth < 0) {
490 dx1 = dx1 + dWidth;
491 dx2 = dx1 - dWidth;
492 }
493 else {
494 dx2 = dx1 + dWidth;
495 }
496 }
497
498 int dy1 = dy;
499 final int dy2;
500 if (dHeight == null) {
501 dy2 = dy + img.getHeight();
502 }
503 else {
504 if (dHeight < 0) {
505 dy1 = dy1 + dHeight;
506 dy2 = dy1 - dHeight;
507 }
508 else {
509 dy2 = dy1 + dHeight;
510 }
511 }
512
513 final Object done = new Object();
514 final ImageObserver imageObserver = (img1, flags, x, y, width, height) -> {
515
516 if ((flags & ImageObserver.ALLBITS) == ImageObserver.ALLBITS) {
517 return true;
518 }
519
520 if ((flags & ImageObserver.ABORT) == ImageObserver.ABORT
521 || (flags & ImageObserver.ERROR) == ImageObserver.ERROR) {
522 return true;
523 }
524
525 synchronized (done) {
526 done.notify();
527 }
528
529 return false;
530 };
531
532 synchronized (done) {
533 final boolean completelyLoaded =
534 graphics2D_.drawImage(img, dx1, dy1, dx2, dy2, sx, sy, sx2, sy2, imageObserver);
535 if (!completelyLoaded) {
536 while (true) {
537 try {
538 done.wait(4 * 1000);
539 break;
540 }
541 catch (final InterruptedException e) {
542 LOG.error("[" + id_ + "] AwtRenderingBackend interrupted "
543 + "while waiting for drawImage to finish.", e);
544
545
546 Thread.currentThread().interrupt();
547 }
548 }
549 }
550 }
551 }
552 finally {
553 graphics2D_.setTransform(savedTransform);
554 }
555 }
556 }
557 catch (final ClassCastException e) {
558 LOG.error("[" + id_ + "] drawImage(..) failed", e);
559 }
560 }
561
562
563
564
565 @Override
566 public String encodeToString(final String type) throws IOException {
567 String imageType = type;
568 if (imageType != null && imageType.startsWith("image/")) {
569 imageType = imageType.substring(6);
570 }
571 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
572 ImageIO.write(image_, imageType, bos);
573
574 final byte[] imageBytes = bos.toByteArray();
575 return new String(Base64.getEncoder().encode(imageBytes), StandardCharsets.US_ASCII);
576 }
577 }
578
579
580
581
582 @Override
583 public void fill(final RenderingBackend.WindingRule windingRule) {
584 if (LOG.isDebugEnabled()) {
585 LOG.debug("[" + id_ + "] fill("
586 + (windingRule == RenderingBackend.WindingRule.EVEN_ODD ? "evenOdd" : "nonZero") + ")");
587 }
588
589 final int awtRule = (windingRule == RenderingBackend.WindingRule.EVEN_ODD)
590 ? Path2D.WIND_EVEN_ODD
591 : Path2D.WIND_NON_ZERO;
592
593 graphics2D_.setColor(fillColor_);
594 for (final Path2D path2d : subPaths_) {
595 path2d.setWindingRule(awtRule);
596 graphics2D_.fill(path2d);
597 }
598 }
599
600
601
602
603 @Override
604 public void fillRect(final double x, final double y, final double w, final double h) {
605 if (LOG.isDebugEnabled()) {
606 LOG.debug("[" + id_ + "] fillRect(" + x + ", " + y + ", " + w + ", " + h + ")");
607 }
608
609 graphics2D_.setColor(fillColor_);
610 graphics2D_.fill(rectPath(x, y, w, h));
611 }
612
613
614
615
616 @Override
617 public void fillText(final String text, final double x, final double y) {
618 if (LOG.isDebugEnabled()) {
619 LOG.debug("[" + id_ + "] fillText('" + text + "', " + x + ", " + y + ")");
620 }
621
622 final AffineTransform savedTransform = graphics2D_.getTransform();
623 try {
624 graphics2D_.setTransform(transformation_);
625 graphics2D_.setColor(fillColor_);
626 graphics2D_.drawString(text, (float) x, (float) y);
627 }
628 finally {
629 graphics2D_.setTransform(savedTransform);
630 }
631 }
632
633
634
635
636 @Override
637 public byte[] getBytes(final int width, final int height, final int sx, final int sy) {
638 if (LOG.isDebugEnabled()) {
639 LOG.debug("[" + id_ + "] getBytes(" + width + ", " + height + ", " + sx + ", " + sy + ")");
640 }
641
642 final byte[] array = new byte[width * height * 4];
643 int index = 0;
644
645
646 for (int y = sy; y < sy + height; y++) {
647 for (int x = sx; x < sx + width; x++) {
648 if (x < 0 || x >= image_.getWidth() || y < 0 || y >= image_.getHeight()) {
649 array[index++] = (byte) 0;
650 array[index++] = (byte) 0;
651 array[index++] = (byte) 0;
652 array[index++] = (byte) 0;
653 }
654 else {
655 final int color = image_.getRGB(x, y);
656 array[index++] = (byte) ((color >> 16) & 0xFF);
657 array[index++] = (byte) ((color >> 8) & 0xFF);
658 array[index++] = (byte) (color & 0xFF);
659 array[index++] = (byte) ((color >>> 24) & 0xFF);
660 }
661 }
662 }
663
664 return array;
665 }
666
667
668
669
670 @Override
671 public void lineTo(final double x, final double y) {
672 if (LOG.isDebugEnabled()) {
673 LOG.debug("[" + id_ + "] lineTo(" + x + ", " + y + ")");
674 }
675
676 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
677 final Path2D subPath = getCurrentSubPath();
678 if (subPath.getCurrentPoint() == null) {
679 subPath.moveTo(p.getX(), p.getY());
680 }
681 else {
682 subPath.lineTo(p.getX(), p.getY());
683 }
684 }
685
686
687
688
689 @Override
690 public void moveTo(final double x, final double y) {
691 if (LOG.isDebugEnabled()) {
692 LOG.debug("[" + id_ + "] moveTo(" + x + ", " + y + ")");
693 }
694
695 final Point2D p = transformation_.transform(new Point2D.Double(x, y), null);
696
697 if (!subPaths_.isEmpty()) {
698 final Path2D last = subPaths_.get(subPaths_.size() - 1);
699 if (!hasSegments(last)) {
700 last.reset();
701 last.moveTo(p.getX(), p.getY());
702 return;
703 }
704 }
705
706 final Path2D subPath = new Path2D.Double();
707 subPath.moveTo(p.getX(), p.getY());
708 subPaths_.add(subPath);
709 }
710
711
712
713
714 @Override
715 public void putImageData(final ByteBuffer imageDataBuffer,
716 final int imageDataWidth, final int imageDataHeight,
717 final int dx, final int dy, final int dirtyX, final int dirtyY,
718 final int dirtyWidth, final int dirtyHeight) {
719
720 if (LOG.isDebugEnabled()) {
721 LOG.debug("[" + id_ + "] putImageData()");
722 }
723 if (imageDataBuffer == 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 = imageDataBuffer.get(byteIdx) & 0xFF;
761 final int g = imageDataBuffer.get(byteIdx + 1) & 0xFF;
762 final int b = imageDataBuffer.get(byteIdx + 2) & 0xFF;
763 final int a = imageDataBuffer.get(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 }