Bug 589648 part 3. Work around lack of cairo support to display square stroke-linecaps for zero length paths. r=longsonr.

--HG--
extra : rebase_source : 5aaa65a2bc888b4a64884f5f2a87beb7803f646e
This commit is contained in:
Jonathan Watt 2011-04-20 10:16:02 +01:00
parent 45ffe4244b
commit 3045bb10aa
7 changed files with 347 additions and 0 deletions

View File

@ -237,6 +237,50 @@ SVGPathData::GetPathSegAtLength(float aDistance) const
return NS_MAX(0U, segIndex - 1); // -1 because while loop takes us 1 too far
}
/**
* The SVG spec says we have to paint stroke caps for zero length subpaths:
*
* http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes
*
* Cairo only does this for |stroke-linecap: round| and not for
* |stroke-linecap: square| (since that's what Adobe Acrobat has always done).
*
* To help us conform to the SVG spec we have this helper function to draw an
* approximation of square caps for zero length subpaths. It does this by
* inserting a subpath containing a single axis aligned straight line that is
* as small as it can be without cairo throwing it away for being too small to
* affect rendering. Cairo will then draw stroke caps for this axis aligned
* line, creating an axis aligned rectangle (approximating the square that
* would ideally be drawn).
*
* Note that this function inserts a subpath into the current gfx path that
* will be present during both fill and stroke operations.
*/
static void
ApproximateZeroLengthSubpathSquareCaps(const gfxPoint &aPoint, gfxContext *aCtx)
{
// Cairo's fixed point fractional part is 8 bits wide, so its device space
// coordinate granularity is 1/256 pixels. However, to prevent user space
// |aPoint| and |aPoint + tinyAdvance| being rounded to the same device
// coordinates, we double this for |tinyAdvance|:
const gfxSize tinyAdvance = aCtx->DeviceToUser(gfxSize(2.0/256.0, 0.0));
aCtx->MoveTo(aPoint);
aCtx->LineTo(aPoint + gfxPoint(tinyAdvance.width, tinyAdvance.height));
aCtx->MoveTo(aPoint);
}
#define MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS \
do { \
if (capsAreSquare && !subpathHasLength && subpathContainsNonArc && \
SVGPathSegUtils::IsValidType(prevSegType) && \
(!IsMoveto(prevSegType) || \
segType == nsIDOMSVGPathSeg::PATHSEG_CLOSEPATH)) { \
ApproximateZeroLengthSubpathSquareCaps(segStart, aCtx); \
} \
} while(0)
void
SVGPathData::ConstructPath(gfxContext *aCtx) const
{
@ -244,6 +288,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
return; // paths without an initial moveto are invalid
}
PRBool capsAreSquare = aCtx->CurrentLineCap() == gfxContext::LINE_CAP_SQUARE;
PRBool subpathHasLength = PR_FALSE; // visual length
PRBool subpathContainsNonArc = PR_FALSE;
PRUint32 segType, prevSegType = nsIDOMSVGPathSeg::PATHSEG_UNKNOWN;
gfxPoint pathStart(0.0, 0.0); // start point of [sub]path
gfxPoint segStart(0.0, 0.0);
@ -263,28 +311,45 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
switch (segType)
{
case nsIDOMSVGPathSeg::PATHSEG_CLOSEPATH:
// set this early to allow drawing of square caps for "M{x},{y} Z":
subpathContainsNonArc = PR_TRUE;
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS;
segEnd = pathStart;
aCtx->ClosePath();
break;
case nsIDOMSVGPathSeg::PATHSEG_MOVETO_ABS:
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS;
pathStart = segEnd = gfxPoint(mData[i], mData[i+1]);
aCtx->MoveTo(segEnd);
subpathHasLength = PR_FALSE;
subpathContainsNonArc = PR_FALSE;
break;
case nsIDOMSVGPathSeg::PATHSEG_MOVETO_REL:
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS;
pathStart = segEnd = segStart + gfxPoint(mData[i], mData[i+1]);
aCtx->MoveTo(segEnd);
subpathHasLength = PR_FALSE;
subpathContainsNonArc = PR_FALSE;
break;
case nsIDOMSVGPathSeg::PATHSEG_LINETO_ABS:
segEnd = gfxPoint(mData[i], mData[i+1]);
aCtx->LineTo(segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_LINETO_REL:
segEnd = segStart + gfxPoint(mData[i], mData[i+1]);
aCtx->LineTo(segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_CUBIC_ABS:
@ -292,6 +357,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
cp2 = gfxPoint(mData[i+2], mData[i+3]);
segEnd = gfxPoint(mData[i+4], mData[i+5]);
aCtx->CurveTo(cp1, cp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1 || segEnd != cp2);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_CUBIC_REL:
@ -299,6 +368,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
cp2 = segStart + gfxPoint(mData[i+2], mData[i+3]);
segEnd = segStart + gfxPoint(mData[i+4], mData[i+5]);
aCtx->CurveTo(cp1, cp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1 || segEnd != cp2);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_QUADRATIC_ABS:
@ -308,6 +381,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
segEnd = gfxPoint(mData[i+2], mData[i+3]); // set before setting tcp2!
tcp2 = cp1 + (segEnd - cp1) / 3;
aCtx->CurveTo(tcp1, tcp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_QUADRATIC_REL:
@ -317,6 +394,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
segEnd = segStart + gfxPoint(mData[i+2], mData[i+3]); // set before setting tcp2!
tcp2 = cp1 + (segEnd - cp1) / 3;
aCtx->CurveTo(tcp1, tcp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_ARC_ABS:
@ -338,27 +419,46 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
}
}
}
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart);
}
break;
}
case nsIDOMSVGPathSeg::PATHSEG_LINETO_HORIZONTAL_ABS:
segEnd = gfxPoint(mData[i], segStart.y);
aCtx->LineTo(segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_LINETO_HORIZONTAL_REL:
segEnd = segStart + gfxPoint(mData[i], 0.0f);
aCtx->LineTo(segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_LINETO_VERTICAL_ABS:
segEnd = gfxPoint(segStart.x, mData[i]);
aCtx->LineTo(segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_LINETO_VERTICAL_REL:
segEnd = segStart + gfxPoint(0.0f, mData[i]);
aCtx->LineTo(segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_CUBIC_SMOOTH_ABS:
@ -366,6 +466,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
cp2 = gfxPoint(mData[i], mData[i+1]);
segEnd = gfxPoint(mData[i+2], mData[i+3]);
aCtx->CurveTo(cp1, cp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1 || segEnd != cp2);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_CUBIC_SMOOTH_REL:
@ -373,6 +477,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
cp2 = segStart + gfxPoint(mData[i], mData[i+1]);
segEnd = segStart + gfxPoint(mData[i+2], mData[i+3]);
aCtx->CurveTo(cp1, cp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1 || segEnd != cp2);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS:
@ -382,6 +490,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
segEnd = gfxPoint(mData[i], mData[i+1]); // set before setting tcp2!
tcp2 = cp1 + (segEnd - cp1) / 3;
aCtx->CurveTo(tcp1, tcp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1);
}
subpathContainsNonArc = PR_TRUE;
break;
case nsIDOMSVGPathSeg::PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL:
@ -391,6 +503,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
segEnd = segStart + gfxPoint(mData[i], mData[i+1]); // changed before setting tcp2!
tcp2 = cp1 + (segEnd - cp1) / 3;
aCtx->CurveTo(tcp1, tcp2, segEnd);
if (!subpathHasLength) {
subpathHasLength = (segEnd != segStart || segEnd != cp1);
}
subpathContainsNonArc = PR_TRUE;
break;
default:
@ -401,7 +517,10 @@ SVGPathData::ConstructPath(gfxContext *aCtx) const
prevSegType = segType;
segStart = segEnd;
}
NS_ABORT_IF_FALSE(i == mData.Length(), "Very, very bad - mData corrupt");
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS;
}
already_AddRefed<gfxFlattenedPath>

View File

@ -79,6 +79,7 @@ _TEST_FILES = \
test_SVGAnimatedImageSMILDisabled.html \
animated-svg-image-helper.html \
animated-svg-image-helper.svg \
test_stroke-linecap-hit-testing.xhtml \
test_SVGLengthList.xhtml \
test_SVGLengthList-2.xhtml \
test_SVGPathSegList.xhtml \

View File

@ -0,0 +1,48 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=589648
-->
<head>
<title>Test hit-testing of line caps</title>
<script type="text/javascript" src="/MochiKit/packed.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body onload="run()">
<script class="testbody" type="text/javascript">
<![CDATA[
SimpleTest.waitForExplicitFinish();
function run()
{
var svg = document.getElementById('svg');
var div = document.getElementById("div");
var x = div.offsetLeft;
var y = div.offsetTop;
var got, expected;
got = document.elementFromPoint(5 + x, 5 + y);
expected = document.getElementById('zero-length-square-caps');
is(got, expected, 'Check hit on zero length subpath\'s square caps');
SimpleTest.finish();
}
]]>
</script>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=500174">Mozilla Bug 500174</a>
<p id="display"></p>
<div id="content">
<div width="100%" height="1" id="div"></div>
<svg xmlns="http://www.w3.org/2000/svg" id="svg" width="400" height="300">
<path id="zero-length-square-caps" stroke="blue" stroke-width="50"
stroke-linecap="square" d="M25,25 L25,25"/>
</svg>
</div>
<pre id="test">
</pre>
</body>
</html>

View File

@ -196,6 +196,8 @@ fails-if(Android) random-if(gtk2Widget) != text-language-01.xhtml text-language-
== text-layout-03.svg text-layout-03-ref.svg
== text-scale-01.svg text-scale-01-ref.svg
== text-stroke-scaling-01.svg text-stroke-scaling-01-ref.svg
== stroke-linecap-square-w-zero-length-segs-01.svg pass.svg
== stroke-linecap-square-w-zero-length-segs-02.svg pass.svg
== text-style-01a.svg text-style-01-ref.svg
== text-style-01b.svg text-style-01-ref.svg
== text-style-01c.svg text-style-01-ref.svg

View File

@ -0,0 +1,143 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/licenses/publicdomain/
-->
<svg xmlns="http://www.w3.org/2000/svg">
<title>Test 'stroke-linecap: square' with zero length path segments</title>
<!-- From https://bugzilla.mozilla.org/show_bug.cgi?id=589648 -->
<style>
path {
stroke-width: 20px;
stroke-linecap: square;
}
rect {
fill: red;
}
/* expect lime squares to cover red rects */
path.squares-expected {
stroke: lime;
}
path.squares-not-expected {
stroke: red;
}
/* thicker stroke to cover squares-not-expected paths */
path.coverer {
stroke: lime;
stroke-width: 24px;
}
/* to show edges of shapes to help in debugging:
g > rect {
stroke: red;
stroke-width: 5px;
}
path.coverer {
stroke: lime;
stroke-width: 18px;
}
*/
</style>
<rect width="100%" height="100%" style="fill:lime"/>
<!-- Column 1: test single segment zero-length subpaths: -->
<g transform="translate(25,25)">
<rect x="-9" y="-9" width="18" height="18"/>
<rect x="41" y="41" width="18" height="18"/>
<rect x="91" y="91" width="18" height="18"/>
<path class="squares-expected" d="M0,0 L0,0 M20,20 L30,30 M50,50 L50,50 M70,70 L80,80 M100,100 L100,100"/>
</g>
<g transform="translate(25,75)">
<rect x="-9" y="-9" width="18" height="18"/>
<rect x="41" y="41" width="18" height="18"/>
<rect x="91" y="91" width="18" height="18"/>
<path class="squares-expected" d="M0,0 C0,0 0,0 0,0 M20,20 L30,30 M50,50 C50,50 50,50 50,50 M70,70 L80,80 M100,100 C100,100 100,100 100,100"/>
</g>
<g transform="translate(25,125)">
<path class="squares-not-expected" d="M0,0 A0,10 0 0 0 0,0 M20,20 L30,30 M50,50 A0,10 0 0 0 50,50 M70,70 L80,80 M100,100 A0,10 0 0 0 100,100"/>
<path class="coverer" d="M20,20 L30,30 M70,70 L80,80"/>
</g>
<g transform="translate(25,175)">
<rect x="-9" y="-9" width="18" height="18"/>
<rect x="41" y="41" width="18" height="18"/>
<rect x="91" y="91" width="18" height="18"/>
<path class="squares-expected" d="M0,0 Z M20,20 L30,30 M50,50 Z M70,70 L80,80 M100,100 Z"/>
</g>
<!-- Column 2: test multi-segment zero-length subpaths: -->
<g transform="translate(175,25)">
<rect x="-9" y="-9" width="18" height="18"/>
<rect x="41" y="41" width="18" height="18"/>
<rect x="91" y="91" width="18" height="18"/>
<path class="squares-expected" d="M0,0 L0,0 M0,0 L0,0 M20,20 L30,30 M50,50 L50,50 L50,50 M70,70 L80,80 M100,100 L100,100 L100,100"/>
</g>
<g transform="translate(177,75)">
<rect x="-9" y="-9" width="18" height="18"/>
<rect x="41" y="41" width="18" height="18"/>
<rect x="91" y="91" width="18" height="18"/>
<path class="squares-expected" d="M0,0 C0,0 0,0 0,0 C0,0 0,0 0,0 M20,20 L30,30 M50,50 C50,50 50,50 50,50 C50,50 50,50 50,50 M70,70 L80,80 M100,100 C100,100 100,100 100,100 C100,100 100,100 100,100"/>
</g>
<g transform="translate(175,125)">
<path class="squares-not-expected" d="M0,0 A0,10 0 0 0 0,0 A0,10 0 0 0 0,0 M20,20 L30,30 M50,50 A0,10 0 0 0 50,50 A0,10 0 0 0 50,50 M70,70 L80,80 M100,100 A0,10 0 0 0 100,100 A0,10 0 0 0 100,100"/>
<path class="coverer" d="M20,20 L30,30 M70,70 L80,80"/>
</g>
<g transform="translate(175,175)">
<rect x="-9" y="-9" width="18" height="18"/>
<rect x="41" y="41" width="18" height="18"/>
<rect x="91" y="91" width="18" height="18"/>
<path class="squares-expected" d="M0,0 Z Z M20,20 L30,30 M50,50 Z Z M70,70 L80,80 M100,100 Z Z"/>
</g>
<!-- Column 3: test non-zero-length subpaths that begin, end and contain
zero length segments: -->
<g transform="translate(325,25)">
<path class="squares-not-expected" d="M20,20 L20,20 L30,30 L30,30 L40,40 L40,40"/>
<path class="coverer" d="M20,20 L40,40"/>
</g>
<g transform="translate(325,75)">
<path class="squares-not-expected" d="M20,20 C20,20 20,20 20,20 C20,20 30,30 30,30 C30,30 30,30 30,30 C30,30 40,40 40,40 C40,40 40,40 40,40"/>
<path class="coverer" d="M20,20 L40,40"/>
</g>
<g transform="translate(325,125)">
<path class="squares-not-expected" d="M20,20 A0,10 0 0 0 20,20 A0,10 0 0 0 30,30 A0,10 0 0 0 30,30 A0,10 0 0 0 40,40 A0,10 0 0 0 40,40"/>
<path class="coverer" d="M20,20 L40,40"/>
</g>
<!-- this one is shorter because the Z's mean we only have path end points
at 20,20 -->
<g transform="translate(325,175)">
<rect x="11" y="11" width="18" height="18"/>
<path class="squares-expected" d="M20,20 Z L30,30 Z L40,40 Z"/>
</g>
<!-- Column 4: test loan movetos -->
<g transform="translate(425,25)">
<path class="squares-not-expected" d="M0,0 M0,0 M20,20 L30,30 M50,50 M50,50 M70,70 L80,80 M100,100 M100,100"/>
<path class="coverer" d="M20,20 L30,30 M70,70 L80,80"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,28 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/licenses/publicdomain/
-->
<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
<title>Test 'stroke-linecap: square' with zero length path segments</title>
<!-- From https://bugzilla.mozilla.org/show_bug.cgi?id=589648 -->
<script>
function run()
{
document.getElementById('path').setAttribute('stroke-linecap', 'butt');
document.documentElement.removeAttribute('class');
}
window.addEventListener("MozReftestInvalidate", run, false);
</script>
<rect width="100%" height="100%" style="fill:lime"/>
<path id="path" stroke="red" stroke-width="200" stroke-linecap="square"
d="M100,100 L100,100"/>
</svg>

After

Width:  |  Height:  |  Size: 735 B

View File

@ -483,6 +483,12 @@ nsSVGPathGeometryFrame::GeneratePath(gfxContext* aContext,
aContext->Multiply(matrix);
// Hack to let SVGPathData::ConstructPath know if we have square caps:
const nsStyleSVG* style = GetStyleSVG();
if (style->mStrokeLinecap == NS_STYLE_STROKE_LINECAP_SQUARE) {
aContext->SetLineCap(gfxContext::LINE_CAP_SQUARE);
}
aContext->NewPath();
static_cast<nsSVGPathGeometryElement*>(mContent)->ConstructPath(aContext);
}