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 }