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