Bug 803124 - Implement mozIsPointInStroke. r=jmuizelaar
authorTobias Schneider <tschneider@mozilla.com>
Mon, 29 Oct 2012 16:54:53 +0100
changeset 112496 69c7ee7683e00f3ccd997ae3d2295508db1e35e7
parent 112495 870d9bf8dc88fb9f4b0a49488053b0597e0eca66
child 112497 11ff92705a23da160c125f2b1e964b5e167dfa17
push idunknown
push userunknown
push dateunknown
reviewersjmuizelaar
bugs803124
milestone19.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 803124 - Implement mozIsPointInStroke. r=jmuizelaar
content/canvas/src/CanvasRenderingContext2D.cpp
content/canvas/src/CanvasRenderingContext2D.h
content/canvas/test/Makefile.in
content/canvas/test/test_mozIsPointInStroke.html
dom/interfaces/canvas/nsIDOMCanvasRenderingContext2D.idl
dom/webidl/CanvasRenderingContext2D.webidl
gfx/2d/2D.h
gfx/2d/PathCG.cpp
gfx/2d/PathCG.h
gfx/2d/PathCairo.cpp
gfx/2d/PathCairo.h
gfx/2d/PathD2D.cpp
gfx/2d/PathD2D.h
gfx/2d/PathRecording.h
gfx/2d/PathSkia.cpp
gfx/2d/PathSkia.h
gfx/thebes/gfxContext.cpp
--- a/content/canvas/src/CanvasRenderingContext2D.cpp
+++ b/content/canvas/src/CanvasRenderingContext2D.cpp
@@ -3643,16 +3643,51 @@ CanvasRenderingContext2D::IsPointInPath(
 
 NS_IMETHODIMP
 CanvasRenderingContext2D::IsPointInPath(float x, float y, bool *retVal)
 {
   *retVal = IsPointInPath(x, y);
   return NS_OK;
 }
 
+bool
+CanvasRenderingContext2D::MozIsPointInStroke(double x, double y)
+{
+  if (!FloatValidate(x,y)) {
+    return false;
+  }
+
+  EnsureUserSpacePath(false);
+  if (!mPath) {
+    return false;
+  }
+
+  const ContextState &state = CurrentState();
+
+  StrokeOptions strokeOptions(state.lineWidth,
+                              state.lineJoin,
+                              state.lineCap,
+                              state.miterLimit,
+                              state.dash.Length(),
+                              state.dash.Elements(),
+                              state.dashOffset);
+
+  if (mPathTransformWillUpdate) {
+    return mPath->StrokeContainsPoint(strokeOptions, Point(x, y), mPathToDS);
+  }
+  return mPath->StrokeContainsPoint(strokeOptions, Point(x, y), mTarget->GetTransform());
+}
+
+NS_IMETHODIMP
+CanvasRenderingContext2D::MozIsPointInStroke(float x, float y, bool *retVal)
+{
+  *retVal = MozIsPointInStroke(x, y);
+  return NS_OK;
+}
+
 //
 // image
 //
 
 // drawImage(in HTMLImageElement image, in float dx, in float dy);
 //   -- render image from 0,0 at dx,dy top-left coords
 // drawImage(in HTMLImageElement image, in float dx, in float dy, in float sw, in float sh);
 //   -- render image from 0,0 at dx,dy top-left coords clipping it to sw,sh
--- a/content/canvas/src/CanvasRenderingContext2D.h
+++ b/content/canvas/src/CanvasRenderingContext2D.h
@@ -241,16 +241,17 @@ public:
   void ClearRect(double x, double y, double w, double h);
   void FillRect(double x, double y, double w, double h);
   void StrokeRect(double x, double y, double w, double h);
   void BeginPath();
   void Fill();
   void Stroke();
   void Clip();
   bool IsPointInPath(double x, double y);
+  bool MozIsPointInStroke(double x, double y);
   void FillText(const nsAString& text, double x, double y,
                 const mozilla::dom::Optional<double>& maxWidth,
                 mozilla::ErrorResult& error);
   void StrokeText(const nsAString& text, double x, double y,
                   const mozilla::dom::Optional<double>& maxWidth,
                   mozilla::ErrorResult& error);
   already_AddRefed<nsIDOMTextMetrics>
     MeasureText(const nsAString& rawText, mozilla::ErrorResult& error);
--- a/content/canvas/test/Makefile.in
+++ b/content/canvas/test/Makefile.in
@@ -50,16 +50,17 @@ MOCHITEST_FILES = \
 	test_2d.composite.uncovered.image.source-out.html \
 	test_2d.drawImage.zerocanvas.html \
 	test_2d.strokeRect.zero.5.html \
 	test_toBlob.html \
 	test_toDataURL_alpha.html \
 	test_toDataURL_lowercase_ascii.html \
 	test_toDataURL_parameters.html \
 	test_mozGetAsFile.html \
+	test_mozIsPointInStroke.html \
 	test_canvas_strokeStyle_getter.html \
 	test_bug613794.html \
 	test_bug753758.html \
 	test_bug764125.html \
 	test_drawImage_edge_cases.html \
 	test_drawImage_document_domain.html \
 	file_drawImage_document_domain.html \
 	$(NULL)
new file mode 100644
--- /dev/null
+++ b/content/canvas/test/test_mozIsPointInStroke.html
@@ -0,0 +1,244 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Canvas test: mozIsPointInStroke</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<canvas id="c" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
+<script type="application/javascript">
+
+var canvas = document.getElementById('c');
+var ctx = canvas.getContext('2d');
+
+
+ctx.lineWidth = 5;
+
+
+ok(ctx.mozIsPointInStroke(50, 25) === false, 'ctx.mozIsPointInStroke(50, 25) === false');
+
+
+ctx.beginPath();
+ctx.rect(0, 0, 20, 20);
+
+ok(ctx.mozIsPointInStroke(-4, -4) === false, 'ctx.mozIsPointInStroke(-4, -4) === false');
+ok(ctx.mozIsPointInStroke(4, 4) === false, 'ctx.mozIsPointInStroke(4, 4) === false');
+ok(ctx.mozIsPointInStroke(16, 16) === false, 'ctx.mozIsPointInStroke(16, 16) === false');
+ok(ctx.mozIsPointInStroke(24, 24) === false, 'ctx.mozIsPointInStroke(24, 24) === false');
+
+ok(ctx.mozIsPointInStroke(-2, -2) === true, 'ctx.mozIsPointInStroke(-2, -2) === true');
+ok(ctx.mozIsPointInStroke(2, 2) === true, 'ctx.mozIsPointInStroke(2, 2) === true');
+ok(ctx.mozIsPointInStroke(18, 18) === true, 'ctx.mozIsPointInStroke(18, 18) === true');
+ok(ctx.mozIsPointInStroke(22, 22) === true, 'ctx.mozIsPointInStroke(22, 22) === true');
+
+
+ctx.beginPath();
+ctx.rect(25, 25, 20, 20);
+
+ok(ctx.mozIsPointInStroke(21, 21) === false, 'ctx.mozIsPointInStroke(21, 21) === false');
+ok(ctx.mozIsPointInStroke(29, 29) === false, 'ctx.mozIsPointInStroke(29, 29) === false');
+ok(ctx.mozIsPointInStroke(42, 42) === false, 'ctx.mozIsPointInStroke(42, 42) === false');
+ok(ctx.mozIsPointInStroke(50, 50) === false, 'ctx.mozIsPointInStroke(50, 50) === false');
+
+ok(ctx.mozIsPointInStroke(23, 23) === true, 'ctx.mozIsPointInStroke(23, 23) === true');
+ok(ctx.mozIsPointInStroke(27, 27) === true, 'ctx.mozIsPointInStroke(27, 27) === true');
+ok(ctx.mozIsPointInStroke(43, 43) === true, 'ctx.mozIsPointInStroke(43, 43) === true');
+ok(ctx.mozIsPointInStroke(47, 47) === true, 'ctx.mozIsPointInStroke(47, 47) === true');
+
+
+ctx.beginPath();
+ctx.moveTo(25, 25);
+ctx.bezierCurveTo(50, -50, 50, 100, 75, 25);
+
+ok(ctx.mozIsPointInStroke(23, 26) === false, 'ctx.mozIsPointInStroke(23, 26) === false');
+ok(ctx.mozIsPointInStroke(75, 23) === false, 'ctx.mozIsPointInStroke(75, 23) === false');
+ok(ctx.mozIsPointInStroke(37, 8) === false, 'ctx.mozIsPointInStroke(37, 8) === false');
+ok(ctx.mozIsPointInStroke(61, 42) === false, 'ctx.mozIsPointInStroke(61, 42) === false');
+
+ok(ctx.mozIsPointInStroke(24, 24) === true, 'ctx.mozIsPointInStroke(24, 24) === true');
+ok(ctx.mozIsPointInStroke(74, 25) === true, 'ctx.mozIsPointInStroke(74, 25) === true');
+ok(ctx.mozIsPointInStroke(37, 2) === true, 'ctx.mozIsPointInStroke(37, 2) === true');
+ok(ctx.mozIsPointInStroke(61, 47) === true, 'ctx.mozIsPointInStroke(61, 47) === true');
+
+
+ctx.beginPath();
+ctx.arc(50, 25, 10, 0, Math.PI, false);
+
+ok(ctx.mozIsPointInStroke(39, 23) === false, 'ctx.mozIsPointInStroke(39, 23) === false');
+ok(ctx.mozIsPointInStroke(50, 15) === false, 'ctx.mozIsPointInStroke(50, 15) === false');
+ok(ctx.mozIsPointInStroke(60, 23) === false, 'ctx.mozIsPointInStroke(60, 23) === false');
+ok(ctx.mozIsPointInStroke(50, 25) === false, 'ctx.mozIsPointInStroke(50, 25) === false');
+
+ok(ctx.mozIsPointInStroke(39, 26) === true, 'ctx.mozIsPointInStroke(39, 26) === true');
+ok(ctx.mozIsPointInStroke(45, 33) === true, 'ctx.mozIsPointInStroke(45, 33) === true');
+ok(ctx.mozIsPointInStroke(53, 33) === true, 'ctx.mozIsPointInStroke(53, 33) === true');
+ok(ctx.mozIsPointInStroke(59, 26) === true, 'ctx.mozIsPointInStroke(59, 26) === true');
+
+
+ctx.beginPath();
+ctx.arc(50, 25, 10, 0, 2 * Math.PI, false);
+
+ok(ctx.mozIsPointInStroke(34, 25) === false, 'ctx.mozIsPointInStroke(34, 25) === false');
+ok(ctx.mozIsPointInStroke(44, 25) === false, 'ctx.mozIsPointInStroke(44, 25) === false');
+ok(ctx.mozIsPointInStroke(49, 30) === false, 'ctx.mozIsPointInStroke(49, 30) === false');
+ok(ctx.mozIsPointInStroke(49, 40) === false, 'ctx.mozIsPointInStroke(49, 40) === false');
+
+ok(ctx.mozIsPointInStroke(39, 23) === true, 'ctx.mozIsPointInStroke(39, 23) === true');
+ok(ctx.mozIsPointInStroke(50, 15) === true, 'ctx.mozIsPointInStroke(50, 15) === true');
+ok(ctx.mozIsPointInStroke(60, 23) === true, 'ctx.mozIsPointInStroke(60, 23) === true');
+ok(ctx.mozIsPointInStroke(49, 34) === true, 'ctx.mozIsPointInStroke(49, 34) === true');
+
+
+ctx.beginPath();
+ctx.save();
+ctx.translate(20, 20);
+ctx.rect(0, 0, 20, 20);
+
+ok(ctx.mozIsPointInStroke(16, 16) === false, 'ctx.mozIsPointInStroke(16, 16) === false');
+ok(ctx.mozIsPointInStroke(24, 24) === false, 'ctx.mozIsPointInStroke(24, 24) === false');
+ok(ctx.mozIsPointInStroke(36, 36) === false, 'ctx.mozIsPointInStroke(36, 36) === false');
+ok(ctx.mozIsPointInStroke(44, 44) === false, 'ctx.mozIsPointInStroke(44, 44) === false');
+
+ok(ctx.mozIsPointInStroke(18, 18) === true, 'ctx.mozIsPointInStroke(18, 18) === true');
+ok(ctx.mozIsPointInStroke(22, 22) === true, 'ctx.mozIsPointInStroke(22, 22) === true');
+ok(ctx.mozIsPointInStroke(38, 38) === true, 'ctx.mozIsPointInStroke(38, 38) === true');
+ok(ctx.mozIsPointInStroke(42, 42) === true, 'ctx.mozIsPointInStroke(42, 42) === true');
+
+ctx.restore();
+
+
+ctx.beginPath();
+ctx.save();
+ctx.scale(-1, 1);
+ctx.rect(-30, 20, 20, 20);
+
+ok(ctx.mozIsPointInStroke(16, 16) === false, 'ctx.mozIsPointInStroke(16, 16) === false');
+ok(ctx.mozIsPointInStroke(14, 24) === false, 'ctx.mozIsPointInStroke(14, 24) === false');
+ok(ctx.mozIsPointInStroke(26, 36) === false, 'ctx.mozIsPointInStroke(26, 36) === false');
+ok(ctx.mozIsPointInStroke(34, 44) === false, 'ctx.mozIsPointInStroke(34, 44) === false');
+
+ok(ctx.mozIsPointInStroke(8, 18) === true, 'ctx.mozIsPointInStroke(8, 18) === true');
+ok(ctx.mozIsPointInStroke(12, 22) === true, 'ctx.mozIsPointInStroke(12, 22) === true');
+ok(ctx.mozIsPointInStroke(28, 38) === true, 'ctx.mozIsPointInStroke(28, 38) === true');
+ok(ctx.mozIsPointInStroke(32, 42) === true, 'ctx.mozIsPointInStroke(32, 42) === true');
+
+ctx.restore();
+
+
+ctx.beginPath();
+ctx.save();
+ctx.lineWidth = 2;
+ctx.translate(50, 25);
+ctx.rotate(180 * Math.PI / 180);
+ctx.scale(5, 5);
+ctx.arc(0, 0, 2, 0, Math.PI, false);
+
+ok(ctx.mozIsPointInStroke(39, 26) === false, 'ctx.mozIsPointInStroke(39, 26) === false');
+ok(ctx.mozIsPointInStroke(45, 33) === false, 'ctx.mozIsPointInStroke(45, 33) === false');
+ok(ctx.mozIsPointInStroke(53, 33) === false, 'ctx.mozIsPointInStroke(53, 33) === false');
+ok(ctx.mozIsPointInStroke(59, 26) === false, 'ctx.mozIsPointInStroke(59, 26) === false');
+
+ok(ctx.mozIsPointInStroke(39, 23) === true, 'ctx.mozIsPointInStroke(39, 23) === true');
+ok(ctx.mozIsPointInStroke(45, 15) === true, 'ctx.mozIsPointInStroke(50, 15) === true');
+ok(ctx.mozIsPointInStroke(55, 15) === true, 'ctx.mozIsPointInStroke(50, 25) === true');
+ok(ctx.mozIsPointInStroke(60, 23) === true, 'ctx.mozIsPointInStroke(60, 23) === true');
+
+ctx.restore();
+
+
+ctx.beginPath();
+
+ctx.moveTo(10, 10);
+ctx.lineTo(30, 10);
+ctx.save();
+ctx.lineWidth = 2;
+ctx.scale(5, 5);
+ctx.lineTo(6, 6);
+ctx.lineTo(2, 6);
+ctx.restore();
+ctx.closePath();
+
+ok(ctx.mozIsPointInStroke(6, 6) === false, 'ctx.mozIsPointInStroke(6, 6) === false');
+ok(ctx.mozIsPointInStroke(14, 14) === false, 'ctx.mozIsPointInStroke(14, 14) === false');
+ok(ctx.mozIsPointInStroke(26, 26) === false, 'ctx.mozIsPointInStroke(26, 26) === false');
+ok(ctx.mozIsPointInStroke(34, 34) === false, 'ctx.mozIsPointInStroke(34, 34) === false');
+
+ok(ctx.mozIsPointInStroke(8, 8) === true, 'ctx.mozIsPointInStroke(8, 8) === true');
+ok(ctx.mozIsPointInStroke(12, 12) === true, 'ctx.mozIsPointInStroke(12, 12) === true');
+ok(ctx.mozIsPointInStroke(28, 28) === true, 'ctx.mozIsPointInStroke(28, 28) === true');
+ok(ctx.mozIsPointInStroke(32, 32) === true, 'ctx.mozIsPointInStroke(32, 32) === true');
+
+
+ctx.beginPath();
+ctx.rect(-30, -30, 20, 20);
+
+ok(ctx.mozIsPointInStroke(-34, -34) === false, 'ctx.mozIsPointInStroke(-34, -34) === false');
+ok(ctx.mozIsPointInStroke(-26, -26) === false, 'ctx.mozIsPointInStroke(-26, -26) === false');
+ok(ctx.mozIsPointInStroke(-14, -14) === false, 'ctx.mozIsPointInStroke(-14, -14) === false');
+ok(ctx.mozIsPointInStroke(-6, -6) === false, 'ctx.mozIsPointInStroke(-6, -6) === false');
+
+ok(ctx.mozIsPointInStroke(-32, -32) === true, 'ctx.mozIsPointInStroke(-32, -32) === true');
+ok(ctx.mozIsPointInStroke(-28, -28) === true, 'ctx.mozIsPointInStroke(-28, -28) === true');
+ok(ctx.mozIsPointInStroke(-12, -12) === true, 'ctx.mozIsPointInStroke(-12, -12) === true');
+ok(ctx.mozIsPointInStroke(-8, -8) === true, 'ctx.mozIsPointInStroke(-8, -8) === true');
+
+
+ctx.beginPath();
+ctx.moveTo(20, 25);
+ctx.lineTo(80, 25);
+
+ok(ctx.mozIsPointInStroke(19, 25) === false, 'ctx.mozIsPointInStroke(19, 25) === false');
+ok(ctx.mozIsPointInStroke(50, 21) === false, 'ctx.mozIsPointInStroke(50, 21) === false');
+ok(ctx.mozIsPointInStroke(81, 25) === false, 'ctx.mozIsPointInStroke(81, 25) === false');
+ok(ctx.mozIsPointInStroke(50, 29) === false, 'ctx.mozIsPointInStroke(50, 29) === false');
+
+ok(ctx.mozIsPointInStroke(21, 25) === true, 'ctx.mozIsPointInStroke(21, 25) === true');
+ok(ctx.mozIsPointInStroke(50, 23) === true, 'ctx.mozIsPointInStroke(50, 23) === true');
+ok(ctx.mozIsPointInStroke(79, 25) === true, 'ctx.mozIsPointInStroke(79, 25) === true');
+ok(ctx.mozIsPointInStroke(50, 27) === true, 'ctx.mozIsPointInStroke(50, 27) === true');
+
+
+ctx.lineWidth = 15;
+ctx.lineCap = 'round';
+
+
+ctx.beginPath();
+ctx.moveTo(20, 25);
+ctx.lineTo(80, 25);
+
+ok(ctx.mozIsPointInStroke(13, 18) === false, 'ctx.mozIsPointInStroke(13, 18) === false');
+ok(ctx.mozIsPointInStroke(13, 31) === false, 'ctx.mozIsPointInStroke(13, 31) === false');
+ok(ctx.mozIsPointInStroke(86, 18) === false, 'ctx.mozIsPointInStroke(86, 18) === false');
+ok(ctx.mozIsPointInStroke(86, 31) === false, 'ctx.mozIsPointInStroke(86, 31) === false');
+
+ok(ctx.mozIsPointInStroke(13, 25) === true, 'ctx.mozIsPointInStroke(13, 25) === true');
+ok(ctx.mozIsPointInStroke(50, 18) === true, 'ctx.mozIsPointInStroke(50, 18) === true');
+ok(ctx.mozIsPointInStroke(86, 25) === true, 'ctx.mozIsPointInStroke(86, 25) === true');
+ok(ctx.mozIsPointInStroke(50, 31) === true, 'ctx.mozIsPointInStroke(50, 31) === true');
+
+
+ctx.lineJoin = 'round';
+
+
+ctx.beginPath();
+ctx.moveTo(20, 15);
+ctx.lineTo(80, 15);
+ctx.lineTo(80, 35);
+
+ok(ctx.mozIsPointInStroke(86, 8) === false, 'ctx.mozIsPointInStroke(86, 8) === false');
+ok(ctx.mozIsPointInStroke(70, 24) === false, 'ctx.mozIsPointInStroke(70, 24) === false');
+ok(ctx.mozIsPointInStroke(73, 41) === false, 'ctx.mozIsPointInStroke(73, 41) === false');
+ok(ctx.mozIsPointInStroke(86, 41) === false, 'ctx.mozIsPointInStroke(86, 41) === false');
+
+ok(ctx.mozIsPointInStroke(14, 15) === true, 'ctx.mozIsPointInStroke(14, 15) === true');
+ok(ctx.mozIsPointInStroke(81, 15) === true, 'ctx.mozIsPointInStroke(81, 15) === true');
+ok(ctx.mozIsPointInStroke(79, 41) === true, 'ctx.mozIsPointInStroke(79, 41) === true');
+ok(ctx.mozIsPointInStroke(73, 21) === true, 'ctx.mozIsPointInStroke(73, 21) === true');
+
+</script>
+</pre>
+</body>
+</html>
+
--- a/dom/interfaces/canvas/nsIDOMCanvasRenderingContext2D.idl
+++ b/dom/interfaces/canvas/nsIDOMCanvasRenderingContext2D.idl
@@ -25,17 +25,17 @@ interface nsIDOMCanvasPattern : nsISuppo
 };
 
 [scriptable, uuid(2d01715c-ec7d-424a-ab85-e0fd70c8665c)]
 interface nsIDOMTextMetrics : nsISupports
 {
   readonly attribute float width;
 };
 
-[scriptable, uuid(c835c768-2dcc-461c-82f5-3653710d2942)]
+[scriptable, uuid(438fe14d-6501-40ab-b6c2-130fa38bd2c9)]
 interface nsIDOMCanvasRenderingContext2D : nsISupports
 {
   // back-reference to the canvas element for which
   // this context was created
   readonly attribute nsIDOMHTMLCanvasElement canvas;
 
   // state
   [binaryname(MozSave)]
@@ -161,16 +161,17 @@ enum CanvasMultiGetterType {
 /*
   void drawImage(in HTMLImageElement image, in float dx, in float dy);
   void drawImage(in HTMLImageElement image, in float dx, in float dy, in float sw, in float sh);
   void drawImage(in HTMLImageElement image, in float sx, in float sy, in float sw, in float sh, in float dx, in float dy, in float dw, in float dh);
 */
 
   // point-membership test
   boolean isPointInPath(in float x, in float y);
+  boolean mozIsPointInStroke(in float x, in float y);
 
   // pixel manipulation
   // ImageData getImageData (in float x, in float y, in float width, in float height);
   // void putImageData (in ImageData d, in float x, in float y);
   // ImageData = { width: #, height: #, data: [r, g, b, a, ...] }
 
   // nsISupports is OK, because the binding code will deal correctly
   [implicit_jscontext]
--- a/dom/webidl/CanvasRenderingContext2D.webidl
+++ b/dom/webidl/CanvasRenderingContext2D.webidl
@@ -139,16 +139,18 @@ interface CanvasRenderingContext2D {
 
   [SetterThrows]
   attribute DOMString mozTextStyle;
 
   // image smoothing mode -- if disabled, images won't be smoothed
   // if scaled.
   attribute boolean mozImageSmoothingEnabled;
 
+  boolean mozIsPointInStroke(double x, double y);
+
   // Show the caret if appropriate when drawing
   [ChromeOnly]
   const unsigned long DRAWWINDOW_DRAW_CARET   = 0x01;
   // Don't flush pending layout notifications that could otherwise
   // be batched up
   [ChromeOnly]
   const unsigned long DRAWWINDOW_DO_NOT_FLUSH = 0x02;
   // Draw scrollbars and scroll the viewport if they are present
--- a/gfx/2d/2D.h
+++ b/gfx/2d/2D.h
@@ -393,16 +393,25 @@ public:
                                                              FillRule aFillRule = FILL_WINDING) const = 0;
 
   /* This function checks if a point lies within a path. It allows passing a
    * transform that will transform the path to the coordinate space in which
    * aPoint is given.
    */
   virtual bool ContainsPoint(const Point &aPoint, const Matrix &aTransform) const = 0;
 
+
+  /* This function checks if a point lies within the stroke of a path using the
+   * specified strokeoptions. It allows passing a transform that will transform
+   * the path to the coordinate space in which aPoint is given.
+   */
+  virtual bool StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                                   const Point &aPoint,
+                                   const Matrix &aTransform) const = 0;
+
   /* This functions gets the bounds of this path. These bounds are not
    * guaranteed to be tight. A transform may be specified that gives the bounds
    * after application of the transform.
    */
   virtual Rect GetBounds(const Matrix &aTransform = Matrix()) const = 0;
 
   /* This function gets the bounds of the stroke of this path using the
    * specified strokeoptions. These bounds are not guaranteed to be tight.
--- a/gfx/2d/PathCG.cpp
+++ b/gfx/2d/PathCG.cpp
@@ -193,16 +193,44 @@ CreateScratchContext()
 
 static CGContextRef
 ScratchContext()
 {
   static CGContextRef cg = CreateScratchContext();
   return cg;
 }
 
+bool
+PathCG::StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                            const Point &aPoint,
+                            const Matrix &aTransform) const
+{
+  Matrix inverse = aTransform;
+  inverse.Invert();
+  Point transformedPoint = inverse*aPoint;
+  // We could probably drop the input transform and just transform the point at the caller?
+  CGPoint point = {transformedPoint.x, transformedPoint.y};
+
+  CGContextRef cg = ScratchContext();
+
+  CGContextSaveGState(cg);
+
+  CGContextBeginPath(cg);
+  CGContextAddPath(cg, mPath);
+
+  SetStrokeOptions(cg, aStrokeOptions);
+
+  CGContextReplacePathWithStrokedPath(cg);
+  CGContextRestoreGState(cg);
+
+  CGPathRef sPath = CGContextCopyPath(cg);
+
+  return CGPathContainsPoint(sPath, nullptr, point, false);
+}
+
 //XXX: what should these functions return for an empty path?
 // currently they return CGRectNull {inf,inf, 0, 0}
 Rect
 PathCG::GetBounds(const Matrix &aTransform) const
 {
   //XXX: are these bounds tight enough
   Rect bounds = CGRectToRect(CGPathGetBoundingBox(mPath));
   //XXX: curretnly this returns the bounds of the transformed bounds
--- a/gfx/2d/PathCG.h
+++ b/gfx/2d/PathCG.h
@@ -72,16 +72,19 @@ public:
   // are compatible with BACKEND_COREGRAPHICS_ACCELERATED backend.
   virtual BackendType GetBackendType() const { return BACKEND_COREGRAPHICS; }
 
   virtual TemporaryRef<PathBuilder> CopyToBuilder(FillRule aFillRule = FILL_WINDING) const;
   virtual TemporaryRef<PathBuilder> TransformedCopyToBuilder(const Matrix &aTransform,
                                                              FillRule aFillRule = FILL_WINDING) const;
 
   virtual bool ContainsPoint(const Point &aPoint, const Matrix &aTransform) const;
+  virtual bool StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                                   const Point &aPoint,
+                                   const Matrix &aTransform) const;
   virtual Rect GetBounds(const Matrix &aTransform = Matrix()) const;
   virtual Rect GetStrokedBounds(const StrokeOptions &aStrokeOptions,
                                 const Matrix &aTransform = Matrix()) const;
 
   virtual FillRule GetFillRule() const { return mFillRule; }
 
   CGMutablePathRef GetPath() const { return mPath; }
 
--- a/gfx/2d/PathCairo.cpp
+++ b/gfx/2d/PathCairo.cpp
@@ -258,16 +258,31 @@ PathCairo::ContainsPoint(const Point &aP
   inverse.Invert();
   Point transformed = inverse * aPoint;
 
   // Needs the correct fill rule set.
   cairo_set_fill_rule(*mPathContext, GfxFillRuleToCairoFillRule(mFillRule));
   return cairo_in_fill(*mPathContext, transformed.x, transformed.y);
 }
 
+bool
+PathCairo::StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                               const Point &aPoint,
+                               const Matrix &aTransform) const
+{
+  CairoTempMatrix(*mPathContext, mTransform);
+
+  Matrix inverse = aTransform;
+  inverse.Invert();
+  Point transformed = inverse * aPoint;
+
+  SetCairoStrokeOptions(*mPathContext, aStrokeOptions);
+  return cairo_in_stroke(*mPathContext, transformed.x, transformed.y);
+}
+
 Rect
 PathCairo::GetBounds(const Matrix &aTransform) const
 {
   CairoTempMatrix(*mPathContext, mTransform);
 
   double x1, y1, x2, y2;
 
   cairo_path_extents(*mPathContext, &x1, &y1, &x2, &y2);
--- a/gfx/2d/PathCairo.h
+++ b/gfx/2d/PathCairo.h
@@ -120,16 +120,20 @@ public:
   virtual BackendType GetBackendType() const { return BACKEND_CAIRO; }
 
   virtual TemporaryRef<PathBuilder> CopyToBuilder(FillRule aFillRule = FILL_WINDING) const;
   virtual TemporaryRef<PathBuilder> TransformedCopyToBuilder(const Matrix &aTransform,
                                                              FillRule aFillRule = FILL_WINDING) const;
 
   virtual bool ContainsPoint(const Point &aPoint, const Matrix &aTransform) const;
 
+  virtual bool StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                                   const Point &aPoint,
+                                   const Matrix &aTransform) const;
+
   virtual Rect GetBounds(const Matrix &aTransform = Matrix()) const;
 
   virtual Rect GetStrokedBounds(const StrokeOptions &aStrokeOptions,
                                 const Matrix &aTransform = Matrix()) const;
 
   virtual FillRule GetFillRule() const { return mFillRule; }
 
   TemporaryRef<CairoPathContext> GetPathContext();
--- a/gfx/2d/PathD2D.cpp
+++ b/gfx/2d/PathD2D.cpp
@@ -302,16 +302,39 @@ PathD2D::ContainsPoint(const Point &aPoi
   if (FAILED(hr)) {
     // Log
     return false;
   }
 
   return !!result;
 }
 
+bool
+PathD2D::StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                             const Point &aPoint,
+                             const Matrix &aTransform) const
+{
+  BOOL result;
+
+  RefPtr<ID2D1StrokeStyle> strokeStyle =
+    DrawTargetD2D::CreateStrokeStyleForOptions(aStrokeOptions);
+  HRESULT hr = mGeometry->StrokeContainsPoint(D2DPoint(aPoint),
+                                              aStrokeOptions.mLineWidth,
+                                              strokeStyle,
+                                              D2DMatrix(aTransform),
+                                              &result);
+
+  if (FAILED(hr)) {
+    // Log
+    return false;
+  }
+
+  return !!result;
+}
+
 Rect
 PathD2D::GetBounds(const Matrix &aTransform) const
 {
   D2D1_RECT_F bounds;
 
   HRESULT hr = mGeometry->GetBounds(D2DMatrix(aTransform), &bounds);
 
   if (FAILED(hr)) {
--- a/gfx/2d/PathD2D.h
+++ b/gfx/2d/PathD2D.h
@@ -70,16 +70,20 @@ public:
   virtual BackendType GetBackendType() const { return BACKEND_DIRECT2D; }
 
   virtual TemporaryRef<PathBuilder> CopyToBuilder(FillRule aFillRule = FILL_WINDING) const;
   virtual TemporaryRef<PathBuilder> TransformedCopyToBuilder(const Matrix &aTransform,
                                                              FillRule aFillRule = FILL_WINDING) const;
 
   virtual bool ContainsPoint(const Point &aPoint, const Matrix &aTransform) const;
 
+  virtual bool StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                                   const Point &aPoint,
+                                   const Matrix &aTransform) const;
+
   virtual Rect GetBounds(const Matrix &aTransform = Matrix()) const;
 
   virtual Rect GetStrokedBounds(const StrokeOptions &aStrokeOptions,
                                 const Matrix &aTransform = Matrix()) const;
 
   virtual FillRule GetFillRule() const { return mFillRule; }
 
   ID2D1Geometry *GetGeometry() { return mGeometry; }
--- a/gfx/2d/PathRecording.h
+++ b/gfx/2d/PathRecording.h
@@ -91,16 +91,20 @@ public:
   ~PathRecording();
 
   virtual BackendType GetBackendType() const { return BACKEND_RECORDING; }
   virtual TemporaryRef<PathBuilder> CopyToBuilder(FillRule aFillRule = FILL_WINDING) const;
   virtual TemporaryRef<PathBuilder> TransformedCopyToBuilder(const Matrix &aTransform,
                                                              FillRule aFillRule = FILL_WINDING) const;
   virtual bool ContainsPoint(const Point &aPoint, const Matrix &aTransform) const
   { return mPath->ContainsPoint(aPoint, aTransform); }
+  virtual bool StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                                   const Point &aPoint,
+                                   const Matrix &aTransform) const
+  { return mPath->StrokeContainsPoint(aStrokeOptions, aPoint, aTransform); }
   
   virtual Rect GetBounds(const Matrix &aTransform = Matrix()) const
   { return mPath->GetBounds(aTransform); }
   
   virtual Rect GetStrokedBounds(const StrokeOptions &aStrokeOptions,
                                 const Matrix &aTransform = Matrix()) const
   { return mPath->GetStrokedBounds(aStrokeOptions, aTransform); }
   
--- a/gfx/2d/PathSkia.cpp
+++ b/gfx/2d/PathSkia.cpp
@@ -143,16 +143,42 @@ PathSkia::ContainsPoint(const Point &aPo
 static Rect SkRectToRect(const SkRect& aBounds)
 {
   return Rect(SkScalarToFloat(aBounds.fLeft),
               SkScalarToFloat(aBounds.fTop),
               SkScalarToFloat(aBounds.fRight - aBounds.fLeft),
               SkScalarToFloat(aBounds.fBottom - aBounds.fTop));
 }
 
+bool
+PathSkia::StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                              const Point &aPoint,
+                              const Matrix &aTransform) const
+{
+  Matrix inverse = aTransform;
+  inverse.Invert();
+  Point transformed = inverse * aPoint;
+
+  SkPaint paint;
+  StrokeOptionsToPaint(paint, aStrokeOptions);
+
+  SkPath strokePath;
+  paint.getFillPath(mPath, &strokePath);
+
+  Rect bounds = aTransform.TransformBounds(SkRectToRect(strokePath.getBounds()));
+
+  if (aPoint.x < bounds.x || aPoint.y < bounds.y ||
+      aPoint.x > bounds.XMost() || aPoint.y > bounds.YMost()) {
+    return false;
+  }
+
+  return strokePath.contains(SkFloatToScalar(transformed.x),
+                             SkFloatToScalar(transformed.y));
+}
+
 Rect
 PathSkia::GetBounds(const Matrix &aTransform) const
 {
   Rect bounds = SkRectToRect(mPath.getBounds());
   return aTransform.TransformBounds(bounds);
 }
 
 Rect
--- a/gfx/2d/PathSkia.h
+++ b/gfx/2d/PathSkia.h
@@ -52,16 +52,20 @@ public:
   virtual BackendType GetBackendType() const { return BACKEND_SKIA; }
 
   virtual TemporaryRef<PathBuilder> CopyToBuilder(FillRule aFillRule = FILL_WINDING) const;
   virtual TemporaryRef<PathBuilder> TransformedCopyToBuilder(const Matrix &aTransform,
                                                              FillRule aFillRule = FILL_WINDING) const;
 
   virtual bool ContainsPoint(const Point &aPoint, const Matrix &aTransform) const;
   
+  virtual bool StrokeContainsPoint(const StrokeOptions &aStrokeOptions,
+                                   const Point &aPoint,
+                                   const Matrix &aTransform) const;
+
   virtual Rect GetBounds(const Matrix &aTransform = Matrix()) const;
   
   virtual Rect GetStrokedBounds(const StrokeOptions &aStrokeOptions,
                                 const Matrix &aTransform = Matrix()) const;
 
   virtual FillRule GetFillRule() const { return mFillRule; }
 
   const SkPath& GetPath() const { return mPath; }
--- a/gfx/thebes/gfxContext.cpp
+++ b/gfx/thebes/gfxContext.cpp
@@ -1609,18 +1609,19 @@ gfxContext::PointInFill(const gfxPoint& 
 }
 
 bool
 gfxContext::PointInStroke(const gfxPoint& pt)
 {
   if (mCairo) {
     return cairo_in_stroke(mCairo, pt.x, pt.y);
   } else {
-    // XXX - Used by SVG, needs fixing.
-    return false;
+    return mPath->StrokeContainsPoint(CurrentState().strokeOptions,
+                                      ToPoint(pt),
+                                      mTransform);
   }
 }
 
 gfxRect
 gfxContext::GetUserPathExtent()
 {
   if (mCairo) {
     double xmin, ymin, xmax, ymax;