Skip to content

Commit 9fe4e38

Browse files
committed
CanvasRenderingContext2D lineCap property support added
1 parent 3144f8d commit 9fe4e38

6 files changed

Lines changed: 210 additions & 5 deletions

File tree

src/changes/changes.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
<body>
1010
<release version="5.3.0" date="July xx, 2026" description="Chrome/Edge 150, Bugfixes">
11+
<action type="add" dev="rbri">
12+
CanvasRenderingContext2D lineCap property support added.
13+
</action>
1114
<action type="add" dev="rbri">
1215
CanvasRenderingContext2D lineJoin property support added.
1316
</action>

src/main/java/org/htmlunit/javascript/host/canvas/CanvasRenderingContext2D.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.htmlunit.javascript.host.html.HTMLImageElement;
3636
import org.htmlunit.platform.Platform;
3737
import org.htmlunit.platform.canvas.rendering.RenderingBackend;
38+
import org.htmlunit.platform.canvas.rendering.RenderingBackend.LineCap;
3839
import org.htmlunit.platform.canvas.rendering.RenderingBackend.LineJoin;
3940
import org.htmlunit.platform.canvas.rendering.RenderingBackend.WindingRule;
4041
import org.htmlunit.protocol.data.DataURLConnection;
@@ -855,4 +856,44 @@ public void setLineJoin(final String lineJoin) {
855856
// ignore invalid values per spec
856857
}
857858
}
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+
}
858899
}

src/main/java/org/htmlunit/platform/canvas/rendering/AwtRenderingBackend.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public class AwtRenderingBackend implements RenderingBackend {
7171
private Color fillColor_;
7272
private Color strokeColor_;
7373

74-
private int lineCap_;
74+
private LineCap lineCap_;
7575
private LineJoin lineJoin_;
7676
private float miterLimit_;
7777

@@ -279,7 +279,7 @@ public AwtRenderingBackend(final int imageWidth, final int imageHeight) {
279279
graphics2D_.setColor(Color.black);
280280
graphics2D_.clearRect(0, 0, imageWidth, imageHeight);
281281

282-
lineCap_ = BasicStroke.CAP_BUTT;
282+
lineCap_ = LineCap.BUTT;
283283
lineJoin_ = LineJoin.MITER;
284284
miterLimit_ = 10.0f;
285285

@@ -946,7 +946,8 @@ public void stroke() {
946946
LOG.debug("[" + id_ + "] stroke()");
947947
}
948948

949-
graphics2D_.setStroke(new BasicStroke(getLineWidth(), lineCap_, toAwtLineJoin(lineJoin_), miterLimit_));
949+
graphics2D_.setStroke(new BasicStroke(getLineWidth(),
950+
toAwtLineCap(lineCap_), toAwtLineJoin(lineJoin_), miterLimit_));
950951
graphics2D_.setColor(strokeColor_);
951952
for (final Path2D path2d : subPaths_) {
952953
if (hasSegments(path2d)) {
@@ -964,7 +965,8 @@ public void strokeRect(final double x, final double y, final double w, final dou
964965
LOG.debug("[" + id_ + "] strokeRect(" + x + ", " + y + ", " + w + ", " + h + ")");
965966
}
966967

967-
graphics2D_.setStroke(new BasicStroke(getLineWidth(), lineCap_, toAwtLineJoin(lineJoin_), miterLimit_));
968+
graphics2D_.setStroke(new BasicStroke(getLineWidth(),
969+
toAwtLineCap(lineCap_), toAwtLineJoin(lineJoin_), miterLimit_));
968970
graphics2D_.setColor(strokeColor_);
969971
graphics2D_.draw(rectPath(x, y, w, h));
970972
}
@@ -1088,6 +1090,28 @@ public void setLineJoin(final LineJoin lineJoin) {
10881090
lineJoin_ = lineJoin;
10891091
}
10901092

1093+
/**
1094+
* {@inheritDoc}
1095+
*/
1096+
@Override
1097+
public LineCap getLineCap() {
1098+
if (LOG.isDebugEnabled()) {
1099+
LOG.debug("[" + id_ + "] getLineCap()");
1100+
}
1101+
return lineCap_;
1102+
}
1103+
1104+
/**
1105+
* {@inheritDoc}
1106+
*/
1107+
@Override
1108+
public void setLineCap(final LineCap lineCap) {
1109+
if (LOG.isDebugEnabled()) {
1110+
LOG.debug("[" + id_ + "] setLineCap(" + lineCap + ")");
1111+
}
1112+
lineCap_ = lineCap;
1113+
}
1114+
10911115
private Path2D getCurrentSubPath() {
10921116
if (subPaths_.isEmpty()) {
10931117
final Path2D subPath = new Path2D.Double();
@@ -1156,6 +1180,14 @@ private static int toAwtLineJoin(final LineJoin lineJoin) {
11561180
};
11571181
}
11581182

1183+
private static int toAwtLineCap(final LineCap lineCap) {
1184+
return switch (lineCap) {
1185+
case ROUND -> BasicStroke.CAP_ROUND;
1186+
case SQUARE -> BasicStroke.CAP_SQUARE;
1187+
case BUTT -> BasicStroke.CAP_BUTT;
1188+
};
1189+
}
1190+
11591191
private static final class SaveState {
11601192
private final AffineTransform transformation_;
11611193
private final float globalAlpha_;
@@ -1164,7 +1196,7 @@ private static final class SaveState {
11641196
private final Color strokeColor_;
11651197
private final Shape clip_;
11661198

1167-
private final int lineCap_;
1199+
private final LineCap lineCap_;
11681200
private final LineJoin lineJoin_;
11691201
private final float miterLimit_;
11701202

src/main/java/org/htmlunit/platform/canvas/rendering/NoOpRenderingBackend.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,25 @@ public void setLineJoin(final LineJoin lineJoin) {
397397
LOG.debug("[" + id_ + "] setLineJoin(" + lineJoin + ")");
398398
}
399399
}
400+
401+
/**
402+
* {@inheritDoc}
403+
*/
404+
@Override
405+
public LineCap getLineCap() {
406+
if (LOG.isDebugEnabled()) {
407+
LOG.debug("[" + id_ + "] getLineCap()");
408+
}
409+
return LineCap.BUTT;
410+
}
411+
412+
/**
413+
* {@inheritDoc}
414+
*/
415+
@Override
416+
public void setLineCap(final LineCap lineCap) {
417+
if (LOG.isDebugEnabled()) {
418+
LOG.debug("[" + id_ + "] setLineCap(" + lineCap + ")");
419+
}
420+
}
400421
}

src/main/java/org/htmlunit/platform/canvas/rendering/RenderingBackend.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ enum LineJoin {
4848
BEVEL
4949
}
5050

51+
/**
52+
* LineCap to be used while rendering.
53+
*/
54+
enum LineCap {
55+
/** LineCap.BUTT. */
56+
BUTT,
57+
/** LineCap.ROUND. */
58+
ROUND,
59+
/** LineCap.SQUARE. */
60+
SQUARE
61+
}
62+
5163
/**
5264
* Starts a new path by emptying the list of sub-paths.
5365
*/
@@ -363,4 +375,17 @@ void putImageData(byte[] imageDataBytes, int imageDataWidth, int imageDataHeight
363375
* @param lineJoin the {@code lineJoin} property value
364376
*/
365377
void setLineJoin(LineJoin lineJoin);
378+
379+
/**
380+
* Returns the shape used to draw the end points of lines.
381+
*
382+
* @return the shape used to draw the end points of lines.
383+
*/
384+
LineCap getLineCap();
385+
386+
/**
387+
* Sets the {@code lineCap} property.
388+
* @param lineCap the {@code lineCap} property value
389+
*/
390+
void setLineCap(LineCap lineCap);
366391
}

src/test/java/org/htmlunit/javascript/host/canvas/CanvasRenderingContext2DTest.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2923,6 +2923,89 @@ public void drawBevel() throws Exception {
29232923
+ " context.stroke();\n");
29242924
}
29252925

2926+
2927+
/**
2928+
* @throws Exception if the test fails
2929+
*/
2930+
@Test
2931+
@Alerts("butt")
2932+
public void lineCapDefault() throws Exception {
2933+
verify("<canvas id='myCanvas' width='2' height='2'></canvas>\n",
2934+
"log(context.lineCap);");
2935+
}
2936+
2937+
/**
2938+
* @throws Exception if the test fails
2939+
*/
2940+
@Test
2941+
@Alerts({"miter", "round", "round", "butt", "butt", "butt"})
2942+
public void lineJoinCapGet() throws Exception {
2943+
verify("<canvas id='myCanvas' width='2' height='2'></canvas>\n",
2944+
"log(context.lineCap);\n"
2945+
+ "context.lineCap = 'round'\n"
2946+
+ "log(context.lineCap);\n"
2947+
+ "context.lineCap = 'SquARE'\n"
2948+
+ "log(context.lineCap);\n"
2949+
+ "context.lineCap = 'butt'\n"
2950+
+ "log(context.lineCap);\n"
2951+
+ "context.lineCap = ' square '\n"
2952+
+ "log(context.lineCap);\n"
2953+
+ "context.lineCap = ' unknow '\n"
2954+
+ "log(context.lineCap);\n");
2955+
}
2956+
2957+
/**
2958+
* @throws Exception if the test fails
2959+
*/
2960+
@Test
2961+
@Alerts("data:image/png;base64,"
2962+
+ "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAUCAYAAAC07qxWAAAALklEQVR4AdTRsQ0AAAgCQXT/nZUBKGjfSEdzYVUesXi2pYiI"
2963+
+ "sSU/ETOmpCCWeQAAAP//NWxm8gAAAAZJREFUAwALrgYppm2j7wAAAABJRU5ErkJggg==")
2964+
public void drawCapButt() throws Exception {
2965+
draw("<canvas id='myCanvas' width='10', height='20' style='border: 1px solid red;'></canvas>\n",
2966+
" context.lineCap = 'butt';\n"
2967+
+ " context.lineWidth = 6;\n"
2968+
+ " context.beginPath();\n"
2969+
+ " context.moveTo(6, 5);\n"
2970+
+ " context.lineTo(6, 15);\n"
2971+
+ " context.stroke();\n");
2972+
}
2973+
2974+
/**
2975+
* @throws Exception if the test fails
2976+
*/
2977+
@Test
2978+
@Alerts("data:image/png;base64,"
2979+
+ "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAUCAYAAAC07qxWAAAAdklEQVR4AeySwQmAMBRDP951QxfQeXQBN9QFTELpIf8fRK+K"
2980+
+ "KWn+I4XSIR5+n8ANh5xN9LAR3sjBgsnYRM8sgTMA/5V5I5scVOagQ33/g/0qKvP6eq6iTZk3HgWozMEV4A6xhaJnll4PmOBg"
2981+
+ "gqHoYfN7VFgtNwAAAP//ybmhIAAAAAZJREFUAwD8YQ4p7/bWhAAAAABJRU5ErkJggg==")
2982+
public void drawCapRound() throws Exception {
2983+
draw("<canvas id='myCanvas' width='10', height='20' style='border: 1px solid red;'></canvas>\n",
2984+
" context.lineCap = 'round';\n"
2985+
+ " context.lineWidth = 6;\n"
2986+
+ " context.beginPath();\n"
2987+
+ " context.moveTo(6, 5);\n"
2988+
+ " context.lineTo(6, 15);\n"
2989+
+ " context.stroke();\n");
2990+
}
2991+
2992+
/**
2993+
* @throws Exception if the test fails
2994+
*/
2995+
@Test
2996+
@Alerts("data:image/png;base64,"
2997+
+ "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAUCAYAAAC07qxWAAAANUlEQVR4AeySMQoAAAgCq///uQwaHcLZyIYIg8OKZ8mHjQdM"
2998+
+ "ITvCkLcdOZfbGs+CSAymfx4HAAD//5I7/cIAAAAGSURBVAMA6AgGKb9SbOsAAAAASUVORK5CYII=")
2999+
public void drawCapSquare() throws Exception {
3000+
draw("<canvas id='myCanvas' width='10', height='20' style='border: 1px solid red;'></canvas>\n",
3001+
" context.lineCap = 'square';\n"
3002+
+ " context.lineWidth = 6;\n"
3003+
+ " context.beginPath();\n"
3004+
+ " context.moveTo(6, 5);\n"
3005+
+ " context.lineTo(6, 15);\n"
3006+
+ " context.stroke();\n");
3007+
}
3008+
29263009
private void draw(final String canvasSetup, final String drawJS) throws Exception {
29273010
final String html = DOCTYPE_HTML
29283011
+ "<html><head>\n"

0 commit comments

Comments
 (0)