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 }