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