1 /*
   2  * Copyright (c) 2011, 2018, 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.prism.impl.shape;
  27 
  28 
  29 import com.sun.javafx.geom.PathConsumer2D;
  30 import com.sun.javafx.geom.PathIterator;
  31 import com.sun.javafx.geom.Path2D;
  32 import com.sun.javafx.geom.Rectangle;
  33 import com.sun.javafx.geom.Shape;
  34 import com.sun.javafx.geom.transform.BaseTransform;
  35 import com.sun.marlin.MarlinConst;
  36 import com.sun.marlin.MarlinProperties;
  37 import com.sun.marlin.MarlinRenderer;
  38 import com.sun.marlin.MarlinUtils;
  39 import com.sun.marlin.RendererContext;
  40 import com.sun.marlin.Stroker;
  41 import com.sun.marlin.TransformingPathConsumer2D;
  42 import com.sun.prism.BasicStroke;
  43 import java.util.Arrays;
  44 
  45 public final class MarlinPrismUtils {
  46 
  47     private static final boolean FORCE_NO_AA = false;
  48 
  49     // slightly slower ~2% if enabled stroker clipping (lines) but skipping cap / join handling is few percents faster in specific cases
  50     static final boolean DISABLE_2ND_STROKER_CLIPPING = true;
  51 
  52     static final boolean DO_TRACE_PATH = false;
  53 
  54     static final boolean DO_CLIP = MarlinProperties.isDoClip();
  55     static final boolean DO_CLIP_FILL = true;
  56     static final boolean DO_CLIP_RUNTIME_ENABLE = MarlinProperties.isDoClipRuntimeFlag();
  57 
  58     static final float UPPER_BND = Float.MAX_VALUE / 2.0f;
  59     static final float LOWER_BND = -UPPER_BND;
  60 
  61     /**
  62      * Private constructor to prevent instantiation.
  63      */
  64     private MarlinPrismUtils() {
  65     }
  66 
  67     private static PathConsumer2D initStroker(
  68             final RendererContext rdrCtx,
  69             final BasicStroke stroke,
  70             final float lineWidth,
  71             BaseTransform tx,
  72             final PathConsumer2D out)
  73     {
  74         // We use strokerat so that in Stroker and Dasher we can work only
  75         // with the pre-transformation coordinates. This will repeat a lot of
  76         // computations done in the path iterator, but the alternative is to
  77         // work with transformed paths and compute untransformed coordinates
  78         // as needed. This would be faster but I do not think the complexity
  79         // of working with both untransformed and transformed coordinates in
  80         // the same code is worth it.
  81         // However, if a path's width is constant after a transformation,
  82         // we can skip all this untransforming.
  83 
  84         // As pathTo() will check transformed coordinates for invalid values
  85         // (NaN / Infinity) to ignore such points, it is necessary to apply the
  86         // transformation before the path processing.
  87         BaseTransform strokerTx = null;
  88 
  89         int dashLen = -1;
  90         boolean recycleDashes = false;
  91         float width = lineWidth;
  92         float[] dashes = stroke.getDashArray();
  93         float dashphase = stroke.getDashPhase();
  94 
  95         if ((tx != null) && !tx.isIdentity()) {
  96             final double a = tx.getMxx();
  97             final double b = tx.getMxy();
  98             final double c = tx.getMyx();
  99             final double d = tx.getMyy();
 100 
 101             // If the transform is a constant multiple of an orthogonal transformation
 102             // then every length is just multiplied by a constant, so we just
 103             // need to transform input paths to stroker and tell stroker
 104             // the scaled width. This condition is satisfied if
 105             // a*b == -c*d && a*a+c*c == b*b+d*d. In the actual check below, we
 106             // leave a bit of room for error.
 107             if (nearZero(a*b + c*d) && nearZero(a*a + c*c - (b*b + d*d))) {
 108                 final float scale = (float) Math.sqrt(a*a + c*c);
 109 
 110                 if (dashes != null) {
 111                     recycleDashes = true;
 112                     dashLen = dashes.length;
 113                     dashes = rdrCtx.dasher.copyDashArray(dashes);
 114                     for (int i = 0; i < dashLen; i++) {
 115                         dashes[i] *= scale;
 116                     }
 117                     dashphase *= scale;
 118                 }
 119                 width *= scale;
 120 
 121                 // by now strokerat == null. Input paths to
 122                 // stroker (and maybe dasher) will have the full transform tx
 123                 // applied to them and nothing will happen to the output paths.
 124             } else {
 125                 strokerTx = tx;
 126 
 127                 // by now strokerat == tx. Input paths to
 128                 // stroker (and maybe dasher) will have the full transform tx
 129                 // applied to them, then they will be normalized, and then
 130                 // the inverse of *only the non translation part of tx* will
 131                 // be applied to the normalized paths. This won't cause problems
 132                 // in stroker, because, suppose tx = T*A, where T is just the
 133                 // translation part of tx, and A is the rest. T*A has already
 134                 // been applied to Stroker/Dasher's input. Then Ainv will be
 135                 // applied. Ainv*T*A is not equal to T, but it is a translation,
 136                 // which means that none of stroker's assumptions about its
 137                 // input will be violated. After all this, A will be applied
 138                 // to stroker's output.
 139             }
 140         } else {
 141             // either tx is null or it's the identity. In either case
 142             // we don't transform the path.
 143             tx = null;
 144         }
 145 
 146         // Prepare the pipeline:
 147         PathConsumer2D pc = out;
 148 
 149         final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 150 
 151         if (DO_TRACE_PATH) {
 152             // trace Stroker:
 153             pc = transformerPC2D.traceStroker(pc);
 154         }
 155 
 156         if (MarlinConst.USE_SIMPLIFIER) {
 157             // Use simplifier after stroker before Renderer
 158             // to remove collinear segments (notably due to cap square)
 159             pc = rdrCtx.simplifier.init(pc);
 160         }
 161 
 162         // deltaTransformConsumer may adjust the clip rectangle:
 163         pc = transformerPC2D.deltaTransformConsumer(pc, strokerTx);
 164 
 165         // stroker will adjust the clip rectangle (width / miter limit):
 166         pc = rdrCtx.stroker.init(pc, width, stroke.getEndCap(),
 167                 stroke.getLineJoin(), stroke.getMiterLimit(),
 168                 (dashes == null));
 169 
 170         // Curve Monotizer:
 171         rdrCtx.monotonizer.init(width);
 172 
 173         if (dashes != null) {
 174             if (!recycleDashes) {
 175                 dashLen = dashes.length;
 176             }
 177             if (DO_TRACE_PATH) {
 178                 pc = transformerPC2D.traceDasher(pc);
 179             }
 180             pc = rdrCtx.dasher.init(pc, dashes, dashLen, dashphase,
 181                                     recycleDashes);
 182 
 183             if (DISABLE_2ND_STROKER_CLIPPING) {
 184                 // disable stoker clipping:
 185                 rdrCtx.stroker.disableClipping();
 186             }
 187 
 188         } else if (rdrCtx.doClip && (stroke.getEndCap() != Stroker.CAP_BUTT)) {
 189             if (DO_TRACE_PATH) {
 190                 pc = transformerPC2D.traceClosedPathDetector(pc);
 191             }
 192 
 193             // If no dash and clip is enabled:
 194             // detect closedPaths (polygons) for caps
 195             pc = transformerPC2D.detectClosedPath(pc);
 196         }
 197         pc = transformerPC2D.inverseDeltaTransformConsumer(pc, strokerTx);
 198 
 199         if (DO_TRACE_PATH) {
 200             // trace Input:
 201             pc = transformerPC2D.traceInput(pc);
 202         }
 203         /*
 204          * Pipeline seems to be:
 205          * shape.getPathIterator(tx)
 206          * -> (inverseDeltaTransformConsumer)
 207          * -> (Dasher)
 208          * -> Stroker
 209          * -> (deltaTransformConsumer)
 210          *
 211          * -> (CollinearSimplifier) to remove redundant segments
 212          *
 213          * -> pc2d = Renderer (bounding box)
 214          */
 215         return pc;
 216     }
 217 
 218     private static boolean nearZero(final double num) {
 219         return Math.abs(num) < 2.0d * Math.ulp(num);
 220     }
 221 
 222     private static PathConsumer2D initRenderer(
 223             final RendererContext rdrCtx,
 224             final BasicStroke stroke,
 225             final BaseTransform tx,
 226             final Rectangle clip,
 227             final int piRule,
 228             final MarlinRenderer renderer)
 229     {
 230         if (DO_CLIP || (DO_CLIP_RUNTIME_ENABLE && MarlinProperties.isDoClipAtRuntime())) {
 231             // Define the initial clip bounds:
 232             final float[] clipRect = rdrCtx.clipRect;
 233 
 234             // Adjust the clipping rectangle with the renderer offsets
 235             final float rdrOffX = renderer.getOffsetX();
 236             final float rdrOffY = renderer.getOffsetY();
 237 
 238             // add a small rounding error:
 239             final float margin = 1e-3f;
 240 
 241             clipRect[0] = clip.y
 242                             - margin + rdrOffY;
 243             clipRect[1] = clip.y + clip.height
 244                             + margin + rdrOffY;
 245             clipRect[2] = clip.x
 246                             - margin + rdrOffX;
 247             clipRect[3] = clip.x + clip.width
 248                             + margin + rdrOffX;
 249 
 250             if (MarlinConst.DO_LOG_CLIP) {
 251                 MarlinUtils.logInfo("clipRect (clip): "
 252                                     + Arrays.toString(rdrCtx.clipRect));
 253             }
 254 
 255             // Enable clipping:
 256             rdrCtx.doClip = true;
 257         }
 258 
 259         if (stroke != null) {
 260             renderer.init(clip.x, clip.y, clip.width, clip.height,
 261                           MarlinConst.WIND_NON_ZERO);
 262 
 263             return initStroker(rdrCtx, stroke, stroke.getLineWidth(), tx, renderer);
 264         } else {
 265             // Filler:
 266             final int oprule = (piRule == PathIterator.WIND_EVEN_ODD) ?
 267                 MarlinConst.WIND_EVEN_ODD : MarlinConst.WIND_NON_ZERO;
 268 
 269             renderer.init(clip.x, clip.y, clip.width, clip.height, oprule);
 270 
 271             PathConsumer2D pc = renderer;
 272 
 273             final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 274 
 275             if (DO_CLIP_FILL && rdrCtx.doClip) {
 276                 if (DO_TRACE_PATH) {
 277                     // trace Filler:
 278                     pc = rdrCtx.transformerPC2D.traceFiller(pc);
 279                 }
 280                 pc = rdrCtx.transformerPC2D.pathClipper(pc);
 281             }
 282 
 283             if (DO_TRACE_PATH) {
 284                 // trace Input:
 285                 pc = transformerPC2D.traceInput(pc);
 286             }
 287             return pc;
 288         }
 289     }
 290 
 291     public static MarlinRenderer setupRenderer(
 292             final RendererContext rdrCtx,
 293             final Shape shape,
 294             final BasicStroke stroke,
 295             final BaseTransform xform,
 296             final Rectangle rclip,
 297             final boolean antialiasedShape)
 298     {
 299         // Test if transform is identity:
 300         final BaseTransform tf = ((xform != null) && !xform.isIdentity()) ? xform : null;
 301 
 302         final MarlinRenderer r =  (!FORCE_NO_AA && antialiasedShape) ?
 303                 rdrCtx.renderer : rdrCtx.getRendererNoAA();
 304 
 305         if (shape instanceof Path2D) {
 306             final Path2D p2d = (Path2D)shape;
 307             final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, p2d.getWindingRule(), r);
 308             feedConsumer(rdrCtx, p2d, tf, pc2d);
 309         } else {
 310             final PathIterator pi = shape.getPathIterator(tf);
 311             final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, pi.getWindingRule(), r);
 312             feedConsumer(rdrCtx, pi, pc2d);
 313         }
 314         return r;
 315     }
 316 
 317     public static void strokeTo(
 318             final RendererContext rdrCtx,
 319             final Shape shape,
 320             final BasicStroke stroke,
 321             final float lineWidth,
 322             final PathConsumer2D out)
 323     {
 324         final PathConsumer2D pc2d = initStroker(rdrCtx, stroke, lineWidth, null, out);
 325 
 326         if (shape instanceof Path2D) {
 327             feedConsumer(rdrCtx, (Path2D)shape, null, pc2d);
 328         } else {
 329             feedConsumer(rdrCtx, shape.getPathIterator(null), pc2d);
 330         }
 331     }
 332 
 333     private static void feedConsumer(final RendererContext rdrCtx, final PathIterator pi,
 334                                      PathConsumer2D pc2d)
 335     {
 336         if (MarlinConst.USE_PATH_SIMPLIFIER) {
 337             // Use path simplifier at the first step
 338             // to remove useless points
 339             pc2d = rdrCtx.pathSimplifier.init(pc2d);
 340         }
 341 
 342         // mark context as DIRTY:
 343         rdrCtx.dirty = true;
 344 
 345         final float[] coords = rdrCtx.float6;
 346 
 347         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 348         // - removed skip flag = !subpathStarted
 349         // - removed pathClosed (ie subpathStarted not set to false)
 350         boolean subpathStarted = false;
 351 
 352         for (; !pi.isDone(); pi.next()) {
 353             switch (pi.currentSegment(coords)) {
 354             case PathIterator.SEG_MOVETO:
 355                 /* Checking SEG_MOVETO coordinates if they are out of the
 356                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 357                  * and Infinity values. Skipping next path segment in case of
 358                  * invalid data.
 359                  */
 360                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 361                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 362                 {
 363                     pc2d.moveTo(coords[0], coords[1]);
 364                     subpathStarted = true;
 365                 }
 366                 break;
 367             case PathIterator.SEG_LINETO:
 368                 /* Checking SEG_LINETO coordinates if they are out of the
 369                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 370                  * and Infinity values. Ignoring current path segment in case
 371                  * of invalid data. If segment is skipped its endpoint
 372                  * (if valid) is used to begin new subpath.
 373                  */
 374                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 375                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 376                 {
 377                     if (subpathStarted) {
 378                         pc2d.lineTo(coords[0], coords[1]);
 379                     } else {
 380                         pc2d.moveTo(coords[0], coords[1]);
 381                         subpathStarted = true;
 382                     }
 383                 }
 384                 break;
 385             case PathIterator.SEG_QUADTO:
 386                 // Quadratic curves take two points
 387                 /* Checking SEG_QUADTO coordinates if they are out of the
 388                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 389                  * and Infinity values. Ignoring current path segment in case
 390                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 391                  * if endpoint coordinates are valid but there are invalid data
 392                  * among other coordinates
 393                  */
 394                 if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 395                     coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 396                 {
 397                     if (subpathStarted) {
 398                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 399                             coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 400                         {
 401                             pc2d.quadTo(coords[0], coords[1],
 402                                         coords[2], coords[3]);
 403                         } else {
 404                             pc2d.lineTo(coords[2], coords[3]);
 405                         }
 406                     } else {
 407                         pc2d.moveTo(coords[2], coords[3]);
 408                         subpathStarted = true;
 409                     }
 410                 }
 411                 break;
 412             case PathIterator.SEG_CUBICTO:
 413                 // Cubic curves take three points
 414                 /* Checking SEG_CUBICTO coordinates if they are out of the
 415                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 416                  * and Infinity values. Ignoring current path segment in case
 417                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 418                  * if endpoint coordinates are valid but there are invalid data
 419                  * among other coordinates
 420                  */
 421                 if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
 422                     coords[5] < UPPER_BND && coords[5] > LOWER_BND)
 423                 {
 424                     if (subpathStarted) {
 425                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 426                             coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
 427                             coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 428                             coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 429                         {
 430                             pc2d.curveTo(coords[0], coords[1],
 431                                          coords[2], coords[3],
 432                                          coords[4], coords[5]);
 433                         } else {
 434                             pc2d.lineTo(coords[4], coords[5]);
 435                         }
 436                     } else {
 437                         pc2d.moveTo(coords[4], coords[5]);
 438                         subpathStarted = true;
 439                     }
 440                 }
 441                 break;
 442             case PathIterator.SEG_CLOSE:
 443                 if (subpathStarted) {
 444                     pc2d.closePath();
 445                     // do not set subpathStarted to false
 446                     // in case of missing moveTo() after close()
 447                 }
 448                 break;
 449             default:
 450             }
 451         }
 452         pc2d.pathDone();
 453 
 454         // mark context as CLEAN:
 455         rdrCtx.dirty = false;
 456     }
 457 
 458     private static void feedConsumer(final RendererContext rdrCtx,
 459                                      final Path2D p2d,
 460                                      final BaseTransform xform,
 461                                      PathConsumer2D pc2d)
 462     {
 463         if (MarlinConst.USE_PATH_SIMPLIFIER) {
 464             // Use path simplifier at the first step
 465             // to remove useless points
 466             pc2d = rdrCtx.pathSimplifier.init(pc2d);
 467         }
 468 
 469         // mark context as DIRTY:
 470         rdrCtx.dirty = true;
 471 
 472         final float[] coords = rdrCtx.float6;
 473 
 474         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 475         // - removed skip flag = !subpathStarted
 476         // - removed pathClosed (ie subpathStarted not set to false)
 477         boolean subpathStarted = false;
 478 
 479         final float[] pCoords = p2d.getFloatCoordsNoClone();
 480         final byte[] pTypes = p2d.getCommandsNoClone();
 481         final int nsegs = p2d.getNumCommands();
 482 
 483         for (int i = 0, coff = 0; i < nsegs; i++) {
 484             switch (pTypes[i]) {
 485             case PathIterator.SEG_MOVETO:
 486                 if (xform == null) {
 487                     coords[0] = pCoords[coff];
 488                     coords[1] = pCoords[coff+1];
 489                 } else {
 490                     xform.transform(pCoords, coff, coords, 0, 1);
 491                 }
 492                 coff += 2;
 493                 /* Checking SEG_MOVETO coordinates if they are out of the
 494                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 495                  * and Infinity values. Skipping next path segment in case of
 496                  * invalid data.
 497                  */
 498                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 499                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 500                 {
 501                     pc2d.moveTo(coords[0], coords[1]);
 502                     subpathStarted = true;
 503                 }
 504                 break;
 505             case PathIterator.SEG_LINETO:
 506                 if (xform == null) {
 507                     coords[0] = pCoords[coff];
 508                     coords[1] = pCoords[coff+1];
 509                 } else {
 510                     xform.transform(pCoords, coff, coords, 0, 1);
 511                 }
 512                 coff += 2;
 513                 /* Checking SEG_LINETO coordinates if they are out of the
 514                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 515                  * and Infinity values. Ignoring current path segment in case
 516                  * of invalid data. If segment is skipped its endpoint
 517                  * (if valid) is used to begin new subpath.
 518                  */
 519                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 520                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 521                 {
 522                     if (subpathStarted) {
 523                         pc2d.lineTo(coords[0], coords[1]);
 524                     } else {
 525                         pc2d.moveTo(coords[0], coords[1]);
 526                         subpathStarted = true;
 527                     }
 528                 }
 529                 break;
 530             case PathIterator.SEG_QUADTO:
 531                 if (xform == null) {
 532                     coords[0] = pCoords[coff];
 533                     coords[1] = pCoords[coff+1];
 534                     coords[2] = pCoords[coff+2];
 535                     coords[3] = pCoords[coff+3];
 536                 } else {
 537                     xform.transform(pCoords, coff, coords, 0, 2);
 538                 }
 539                 coff += 4;
 540                 // Quadratic curves take two points
 541                 /* Checking SEG_QUADTO coordinates if they are out of the
 542                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 543                  * and Infinity values. Ignoring current path segment in case
 544                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 545                  * if endpoint coordinates are valid but there are invalid data
 546                  * among other coordinates
 547                  */
 548                 if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 549                     coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 550                 {
 551                     if (subpathStarted) {
 552                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 553                             coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 554                         {
 555                             pc2d.quadTo(coords[0], coords[1],
 556                                         coords[2], coords[3]);
 557                         } else {
 558                             pc2d.lineTo(coords[2], coords[3]);
 559                         }
 560                     } else {
 561                         pc2d.moveTo(coords[2], coords[3]);
 562                         subpathStarted = true;
 563                     }
 564                 }
 565                 break;
 566             case PathIterator.SEG_CUBICTO:
 567                 if (xform == null) {
 568                     coords[0] = pCoords[coff];
 569                     coords[1] = pCoords[coff+1];
 570                     coords[2] = pCoords[coff+2];
 571                     coords[3] = pCoords[coff+3];
 572                     coords[4] = pCoords[coff+4];
 573                     coords[5] = pCoords[coff+5];
 574                 } else {
 575                     xform.transform(pCoords, coff, coords, 0, 3);
 576                 }
 577                 coff += 6;
 578                 // Cubic curves take three points
 579                 /* Checking SEG_CUBICTO coordinates if they are out of the
 580                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 581                  * and Infinity values. Ignoring current path segment in case
 582                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 583                  * if endpoint coordinates are valid but there are invalid data
 584                  * among other coordinates
 585                  */
 586                 if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
 587                     coords[5] < UPPER_BND && coords[5] > LOWER_BND)
 588                 {
 589                     if (subpathStarted) {
 590                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 591                             coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
 592                             coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 593                             coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 594                         {
 595                             pc2d.curveTo(coords[0], coords[1],
 596                                          coords[2], coords[3],
 597                                          coords[4], coords[5]);
 598                         } else {
 599                             pc2d.lineTo(coords[4], coords[5]);
 600                         }
 601                     } else {
 602                         pc2d.moveTo(coords[4], coords[5]);
 603                         subpathStarted = true;
 604                     }
 605                 }
 606                 break;
 607             case PathIterator.SEG_CLOSE:
 608                 if (subpathStarted) {
 609                     pc2d.closePath();
 610                     // do not set subpathStarted to false
 611                     // in case of missing moveTo() after close()
 612                 }
 613                 break;
 614             default:
 615             }
 616         }
 617         pc2d.pathDone();
 618 
 619         // mark context as CLEAN:
 620         rdrCtx.dirty = false;
 621     }
 622 }