1 /* 2 * Copyright (c) 2007, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.marlin; 27 28 import com.sun.javafx.geom.Path2D; 29 import com.sun.javafx.geom.PathConsumer2D; 30 import com.sun.javafx.geom.transform.BaseTransform; 31 import com.sun.marlin.Helpers.IndexStack; 32 import com.sun.marlin.Helpers.PolyStack; 33 34 public final class TransformingPathConsumer2D { 35 36 private final RendererContext rdrCtx; 37 38 // recycled ClosedPathDetector instance from detectClosedPath() 39 private final ClosedPathDetector cpDetector; 40 41 // recycled PathClipFilter instance from pathClipper() 42 private final PathClipFilter pathClipper; 43 44 // recycled PathConsumer2D instance from wrapPath2D() 45 private final Path2DWrapper wp_Path2DWrapper = new Path2DWrapper(); 46 47 // recycled PathConsumer2D instances from deltaTransformConsumer() 48 private final DeltaScaleFilter dt_DeltaScaleFilter = new DeltaScaleFilter(); 49 private final DeltaTransformFilter dt_DeltaTransformFilter = new DeltaTransformFilter(); 50 51 // recycled PathConsumer2D instances from inverseDeltaTransformConsumer() 52 private final DeltaScaleFilter iv_DeltaScaleFilter = new DeltaScaleFilter(); 53 private final DeltaTransformFilter iv_DeltaTransformFilter = new DeltaTransformFilter(); 54 55 // recycled PathTracer instances from tracer...() methods 56 private final PathTracer tracerInput = new PathTracer("[Input]"); 57 private final PathTracer tracerCPDetector = new PathTracer("ClosedPathDetector"); 58 private final PathTracer tracerFiller = new PathTracer("Filler"); 59 private final PathTracer tracerStroker = new PathTracer("Stroker"); 60 61 TransformingPathConsumer2D(final RendererContext rdrCtx) { 62 // used by RendererContext 63 this.rdrCtx = rdrCtx; 64 this.cpDetector = new ClosedPathDetector(rdrCtx); 65 this.pathClipper = new PathClipFilter(rdrCtx); 66 } 67 68 public PathConsumer2D wrapPath2D(Path2D p2d) { 69 return wp_Path2DWrapper.init(p2d); 70 } 71 72 public PathConsumer2D traceInput(PathConsumer2D out) { 73 return tracerInput.init(out); 74 } 75 76 public PathConsumer2D traceClosedPathDetector(PathConsumer2D out) { 77 return tracerCPDetector.init(out); 78 } 79 80 public PathConsumer2D traceFiller(PathConsumer2D out) { 81 return tracerFiller.init(out); 82 } 83 84 public PathConsumer2D traceStroker(PathConsumer2D out) { 85 return tracerStroker.init(out); 86 } 87 88 public PathConsumer2D detectClosedPath(PathConsumer2D out) { 89 return cpDetector.init(out); 90 } 91 92 public PathConsumer2D pathClipper(PathConsumer2D out, 93 final float rdrOffX, 94 final float rdrOffY) 95 { 96 return pathClipper.init(out, rdrOffX, rdrOffY); 97 } 98 99 public PathConsumer2D deltaTransformConsumer(PathConsumer2D out, 100 BaseTransform at, 101 final float rdrOffX, 102 final float rdrOffY) 103 { 104 if (at == null) { 105 return out; 106 } 107 final float mxx = (float) at.getMxx(); 108 final float mxy = (float) at.getMxy(); 109 final float myx = (float) at.getMyx(); 110 final float myy = (float) at.getMyy(); 111 112 if (mxy == 0.0f && myx == 0.0f) { 113 if (mxx == 1.0f && myy == 1.0f) { 114 return out; 115 } else { 116 // Scale only 117 if (rdrCtx.doClip) { 118 // adjust clip rectangle (ymin, ymax, xmin, xmax): 119 adjustClipOffset(rdrCtx.clipRect, rdrOffX, rdrOffY); 120 adjustClipScale(rdrCtx.clipRect, mxx, myy); 121 } 122 return dt_DeltaScaleFilter.init(out, mxx, myy); 123 } 124 } else { 125 if (rdrCtx.doClip) { 126 // adjust clip rectangle (ymin, ymax, xmin, xmax): 127 adjustClipOffset(rdrCtx.clipRect, rdrOffX, rdrOffY); 128 adjustClipInverseDelta(rdrCtx.clipRect, mxx, mxy, myx, myy); 129 } 130 return dt_DeltaTransformFilter.init(out, mxx, mxy, myx, myy); 131 } 132 } 133 134 private static void adjustClipOffset(final float[] clipRect, 135 final float rdrOffX, 136 final float rdrOffY) 137 { 138 clipRect[0] += rdrOffY; 139 clipRect[1] += rdrOffY; 140 clipRect[2] += rdrOffX; 141 clipRect[3] += rdrOffX; 142 } 143 144 private static void adjustClipScale(final float[] clipRect, 145 final float mxx, final float myy) 146 { 147 // Adjust the clipping rectangle (iv_DeltaScaleFilter): 148 clipRect[0] /= myy; 149 clipRect[1] /= myy; 150 clipRect[2] /= mxx; 151 clipRect[3] /= mxx; 152 } 153 154 private static void adjustClipInverseDelta(final float[] clipRect, 155 final float mxx, final float mxy, 156 final float myx, final float myy) 157 { 158 // Adjust the clipping rectangle (iv_DeltaTransformFilter): 159 final float det = mxx * myy - mxy * myx; 160 final float imxx = myy / det; 161 final float imxy = -mxy / det; 162 final float imyx = -myx / det; 163 final float imyy = mxx / det; 164 165 float xmin, xmax, ymin, ymax; 166 float x, y; 167 // xmin, ymin: 168 x = clipRect[2] * imxx + clipRect[0] * imxy; 169 y = clipRect[2] * imyx + clipRect[0] * imyy; 170 171 xmin = xmax = x; 172 ymin = ymax = y; 173 174 // xmax, ymin: 175 x = clipRect[3] * imxx + clipRect[0] * imxy; 176 y = clipRect[3] * imyx + clipRect[0] * imyy; 177 178 if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } 179 if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } 180 181 // xmin, ymax: 182 x = clipRect[2] * imxx + clipRect[1] * imxy; 183 y = clipRect[2] * imyx + clipRect[1] * imyy; 184 185 if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } 186 if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } 187 188 // xmax, ymax: 189 x = clipRect[3] * imxx + clipRect[1] * imxy; 190 y = clipRect[3] * imyx + clipRect[1] * imyy; 191 192 if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } 193 if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } 194 195 clipRect[0] = ymin; 196 clipRect[1] = ymax; 197 clipRect[2] = xmin; 198 clipRect[3] = xmax; 199 } 200 201 public PathConsumer2D inverseDeltaTransformConsumer(PathConsumer2D out, 202 BaseTransform at) 203 { 204 if (at == null) { 205 return out; 206 } 207 float mxx = (float) at.getMxx(); 208 float mxy = (float) at.getMxy(); 209 float myx = (float) at.getMyx(); 210 float myy = (float) at.getMyy(); 211 212 if (mxy == 0.0f && myx == 0.0f) { 213 if (mxx == 1.0f && myy == 1.0f) { 214 return out; 215 } else { 216 return iv_DeltaScaleFilter.init(out, 1.0f/mxx, 1.0f/myy); 217 } 218 } else { 219 final float det = mxx * myy - mxy * myx; 220 return iv_DeltaTransformFilter.init(out, 221 myy / det, 222 -mxy / det, 223 -myx / det, 224 mxx / det); 225 } 226 } 227 228 static final class DeltaScaleFilter implements PathConsumer2D { 229 private PathConsumer2D out; 230 private float sx, sy; 231 232 DeltaScaleFilter() {} 233 234 DeltaScaleFilter init(PathConsumer2D out, 235 float mxx, float myy) 236 { 237 this.out = out; 238 sx = mxx; 239 sy = myy; 240 return this; // fluent API 241 } 242 243 @Override 244 public void moveTo(float x0, float y0) { 245 out.moveTo(x0 * sx, y0 * sy); 246 } 247 248 @Override 249 public void lineTo(float x1, float y1) { 250 out.lineTo(x1 * sx, y1 * sy); 251 } 252 253 @Override 254 public void quadTo(float x1, float y1, 255 float x2, float y2) 256 { 257 out.quadTo(x1 * sx, y1 * sy, 258 x2 * sx, y2 * sy); 259 } 260 261 @Override 262 public void curveTo(float x1, float y1, 263 float x2, float y2, 264 float x3, float y3) 265 { 266 out.curveTo(x1 * sx, y1 * sy, 267 x2 * sx, y2 * sy, 268 x3 * sx, y3 * sy); 269 } 270 271 @Override 272 public void closePath() { 273 out.closePath(); 274 } 275 276 @Override 277 public void pathDone() { 278 out.pathDone(); 279 } 280 } 281 282 static final class DeltaTransformFilter implements PathConsumer2D { 283 private PathConsumer2D out; 284 private float mxx, mxy, myx, myy; 285 286 DeltaTransformFilter() {} 287 288 DeltaTransformFilter init(PathConsumer2D out, 289 float mxx, float mxy, 290 float myx, float myy) 291 { 292 this.out = out; 293 this.mxx = mxx; 294 this.mxy = mxy; 295 this.myx = myx; 296 this.myy = myy; 297 return this; // fluent API 298 } 299 300 @Override 301 public void moveTo(float x0, float y0) { 302 out.moveTo(x0 * mxx + y0 * mxy, 303 x0 * myx + y0 * myy); 304 } 305 306 @Override 307 public void lineTo(float x1, float y1) { 308 out.lineTo(x1 * mxx + y1 * mxy, 309 x1 * myx + y1 * myy); 310 } 311 312 @Override 313 public void quadTo(float x1, float y1, 314 float x2, float y2) 315 { 316 out.quadTo(x1 * mxx + y1 * mxy, 317 x1 * myx + y1 * myy, 318 x2 * mxx + y2 * mxy, 319 x2 * myx + y2 * myy); 320 } 321 322 @Override 323 public void curveTo(float x1, float y1, 324 float x2, float y2, 325 float x3, float y3) 326 { 327 out.curveTo(x1 * mxx + y1 * mxy, 328 x1 * myx + y1 * myy, 329 x2 * mxx + y2 * mxy, 330 x2 * myx + y2 * myy, 331 x3 * mxx + y3 * mxy, 332 x3 * myx + y3 * myy); 333 } 334 335 @Override 336 public void closePath() { 337 out.closePath(); 338 } 339 340 @Override 341 public void pathDone() { 342 out.pathDone(); 343 } 344 } 345 346 static final class Path2DWrapper implements PathConsumer2D { 347 private Path2D p2d; 348 349 Path2DWrapper() {} 350 351 Path2DWrapper init(Path2D p2d) { 352 this.p2d = p2d; 353 return this; 354 } 355 356 @Override 357 public void moveTo(float x0, float y0) { 358 p2d.moveTo(x0, y0); 359 } 360 361 @Override 362 public void lineTo(float x1, float y1) { 363 p2d.lineTo(x1, y1); 364 } 365 366 @Override 367 public void closePath() { 368 p2d.closePath(); 369 } 370 371 @Override 372 public void pathDone() {} 373 374 @Override 375 public void curveTo(float x1, float y1, 376 float x2, float y2, 377 float x3, float y3) 378 { 379 p2d.curveTo(x1, y1, x2, y2, x3, y3); 380 } 381 382 @Override 383 public void quadTo(float x1, float y1, float x2, float y2) { 384 p2d.quadTo(x1, y1, x2, y2); 385 } 386 } 387 388 static final class ClosedPathDetector implements PathConsumer2D { 389 390 private final RendererContext rdrCtx; 391 private final PolyStack stack; 392 393 private PathConsumer2D out; 394 395 ClosedPathDetector(final RendererContext rdrCtx) { 396 this.rdrCtx = rdrCtx; 397 this.stack = (rdrCtx.stats != null) ? 398 new PolyStack(rdrCtx, 399 rdrCtx.stats.stat_cpd_polystack_types, 400 rdrCtx.stats.stat_cpd_polystack_curves, 401 rdrCtx.stats.hist_cpd_polystack_curves, 402 rdrCtx.stats.stat_array_cpd_polystack_curves, 403 rdrCtx.stats.stat_array_cpd_polystack_types) 404 : new PolyStack(rdrCtx); 405 } 406 407 ClosedPathDetector init(PathConsumer2D out) { 408 this.out = out; 409 return this; // fluent API 410 } 411 412 /** 413 * Disposes this instance: 414 * clean up before reusing this instance 415 */ 416 void dispose() { 417 stack.dispose(); 418 } 419 420 @Override 421 public void pathDone() { 422 // previous path is not closed: 423 finish(false); 424 out.pathDone(); 425 426 // TODO: fix possible leak if exception happened 427 // Dispose this instance: 428 dispose(); 429 } 430 431 @Override 432 public void closePath() { 433 // path is closed 434 finish(true); 435 out.closePath(); 436 } 437 438 @Override 439 public void moveTo(float x0, float y0) { 440 // previous path is not closed: 441 finish(false); 442 out.moveTo(x0, y0); 443 } 444 445 private void finish(final boolean closed) { 446 rdrCtx.closedPath = closed; 447 stack.pullAll(out); 448 } 449 450 @Override 451 public void lineTo(float x1, float y1) { 452 stack.pushLine(x1, y1); 453 } 454 455 @Override 456 public void curveTo(float x3, float y3, 457 float x2, float y2, 458 float x1, float y1) 459 { 460 stack.pushCubic(x1, y1, x2, y2, x3, y3); 461 } 462 463 @Override 464 public void quadTo(float x2, float y2, float x1, float y1) { 465 stack.pushQuad(x1, y1, x2, y2); 466 } 467 } 468 469 static final class PathClipFilter implements PathConsumer2D { 470 471 private PathConsumer2D out; 472 473 // Bounds of the drawing region, at pixel precision. 474 private final float[] clipRect; 475 476 private final float[] corners = new float[8]; 477 private boolean init_corners = false; 478 479 private final IndexStack stack; 480 481 // the current outcode of the current sub path 482 private int cOutCode = 0; 483 484 // the cumulated (and) outcode of the complete path 485 private int gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; 486 487 private boolean outside = false; 488 489 // The current point OUTSIDE 490 private float cx0, cy0; 491 492 PathClipFilter(final RendererContext rdrCtx) { 493 this.clipRect = rdrCtx.clipRect; 494 this.stack = (rdrCtx.stats != null) ? 495 new IndexStack(rdrCtx, 496 rdrCtx.stats.stat_pcf_idxstack_indices, 497 rdrCtx.stats.hist_pcf_idxstack_indices, 498 rdrCtx.stats.stat_array_pcf_idxstack_indices) 499 : new IndexStack(rdrCtx); 500 } 501 502 PathClipFilter init(final PathConsumer2D out, 503 final double rdrOffX, 504 final double rdrOffY) 505 { 506 this.out = out; 507 508 // add a small rounding error: 509 final float margin = 1e-3f; 510 511 final float[] _clipRect = this.clipRect; 512 // Adjust the clipping rectangle with the renderer offsets 513 _clipRect[0] -= margin - rdrOffY; 514 _clipRect[1] += margin + rdrOffY; 515 _clipRect[2] -= margin - rdrOffX; 516 _clipRect[3] += margin + rdrOffX; 517 518 this.init_corners = true; 519 this.gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; 520 521 return this; // fluent API 522 } 523 524 /** 525 * Disposes this instance: 526 * clean up before reusing this instance 527 */ 528 void dispose() { 529 stack.dispose(); 530 } 531 532 private void finishPath() { 533 if (outside) { 534 // criteria: inside or totally outside ? 535 if (gOutCode == 0) { 536 finish(); 537 } else { 538 this.outside = false; 539 stack.reset(); 540 } 541 } 542 } 543 544 private void finish() { 545 this.outside = false; 546 547 if (!stack.isEmpty()) { 548 if (init_corners) { 549 init_corners = false; 550 551 final float[] _corners = corners; 552 final float[] _clipRect = clipRect; 553 // Top Left (0): 554 _corners[0] = _clipRect[2]; 555 _corners[1] = _clipRect[0]; 556 // Bottom Left (1): 557 _corners[2] = _clipRect[2]; 558 _corners[3] = _clipRect[1]; 559 // Top right (2): 560 _corners[4] = _clipRect[3]; 561 _corners[5] = _clipRect[0]; 562 // Bottom Right (3): 563 _corners[6] = _clipRect[3]; 564 _corners[7] = _clipRect[1]; 565 } 566 stack.pullAll(corners, out); 567 } 568 out.lineTo(cx0, cy0); 569 } 570 571 @Override 572 public void pathDone() { 573 finishPath(); 574 575 out.pathDone(); 576 577 // TODO: fix possible leak if exception happened 578 // Dispose this instance: 579 dispose(); 580 } 581 582 @Override 583 public void closePath() { 584 finishPath(); 585 586 out.closePath(); 587 } 588 589 @Override 590 public void moveTo(final float x0, final float y0) { 591 finishPath(); 592 593 final int outcode = Helpers.outcode(x0, y0, clipRect); 594 this.cOutCode = outcode; 595 this.outside = false; 596 out.moveTo(x0, y0); 597 } 598 599 @Override 600 public void lineTo(final float xe, final float ye) { 601 final int outcode0 = this.cOutCode; 602 final int outcode1 = Helpers.outcode(xe, ye, clipRect); 603 this.cOutCode = outcode1; 604 605 final int sideCode = (outcode0 & outcode1); 606 607 // basic rejection criteria: 608 if (sideCode == 0) { 609 this.gOutCode = 0; 610 } else { 611 this.gOutCode &= sideCode; 612 // keep last point coordinate before entering the clip again: 613 this.outside = true; 614 this.cx0 = xe; 615 this.cy0 = ye; 616 617 clip(sideCode, outcode0, outcode1); 618 return; 619 } 620 if (outside) { 621 finish(); 622 } 623 // clipping disabled: 624 out.lineTo(xe, ye); 625 } 626 627 private void clip(final int sideCode, 628 final int outcode0, 629 final int outcode1) 630 { 631 // corner or cross-boundary on left or right side: 632 if ((outcode0 != outcode1) 633 && ((sideCode & MarlinConst.OUTCODE_MASK_L_R) != 0)) 634 { 635 // combine outcodes: 636 final int mergeCode = (outcode0 | outcode1); 637 final int tbCode = mergeCode & MarlinConst.OUTCODE_MASK_T_B; 638 final int lrCode = mergeCode & MarlinConst.OUTCODE_MASK_L_R; 639 final int off = (lrCode == MarlinConst.OUTCODE_LEFT) ? 0 : 2; 640 641 // add corners to outside stack: 642 switch (tbCode) { 643 case MarlinConst.OUTCODE_TOP: 644 // System.out.println("TOP "+ ((off == 0) ? "LEFT" : "RIGHT")); 645 stack.push(off); // top 646 return; 647 case MarlinConst.OUTCODE_BOTTOM: 648 // System.out.println("BOTTOM "+ ((off == 0) ? "LEFT" : "RIGHT")); 649 stack.push(off + 1); // bottom 650 return; 651 default: 652 // both TOP / BOTTOM: 653 if ((outcode0 & MarlinConst.OUTCODE_TOP) != 0) { 654 // System.out.println("TOP + BOTTOM "+ ((off == 0) ? "LEFT" : "RIGHT")); 655 // top to bottom 656 stack.push(off); // top 657 stack.push(off + 1); // bottom 658 } else { 659 // System.out.println("BOTTOM + TOP "+ ((off == 0) ? "LEFT" : "RIGHT")); 660 // bottom to top 661 stack.push(off + 1); // bottom 662 stack.push(off); // top 663 } 664 } 665 } 666 } 667 668 @Override 669 public void curveTo(final float x1, final float y1, 670 final float x2, final float y2, 671 final float xe, final float ye) 672 { 673 final int outcode0 = this.cOutCode; 674 final int outcode3 = Helpers.outcode(xe, ye, clipRect); 675 this.cOutCode = outcode3; 676 677 int sideCode = outcode0 & outcode3; 678 679 if (sideCode == 0) { 680 this.gOutCode = 0; 681 } else { 682 sideCode &= Helpers.outcode(x1, y1, clipRect); 683 sideCode &= Helpers.outcode(x2, y2, clipRect); 684 this.gOutCode &= sideCode; 685 686 // basic rejection criteria: 687 if (sideCode != 0) { 688 // keep last point coordinate before entering the clip again: 689 this.outside = true; 690 this.cx0 = xe; 691 this.cy0 = ye; 692 693 clip(sideCode, outcode0, outcode3); 694 return; 695 } 696 } 697 if (outside) { 698 finish(); 699 } 700 // clipping disabled: 701 out.curveTo(x1, y1, x2, y2, xe, ye); 702 } 703 704 @Override 705 public void quadTo(final float x1, final float y1, 706 final float xe, final float ye) 707 { 708 final int outcode0 = this.cOutCode; 709 final int outcode2 = Helpers.outcode(xe, ye, clipRect); 710 this.cOutCode = outcode2; 711 712 int sideCode = outcode0 & outcode2; 713 714 if (sideCode == 0) { 715 this.gOutCode = 0; 716 } else { 717 sideCode &= Helpers.outcode(x1, y1, clipRect); 718 this.gOutCode &= sideCode; 719 720 // basic rejection criteria: 721 if (sideCode != 0) { 722 // keep last point coordinate before entering the clip again: 723 this.outside = true; 724 this.cx0 = xe; 725 this.cy0 = ye; 726 727 clip(sideCode, outcode0, outcode2); 728 return; 729 } 730 } 731 if (outside) { 732 finish(); 733 } 734 // clipping disabled: 735 out.quadTo(x1, y1, xe, ye); 736 } 737 } 738 739 static final class PathTracer implements PathConsumer2D { 740 private final String prefix; 741 private PathConsumer2D out; 742 743 PathTracer(String name) { 744 this.prefix = name + ": "; 745 } 746 747 PathTracer init(PathConsumer2D out) { 748 this.out = out; 749 return this; // fluent API 750 } 751 752 @Override 753 public void moveTo(float x0, float y0) { 754 log("moveTo (" + x0 + ", " + y0 + ')'); 755 out.moveTo(x0, y0); 756 } 757 758 @Override 759 public void lineTo(float x1, float y1) { 760 log("lineTo (" + x1 + ", " + y1 + ')'); 761 out.lineTo(x1, y1); 762 } 763 764 @Override 765 public void curveTo(float x1, float y1, 766 float x2, float y2, 767 float x3, float y3) 768 { 769 log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ") P3(" + x3 + ", " + y3 + ')'); 770 out.curveTo(x1, y1, x2, y2, x3, y3); 771 } 772 773 @Override 774 public void quadTo(float x1, float y1, float x2, float y2) { 775 log("quadTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ')'); 776 out.quadTo(x1, y1, x2, y2); 777 } 778 779 @Override 780 public void closePath() { 781 log("closePath"); 782 out.closePath(); 783 } 784 785 @Override 786 public void pathDone() { 787 log("pathDone"); 788 out.pathDone(); 789 } 790 791 private void log(final String message) { 792 System.out.println(prefix + message); 793 } 794 } 795 }