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_CLIP_FILL = true; 52 53 static final boolean DO_TRACE_PATH = false; 54 55 static final boolean DO_CLIP_RUNTIME_ENABLE = MarlinProperties.isDoClipRuntimeFlag(); 56 57 /** 58 * Private constructor to prevent instantiation. 59 */ 60 private MarlinPrismUtils() { 61 } 62 63 private static boolean nearZero(final double num) { 64 return Math.abs(num) < 2.0d * Math.ulp(num); 65 } 66 67 private static PathConsumer2D initStroker( 68 final RendererContext rdrCtx, 69 final BasicStroke stroke, 70 final float lineWidth, 71 final 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 scale = 1.0f; 92 float width = lineWidth; 93 float[] dashes = stroke.getDashArray(); 94 float 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 // Get renderer offsets: 144 float rdrOffX = 0.0f, rdrOffY = 0.0f; 145 146 if (rdrCtx.doClip && (tx != null)) { 147 final MarlinRenderer renderer = (MarlinRenderer)out; 148 rdrOffX = renderer.getOffsetX(); 149 rdrOffY = renderer.getOffsetY(); 150 } 151 152 // Prepare the pipeline: 153 PathConsumer2D pc = out; 154 155 final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D; 156 157 if (DO_TRACE_PATH) { 158 // trace Stroker: 159 pc = transformerPC2D.traceStroker(pc); 160 } 161 162 if (MarlinConst.USE_SIMPLIFIER) { 163 // Use simplifier after stroker before Renderer 164 // to remove collinear segments (notably due to cap square) 165 pc = rdrCtx.simplifier.init(pc); 166 } 167 168 // deltaTransformConsumer may adjust the clip rectangle: 169 pc = transformerPC2D.deltaTransformConsumer(pc, strokerTx, rdrOffX, rdrOffY); 170 171 // stroker will adjust the clip rectangle (width / miter limit): 172 pc = rdrCtx.stroker.init(pc, width, stroke.getEndCap(), 173 stroke.getLineJoin(), stroke.getMiterLimit(), 174 scale, rdrOffX, rdrOffY); 175 176 if (dashes != null) { 177 if (!recycleDashes) { 178 dashLen = dashes.length; 179 } 180 pc = rdrCtx.dasher.init(pc, dashes, dashLen, dashphase, recycleDashes); 181 } else if (rdrCtx.doClip && (stroke.getEndCap() != Stroker.CAP_BUTT)) { 182 if (DO_TRACE_PATH) { 183 pc = transformerPC2D.traceClosedPathDetector(pc); 184 } 185 186 // If no dash and clip is enabled: 187 // detect closedPaths (polygons) for caps 188 pc = transformerPC2D.detectClosedPath(pc); 189 } 190 pc = transformerPC2D.inverseDeltaTransformConsumer(pc, strokerTx); 191 192 if (DO_TRACE_PATH) { 193 // trace Input: 194 pc = transformerPC2D.traceInput(pc); 195 } 196 /* 197 * Pipeline seems to be: 198 * shape.getPathIterator(tx) 199 * -> (inverseDeltaTransformConsumer) 200 * -> (Dasher) 201 * -> Stroker 202 * -> (deltaTransformConsumer) 203 * 204 * -> (CollinearSimplifier) to remove redundant segments 205 * 206 * -> pc2d = Renderer (bounding box) 207 */ 208 return pc; 209 } 210 211 private static PathConsumer2D initRenderer( 212 final RendererContext rdrCtx, 213 final BasicStroke stroke, 214 final BaseTransform tx, 215 final Rectangle clip, 216 final int piRule, 217 final MarlinRenderer renderer) 218 { 219 if (DO_CLIP || (DO_CLIP_RUNTIME_ENABLE && MarlinProperties.isDoClipAtRuntime())) { 220 // Define the initial clip bounds: 221 final float[] clipRect = rdrCtx.clipRect; 222 223 clipRect[0] = clip.y; 224 clipRect[1] = clip.y + clip.height; 225 clipRect[2] = clip.x; 226 clipRect[3] = clip.x + clip.width; 227 228 // Enable clipping: 229 rdrCtx.doClip = true; 230 } 231 232 if (stroke != null) { 233 renderer.init(clip.x, clip.y, clip.width, clip.height, 234 MarlinConst.WIND_NON_ZERO); 235 236 return initStroker(rdrCtx, stroke, stroke.getLineWidth(), tx, renderer); 237 } else { 238 // Filler: 239 final int oprule = (piRule == PathIterator.WIND_EVEN_ODD) ? 240 MarlinConst.WIND_EVEN_ODD : MarlinConst.WIND_NON_ZERO; 241 242 renderer.init(clip.x, clip.y, clip.width, clip.height, oprule); 243 244 PathConsumer2D pc = renderer; 245 246 final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D; 247 248 if (DO_CLIP_FILL && rdrCtx.doClip) { 249 float rdrOffX = renderer.getOffsetX(); 250 float rdrOffY = renderer.getOffsetY(); 251 252 if (DO_TRACE_PATH) { 253 // trace Filler: 254 pc = rdrCtx.transformerPC2D.traceFiller(pc); 255 } 256 pc = rdrCtx.transformerPC2D.pathClipper(pc, rdrOffX, rdrOffY); 257 } 258 259 if (DO_TRACE_PATH) { 260 // trace Input: 261 pc = transformerPC2D.traceInput(pc); 262 } 263 return pc; 264 } 265 } 266 267 public static MarlinRenderer setupRenderer( 268 final RendererContext rdrCtx, 269 final Shape shape, 270 final BasicStroke stroke, 271 final BaseTransform xform, 272 final Rectangle rclip, 273 final boolean antialiasedShape) 274 { 275 // Test if transform is identity: 276 final BaseTransform tf = ((xform != null) && !xform.isIdentity()) ? xform : null; 277 278 final MarlinRenderer r = (!FORCE_NO_AA && antialiasedShape) ? 279 rdrCtx.renderer : rdrCtx.getRendererNoAA(); 280 281 if (shape instanceof Path2D) { 282 final Path2D p2d = (Path2D)shape; 283 final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, p2d.getWindingRule(), r); 284 feedConsumer(rdrCtx, p2d, tf, pc2d); 285 } else { 286 final PathIterator pi = shape.getPathIterator(tf); 287 final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, pi.getWindingRule(), r); 288 feedConsumer(rdrCtx, pi, pc2d); 289 } 290 return r; 291 } 292 293 public static void strokeTo( 294 final RendererContext rdrCtx, 295 final Shape shape, 296 final BasicStroke stroke, 297 final float lineWidth, 298 final PathConsumer2D out) 299 { 300 final PathConsumer2D pc2d = initStroker(rdrCtx, stroke, lineWidth, null, out); 301 302 if (shape instanceof Path2D) { 303 feedConsumer(rdrCtx, (Path2D)shape, null, pc2d); 304 } else { 305 feedConsumer(rdrCtx, shape.getPathIterator(null), pc2d); 306 } 307 } 308 309 private static void feedConsumer(final RendererContext rdrCtx, final PathIterator pi, 310 final PathConsumer2D pc2d) 311 { 312 // mark context as DIRTY: 313 rdrCtx.dirty = true; 314 315 final float[] coords = rdrCtx.float6; 316 317 // ported from DuctusRenderingEngine.feedConsumer() but simplified: 318 // - removed skip flag = !subpathStarted 319 // - removed pathClosed (ie subpathStarted not set to false) 320 boolean subpathStarted = false; 321 322 for (; !pi.isDone(); pi.next()) { 323 switch (pi.currentSegment(coords)) { 324 case PathIterator.SEG_MOVETO: 325 /* Checking SEG_MOVETO coordinates if they are out of the 326 * [LOWER_BND, UPPER_BND] range. This check also handles NaN 327 * and Infinity values. Skipping next path segment in case of 328 * invalid data. 329 */ 330 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND && 331 coords[1] < UPPER_BND && coords[1] > LOWER_BND) 332 { 333 pc2d.moveTo(coords[0], coords[1]); 334 subpathStarted = true; 335 } 336 break; 337 case PathIterator.SEG_LINETO: 338 /* Checking SEG_LINETO coordinates if they are out of the 339 * [LOWER_BND, UPPER_BND] range. This check also handles NaN 340 * and Infinity values. Ignoring current path segment in case 341 * of invalid data. If segment is skipped its endpoint 342 * (if valid) is used to begin new subpath. 343 */ 344 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND && 345 coords[1] < UPPER_BND && coords[1] > LOWER_BND) 346 { 347 if (subpathStarted) { 348 pc2d.lineTo(coords[0], coords[1]); 349 } else { 350 pc2d.moveTo(coords[0], coords[1]); 351 subpathStarted = true; 352 } 353 } 354 break; 355 case PathIterator.SEG_QUADTO: 356 // Quadratic curves take two points 357 /* Checking SEG_QUADTO coordinates if they are out of the 358 * [LOWER_BND, UPPER_BND] range. This check also handles NaN 359 * and Infinity values. Ignoring current path segment in case 360 * of invalid endpoints's data. Equivalent to the SEG_LINETO 361 * if endpoint coordinates are valid but there are invalid data 362 * among other coordinates 363 */ 364 if (coords[2] < UPPER_BND && coords[2] > LOWER_BND && 365 coords[3] < UPPER_BND && coords[3] > LOWER_BND) 366 { 367 if (subpathStarted) { 368 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND && 369 coords[1] < UPPER_BND && coords[1] > LOWER_BND) 370 { 371 pc2d.quadTo(coords[0], coords[1], 372 coords[2], coords[3]); 373 } else { 374 pc2d.lineTo(coords[2], coords[3]); 375 } 376 } else { 377 pc2d.moveTo(coords[2], coords[3]); 378 subpathStarted = true; 379 } 380 } 381 break; 382 case PathIterator.SEG_CUBICTO: 383 // Cubic curves take three points 384 /* Checking SEG_CUBICTO coordinates if they are out of the 385 * [LOWER_BND, UPPER_BND] range. This check also handles NaN 386 * and Infinity values. Ignoring current path segment in case 387 * of invalid endpoints's data. Equivalent to the SEG_LINETO 388 * if endpoint coordinates are valid but there are invalid data 389 * among other coordinates 390 */ 391 if (coords[4] < UPPER_BND && coords[4] > LOWER_BND && 392 coords[5] < UPPER_BND && coords[5] > LOWER_BND) 393 { 394 if (subpathStarted) { 395 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND && 396 coords[1] < UPPER_BND && coords[1] > LOWER_BND && 397 coords[2] < UPPER_BND && coords[2] > LOWER_BND && 398 coords[3] < UPPER_BND && coords[3] > LOWER_BND) 399 { 400 pc2d.curveTo(coords[0], coords[1], 401 coords[2], coords[3], 402 coords[4], coords[5]); 403 } else { 404 pc2d.lineTo(coords[4], coords[5]); 405 } 406 } else { 407 pc2d.moveTo(coords[4], coords[5]); 408 subpathStarted = true; 409 } 410 } 411 break; 412 case PathIterator.SEG_CLOSE: 413 if (subpathStarted) { 414 pc2d.closePath(); 415 // do not set subpathStarted to false 416 // in case of missing moveTo() after close() 417 } 418 break; 419 default: 420 } 421 } 422 pc2d.pathDone(); 423 424 // mark context as CLEAN: 425 rdrCtx.dirty = false; 426 } 427 428 private static void feedConsumer(final RendererContext rdrCtx, 429 final Path2D p2d, 430 final BaseTransform xform, 431 final PathConsumer2D pc2d) 432 { 433 // mark context as DIRTY: 434 rdrCtx.dirty = true; 435 436 final float[] coords = rdrCtx.float6; 437 438 // ported from DuctusRenderingEngine.feedConsumer() but simplified: 439 // - removed skip flag = !subpathStarted 440 // - removed pathClosed (ie subpathStarted not set to false) 441 boolean subpathStarted = false; 442 443 final float[] pCoords = p2d.getFloatCoordsNoClone(); 444 final byte[] pTypes = p2d.getCommandsNoClone(); 445 final int nsegs = p2d.getNumCommands(); 446 447 for (int i = 0, coff = 0; i < nsegs; i++) { 448 switch (pTypes[i]) { 449 case PathIterator.SEG_MOVETO: 450 if (xform == null) { 451 coords[0] = pCoords[coff]; 452 coords[1] = pCoords[coff+1]; 453 } else { 454 xform.transform(pCoords, coff, coords, 0, 1); 455 } 456 coff += 2; 457 /* Checking SEG_MOVETO coordinates if they are out of the 458 * [LOWER_BND, UPPER_BND] range. This check also handles NaN 459 * and Infinity values. Skipping next path segment in case of 460 * invalid data. 461 */ 462 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND && 463 coords[1] < UPPER_BND && coords[1] > LOWER_BND) 464 { 465 pc2d.moveTo(coords[0], coords[1]); 466 subpathStarted = true; 467 } 468 break; 469 case PathIterator.SEG_LINETO: 470 if (xform == null) { 471 coords[0] = pCoords[coff]; 472 coords[1] = pCoords[coff+1]; 473 } else { 474 xform.transform(pCoords, coff, coords, 0, 1); 475 } 476 coff += 2; 477 /* Checking SEG_LINETO coordinates if they are out of the 478 * [LOWER_BND, UPPER_BND] range. This check also handles NaN 479 * and Infinity values. Ignoring current path segment in case 480 * of invalid data. If segment is skipped its endpoint 481 * (if valid) is used to begin new subpath. 482 */ 483 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND && 484 coords[1] < UPPER_BND && coords[1] > LOWER_BND) 485 { 486 if (subpathStarted) { 487 pc2d.lineTo(coords[0], coords[1]); 488 } else { 489 pc2d.moveTo(coords[0], coords[1]); 490 subpathStarted = true; 491 } 492 } 493 break; 494 case PathIterator.SEG_QUADTO: 495 if (xform == null) { 496 coords[0] = pCoords[coff]; 497 coords[1] = pCoords[coff+1]; 498 coords[2] = pCoords[coff+2]; 499 coords[3] = pCoords[coff+3]; 500 } else { 501 xform.transform(pCoords, coff, coords, 0, 2); 502 } 503 coff += 4; 504 // Quadratic curves take two points 505 /* Checking SEG_QUADTO 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[2] < UPPER_BND && coords[2] > LOWER_BND && 513 coords[3] < UPPER_BND && coords[3] > 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 { 519 pc2d.quadTo(coords[0], coords[1], 520 coords[2], coords[3]); 521 } else { 522 pc2d.lineTo(coords[2], coords[3]); 523 } 524 } else { 525 pc2d.moveTo(coords[2], coords[3]); 526 subpathStarted = true; 527 } 528 } 529 break; 530 case PathIterator.SEG_CUBICTO: 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 coords[4] = pCoords[coff+4]; 537 coords[5] = pCoords[coff+5]; 538 } else { 539 xform.transform(pCoords, coff, coords, 0, 3); 540 } 541 coff += 6; 542 // Cubic curves take three points 543 /* Checking SEG_CUBICTO 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[4] < UPPER_BND && coords[4] > LOWER_BND && 551 coords[5] < UPPER_BND && coords[5] > 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 coords[2] < UPPER_BND && coords[2] > LOWER_BND && 557 coords[3] < UPPER_BND && coords[3] > LOWER_BND) 558 { 559 pc2d.curveTo(coords[0], coords[1], 560 coords[2], coords[3], 561 coords[4], coords[5]); 562 } else { 563 pc2d.lineTo(coords[4], coords[5]); 564 } 565 } else { 566 pc2d.moveTo(coords[4], coords[5]); 567 subpathStarted = true; 568 } 569 } 570 break; 571 case PathIterator.SEG_CLOSE: 572 if (subpathStarted) { 573 pc2d.closePath(); 574 // do not set subpathStarted to false 575 // in case of missing moveTo() after close() 576 } 577 break; 578 default: 579 } 580 } 581 pc2d.pathDone(); 582 583 // mark context as CLEAN: 584 rdrCtx.dirty = false; 585 } 586 }