View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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.javascript.host.canvas;
16  
17  import java.io.IOException;
18  
19  import org.apache.commons.lang3.StringUtils;
20  import org.apache.commons.logging.Log;
21  import org.apache.commons.logging.LogFactory;
22  import org.htmlunit.corejs.javascript.Context;
23  import org.htmlunit.corejs.javascript.Function;
24  import org.htmlunit.corejs.javascript.Scriptable;
25  import org.htmlunit.corejs.javascript.VarScope;
26  import org.htmlunit.html.HtmlImage;
27  import org.htmlunit.javascript.HtmlUnitScriptable;
28  import org.htmlunit.javascript.JavaScriptEngine;
29  import org.htmlunit.javascript.configuration.JsxClass;
30  import org.htmlunit.javascript.configuration.JsxConstructor;
31  import org.htmlunit.javascript.configuration.JsxFunction;
32  import org.htmlunit.javascript.configuration.JsxGetter;
33  import org.htmlunit.javascript.configuration.JsxSetter;
34  import org.htmlunit.javascript.host.html.HTMLCanvasElement;
35  import org.htmlunit.javascript.host.html.HTMLImageElement;
36  import org.htmlunit.platform.Platform;
37  import org.htmlunit.platform.canvas.rendering.RenderingBackend;
38  import org.htmlunit.platform.canvas.rendering.RenderingBackend.LineCap;
39  import org.htmlunit.platform.canvas.rendering.RenderingBackend.LineJoin;
40  import org.htmlunit.platform.canvas.rendering.RenderingBackend.WindingRule;
41  import org.htmlunit.protocol.data.DataURLConnection;
42  import org.htmlunit.util.MimeType;
43  
44  /**
45   * A JavaScript object for {@code CanvasRenderingContext2D}.
46   *
47   * @author Ahmed Ashour
48   * @author Marc Guillemot
49   * @author Frank Danek
50   * @author Ronald Brill
51   *
52   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D">MDN Documentation</a>
53   */
54  @JsxClass
55  public class CanvasRenderingContext2D extends HtmlUnitScriptable {
56  
57      private static final Log LOG = LogFactory.getLog(CanvasRenderingContext2D.class);
58  
59      private HTMLCanvasElement canvas_;
60      private RenderingBackend renderingBackend_;
61  
62      /**
63       * Default constructor.
64       */
65      public CanvasRenderingContext2D() {
66          super();
67      }
68  
69      /**
70       * JavaScript constructor.
71       */
72      @JsxConstructor
73      public void jsConstructor() {
74          // nothing to do
75      }
76  
77      /**
78       * Constructs in association with {@link HTMLCanvasElement}.
79       * @param canvas the {@link HTMLCanvasElement}
80       */
81      public CanvasRenderingContext2D(final HTMLCanvasElement canvas) {
82          super();
83          canvas_ = canvas;
84          renderingBackend_ = null;
85      }
86  
87      private RenderingBackend getRenderingBackend() {
88          if (renderingBackend_ == null) {
89              final int imageWidth = Math.max(1, canvas_.getWidth());
90              final int imageHeight = Math.max(1, canvas_.getHeight());
91  
92              renderingBackend_ = Platform.getRenderingBackend(imageWidth, imageHeight);
93          }
94          return renderingBackend_;
95      }
96  
97      /**
98       * Specifies the alpha (transparency) value that is applied to shapes and images
99       * before they are drawn onto the canvas.
100      * @return the {@code globalAlpha} property
101      */
102     @JsxGetter
103     public double getGlobalAlpha() {
104         return getRenderingBackend().getGlobalAlpha();
105     }
106 
107     /**
108      * Sets the {@code globalAlpha} property.
109      * @param globalAlpha the {@code globalAlpha} property value
110      */
111     @JsxSetter
112     public void setGlobalAlpha(final double globalAlpha) {
113         getRenderingBackend().setGlobalAlpha(globalAlpha);
114     }
115 
116     /**
117      * Returns the {@code fillStyle} property.
118      * @return the {@code fillStyle} property
119      */
120     @JsxGetter
121     public HtmlUnitScriptable getFillStyle() {
122         LOG.info("CanvasRenderingContext2D.getFillStyle() not yet implemented");
123         return null;
124     }
125 
126     /**
127      * Sets the {@code fillStyle} property.
128      * @param fillStyle the {@code fillStyle} property value
129      */
130     @JsxSetter
131     public void setFillStyle(final String fillStyle) {
132         getRenderingBackend().setFillStyle(fillStyle);
133     }
134 
135     /**
136      * Returns the {@code strokeStyle} property.
137      * @return the {@code strokeStyle} property
138      */
139     @JsxGetter
140     public HtmlUnitScriptable getStrokeStyle() {
141         LOG.info("CanvasRenderingContext2D.getStrokeStyle() not yet implemented");
142         return null;
143     }
144 
145     /**
146      * Sets the {@code strokeStyle} property.
147      * @param strokeStyle the {@code strokeStyle} property value
148      */
149     @JsxSetter
150     public void setStrokeStyle(final String strokeStyle) {
151         getRenderingBackend().setStrokeStyle(strokeStyle);
152     }
153 
154     /**
155      * Returns the {@code lineWidth} property.
156      * @return the {@code lineWidth} property
157      */
158     @JsxGetter
159     public double getLineWidth() {
160         return getRenderingBackend().getLineWidth();
161     }
162 
163     /**
164      * Sets the {@code lineWidth} property.
165      * @param lineWidth the {@code lineWidth} property value
166      */
167     @JsxSetter
168     public void setLineWidth(final Object lineWidth) {
169         if (!JavaScriptEngine.isUndefined(lineWidth)) {
170             final double width = JavaScriptEngine.toNumber(lineWidth);
171             if (!Double.isNaN(width)) {
172                 getRenderingBackend().setLineWidth((float) width);
173             }
174         }
175     }
176 
177     /**
178      * Draws an arc.
179      * @param x the x
180      * @param y the y
181      * @param radius the radius
182      * @param startAngle the start angle
183      * @param endAngle the end angle
184      * @param anticlockwise is anti-clockwise
185      */
186     @JsxFunction
187     public void arc(final double x, final double y, final double radius, final double startAngle,
188                 final double endAngle, final boolean anticlockwise) {
189         getRenderingBackend().arc(x, y, radius, startAngle, endAngle, anticlockwise);
190     }
191 
192     /**
193      * Draws an arc.
194      * @param x1 the x1
195      * @param y1 the y1
196      * @param x2 the x2
197      * @param y2 the y2
198      * @param radius the radius
199      */
200     @JsxFunction
201     public void arcTo(final double x1, final double y1, final double x2, final double y2,
202                 final double radius) {
203         LOG.info("CanvasRenderingContext2D.arcTo() not yet implemented");
204     }
205 
206     /**
207      * Begins the subpaths.
208      */
209     @JsxFunction
210     public void beginPath() {
211         getRenderingBackend().beginPath();
212     }
213 
214     /**
215      * Draws a cubic Bézier curve.
216      * @param cp1x the cp1x
217      * @param cp1y the cp1y
218      * @param cp2x the cp2x
219      * @param cp2y the cp2y
220      * @param x the x
221      * @param y the y
222      */
223     @JsxFunction
224     public void bezierCurveTo(final double cp1x, final double cp1y, final double cp2x, final double cp2y,
225             final double x, final double y) {
226         getRenderingBackend().bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
227     }
228 
229     /**
230      * Clears the specified rectangular area.
231      * @param x the x
232      * @param y the y
233      * @param w the width
234      * @param h the height
235      */
236     @JsxFunction
237     public void clearRect(final double x, final double y, final double w, final double h) {
238         getRenderingBackend().clearRect(x, y, w, h);
239     }
240 
241     /**
242      * Creates a new clipping region.
243      * @param context the JavaScript context
244      * @param scope the scope
245      * @param thisObj the scriptable
246      * @param args the arguments passed into the method
247      * @param function the function
248      */
249     @JsxFunction
250     public static void clip(final Context context, final VarScope scope,
251             final Scriptable thisObj, final Object[] args, final Function function) {
252         if (!(thisObj instanceof CanvasRenderingContext2D canvas)) {
253             throw JavaScriptEngine.reportRuntimeError(
254                     "CanvasRenderingContext2D.clip() failed - this is not a CanvasRenderingContext2D");
255         }
256 
257         RenderingBackend.WindingRule windingRule = WindingRule.NON_ZERO;
258         if (args.length == 1) {
259             final String windingRuleParam = JavaScriptEngine.toString(args[0]);
260             if ("evenodd".contentEquals(windingRuleParam)) {
261                 windingRule = WindingRule.EVEN_ODD;
262             }
263             canvas.getRenderingBackend().clip(windingRule, null);
264             return;
265         }
266 
267         if (args.length > 1) {
268             if (!(args[0] instanceof Path2D)) {
269                 throw JavaScriptEngine.reportRuntimeError(
270                         "CanvasRenderingContext2D.clip() failed - the first parameter has to be a Path2D");
271             }
272 
273             final String windingRuleParam = JavaScriptEngine.toString(args[1]);
274             if ("evenodd".contentEquals(windingRuleParam)) {
275                 windingRule = WindingRule.EVEN_ODD;
276             }
277 
278             LOG.info("CanvasRenderingContext2D.clip(path, fillRule) not yet implemented");
279             // canvas.getRenderingBackend().clip(windingRule, (Path2D) args[0]);
280             return;
281         }
282 
283         canvas.getRenderingBackend().clip(WindingRule.NON_ZERO, null);
284     }
285 
286     /**
287      * Closes the current subpath.
288      */
289     @JsxFunction
290     public void closePath() {
291         getRenderingBackend().closePath();
292     }
293 
294     /**
295      * Returns the {@code ImageData} object.
296      * This may accept a variable number of arguments.
297      * @param context the JavaScript context
298      * @param scope the scope
299      * @param thisObj the scriptable
300      * @param args the arguments passed into the method
301      * @param function the function
302      * @return the {@code ImageData} object
303      */
304     @JsxFunction
305     public static ImageData createImageData(final Context context, final VarScope scope,
306             final Scriptable thisObj, final Object[] args, final Function function) {
307         if (!(thisObj instanceof CanvasRenderingContext2D canvas)) {
308             throw JavaScriptEngine.reportRuntimeError(
309                     "CanvasRenderingContext2D.createImageData() failed - this is not a CanvasRenderingContext2D");
310         }
311 
312         if (args.length > 0 && args[0] instanceof ImageData imageDataParameter) {
313             final ImageData imageData = new ImageData(null,
314                     0, 0, imageDataParameter.getWidth(), imageDataParameter.getHeight());
315             imageData.setParentScope(scope);
316             imageData.setPrototype(canvas.getPrototype(imageData.getClass()));
317             return imageData;
318         }
319 
320         if (args.length > 1) {
321             final int width = Math.abs((int) JavaScriptEngine.toInteger(args, 0));
322             final int height = Math.abs((int) JavaScriptEngine.toInteger(args, 1));
323             final ImageData imageData = new ImageData(null, 0, 0, width, height);
324             imageData.setParentScope(canvas.getParentScope());
325             imageData.setPrototype(canvas.getPrototype(imageData.getClass()));
326             return imageData;
327         }
328 
329         throw JavaScriptEngine.reportRuntimeError(
330                 "CanvasRenderingContext2D.createImageData() failed - "
331                 + "wrong parameters given (" + StringUtils.join(args, ", ") + ")");
332     }
333 
334     /**
335      * Creates linear gradient.
336      * @param x0 the x0
337      * @param y0 the y0
338      * @param x1 the x1
339      * @param y1 the y1
340      * @return the new CanvasGradient
341      */
342     @JsxFunction
343     public CanvasGradient createLinearGradient(final double x0, final double y0, final double x1,
344             final Object y1) {
345         final CanvasGradient canvasGradient = new CanvasGradient();
346         canvasGradient.setParentScope(getParentScope());
347         canvasGradient.setPrototype(getPrototype(canvasGradient.getClass()));
348         return canvasGradient;
349     }
350 
351     /**
352      * Creates a pattern.
353      */
354     @JsxFunction
355     public void createPattern() {
356         LOG.info("CanvasRenderingContext2D.createPattern() not yet implemented");
357     }
358 
359     /**
360      * Creates a gradient.
361      * @param x0 the x axis of the coordinate of the start circle
362      * @param y0 the y axis of the coordinate of the start circle
363      * @param r0 the radius of the start circle
364      * @param x1 the x axis of the coordinate of the end circle
365      * @param y1 the y axis of the coordinate of the end circle
366      * @param r1 the radius of the end circle
367      * @return the new CanvasGradient
368      */
369     @JsxFunction
370     public CanvasGradient createRadialGradient(final double x0, final double y0,
371                             final double r0, final double x1, final double y1, final double r1) {
372         final CanvasGradient canvasGradient = new CanvasGradient();
373         canvasGradient.setParentScope(getParentScope());
374         canvasGradient.setPrototype(getPrototype(canvasGradient.getClass()));
375         return canvasGradient;
376     }
377 
378     /**
379      * Draws images onto the canvas.
380      *
381      * @param image an element to draw into the context
382      * @param sx the X coordinate of the top left corner of the sub-rectangle of the source image
383      *        to draw into the destination context
384      * @param sy the Y coordinate of the top left corner of the sub-rectangle of the source image
385      *        to draw into the destination context
386      * @param sWidth the width of the sub-rectangle of the source image to draw into the destination context
387      * @param sHeight the height of the sub-rectangle of the source image to draw into the destination context
388      * @param dx the X coordinate in the destination canvas at which to place the top-left corner of the source image
389      * @param dy the Y coordinate in the destination canvas at which to place the top-left corner of the source image
390      * @param dWidth the width to draw the image in the destination canvas. This allows scaling of the drawn image
391      * @param dHeight the height to draw the image in the destination canvas. This allows scaling of the drawn image
392      */
393     @JsxFunction
394     @SuppressWarnings("unused")
395     public void drawImage(final Object image, final int sx, final int sy, final Object sWidth, final Object sHeight,
396             final Object dx, final Object dy, final Object dWidth, final Object dHeight) {
397 
398         if (image instanceof HTMLImageElement imageElem) {
399             try {
400                 final org.htmlunit.platform.image.ImageData imageData
401                             = ((HtmlImage) imageElem.getDomNodeOrDie()).getImageData();
402 
403                 // 3 arguments
404                 //   void ctx.drawImage(image, dx, dy);
405                 if (JavaScriptEngine.isUndefined(sWidth)) {
406                     getRenderingBackend().drawImage(imageData, 0, 0, null, null, sx, sy, null, null);
407                 }
408 
409                 // 5 arguments
410                 //   void ctx.drawImage(image, dx, dy, dWidth, dHeight);
411                 else if (JavaScriptEngine.isUndefined(dx)) {
412                     final int dWidthI = JavaScriptEngine.toInt32(sWidth);
413                     final int dHeightI = JavaScriptEngine.toInt32(sHeight);
414 
415                     getRenderingBackend().drawImage(imageData, 0, 0, null, null, sx, sy, dWidthI, dHeightI);
416                 }
417 
418                 // all 9 arguments
419                 //   void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
420                 else {
421                     final int sWidthI = JavaScriptEngine.toInt32(sWidth);
422                     final int sHeightI = JavaScriptEngine.toInt32(sHeight);
423 
424                     final int dxI = JavaScriptEngine.toInt32(dx);
425                     final int dyI = JavaScriptEngine.toInt32(dy);
426                     final int dWidthI = JavaScriptEngine.toInt32(dWidth);
427                     final int dHeightI = JavaScriptEngine.toInt32(dHeight);
428 
429                     getRenderingBackend().drawImage(imageData,
430                             sx, sy, sWidthI, sHeightI, dxI, dyI, dWidthI, dHeightI);
431                 }
432             }
433             catch (final IOException ex) {
434                 LOG.info("There is no ImageReader available for your image with src '" + imageElem.getSrc() + "'. "
435                         + "Please have a look at https://www.htmlunit.org/images-howto.html "
436                         + "for a possible solution.");
437             }
438         }
439     }
440 
441     /**
442      * Returns the Data URL.
443      *
444      * @param type an optional type
445      * @return the dataURL
446      */
447     public String toDataURL(String type) {
448         try {
449             if (type == null) {
450                 type = MimeType.IMAGE_PNG;
451             }
452             return DataURLConnection.DATA_PREFIX + type + ";base64," + getRenderingBackend().encodeToString(type);
453         }
454         catch (final IOException ex) {
455             throw JavaScriptEngine.throwAsScriptRuntimeEx(ex);
456         }
457     }
458 
459     /**
460      * Paints the specified ellipse.
461      * @param x the x
462      * @param y the y
463      * @param radiusX the radiusX
464      * @param radiusY the radiusY
465      * @param rotation the rotation
466      * @param startAngle the startAngle
467      * @param endAngle the endAngle
468      * @param anticlockwise the anticlockwise
469      */
470     @JsxFunction
471     public void ellipse(final double x, final double y,
472                     final double radiusX, final double radiusY,
473                     final double rotation, final double startAngle, final double endAngle,
474                     final boolean anticlockwise) {
475         getRenderingBackend().ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
476     }
477 
478     /**
479      * Fills the shape.
480      * @param context the context
481      * @param scope the scope
482      * @param thisObj this object
483      * @param args the arguments
484      * @param function the function
485      */
486     @JsxFunction
487     public static void fill(final Context context, final VarScope scope,
488             final Scriptable thisObj, final Object[] args, final Function function) {
489         if (!(thisObj instanceof CanvasRenderingContext2D renderingCtx)) {
490             throw JavaScriptEngine.reportRuntimeError(
491                     "CanvasRenderingContext2D.fill() failed - this is not a CanvasRenderingContext2D");
492         }
493 
494         // Determine which argument (if any) is the fill rule string.
495         // Signature 1: fill(optional CanvasFillRule fillRule)
496         // Signature 2: fill(Path2D path, optional CanvasFillRule fillRule)
497         String fillRuleStr = null;
498         if (args.length > 1) {
499             fillRuleStr = JavaScriptEngine.toString(args[1]);
500         }
501         else if (args.length > 0) {
502             fillRuleStr = JavaScriptEngine.toString(args[0]);
503         }
504 
505         RenderingBackend.WindingRule windingRule = RenderingBackend.WindingRule.NON_ZERO;
506         if (fillRuleStr != null) {
507             if ("evenodd".equals(fillRuleStr)) {
508                 windingRule = RenderingBackend.WindingRule.EVEN_ODD;
509             }
510             else if (!"nonzero".equals(fillRuleStr)) {
511                 // Per spec: unrecognised values are ignored entirely.
512                 // Since fill() has no persistent fill-rule state, we just
513                 // use the default nonzero — which is already set above.
514                 // Log it for debugging purposes.
515                 if (LOG.isWarnEnabled()) {
516                     LOG.warn("fill() called with unrecognised fillRule: '" + fillRuleStr + "', using 'nonzero'.");
517                 }
518             }
519         }
520 
521         renderingCtx.getRenderingBackend().fill(windingRule);
522     }
523 
524     /**
525      * Paints the specified rectangular area.
526      * @param x the x
527      * @param y the y
528      * @param w the width
529      * @param h the height
530      */
531     @JsxFunction
532     public void fillRect(final int x, final int y, final int w, final int h) {
533         getRenderingBackend().fillRect(x, y, w, h);
534     }
535 
536     /**
537      * Fills a given text at the given (x, y) position.
538      * @param text the text
539      * @param x the x
540      * @param y the y
541      */
542     @JsxFunction
543     public void fillText(final String text, final double x, final double y) {
544         getRenderingBackend().fillText(text, x, y);
545     }
546 
547     /**
548      * Returns the {@code ImageData} object.
549      * @param sx x
550      * @param sy y
551      * @param sw width
552      * @param sh height
553      * @return the {@code ImageData} object
554      */
555     @JsxFunction
556     public ImageData getImageData(final int sx, final int sy, final int sw, final int sh) {
557         final ImageData imageData = new ImageData(getRenderingBackend(), sx, sy, sw, sh);
558         imageData.setParentScope(getParentScope());
559         imageData.setPrototype(getPrototype(imageData.getClass()));
560         return imageData;
561     }
562 
563     /**
564      * Dummy placeholder.
565      */
566     @JsxFunction(functionName = "getLineDash")
567     public void lineDash() {
568         LOG.info("CanvasRenderingContext2D.getLineDash() not yet implemented");
569     }
570 
571     /**
572      * Dummy placeholder.
573      */
574     @JsxFunction(functionName = "getLineData")
575     public void lineData() {
576         LOG.info("CanvasRenderingContext2D.getLineData() not yet implemented");
577     }
578 
579     /**
580      * Dummy placeholder.
581      */
582     @JsxFunction
583     public void isPointInPath() {
584         LOG.info("CanvasRenderingContext2D.isPointInPath() not yet implemented");
585     }
586 
587     /**
588      * Connect the last point to the given point.
589      * @param x the x
590      * @param y the y
591      */
592     @JsxFunction
593     public void lineTo(final double x, final double y) {
594         getRenderingBackend().lineTo(x, y);
595     }
596 
597     /**
598      * Calculate TextMetrics for the given text.
599      * @param text the text to measure
600      * @return the text metrics
601      */
602     @JsxFunction
603     public TextMetrics measureText(final Object text) {
604         if (text == null || JavaScriptEngine.isUndefined(text)) {
605             throw JavaScriptEngine.typeError("Missing argument for CanvasRenderingContext2D.measureText().");
606         }
607 
608         final String textValue = JavaScriptEngine.toString(text);
609 
610         // TODO take font into account
611         final int width = textValue.length() * getBrowserVersion().getPixelsPerChar();
612 
613         final TextMetrics metrics = new TextMetrics(width);
614         metrics.setParentScope(getParentScope());
615         metrics.setPrototype(getPrototype(metrics.getClass()));
616         return metrics;
617     }
618 
619     /**
620      * Creates a new subpath.
621      * @param x the x
622      * @param y the y
623      */
624     @JsxFunction
625     public void moveTo(final double x, final double y) {
626         getRenderingBackend().moveTo(x, y);
627     }
628 
629     /**
630      * Paints data from the given ImageData object onto the canvas.
631      * @param imageData an ImageData object containing the array of pixel values
632      * @param dx horizontal position (x coordinate) at which to place the image data in the destination canvas
633      * @param dy vertical position (y coordinate) at which to place the image data in the destination canvas
634      * @param dirtyX horizontal position (x coordinate) of the top-left corner
635      *        from which the image data will be extracted. Defaults to 0.
636      * @param dirtyY vertical position (y coordinate) of the top-left corner
637      *        from which the image data will be extracted. Defaults to 0.
638      * @param dirtyWidth width of the rectangle to be painted.
639      *        Defaults to the width of the image data.
640      * @param dirtyHeight height of the rectangle to be painted.
641      *        Defaults to the height of the image data.
642      */
643     @JsxFunction
644     public void putImageData(final ImageData imageData,
645                 final int dx, final int dy, final Object dirtyX, final Object dirtyY,
646                 final Object dirtyWidth, final Object dirtyHeight) {
647         int dirtyXArg = 0;
648         int dirtyYArg = 0;
649         int dirtyWidthArg = imageData.getWidth();
650         int dirtyHeightArg = imageData.getHeight();
651 
652         if (!JavaScriptEngine.isUndefined(dirtyX)) {
653             dirtyXArg = (int) JavaScriptEngine.toInteger(dirtyX);
654 
655             if (JavaScriptEngine.isUndefined(dirtyY)
656                     || JavaScriptEngine.isUndefined(dirtyWidth)
657                     || JavaScriptEngine.isUndefined(dirtyHeight)) {
658                 throw JavaScriptEngine.reportRuntimeError(
659                         "CanvasRenderingContext2D.putImageData() failed - seven parameters expected");
660             }
661             dirtyYArg = (int) JavaScriptEngine.toInteger(dirtyY);
662             dirtyWidthArg = (int) JavaScriptEngine.toInteger(dirtyWidth);
663             dirtyHeightArg = (int) JavaScriptEngine.toInteger(dirtyHeight);
664         }
665 
666         getRenderingBackend().putImageData(
667                 imageData.getData().getBuffer().getBuffer(), imageData.getWidth(), imageData.getHeight(),
668                 dx, dy, dirtyXArg, dirtyYArg, dirtyWidthArg, dirtyHeightArg);
669     }
670 
671     /**
672      * Draws a quadratic Bézier curve.
673      * @param controlPointX the x-coordinate of the control point
674      * @param controlPointY the y-coordinate of the control point
675      * @param endPointX the x-coordinate of the end point
676      * @param endPointY the y-coordinate of the end point
677      */
678     @JsxFunction
679     public void quadraticCurveTo(final double controlPointX, final double controlPointY,
680             final double endPointX, final double endPointY) {
681         getRenderingBackend().quadraticCurveTo(controlPointX, controlPointY, endPointX, endPointY);
682     }
683 
684     /**
685      * Renders a rectangle.
686      * @param x the x
687      * @param y the y
688      * @param w the width
689      * @param h the height
690      */
691     @JsxFunction
692     public void rect(final double x, final double y, final double w, final double h) {
693         getRenderingBackend().rect(x, y, w, h);
694     }
695 
696     /**
697      * Pops state stack and restore state.
698      */
699     @JsxFunction
700     public void restore() {
701         getRenderingBackend().restore();
702     }
703 
704     /**
705      * Adds a rotation to the transformation matrix.
706      * @param angle the angle
707      */
708     @JsxFunction
709     public void rotate(final double angle) {
710         getRenderingBackend().rotate(angle);
711     }
712 
713     /**
714      * Pushes state on state stack.
715      */
716     @JsxFunction
717     public void save() {
718         getRenderingBackend().save();
719     }
720 
721     /**
722      * Changes the transformation matrix to apply a scaling transformation with the given characteristics.
723      * @param x the scale factor in the horizontal direction
724      * @param y the scale factor in the vertical direction
725      */
726     @JsxFunction
727     public void scale(final Object x, final Object y) {
728         LOG.info("CanvasRenderingContext2D.scale() not yet implemented");
729     }
730 
731     /**
732      * Dummy placeholder.
733      */
734     @JsxFunction
735     public void setLineDash() {
736         LOG.info("CanvasRenderingContext2D.setLineDash() not yet implemented");
737     }
738 
739     /**
740      * Resets (overrides) the current transformation to the identity matrix,
741      * and then invokes a transformation described by the arguments of this method.
742      * This lets you scale, rotate, translate (move), and skew the context.
743      * @param m11 Horizontal scaling. A value of 1 results in no scaling
744      * @param m12 Vertical skewing
745      * @param m21 Horizontal skewing
746      * @param m22 Vertical scaling. A value of 1 results in no scaling
747      * @param dx Horizontal translation (moving)
748      * @param dy Vertical translation (moving).
749      */
750     @JsxFunction
751     public void setTransform(final double m11, final double m12,
752                     final double m21, final double m22, final double dx, final double dy) {
753         getRenderingBackend().setTransform(m11, m12, m21, m22, dx, dy);
754     }
755 
756     /**
757      * Calculates the strokes of all the subpaths of the current path.
758      */
759     @JsxFunction
760     public void stroke() {
761         getRenderingBackend().stroke();
762     }
763 
764     /**
765      * Strokes the specified rectangular area.
766      * @param x the x
767      * @param y the y
768      * @param w the width
769      * @param h the height
770      */
771     @JsxFunction
772     public void strokeRect(final double x, final double y, final double w, final double h) {
773         getRenderingBackend().strokeRect(x, y, w, h);
774     }
775 
776     /**
777      * Dummy placeholder.
778      */
779     @JsxFunction
780     public void strokeText() {
781         LOG.info("CanvasRenderingContext2D.strokeText() not yet implemented");
782     }
783 
784     /**
785      * Multiplies the current transformation with the matrix described by the
786      * arguments of this method. This lets you scale, rotate, translate (move),
787      * and skew the context.
788      * @param m11 Horizontal scaling. A value of 1 results in no scaling
789      * @param m12 Vertical skewing
790      * @param m21 Horizontal skewing
791      * @param m22 Vertical scaling. A value of 1 results in no scaling
792      * @param dx Horizontal translation (moving)
793      * @param dy Vertical translation (moving).
794      */
795     @JsxFunction
796     public void transform(final double m11, final double m12,
797                     final double m21, final double m22, final double dx, final double dy) {
798         getRenderingBackend().transform(m11, m12, m21, m22, dx, dy);
799     }
800 
801     /**
802      * Changes the transformation matrix to apply a translation transformation with the given characteristics.
803      * @param x the translation distance in the horizontal direction
804      * @param y the translation distance in the vertical direction
805      */
806     @JsxFunction
807     public void translate(final double x, final double y) {
808         getRenderingBackend().translate(x, y);
809     }
810 
811     /**
812      * Returns the associated {@link HTMLCanvasElement}.
813      * @return the associated {@link HTMLCanvasElement}
814      */
815     @JsxGetter
816     public HTMLCanvasElement getCanvas() {
817         return canvas_;
818     }
819 
820     /**
821      * Returns the the shape used to join two line segments where they meet.
822      * There are three possible values for this property: "round", "bevel",
823      * and "miter". The default is "miter".
824      *
825      * @return the the shape used to join two line segments
826      */
827     @JsxGetter
828     public String getLineJoin() {
829         switch (getRenderingBackend().getLineJoin()) {
830             case ROUND:
831                 return "round";
832             case BEVEL:
833                 return "bevel";
834             default:
835                 return "miter";
836         }
837     }
838 
839     /**
840      * Sets the {@code lineJoin} property.
841      * @param lineJoin the {@code lineJoin} property value
842      */
843     @JsxSetter
844     public void setLineJoin(final String lineJoin) {
845         switch (lineJoin) {
846             case "round":
847                 getRenderingBackend().setLineJoin(LineJoin.ROUND);
848                 break;
849             case "bevel":
850                 getRenderingBackend().setLineJoin(LineJoin.BEVEL);
851                 break;
852             case "miter":
853                 getRenderingBackend().setLineJoin(LineJoin.MITER);
854                 break;
855             default:
856                 // ignore invalid values per spec
857         }
858     }
859 
860     /**
861      * Returns the the shape used to join two line segments where they meet.
862      * There are three possible values for this property: "round", "bevel",
863      * and "miter". The default is "miter".
864      *
865      * @return the the shape used to join two line segments
866      */
867     @JsxGetter
868     public String getLineCap() {
869         switch (getRenderingBackend().getLineCap()) {
870             case BUTT:
871                 return "butt";
872             case ROUND:
873                 return "round";
874             default:
875                 return "square";
876         }
877     }
878 
879     /**
880      * Sets the {@code lineCap} property.
881      * @param lineCap the {@code lineCap} property value
882      */
883     @JsxSetter
884     public void setLineCap(final String lineCap) {
885         switch (lineCap) {
886             case "butt":
887                 getRenderingBackend().setLineCap(LineCap.BUTT);
888                 break;
889             case "round":
890                 getRenderingBackend().setLineCap(LineCap.ROUND);
891                 break;
892             case "square":
893                 getRenderingBackend().setLineCap(LineCap.SQUARE);
894                 break;
895             default:
896                 // ignore invalid values per spec
897         }
898     }
899 }