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