8228711: Path rendered incorrectly when it goes outside the clipping region

Fixed closePath() to preserve last position and its outcode in Stroker and TransformingPathConsumer2D.PathClipFilter

Reviewed-by: prr, kcr
This commit is contained in:
Laurent Bourgès 2019-08-07 10:25:50 +02:00
parent c4b6dfbad1
commit 383e7dfb30
10 changed files with 626 additions and 248 deletions

View File

@ -47,6 +47,8 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
static final double CURVE_LEN_ERR = MarlinProperties.getCurveLengthError(); // 0.01 initial static final double CURVE_LEN_ERR = MarlinProperties.getCurveLengthError(); // 0.01 initial
static final double MIN_T_INC = 1.0d / (1 << REC_LIMIT); static final double MIN_T_INC = 1.0d / (1 << REC_LIMIT);
static final double EPS = 1e-6d;
// More than 24 bits of mantissa means we can no longer accurately // More than 24 bits of mantissa means we can no longer accurately
// measure the number of times cycled through the dash array so we // measure the number of times cycled through the dash array so we
// punt and override the phase to just be 0 past that point. // punt and override the phase to just be 0 past that point.
@ -269,6 +271,9 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
private void emitSeg(double[] buf, int off, int type) { private void emitSeg(double[] buf, int off, int type) {
switch (type) { switch (type) {
case 4:
out.lineTo(buf[off], buf[off + 1]);
return;
case 8: case 8:
out.curveTo(buf[off ], buf[off + 1], out.curveTo(buf[off ], buf[off + 1],
buf[off + 2], buf[off + 3], buf[off + 2], buf[off + 3],
@ -278,9 +283,6 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
out.quadTo(buf[off ], buf[off + 1], out.quadTo(buf[off ], buf[off + 1],
buf[off + 2], buf[off + 3]); buf[off + 2], buf[off + 3]);
return; return;
case 4:
out.lineTo(buf[off], buf[off + 1]);
return;
default: default:
} }
} }
@ -361,7 +363,7 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -416,13 +418,13 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
boolean _dashOn = dashOn; boolean _dashOn = dashOn;
double _phase = phase; double _phase = phase;
double leftInThisDashSegment, d; double leftInThisDashSegment, rem;
while (true) { while (true) {
d = _dash[_idx]; leftInThisDashSegment = _dash[_idx] - _phase;
leftInThisDashSegment = d - _phase; rem = len - leftInThisDashSegment;
if (len <= leftInThisDashSegment) { if (rem <= EPS) {
_curCurvepts[0] = x1; _curCurvepts[0] = x1;
_curCurvepts[1] = y1; _curCurvepts[1] = y1;
@ -431,8 +433,8 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
// Advance phase within current dash segment // Advance phase within current dash segment
_phase += len; _phase += len;
// TODO: compare double values using epsilon: // compare values using epsilon:
if (len == leftInThisDashSegment) { if (Math.abs(rem) <= EPS) {
_phase = 0.0d; _phase = 0.0d;
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -440,17 +442,12 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
break; break;
} }
if (_phase == 0.0d) {
_curCurvepts[0] = cx0 + d * cx;
_curCurvepts[1] = cy0 + d * cy;
} else {
_curCurvepts[0] = cx0 + leftInThisDashSegment * cx; _curCurvepts[0] = cx0 + leftInThisDashSegment * cx;
_curCurvepts[1] = cy0 + leftInThisDashSegment * cy; _curCurvepts[1] = cy0 + leftInThisDashSegment * cy;
}
goTo(_curCurvepts, 0, 4, _dashOn); goTo(_curCurvepts, 0, 4, _dashOn);
len -= leftInThisDashSegment; len = rem;
// Advance to next dash segment // Advance to next dash segment
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -506,18 +503,18 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
_dashOn = (iterations + (_dashOn ? 1L : 0L) & 1L) == 1L; _dashOn = (iterations + (_dashOn ? 1L : 0L) & 1L) == 1L;
} }
double leftInThisDashSegment, d; double leftInThisDashSegment, rem;
while (true) { while (true) {
d = _dash[_idx]; leftInThisDashSegment = _dash[_idx] - _phase;
leftInThisDashSegment = d - _phase; rem = len - leftInThisDashSegment;
if (len <= leftInThisDashSegment) { if (rem <= EPS) {
// Advance phase within current dash segment // Advance phase within current dash segment
_phase += len; _phase += len;
// TODO: compare double values using epsilon: // compare values using epsilon:
if (len == leftInThisDashSegment) { if (Math.abs(rem) <= EPS) {
_phase = 0.0d; _phase = 0.0d;
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -525,7 +522,7 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
break; break;
} }
len -= leftInThisDashSegment; len = rem;
// Advance to next dash segment // Advance to next dash segment
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -579,7 +576,9 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
goTo(_curCurvepts, curCurveoff + 2, type, _dashOn); goTo(_curCurvepts, curCurveoff + 2, type, _dashOn);
_phase += _li.lastSegLen(); _phase += _li.lastSegLen();
if (_phase >= _dash[_idx]) {
// compare values using epsilon:
if (_phase + EPS >= _dash[_idx]) {
_phase = 0.0d; _phase = 0.0d;
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -938,7 +937,7 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -1024,7 +1023,7 @@ final class DDasher implements DPathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;

View File

@ -243,7 +243,7 @@ final class DHelpers implements MarlinConst {
final double y12 = pts[3] - pts[1]; final double y12 = pts[3] - pts[1];
// if the curve is already parallel to either axis we gain nothing // if the curve is already parallel to either axis we gain nothing
// from rotating it. // from rotating it.
if ((y12 != 0.0d && x12 != 0.0d)) { if ((y12 != 0.0d) && (x12 != 0.0d)) {
// we rotate it so that the first vector in the control polygon is // we rotate it so that the first vector in the control polygon is
// parallel to the x-axis. This will ensure that rotated quarter // parallel to the x-axis. This will ensure that rotated quarter
// circles won't be subdivided. // circles won't be subdivided.
@ -764,17 +764,17 @@ final class DHelpers implements MarlinConst {
io.lineTo(_curves[e], _curves[e+1]); io.lineTo(_curves[e], _curves[e+1]);
e += 2; e += 2;
continue; continue;
case TYPE_QUADTO:
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
e += 4;
continue;
case TYPE_CUBICTO: case TYPE_CUBICTO:
io.curveTo(_curves[e], _curves[e+1], io.curveTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3], _curves[e+2], _curves[e+3],
_curves[e+4], _curves[e+5]); _curves[e+4], _curves[e+5]);
e += 6; e += 6;
continue; continue;
case TYPE_QUADTO:
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
e += 4;
continue;
default: default:
} }
} }
@ -806,17 +806,17 @@ final class DHelpers implements MarlinConst {
e -= 2; e -= 2;
io.lineTo(_curves[e], _curves[e+1]); io.lineTo(_curves[e], _curves[e+1]);
continue; continue;
case TYPE_QUADTO:
e -= 4;
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
continue;
case TYPE_CUBICTO: case TYPE_CUBICTO:
e -= 6; e -= 6;
io.curveTo(_curves[e], _curves[e+1], io.curveTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3], _curves[e+2], _curves[e+3],
_curves[e+4], _curves[e+5]); _curves[e+4], _curves[e+5]);
continue; continue;
case TYPE_QUADTO:
e -= 4;
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
continue;
default: default:
} }
} }

View File

@ -540,7 +540,7 @@ final class DStroker implements DPathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -634,6 +634,9 @@ final class DStroker implements DPathConsumer2D, MarlinConst {
emitReverse(); emitReverse();
this.prev = CLOSE; this.prev = CLOSE;
this.cx0 = sx0;
this.cy0 = sy0;
this.cOutCode = sOutCode;
if (opened) { if (opened) {
// do not emit close // do not emit close
@ -668,7 +671,9 @@ final class DStroker implements DPathConsumer2D, MarlinConst {
// i.e. if caps must be drawn or not ? // i.e. if caps must be drawn or not ?
// Solution: use the ClosedPathDetector before Stroker to determine // Solution: use the ClosedPathDetector before Stroker to determine
// if the path is a closed path or not // if the path is a closed path or not
if (!rdrCtx.closedPath) { if (rdrCtx.closedPath) {
emitReverse();
} else {
if (outcode == 0) { if (outcode == 0) {
// current point = end's cap: // current point = end's cap:
if (capStyle == CAP_ROUND) { if (capStyle == CAP_ROUND) {
@ -693,8 +698,6 @@ final class DStroker implements DPathConsumer2D, MarlinConst {
} }
} }
} }
} else {
emitReverse();
} }
emitClose(); emitClose();
} }
@ -1058,7 +1061,7 @@ final class DStroker implements DPathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -1206,7 +1209,7 @@ final class DStroker implements DPathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;

View File

@ -530,6 +530,9 @@ final class DTransformingPathConsumer2D {
private boolean outside = false; private boolean outside = false;
// The starting point of the path
private double sx0, sy0;
// The current point (TODO stupid repeated info) // The current point (TODO stupid repeated info)
private double cx0, cy0; private double cx0, cy0;
@ -630,17 +633,26 @@ final class DTransformingPathConsumer2D {
finishPath(); finishPath();
out.closePath(); out.closePath();
// back to starting point:
this.cOutCode = DHelpers.outcode(sx0, sy0, clipRect);
this.cx0 = sx0;
this.cy0 = sy0;
} }
@Override @Override
public void moveTo(final double x0, final double y0) { public void moveTo(final double x0, final double y0) {
finishPath(); finishPath();
this.cOutCode = DHelpers.outcode(x0, y0, clipRect);
this.outside = false;
out.moveTo(x0, y0); out.moveTo(x0, y0);
// update starting point:
this.cOutCode = DHelpers.outcode(x0, y0, clipRect);
this.cx0 = x0; this.cx0 = x0;
this.cy0 = y0; this.cy0 = y0;
this.sx0 = x0;
this.sy0 = y0;
} }
@Override @Override
@ -655,7 +667,7 @@ final class DTransformingPathConsumer2D {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -754,7 +766,7 @@ final class DTransformingPathConsumer2D {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -816,7 +828,7 @@ final class DTransformingPathConsumer2D {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -1153,13 +1165,13 @@ final class DTransformingPathConsumer2D {
@Override @Override
public void moveTo(double x0, double y0) { public void moveTo(double x0, double y0) {
log("moveTo (" + x0 + ", " + y0 + ')'); log("p.moveTo(" + x0 + ", " + y0 + ");");
out.moveTo(x0, y0); out.moveTo(x0, y0);
} }
@Override @Override
public void lineTo(double x1, double y1) { public void lineTo(double x1, double y1) {
log("lineTo (" + x1 + ", " + y1 + ')'); log("p.lineTo(" + x1 + ", " + y1 + ");");
out.lineTo(x1, y1); out.lineTo(x1, y1);
} }
@ -1168,25 +1180,26 @@ final class DTransformingPathConsumer2D {
double x2, double y2, double x2, double y2,
double x3, double y3) double x3, double y3)
{ {
log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ") P3(" + x3 + ", " + y3 + ')'); log("p.curveTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2 + ", " + x3 + ", " + y3 + ");");
out.curveTo(x1, y1, x2, y2, x3, y3); out.curveTo(x1, y1, x2, y2, x3, y3);
} }
@Override @Override
public void quadTo(double x1, double y1, double x2, double y2) { public void quadTo(double x1, double y1,
log("quadTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ')'); double x2, double y2) {
log("p.quadTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2 + ");");
out.quadTo(x1, y1, x2, y2); out.quadTo(x1, y1, x2, y2);
} }
@Override @Override
public void closePath() { public void closePath() {
log("closePath"); log("p.closePath();");
out.closePath(); out.closePath();
} }
@Override @Override
public void pathDone() { public void pathDone() {
log("pathDone"); log("p.pathDone();");
out.pathDone(); out.pathDone();
} }

View File

@ -48,6 +48,8 @@ final class Dasher implements PathConsumer2D, MarlinConst {
static final float CURVE_LEN_ERR = MarlinProperties.getCurveLengthError(); // 0.01 static final float CURVE_LEN_ERR = MarlinProperties.getCurveLengthError(); // 0.01
static final float MIN_T_INC = 1.0f / (1 << REC_LIMIT); static final float MIN_T_INC = 1.0f / (1 << REC_LIMIT);
static final float EPS = 1e-6f;
// More than 24 bits of mantissa means we can no longer accurately // More than 24 bits of mantissa means we can no longer accurately
// measure the number of times cycled through the dash array so we // measure the number of times cycled through the dash array so we
// punt and override the phase to just be 0 past that point. // punt and override the phase to just be 0 past that point.
@ -270,6 +272,9 @@ final class Dasher implements PathConsumer2D, MarlinConst {
private void emitSeg(float[] buf, int off, int type) { private void emitSeg(float[] buf, int off, int type) {
switch (type) { switch (type) {
case 4:
out.lineTo(buf[off], buf[off + 1]);
return;
case 8: case 8:
out.curveTo(buf[off ], buf[off + 1], out.curveTo(buf[off ], buf[off + 1],
buf[off + 2], buf[off + 3], buf[off + 2], buf[off + 3],
@ -279,9 +284,6 @@ final class Dasher implements PathConsumer2D, MarlinConst {
out.quadTo(buf[off ], buf[off + 1], out.quadTo(buf[off ], buf[off + 1],
buf[off + 2], buf[off + 3]); buf[off + 2], buf[off + 3]);
return; return;
case 4:
out.lineTo(buf[off], buf[off + 1]);
return;
default: default:
} }
} }
@ -362,7 +364,7 @@ final class Dasher implements PathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -417,13 +419,13 @@ final class Dasher implements PathConsumer2D, MarlinConst {
boolean _dashOn = dashOn; boolean _dashOn = dashOn;
float _phase = phase; float _phase = phase;
float leftInThisDashSegment, d; float leftInThisDashSegment, rem;
while (true) { while (true) {
d = _dash[_idx]; leftInThisDashSegment = _dash[_idx] - _phase;
leftInThisDashSegment = d - _phase; rem = len - leftInThisDashSegment;
if (len <= leftInThisDashSegment) { if (rem <= EPS) {
_curCurvepts[0] = x1; _curCurvepts[0] = x1;
_curCurvepts[1] = y1; _curCurvepts[1] = y1;
@ -432,8 +434,8 @@ final class Dasher implements PathConsumer2D, MarlinConst {
// Advance phase within current dash segment // Advance phase within current dash segment
_phase += len; _phase += len;
// TODO: compare float values using epsilon: // compare values using epsilon:
if (len == leftInThisDashSegment) { if (Math.abs(rem) <= EPS) {
_phase = 0.0f; _phase = 0.0f;
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -441,17 +443,12 @@ final class Dasher implements PathConsumer2D, MarlinConst {
break; break;
} }
if (_phase == 0.0f) {
_curCurvepts[0] = cx0 + d * cx;
_curCurvepts[1] = cy0 + d * cy;
} else {
_curCurvepts[0] = cx0 + leftInThisDashSegment * cx; _curCurvepts[0] = cx0 + leftInThisDashSegment * cx;
_curCurvepts[1] = cy0 + leftInThisDashSegment * cy; _curCurvepts[1] = cy0 + leftInThisDashSegment * cy;
}
goTo(_curCurvepts, 0, 4, _dashOn); goTo(_curCurvepts, 0, 4, _dashOn);
len -= leftInThisDashSegment; len = rem;
// Advance to next dash segment // Advance to next dash segment
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -507,18 +504,18 @@ final class Dasher implements PathConsumer2D, MarlinConst {
_dashOn = (iterations + (_dashOn ? 1L : 0L) & 1L) == 1L; _dashOn = (iterations + (_dashOn ? 1L : 0L) & 1L) == 1L;
} }
float leftInThisDashSegment, d; float leftInThisDashSegment, rem;
while (true) { while (true) {
d = _dash[_idx]; leftInThisDashSegment = _dash[_idx] - _phase;
leftInThisDashSegment = d - _phase; rem = len - leftInThisDashSegment;
if (len <= leftInThisDashSegment) { if (rem <= EPS) {
// Advance phase within current dash segment // Advance phase within current dash segment
_phase += len; _phase += len;
// TODO: compare float values using epsilon: // compare values using epsilon:
if (len == leftInThisDashSegment) { if (Math.abs(rem) <= EPS) {
_phase = 0.0f; _phase = 0.0f;
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -526,7 +523,7 @@ final class Dasher implements PathConsumer2D, MarlinConst {
break; break;
} }
len -= leftInThisDashSegment; len = rem;
// Advance to next dash segment // Advance to next dash segment
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -580,7 +577,9 @@ final class Dasher implements PathConsumer2D, MarlinConst {
goTo(_curCurvepts, curCurveoff + 2, type, _dashOn); goTo(_curCurvepts, curCurveoff + 2, type, _dashOn);
_phase += _li.lastSegLen(); _phase += _li.lastSegLen();
if (_phase >= _dash[_idx]) {
// compare values using epsilon:
if (_phase + EPS >= _dash[_idx]) {
_phase = 0.0f; _phase = 0.0f;
_idx = (_idx + 1) % _dashLen; _idx = (_idx + 1) % _dashLen;
_dashOn = !_dashOn; _dashOn = !_dashOn;
@ -939,7 +938,7 @@ final class Dasher implements PathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -1025,7 +1024,7 @@ final class Dasher implements PathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;

View File

@ -251,7 +251,7 @@ final class Helpers implements MarlinConst {
final float y12 = pts[3] - pts[1]; final float y12 = pts[3] - pts[1];
// if the curve is already parallel to either axis we gain nothing // if the curve is already parallel to either axis we gain nothing
// from rotating it. // from rotating it.
if ((y12 != 0.0f && x12 != 0.0f)) { if ((y12 != 0.0f) && (x12 != 0.0f)) {
// we rotate it so that the first vector in the control polygon is // we rotate it so that the first vector in the control polygon is
// parallel to the x-axis. This will ensure that rotated quarter // parallel to the x-axis. This will ensure that rotated quarter
// circles won't be subdivided. // circles won't be subdivided.
@ -772,17 +772,17 @@ final class Helpers implements MarlinConst {
io.lineTo(_curves[e], _curves[e+1]); io.lineTo(_curves[e], _curves[e+1]);
e += 2; e += 2;
continue; continue;
case TYPE_QUADTO:
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
e += 4;
continue;
case TYPE_CUBICTO: case TYPE_CUBICTO:
io.curveTo(_curves[e], _curves[e+1], io.curveTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3], _curves[e+2], _curves[e+3],
_curves[e+4], _curves[e+5]); _curves[e+4], _curves[e+5]);
e += 6; e += 6;
continue; continue;
case TYPE_QUADTO:
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
e += 4;
continue;
default: default:
} }
} }
@ -814,17 +814,17 @@ final class Helpers implements MarlinConst {
e -= 2; e -= 2;
io.lineTo(_curves[e], _curves[e+1]); io.lineTo(_curves[e], _curves[e+1]);
continue; continue;
case TYPE_QUADTO:
e -= 4;
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
continue;
case TYPE_CUBICTO: case TYPE_CUBICTO:
e -= 6; e -= 6;
io.curveTo(_curves[e], _curves[e+1], io.curveTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3], _curves[e+2], _curves[e+3],
_curves[e+4], _curves[e+5]); _curves[e+4], _curves[e+5]);
continue; continue;
case TYPE_QUADTO:
e -= 4;
io.quadTo(_curves[e], _curves[e+1],
_curves[e+2], _curves[e+3]);
continue;
default: default:
} }
} }

View File

@ -542,7 +542,7 @@ final class Stroker implements PathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -636,6 +636,9 @@ final class Stroker implements PathConsumer2D, MarlinConst {
emitReverse(); emitReverse();
this.prev = CLOSE; this.prev = CLOSE;
this.cx0 = sx0;
this.cy0 = sy0;
this.cOutCode = sOutCode;
if (opened) { if (opened) {
// do not emit close // do not emit close
@ -670,7 +673,9 @@ final class Stroker implements PathConsumer2D, MarlinConst {
// i.e. if caps must be drawn or not ? // i.e. if caps must be drawn or not ?
// Solution: use the ClosedPathDetector before Stroker to determine // Solution: use the ClosedPathDetector before Stroker to determine
// if the path is a closed path or not // if the path is a closed path or not
if (!rdrCtx.closedPath) { if (rdrCtx.closedPath) {
emitReverse();
} else {
if (outcode == 0) { if (outcode == 0) {
// current point = end's cap: // current point = end's cap:
if (capStyle == CAP_ROUND) { if (capStyle == CAP_ROUND) {
@ -695,8 +700,6 @@ final class Stroker implements PathConsumer2D, MarlinConst {
} }
} }
} }
} else {
emitReverse();
} }
emitClose(); emitClose();
} }
@ -1060,7 +1063,7 @@ final class Stroker implements PathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -1208,7 +1211,7 @@ final class Stroker implements PathConsumer2D, MarlinConst {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;

View File

@ -531,6 +531,9 @@ final class TransformingPathConsumer2D {
private boolean outside = false; private boolean outside = false;
// The starting point of the path
private float sx0, sy0;
// The current point (TODO stupid repeated info) // The current point (TODO stupid repeated info)
private float cx0, cy0; private float cx0, cy0;
@ -631,17 +634,26 @@ final class TransformingPathConsumer2D {
finishPath(); finishPath();
out.closePath(); out.closePath();
// back to starting point:
this.cOutCode = Helpers.outcode(sx0, sy0, clipRect);
this.cx0 = sx0;
this.cy0 = sy0;
} }
@Override @Override
public void moveTo(final float x0, final float y0) { public void moveTo(final float x0, final float y0) {
finishPath(); finishPath();
this.cOutCode = Helpers.outcode(x0, y0, clipRect);
this.outside = false;
out.moveTo(x0, y0); out.moveTo(x0, y0);
// update starting point:
this.cOutCode = Helpers.outcode(x0, y0, clipRect);
this.cx0 = x0; this.cx0 = x0;
this.cy0 = y0; this.cy0 = y0;
this.sx0 = x0;
this.sy0 = y0;
} }
@Override @Override
@ -656,7 +668,7 @@ final class TransformingPathConsumer2D {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -755,7 +767,7 @@ final class TransformingPathConsumer2D {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -817,7 +829,7 @@ final class TransformingPathConsumer2D {
// basic rejection criteria: // basic rejection criteria:
if (sideCode == 0) { if (sideCode == 0) {
// ovelap clip: // overlap clip:
if (subdivide) { if (subdivide) {
// avoid reentrance // avoid reentrance
subdivide = false; subdivide = false;
@ -1154,13 +1166,13 @@ final class TransformingPathConsumer2D {
@Override @Override
public void moveTo(float x0, float y0) { public void moveTo(float x0, float y0) {
log("moveTo (" + x0 + ", " + y0 + ')'); log("p.moveTo(" + x0 + ", " + y0 + ");");
out.moveTo(x0, y0); out.moveTo(x0, y0);
} }
@Override @Override
public void lineTo(float x1, float y1) { public void lineTo(float x1, float y1) {
log("lineTo (" + x1 + ", " + y1 + ')'); log("p.lineTo(" + x1 + ", " + y1 + ");");
out.lineTo(x1, y1); out.lineTo(x1, y1);
} }
@ -1169,25 +1181,26 @@ final class TransformingPathConsumer2D {
float x2, float y2, float x2, float y2,
float x3, float y3) float x3, float y3)
{ {
log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ") P3(" + x3 + ", " + y3 + ')'); log("p.curveTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2 + ", " + x3 + ", " + y3 + ");");
out.curveTo(x1, y1, x2, y2, x3, y3); out.curveTo(x1, y1, x2, y2, x3, y3);
} }
@Override @Override
public void quadTo(float x1, float y1, float x2, float y2) { public void quadTo(float x1, float y1,
log("quadTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ')'); float x2, float y2) {
log("p.quadTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2 + ");");
out.quadTo(x1, y1, x2, y2); out.quadTo(x1, y1, x2, y2);
} }
@Override @Override
public void closePath() { public void closePath() {
log("closePath"); log("p.closePath();");
out.closePath(); out.closePath();
} }
@Override @Override
public void pathDone() { public void pathDone() {
log("pathDone"); log("p.pathDone();");
out.pathDone(); out.pathDone();
} }

View File

@ -27,7 +27,7 @@ package sun.java2d.marlin;
public final class Version { public final class Version {
private static final String VERSION = "marlin-0.9.1.1-Unsafe-OpenJDK"; private static final String VERSION = "marlin-0.9.1.2-Unsafe-OpenJDK";
public static String getVersion() { public static String getVersion() {
return VERSION; return VERSION;

View File

@ -24,11 +24,14 @@ import java.awt.BasicStroke;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke; import java.awt.Stroke;
import java.awt.Shape;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D; import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.geom.PathIterator; import java.awt.geom.PathIterator;
import java.awt.geom.QuadCurve2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt; import java.awt.image.DataBufferInt;
import java.io.File; import java.io.File;
@ -69,27 +72,30 @@ import javax.imageio.stream.ImageOutputStream;
*/ */
public final class ClipShapeTest { public final class ClipShapeTest {
static boolean TX_SCALE = false; // test options:
static boolean TX_SHEAR = false; static int NUM_TESTS;
static final boolean TEST_STROKER = true;
static final boolean TEST_FILLER = true;
// complementary tests in slow mode:
static boolean USE_DASHES = false;
static boolean USE_VAR_STROKE = false;
static int NUM_TESTS = 5000;
static final int TESTW = 100;
static final int TESTH = 100;
// shape settings: // shape settings:
static ShapeMode SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; static ShapeMode SHAPE_MODE;
static boolean USE_DASHES;
static boolean USE_VAR_STROKE;
static int THRESHOLD_DELTA; static int THRESHOLD_DELTA;
static long THRESHOLD_NBPIX; static long THRESHOLD_NBPIX;
static final boolean SHAPE_REPEAT = true; // constants:
static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true"));
static final boolean TEST_STROKER = true;
static final boolean TEST_FILLER = true;
static final boolean SUBDIVIDE_CURVE = true;
static final double SUBDIVIDE_LEN_TH = 50.0;
static final boolean TRACE_SUBDIVIDE_CURVE = false;
static final int TESTW = 100;
static final int TESTH = 100;
// dump path on console: // dump path on console:
static final boolean DUMP_SHAPE = true; static final boolean DUMP_SHAPE = true;
@ -103,7 +109,8 @@ public final class ClipShapeTest {
static final int MAX_SAVE_FRAMES = 100; static final int MAX_SAVE_FRAMES = 100;
// use fixed seed to reproduce always same polygons between tests // use fixed seed to reproduce always same polygons between tests
static final boolean FIXED_SEED = false; static final boolean FIXED_SEED = true;
static final double RAND_SCALE = 3.0; static final double RAND_SCALE = 3.0;
static final double RANDW = TESTW * RAND_SCALE; static final double RANDW = TESTW * RAND_SCALE;
static final double OFFW = (TESTW - RANDW) / 2.0; static final double OFFW = (TESTW - RANDW) / 2.0;
@ -126,6 +133,7 @@ public final class ClipShapeTest {
static final File OUTPUT_DIR = new File("."); static final File OUTPUT_DIR = new File(".");
static final AtomicBoolean isMarlin = new AtomicBoolean(); static final AtomicBoolean isMarlin = new AtomicBoolean();
static final AtomicBoolean isMarlinFloat = new AtomicBoolean();
static final AtomicBoolean isClipRuntime = new AtomicBoolean(); static final AtomicBoolean isClipRuntime = new AtomicBoolean();
static { static {
@ -143,6 +151,7 @@ public final class ClipShapeTest {
// last space to avoid matching other settings: // last space to avoid matching other settings:
if (msg.startsWith("sun.java2d.renderer ")) { if (msg.startsWith("sun.java2d.renderer ")) {
isMarlin.set(msg.contains("MarlinRenderingEngine")); isMarlin.set(msg.contains("MarlinRenderingEngine"));
isMarlinFloat.set(!msg.contains("DMarlinRenderingEngine"));
} }
if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) { if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) {
isClipRuntime.set(msg.contains("true")); isClipRuntime.set(msg.contains("true"));
@ -186,12 +195,22 @@ public final class ClipShapeTest {
// curve length max error: // curve length max error:
System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4"); System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4");
// quad max error:
System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4");
// cubic min/max error: // cubic min/max error:
System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3"); System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3");
System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4"); // or disabled ~ 1e-6 System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4");
// quad max error:
System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4");
}
private static void resetOptions() {
NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000);
// shape settings:
SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
USE_DASHES = false;
USE_VAR_STROKE = false;
} }
/** /**
@ -199,27 +218,42 @@ public final class ClipShapeTest {
* @param args * @param args
*/ */
public static void main(String[] args) { public static void main(String[] args) {
{
// Bootstrap: init Renderer now:
final BufferedImage img = newImage(TESTW, TESTH);
final Graphics2D g2d = initialize(img, null);
try {
paintShape(new Line2D.Double(0,0,100,100), g2d, true, false);
} finally {
g2d.dispose();
}
if (!isMarlin.get()) {
throw new RuntimeException("Marlin renderer not used at runtime !");
}
if (!isClipRuntime.get()) {
throw new RuntimeException("Marlin clipping not enabled at runtime !");
}
}
System.out.println("---------------------------------------");
System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH);
resetOptions();
boolean runSlowTests = false; boolean runSlowTests = false;
for (String arg : args) { for (String arg : args) {
if ("-slow".equals(arg)) { if ("-slow".equals(arg)) {
System.out.println("slow: enabled.");
runSlowTests = true; runSlowTests = true;
} else if ("-doScale".equals(arg)) {
System.out.println("doScale: enabled.");
TX_SCALE = true;
} else if ("-doShear".equals(arg)) {
System.out.println("doShear: enabled.");
TX_SHEAR = true;
} else if ("-doDash".equals(arg)) { } else if ("-doDash".equals(arg)) {
System.out.println("doDash: enabled.");
USE_DASHES = true; USE_DASHES = true;
} else if ("-doVarStroke".equals(arg)) { } else if ("-doVarStroke".equals(arg)) {
System.out.println("doVarStroke: enabled.");
USE_VAR_STROKE = true; USE_VAR_STROKE = true;
} } else {
// shape mode: // shape mode:
else if (arg.equalsIgnoreCase("-poly")) { if (arg.equalsIgnoreCase("-poly")) {
SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
} else if (arg.equalsIgnoreCase("-bigpoly")) { } else if (arg.equalsIgnoreCase("-bigpoly")) {
SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS; SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS;
@ -231,47 +265,72 @@ public final class ClipShapeTest {
SHAPE_MODE = ShapeMode.MIXED; SHAPE_MODE = ShapeMode.MIXED;
} }
} }
}
System.out.println("Shape mode: " + SHAPE_MODE); System.out.println("Shape mode: " + SHAPE_MODE);
// adjust image comparison thresholds: // adjust image comparison thresholds:
switch(SHAPE_MODE) { switch (SHAPE_MODE) {
case TWO_CUBICS: case TWO_CUBICS:
// Define uncertainty for curves: // Define uncertainty for curves:
THRESHOLD_DELTA = 32; // / 256 THRESHOLD_DELTA = 32;
THRESHOLD_NBPIX = 128; // / 10000 THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200;
if (SUBDIVIDE_CURVE) {
THRESHOLD_NBPIX = 4;
}
break; break;
case FOUR_QUADS: case FOUR_QUADS:
case MIXED: case MIXED:
// Define uncertainty for quads: // Define uncertainty for quads:
// curve subdivision causes curves to be smaller // curve subdivision causes curves to be smaller
// then curve offsets are different (more accurate) // then curve offsets are different (more accurate)
THRESHOLD_DELTA = 64; // 64 / 256 THRESHOLD_DELTA = 64;
THRESHOLD_NBPIX = 256; // 256 / 10000 THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420;
if (SUBDIVIDE_CURVE) {
THRESHOLD_NBPIX = 10;
}
break; break;
default: default:
// Define uncertainty for lines: // Define uncertainty for lines:
// float variant have higher uncertainty // float variant have higher uncertainty
THRESHOLD_DELTA = 8; THRESHOLD_DELTA = 2;
THRESHOLD_NBPIX = 8; THRESHOLD_NBPIX = (USE_DASHES) ?
// float variant have higher uncertainty
((isMarlinFloat.get()) ? 30 : 6) // low for double
: (isMarlinFloat.get()) ? 10 : 0;
} }
System.out.println("THRESHOLD_DELTA: "+THRESHOLD_DELTA); // Visual inspection (low threshold):
System.out.println("THRESHOLD_NBPIX: "+THRESHOLD_NBPIX); // THRESHOLD_NBPIX = 2;
System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA);
System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX);
if (runSlowTests) { if (runSlowTests) {
NUM_TESTS = 10000; // or 100000 (very slow) NUM_TESTS = 10000; // or 100000 (very slow)
USE_DASHES = true;
USE_VAR_STROKE = true; USE_VAR_STROKE = true;
} }
System.out.println("ClipShapeTests: image = " + TESTW + " x " + TESTH); System.out.println("NUM_TESTS: " + NUM_TESTS);
if (USE_DASHES) {
System.out.println("USE_DASHES: enabled.");
}
if (USE_VAR_STROKE) {
System.out.println("USE_VAR_STROKE: enabled.");
}
if (!DO_FAIL) {
System.out.println("DO_FAIL: disabled.");
}
System.out.println("---------------------------------------");
final DiffContext allCtx = new DiffContext("All Test setups");
final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)");
int failures = 0; int failures = 0;
final long start = System.nanoTime(); final long start = System.nanoTime();
try { try {
// TODO: test affine transforms ?
if (TEST_STROKER) { if (TEST_STROKER) {
final float[][] dashArrays = (USE_DASHES) ? final float[][] dashArrays = (USE_DASHES) ?
// small // small
@ -291,7 +350,7 @@ public final class ClipShapeTest {
int nsw = 0; int nsw = 0;
if (USE_VAR_STROKE) { if (USE_VAR_STROKE) {
for (float width = 0.1f; width < 110f; width *= 5f) { for (float width = 0.25f; width < 110f; width *= 5f) {
strokeWidths[nsw++] = width; strokeWidths[nsw++] = width;
} }
} else { } else {
@ -310,8 +369,8 @@ public final class ClipShapeTest {
for (int join = 0; join <= 2; join++) { for (int join = 0; join <= 2; join++) {
failures += paintPaths(new TestSetup(SHAPE_MODE, false, width, cap, join, dashes)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));
failures += paintPaths(new TestSetup(SHAPE_MODE, true, width, cap, join, dashes)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));
} }
} }
} }
@ -320,29 +379,26 @@ public final class ClipShapeTest {
if (TEST_FILLER) { if (TEST_FILLER) {
// Filler tests: // Filler tests:
failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));
failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));
failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));
failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));
} }
} catch (IOException ioe) { } catch (IOException ioe) {
throw new RuntimeException(ioe); throw new RuntimeException(ioe);
} }
System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
if (!isMarlin.get()) { allWorstCtx.dump();
throw new RuntimeException("Marlin renderer not used at runtime !"); allCtx.dump();
}
if (!isClipRuntime.get()) { if (DO_FAIL && (failures != 0)) {
throw new RuntimeException("Marlin clipping not enabled at runtime !");
}
if (failures != 0) {
throw new RuntimeException("Clip test failures : " + failures); throw new RuntimeException("Clip test failures : " + failures);
} }
} }
static int paintPaths(final TestSetup ts) throws IOException { static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException {
final long start = System.nanoTime(); final long start = System.nanoTime();
if (FIXED_SEED) { if (FIXED_SEED) {
@ -356,19 +412,24 @@ public final class ClipShapeTest {
final boolean fill = !ts.isStroke(); final boolean fill = !ts.isStroke();
final Path2D p2d = new Path2D.Double(ts.windingRule); final Path2D p2d = new Path2D.Double(ts.windingRule);
final Stroke stroke = (!fill) ? createStroke(ts) : null;
final BufferedImage imgOn = newImage(TESTW, TESTH); final BufferedImage imgOn = newImage(TESTW, TESTH);
final Graphics2D g2dOn = initialize(imgOn, ts); final Graphics2D g2dOn = initialize(imgOn, stroke);
final BufferedImage imgOff = newImage(TESTW, TESTH); final BufferedImage imgOff = newImage(TESTW, TESTH);
final Graphics2D g2dOff = initialize(imgOff, ts); final Graphics2D g2dOff = initialize(imgOff, stroke);
final BufferedImage imgDiff = newImage(TESTW, TESTH); final BufferedImage imgDiff = newImage(TESTW, TESTH);
final DiffContext globalCtx = new DiffContext("All tests"); final DiffContext testSetupCtx = new DiffContext("Test setup");
final DiffContext testWorstCtx = new DiffContext("Worst");
final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)");
int nd = 0; int nd = 0;
try { try {
final DiffContext testCtx = new DiffContext("Test"); final DiffContext testCtx = new DiffContext("Test");
final DiffContext testThCtx = new DiffContext("Test(>threshold)");
BufferedImage diffImage; BufferedImage diffImage;
for (int n = 0; n < NUM_TESTS; n++) { for (int n = 0; n < NUM_TESTS; n++) {
@ -381,15 +442,24 @@ public final class ClipShapeTest {
paintShape(p2d, g2dOn, fill, true); paintShape(p2d, g2dOn, fill, true);
/* compute image difference if possible */ /* compute image difference if possible */
diffImage = computeDiffImage(testCtx, imgOn, imgOff, imgDiff, globalCtx); diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff);
final String testName = "Setup_" + ts.id + "_test_" + n;
// Worst (total)
if (testCtx.isDiff()) {
if (testWorstCtx.isWorse(testCtx, false)) {
testWorstCtx.set(testCtx);
}
if (testWorstThCtx.isWorse(testCtx, true)) {
testWorstThCtx.set(testCtx);
}
// accumulate data:
testSetupCtx.add(testCtx);
}
if (diffImage != null) { if (diffImage != null) {
nd++; nd++;
final double ratio = (100.0 * testCtx.histPix.count) / testCtx.histAll.count; testThCtx.dump();
System.out.println("Diff ratio: " + testName + " = " + trimTo3Digits(ratio) + " %"); testCtx.dump();
if (nd < MAX_SHOW_FRAMES) { if (nd < MAX_SHOW_FRAMES) {
if (SHOW_DETAILS) { if (SHOW_DETAILS) {
@ -401,9 +471,12 @@ public final class ClipShapeTest {
if (DUMP_SHAPE) { if (DUMP_SHAPE) {
dumpShape(p2d); dumpShape(p2d);
} }
final String testName = "Setup_" + ts.id + "_test_" + n;
saveImage(imgOff, OUTPUT_DIR, testName + "-off.png"); saveImage(imgOff, OUTPUT_DIR, testName + "-off.png");
saveImage(imgOn, OUTPUT_DIR, testName + "-on.png"); saveImage(imgOn, OUTPUT_DIR, testName + "-on.png");
saveImage(diffImage, OUTPUT_DIR, testName + "-diff.png"); saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png");
} }
} }
} }
@ -418,13 +491,25 @@ public final class ClipShapeTest {
+ " ratio = " + (100f * nd) / NUM_TESTS + " %"); + " ratio = " + (100f * nd) / NUM_TESTS + " %");
} }
globalCtx.dump(); if (testWorstCtx.isDiff()) {
testWorstCtx.dump();
if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) {
testWorstThCtx.dump();
}
if (allWorstCtx.isWorse(testWorstThCtx, true)) {
allWorstCtx.set(testWorstThCtx);
}
}
testSetupCtx.dump();
// accumulate data:
allCtx.add(testSetupCtx);
} }
System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
return nd; return nd;
} }
private static void paintShape(final Path2D p2d, final Graphics2D g2d, private static void paintShape(final Shape p2d, final Graphics2D g2d,
final boolean fill, final boolean clip) { final boolean fill, final boolean clip) {
reset(g2d); reset(g2d);
@ -438,26 +523,20 @@ public final class ClipShapeTest {
} }
private static Graphics2D initialize(final BufferedImage img, private static Graphics2D initialize(final BufferedImage img,
final TestSetup ts) { final Stroke s) {
final Graphics2D g2d = (Graphics2D) img.getGraphics(); final Graphics2D g2d = (Graphics2D) img.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY); RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE); // Test normalize:
// RenderingHints.VALUE_STROKE_NORMALIZE
RenderingHints.VALUE_STROKE_PURE
);
if (ts.isStroke()) { if (s != null) {
g2d.setStroke(createStroke(ts)); g2d.setStroke(s);
}
g2d.setColor(Color.GRAY);
// Test scale
if (TX_SCALE) {
g2d.scale(1.2, 1.2);
}
// Test shear
if (TX_SHEAR) {
g2d.shear(0.1, 0.2);
} }
g2d.setColor(Color.BLACK);
return g2d; return g2d;
} }
@ -482,20 +561,35 @@ public final class ClipShapeTest {
static void genShape(final Path2D p2d, final TestSetup ts) { static void genShape(final Path2D p2d, final TestSetup ts) {
p2d.reset(); p2d.reset();
final int end = (SHAPE_REPEAT) ? 2 : 1; /*
Test closed path:
0: moveTo + (draw)To + closePath
1: (draw)To + closePath (closePath + (draw)To sequence)
*/
final int end = (ts.closed) ? 2 : 1;
final double[] in = new double[8];
double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0;
for (int p = 0; p < end; p++) { for (int p = 0; p < end; p++) {
p2d.moveTo(randX(), randY()); if (p <= 0) {
x0 = randX(); y0 = randY();
p2d.moveTo(x0, y0);
sx0 = x0; sy0 = y0;
}
switch (ts.shapeMode) { switch (ts.shapeMode) {
case MIXED: case MIXED:
case FIFTY_LINE_POLYS:
case NINE_LINE_POLYS:
case FIVE_LINE_POLYS: case FIVE_LINE_POLYS:
case NINE_LINE_POLYS:
case FIFTY_LINE_POLYS:
p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY());
p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY());
p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY());
p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY());
x0 = randX(); y0 = randY();
p2d.lineTo(x0, y0);
if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) { if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) {
// And an implicit close makes 5 lines // And an implicit close makes 5 lines
break; break;
@ -503,29 +597,75 @@ public final class ClipShapeTest {
p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY());
p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY());
p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY());
p2d.lineTo(randX(), randY()); x0 = randX(); y0 = randY();
p2d.lineTo(x0, y0);
if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) { if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) {
// And an implicit close makes 9 lines // And an implicit close makes 9 lines
break; break;
} }
if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) { if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) {
for (int i = 0; i < 41; i++) { for (int i = 0; i < 41; i++) {
p2d.lineTo(randX(), randY()); x0 = randX(); y0 = randY();
p2d.lineTo(x0, y0);
} }
// And an implicit close makes 50 lines // And an implicit close makes 50 lines
break; break;
} }
case TWO_CUBICS: case TWO_CUBICS:
p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); if (SUBDIVIDE_CURVE) {
p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); in[0] = x0; in[1] = y0;
in[2] = randX(); in[3] = randY();
in[4] = randX(); in[5] = randY();
x0 = randX(); y0 = randY();
in[6] = x0; in[7] = y0;
subdivide(p2d, 8, in);
in[0] = x0; in[1] = y0;
in[2] = randX(); in[3] = randY();
in[4] = randX(); in[5] = randY();
x0 = randX(); y0 = randY();
in[6] = x0; in[7] = y0;
subdivide(p2d, 8, in);
} else {
x0 = randX(); y0 = randY();
p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
x0 = randX(); y0 = randY();
p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
}
if (ts.shapeMode == ShapeMode.TWO_CUBICS) { if (ts.shapeMode == ShapeMode.TWO_CUBICS) {
break; break;
} }
case FOUR_QUADS: case FOUR_QUADS:
p2d.quadTo(randX(), randY(), randX(), randY()); if (SUBDIVIDE_CURVE) {
p2d.quadTo(randX(), randY(), randX(), randY()); in[0] = x0; in[1] = y0;
p2d.quadTo(randX(), randY(), randX(), randY()); in[2] = randX(); in[3] = randY();
p2d.quadTo(randX(), randY(), randX(), randY()); x0 = randX(); y0 = randY();
in[4] = x0; in[5] = y0;
subdivide(p2d, 6, in);
in[0] = x0; in[1] = y0;
in[2] = randX(); in[3] = randY();
x0 = randX(); y0 = randY();
in[4] = x0; in[5] = y0;
subdivide(p2d, 6, in);
in[0] = x0; in[1] = y0;
in[2] = randX(); in[3] = randY();
x0 = randX(); y0 = randY();
in[4] = x0; in[5] = y0;
subdivide(p2d, 6, in);
in[0] = x0; in[1] = y0;
in[2] = randX(); in[3] = randY();
x0 = randX(); y0 = randY();
in[4] = x0; in[5] = y0;
subdivide(p2d, 6, in);
} else {
x0 = randX(); y0 = randY();
p2d.quadTo(randX(), randY(), x0, y0);
x0 = randX(); y0 = randY();
p2d.quadTo(randX(), randY(), x0, y0);
x0 = randX(); y0 = randY();
p2d.quadTo(randX(), randY(), x0, y0);
x0 = randX(); y0 = randY();
p2d.quadTo(randX(), randY(), x0, y0);
}
if (ts.shapeMode == ShapeMode.FOUR_QUADS) { if (ts.shapeMode == ShapeMode.FOUR_QUADS) {
break; break;
} }
@ -534,6 +674,111 @@ public final class ClipShapeTest {
if (ts.closed) { if (ts.closed) {
p2d.closePath(); p2d.closePath();
x0 = sx0; y0 = sy0;
}
}
}
static final int SUBDIVIDE_LIMIT = 5;
static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][];
static {
for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) {
SUBDIVIDE_CURVES[i] = new double[8 * n];
}
}
static void subdivide(final Path2D p2d, final int type, final double[] in) {
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type)));
}
double curveLen = ((type == 8)
? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7])
: quadlen(in[0], in[1], in[2], in[3], in[4], in[5]));
if (curveLen > SUBDIVIDE_LEN_TH) {
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("curvelen: " + curveLen);
}
System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8);
int level = 0;
while (curveLen >= SUBDIVIDE_LEN_TH) {
level++;
curveLen /= 2.0;
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("curvelen: " + curveLen);
}
}
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("level: " + level);
}
if (level > SUBDIVIDE_LIMIT) {
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("max level reached : " + level);
}
level = SUBDIVIDE_LIMIT;
}
for (int l = 0; l < level; l++) {
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("level: " + l);
}
double[] src = SUBDIVIDE_CURVES[l];
double[] dst = SUBDIVIDE_CURVES[l + 1];
for (int i = 0, j = 0; i < src.length; i += 8, j += 16) {
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
}
if (type == 8) {
CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8);
} else {
QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8);
}
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type)));
System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type)));
}
}
}
// Emit curves at last level:
double[] src = SUBDIVIDE_CURVES[level];
double len = 0.0;
for (int i = 0; i < src.length; i += 8) {
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
}
if (type == 8) {
if (TRACE_SUBDIVIDE_CURVE) {
len += curvelen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
}
p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
} else {
if (TRACE_SUBDIVIDE_CURVE) {
len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
}
p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
}
}
if (TRACE_SUBDIVIDE_CURVE) {
System.out.println("curveLen (final) = " + len);
}
} else {
if (type == 8) {
p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]);
} else {
p2d.quadTo(in[2], in[3], in[4], in[5]);
} }
} }
} }
@ -754,18 +999,19 @@ public final class ClipShapeTest {
return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
} }
public static BufferedImage computeDiffImage(final DiffContext localCtx, public static BufferedImage computeDiffImage(final DiffContext testCtx,
final DiffContext testThCtx,
final BufferedImage tstImage, final BufferedImage tstImage,
final BufferedImage refImage, final BufferedImage refImage,
final BufferedImage diffImage, final BufferedImage diffImage) {
final DiffContext globalCtx) {
final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData(); final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData();
final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData(); final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData();
final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData(); final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData();
// reset local diff context: // reset diff contexts:
localCtx.reset(); testCtx.reset();
testThCtx.reset();
int ref, tst, dg, v; int ref, tst, dg, v;
for (int i = 0, len = aRefPix.length; i < len; i++) { for (int i = 0, len = aRefPix.length; i < len; i++) {
@ -777,24 +1023,24 @@ public final class ClipShapeTest {
// max difference on grayscale values: // max difference on grayscale values:
v = (int) Math.ceil(Math.abs(dg / 3.0)); v = (int) Math.ceil(Math.abs(dg / 3.0));
// TODO: count warnings
if (v <= THRESHOLD_DELTA) { if (v <= THRESHOLD_DELTA) {
aDifPix[i] = 0; aDifPix[i] = 0;
} else { } else {
aDifPix[i] = toInt(v, v, v); aDifPix[i] = toInt(v, v, v);
testThCtx.add(v);
localCtx.add(v);
}
globalCtx.add(v);
} }
if (!localCtx.isDiff() || (localCtx.histPix.count <= THRESHOLD_NBPIX)) { if (v != 0) {
testCtx.add(v);
}
}
testCtx.addNbPix(testThCtx.histPix.count);
if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) {
return null; return null;
} }
localCtx.dump();
return diffImage; return diffImage;
} }
@ -895,6 +1141,17 @@ public final class ClipShapeTest {
} }
} }
void add(StatInteger stat) {
count += stat.count;
sum += stat.sum;
if (stat.min < min) {
min = stat.min;
}
if (stat.max > max) {
max = stat.max;
}
}
public final double average() { public final double average() {
return ((double) sum) / count; return ((double) sum) / count;
} }
@ -908,8 +1165,11 @@ public final class ClipShapeTest {
public final StringBuilder toString(final StringBuilder sb) { public final StringBuilder toString(final StringBuilder sb) {
sb.append(name).append("[n: ").append(count); sb.append(name).append("[n: ").append(count);
sb.append("] sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average())); sb.append("] ");
if (count != 0) {
sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));
sb.append(" [").append(min).append(" | ").append(max).append("]"); sb.append(" [").append(min).append(" | ").append(max).append("]");
}
return sb; return sb;
} }
@ -921,6 +1181,7 @@ public final class ClipShapeTest {
static final int MAX = 20; static final int MAX = 20;
static final int LAST = MAX - 1; static final int LAST = MAX - 1;
static final int[] STEPS = new int[MAX]; static final int[] STEPS = new int[MAX];
static final int BUCKET_TH;
static { static {
STEPS[0] = 0; STEPS[0] = 0;
@ -930,6 +1191,12 @@ public final class ClipShapeTest {
STEPS[i] = STEPS[i - 1] * BUCKET; STEPS[i] = STEPS[i - 1] * BUCKET;
} }
// System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS)); // System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS));
if (THRESHOLD_DELTA % 2 != 0) {
throw new IllegalStateException("THRESHOLD_DELTA must be odd");
}
BUCKET_TH = bucket(THRESHOLD_DELTA);
} }
static int bucket(int val) { static int bucket(int val) {
@ -969,6 +1236,37 @@ public final class ClipShapeTest {
add((int) val); add((int) val);
} }
void add(Histogram hist) {
super.add(hist);
for (int i = 0; i < MAX; i++) {
stats[i].add(hist.stats[i]);
}
}
boolean isWorse(Histogram hist, boolean useTh) {
boolean worst = false;
if (!useTh && (hist.sum > sum)) {
worst = true;
} else {
long sumLoc = 0l;
long sumHist = 0l;
// use running sum:
for (int i = MAX - 1; i >= BUCKET_TH; i--) {
sumLoc += stats[i].sum;
sumHist += hist.stats[i].sum;
}
if (sumHist > sumLoc) {
worst = true;
}
}
/*
System.out.println("running sum worst:");
System.out.println("this ? " + toString());
System.out.println("worst ? " + hist.toString());
*/
return worst;
}
@Override @Override
public final String toString() { public final String toString() {
final StringBuilder sb = new StringBuilder(2048); final StringBuilder sb = new StringBuilder(2048);
@ -995,38 +1293,88 @@ public final class ClipShapeTest {
static final class DiffContext { static final class DiffContext {
public final Histogram histAll;
public final Histogram histPix; public final Histogram histPix;
public final StatInteger nbPix;
DiffContext(String name) { DiffContext(String name) {
histAll = new Histogram("All Pixels [" + name + "]");
histPix = new Histogram("Diff Pixels [" + name + "]"); histPix = new Histogram("Diff Pixels [" + name + "]");
nbPix = new StatInteger("NbPixels [" + name + "]");
} }
void reset() { void reset() {
histAll.reset();
histPix.reset(); histPix.reset();
nbPix.reset();
} }
void dump() { void dump() {
if (isDiff()) { if (isDiff()) {
System.out.println("Differences [" + histAll.name + "]:"); System.out.println("Differences [" + histPix.name + "]:\n"
System.out.println("Total [all pixels]:\n" + histAll.toString()); + ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "")
System.out.println("Total [different pixels]:\n" + histPix.toString()); + histPix.toString()
);
} else { } else {
System.out.println("No difference for [" + histAll.name + "]."); System.out.println("No difference for [" + histPix.name + "].");
} }
} }
void add(int val) { void add(int val) {
histAll.add(val);
if (val != 0) {
histPix.add(val); histPix.add(val);
} }
void add(DiffContext ctx) {
histPix.add(ctx.histPix);
if (ctx.nbPix.count != 0L) {
nbPix.add(ctx.nbPix);
}
} }
boolean isDiff() { void addNbPix(long val) {
return histAll.sum != 0l; if (val != 0L) {
nbPix.add(val);
} }
} }
void set(DiffContext ctx) {
reset();
add(ctx);
}
boolean isWorse(DiffContext ctx, boolean useTh) {
return histPix.isWorse(ctx.histPix, useTh);
}
boolean isDiff() {
return histPix.sum != 0l;
}
}
static double linelen(final double x0, final double y0,
final double x1, final double y1)
{
final double dx = x1 - x0;
final double dy = y1 - y0;
return Math.sqrt(dx * dx + dy * dy);
}
static double quadlen(final double x0, final double y0,
final double x1, final double y1,
final double x2, final double y2)
{
return (linelen(x0, y0, x1, y1)
+ linelen(x1, y1, x2, y2)
+ linelen(x0, y0, x2, y2)) / 2.0d;
}
static double curvelen(final double x0, final double y0,
final double x1, final double y1,
final double x2, final double y2,
final double x3, final double y3)
{
return (linelen(x0, y0, x1, y1)
+ linelen(x1, y1, x2, y2)
+ linelen(x2, y2, x3, y3)
+ linelen(x0, y0, x3, y3)) / 2.0d;
}
} }