diff --git a/pom.xml b/pom.xml index 141b40c..9951b26 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ org.marlin marlin jar - 0.8.1.1-Unsafe + 0.8.1.2-Unsafe Marlin software rasterizer https://github.com/bourgesl/marlin-renderer diff --git a/src/main/java/org/marlin/pisces/DHelpers.java b/src/main/java/org/marlin/pisces/DHelpers.java index 6b63064..3b322b5 100644 --- a/src/main/java/org/marlin/pisces/DHelpers.java +++ b/src/main/java/org/marlin/pisces/DHelpers.java @@ -431,13 +431,6 @@ static void subdivideAt(double t, double[] src, int srcoff, // From sun.java2d.loops.GeneralRenderer: - static final int OUTCODE_TOP = 1; - static final int OUTCODE_BOTTOM = 2; - static final int OUTCODE_LEFT = 4; - static final int OUTCODE_RIGHT = 8; - static final int OUTCODE_MASK_T_B = OUTCODE_TOP | OUTCODE_BOTTOM; - static final int OUTCODE_MASK_L_R = OUTCODE_LEFT | OUTCODE_RIGHT; - static int outcode(final double x, final double y, final double[] clipRect) { diff --git a/src/main/java/org/marlin/pisces/DMarlinRenderingEngine.java b/src/main/java/org/marlin/pisces/DMarlinRenderingEngine.java index 3ecf526..e014ac6 100644 --- a/src/main/java/org/marlin/pisces/DMarlinRenderingEngine.java +++ b/src/main/java/org/marlin/pisces/DMarlinRenderingEngine.java @@ -92,8 +92,6 @@ abstract PathIterator getNormalizingPathIterator(DRendererContext rdrCtx, static final boolean DO_CLIP_FILL = true; - static final boolean SKIP_WINDING_RULE_EVEN_ODD = false; - /** * Public constructor */ @@ -858,20 +856,19 @@ public AATileGenerator getAATileGenerator(Shape s, clip.getWidth(), clip.getHeight(), windingRule); - if (!SKIP_WINDING_RULE_EVEN_ODD) { - DPathConsumer2D pc2d = r; + DPathConsumer2D pc2d = r; - if (DO_CLIP_FILL && (windingRule == WIND_NON_ZERO) && rdrCtx.doClip) { - if (DO_TRACE_PATH) { - // trace Input: - pc2d = rdrCtx.transformerPC2D.traceInput(pc2d); - } - pc2d = rdrCtx.transformerPC2D.pathClipper(pc2d); + if (DO_CLIP_FILL && (windingRule == WIND_NON_ZERO) && rdrCtx.doClip) { + if (DO_TRACE_PATH) { + // trace Input: + pc2d = rdrCtx.transformerPC2D.traceInput(pc2d); } - - // TODO: subdivide quad/cubic curves into monotonic curves ? - pathTo(rdrCtx, pi, pc2d); + pc2d = rdrCtx.transformerPC2D.pathClipper(pc2d); } + + // TODO: subdivide quad/cubic curves into monotonic curves ? + pathTo(rdrCtx, pi, pc2d); + } else { // draw shape with given stroke: r = rdrCtx.renderer.init(clip.getLoX(), clip.getLoY(), diff --git a/src/main/java/org/marlin/pisces/DTransformingPathConsumer2D.java b/src/main/java/org/marlin/pisces/DTransformingPathConsumer2D.java index 1db4136..841bfda 100644 --- a/src/main/java/org/marlin/pisces/DTransformingPathConsumer2D.java +++ b/src/main/java/org/marlin/pisces/DTransformingPathConsumer2D.java @@ -495,6 +495,9 @@ static final class PathClipFilter implements DPathConsumer2D { // the current outcode of the current sub path private int cOutCode = 0; + // the cumulated (and) outcode of the complete path + private int gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; + private boolean outside = false; private double cx0, cy0; @@ -524,7 +527,8 @@ PathClipFilter init(final DPathConsumer2D out) { _clipRect[2] -= margin - rdrOffX; _clipRect[3] += margin + rdrOffX; - init_corners = true; + this.init_corners = true; + this.gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; return this; // fluent API } @@ -539,6 +543,8 @@ void dispose() { @Override public void pathDone() { + finishPath(); + out.pathDone(); // TODO: fix possible leak if exception happened @@ -548,35 +554,45 @@ public void pathDone() { @Override public void closePath() { - if (outside) { - this.outside = false; + finishPath(); - if (sOutCode == 0) { + out.closePath(); + this.cOutCode = sOutCode; + } + + private void finishPath() { + if (outside) { + // criteria: inside or totally outside ? + if (gOutCode == 0) { finish(); } else { + this.outside = false; stack.reset(); } } - out.closePath(); - this.cOutCode = sOutCode; } private void finish() { + this.outside = false; + if (!stack.isEmpty()) { if (init_corners) { init_corners = false; + + final double[] _corners = corners; + final double[] _clipRect = clipRect; // Top Left (0): - corners[0] = clipRect[2]; - corners[1] = clipRect[0]; + _corners[0] = _clipRect[2]; + _corners[1] = _clipRect[0]; // Bottom Left (1): - corners[2] = clipRect[2]; - corners[3] = clipRect[1]; + _corners[2] = _clipRect[2]; + _corners[3] = _clipRect[1]; // Top right (2): - corners[4] = clipRect[3]; - corners[5] = clipRect[0]; + _corners[4] = _clipRect[3]; + _corners[5] = _clipRect[0]; // Bottom Right (3): - corners[6] = clipRect[3]; - corners[7] = clipRect[1]; + _corners[6] = _clipRect[3]; + _corners[7] = _clipRect[1]; } stack.pullAll(corners, out); } @@ -601,7 +617,10 @@ public void lineTo(final double xe, final double ye) { final int sideCode = (outcode0 & outcode1); // basic rejection criteria: - if (sideCode != 0) { + if (sideCode == 0) { + this.gOutCode = 0; + } else { + this.gOutCode &= sideCode; // keep last point coordinate before entering the clip again: this.outside = true; this.cx0 = xe; @@ -611,7 +630,6 @@ public void lineTo(final double xe, final double ye) { return; } if (outside) { - this.outside = false; finish(); } // clipping disabled: @@ -624,25 +642,25 @@ private void clip(final int sideCode, { // corner or cross-boundary on left or right side: if ((outcode0 != outcode1) - && ((sideCode & DHelpers.OUTCODE_MASK_T_B) != 0)) + && ((sideCode & MarlinConst.OUTCODE_MASK_T_B) != 0)) { // combine outcodes: final int mergeCode = (outcode0 | outcode1); - final int tbCode = mergeCode & DHelpers.OUTCODE_MASK_T_B; - final int lrCode = mergeCode & DHelpers.OUTCODE_MASK_L_R; - // add corners to outside stack: - final int off = (lrCode == DHelpers.OUTCODE_LEFT) ? 0 : 2; + final int tbCode = mergeCode & MarlinConst.OUTCODE_MASK_T_B; + final int lrCode = mergeCode & MarlinConst.OUTCODE_MASK_L_R; + final int off = (lrCode == MarlinConst.OUTCODE_LEFT) ? 0 : 2; + // add corners to outside stack: switch (tbCode) { - case DHelpers.OUTCODE_TOP: + case MarlinConst.OUTCODE_TOP: stack.push(off); // top return; - case DHelpers.OUTCODE_BOTTOM: + case MarlinConst.OUTCODE_BOTTOM: stack.push(off + 1); // bottom return; default: // both TOP / BOTTOM: - if ((outcode0 & DHelpers.OUTCODE_TOP) != 0) { + if ((outcode0 & MarlinConst.OUTCODE_TOP) != 0) { // top to bottom stack.push(off); // top stack.push(off + 1); // bottom @@ -666,9 +684,12 @@ public void curveTo(final double x1, final double y1, int sideCode = outcode0 & outcode3; - if (sideCode != 0) { + if (sideCode == 0) { + this.gOutCode = 0; + } else { sideCode &= DHelpers.outcode(x1, y1, clipRect); sideCode &= DHelpers.outcode(x2, y2, clipRect); + this.gOutCode &= sideCode; // basic rejection criteria: if (sideCode != 0) { @@ -682,7 +703,6 @@ public void curveTo(final double x1, final double y1, } } if (outside) { - this.outside = false; finish(); } // clipping disabled: @@ -699,8 +719,11 @@ public void quadTo(final double x1, final double y1, int sideCode = outcode0 & outcode2; - if (outcode2 != 0) { + if (sideCode == 0) { + this.gOutCode = 0; + } else { sideCode &= DHelpers.outcode(x1, y1, clipRect); + this.gOutCode &= sideCode; // basic rejection criteria: if (sideCode != 0) { @@ -714,7 +737,6 @@ public void quadTo(final double x1, final double y1, } } if (outside) { - this.outside = false; finish(); } // clipping disabled: diff --git a/src/main/java/org/marlin/pisces/Helpers.java b/src/main/java/org/marlin/pisces/Helpers.java index ab27c7f..8f01d14 100644 --- a/src/main/java/org/marlin/pisces/Helpers.java +++ b/src/main/java/org/marlin/pisces/Helpers.java @@ -437,13 +437,6 @@ static void subdivideAt(float t, float[] src, int srcoff, // From sun.java2d.loops.GeneralRenderer: - static final int OUTCODE_TOP = 1; - static final int OUTCODE_BOTTOM = 2; - static final int OUTCODE_LEFT = 4; - static final int OUTCODE_RIGHT = 8; - static final int OUTCODE_MASK_T_B = OUTCODE_TOP | OUTCODE_BOTTOM; - static final int OUTCODE_MASK_L_R = OUTCODE_LEFT | OUTCODE_RIGHT; - static int outcode(final float x, final float y, final float[] clipRect) { diff --git a/src/main/java/org/marlin/pisces/MarlinConst.java b/src/main/java/org/marlin/pisces/MarlinConst.java index d2849c1..512a403 100644 --- a/src/main/java/org/marlin/pisces/MarlinConst.java +++ b/src/main/java/org/marlin/pisces/MarlinConst.java @@ -163,4 +163,12 @@ interface MarlinConst { */ public static final int CAP_SQUARE = 2; + // Out codes + static final int OUTCODE_TOP = 1; + static final int OUTCODE_BOTTOM = 2; + static final int OUTCODE_LEFT = 4; + static final int OUTCODE_RIGHT = 8; + static final int OUTCODE_MASK_T_B = OUTCODE_TOP | OUTCODE_BOTTOM; + static final int OUTCODE_MASK_L_R = OUTCODE_LEFT | OUTCODE_RIGHT; + static final int OUTCODE_MASK_T_B_L_R = OUTCODE_MASK_T_B | OUTCODE_MASK_L_R; } diff --git a/src/main/java/org/marlin/pisces/MarlinRenderingEngine.java b/src/main/java/org/marlin/pisces/MarlinRenderingEngine.java index eae2a7f..46170b8 100644 --- a/src/main/java/org/marlin/pisces/MarlinRenderingEngine.java +++ b/src/main/java/org/marlin/pisces/MarlinRenderingEngine.java @@ -93,8 +93,6 @@ abstract PathIterator getNormalizingPathIterator(RendererContext rdrCtx, static final boolean DO_CLIP_FILL = true; - static final boolean SKIP_WINDING_RULE_EVEN_ODD = false; - /** * Public constructor */ @@ -856,21 +854,20 @@ public AATileGenerator getAATileGenerator(Shape s, clip.getWidth(), clip.getHeight(), windingRule); - if (!SKIP_WINDING_RULE_EVEN_ODD) { - PathConsumer2D pc2d = r; + PathConsumer2D pc2d = r; - if (DO_CLIP_FILL && (windingRule == WIND_NON_ZERO) && rdrCtx.doClip) { - if (DO_TRACE_PATH) { - // trace Input: - pc2d = rdrCtx.transformerPC2D.traceInput(pc2d); - } - pc2d = rdrCtx.transformerPC2D.pathClipper(pc2d); + if (DO_CLIP_FILL && (windingRule == WIND_NON_ZERO) && rdrCtx.doClip) { + if (DO_TRACE_PATH) { + // trace Input: + pc2d = rdrCtx.transformerPC2D.traceInput(pc2d); } - - // TODO: subdivide quad/cubic curves into monotonic curves ? - pathTo(rdrCtx, pi, pc2d); + pc2d = rdrCtx.transformerPC2D.pathClipper(pc2d); } - } else { + + // TODO: subdivide quad/cubic curves into monotonic curves ? + pathTo(rdrCtx, pi, pc2d); + + } else { // draw shape with given stroke: r = rdrCtx.renderer.init(clip.getLoX(), clip.getLoY(), clip.getWidth(), clip.getHeight(), diff --git a/src/main/java/org/marlin/pisces/MarlinTileGenerator.java b/src/main/java/org/marlin/pisces/MarlinTileGenerator.java index e621738..2759358 100644 --- a/src/main/java/org/marlin/pisces/MarlinTileGenerator.java +++ b/src/main/java/org/marlin/pisces/MarlinTileGenerator.java @@ -32,6 +32,8 @@ final class MarlinTileGenerator implements AATileGenerator, MarlinConst { + private static final boolean DISABLE_BLEND = false; + private static final int MAX_TILE_ALPHA_SUM = TILE_W * TILE_H * MAX_AA_ALPHA; private static final int TH_AA_ALPHA_FILL_EMPTY = ((MAX_AA_ALPHA + 1) / 3); // 33% @@ -142,6 +144,10 @@ public int getTileHeight() { */ @Override public int getTypicalAlpha() { + if (DISABLE_BLEND) { + // always return empty tiles to disable blending operations + return 0x00; + } int al = cache.alphaSumInTile(x); // Note: if we have a filled rectangle that doesn't end on a tile // border, we could still return 0xff, even though al!=maxTileAlphaSum diff --git a/src/main/java/org/marlin/pisces/TransformingPathConsumer2D.java b/src/main/java/org/marlin/pisces/TransformingPathConsumer2D.java index e7d94c9..94bc58e 100644 --- a/src/main/java/org/marlin/pisces/TransformingPathConsumer2D.java +++ b/src/main/java/org/marlin/pisces/TransformingPathConsumer2D.java @@ -496,6 +496,9 @@ static final class PathClipFilter implements PathConsumer2D { // the current outcode of the current sub path private int cOutCode = 0; + // the cumulated (and) outcode of the complete path + private int gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; + private boolean outside = false; private float cx0, cy0; @@ -525,7 +528,8 @@ PathClipFilter init(final PathConsumer2D out) { _clipRect[2] -= margin - rdrOffX; _clipRect[3] += margin + rdrOffX; - init_corners = true; + this.init_corners = true; + this.gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; return this; // fluent API } @@ -540,6 +544,8 @@ void dispose() { @Override public void pathDone() { + finishPath(); + out.pathDone(); // TODO: fix possible leak if exception happened @@ -549,35 +555,45 @@ public void pathDone() { @Override public void closePath() { - if (outside) { - this.outside = false; + finishPath(); - if (sOutCode == 0) { + out.closePath(); + this.cOutCode = sOutCode; + } + + private void finishPath() { + if (outside) { + // criteria: inside or totally outside ? + if (gOutCode == 0) { finish(); } else { + this.outside = false; stack.reset(); } } - out.closePath(); - this.cOutCode = sOutCode; } private void finish() { + this.outside = false; + if (!stack.isEmpty()) { if (init_corners) { init_corners = false; + + final float[] _corners = corners; + final float[] _clipRect = clipRect; // Top Left (0): - corners[0] = clipRect[2]; - corners[1] = clipRect[0]; + _corners[0] = _clipRect[2]; + _corners[1] = _clipRect[0]; // Bottom Left (1): - corners[2] = clipRect[2]; - corners[3] = clipRect[1]; + _corners[2] = _clipRect[2]; + _corners[3] = _clipRect[1]; // Top right (2): - corners[4] = clipRect[3]; - corners[5] = clipRect[0]; + _corners[4] = _clipRect[3]; + _corners[5] = _clipRect[0]; // Bottom Right (3): - corners[6] = clipRect[3]; - corners[7] = clipRect[1]; + _corners[6] = _clipRect[3]; + _corners[7] = _clipRect[1]; } stack.pullAll(corners, out); } @@ -602,7 +618,10 @@ public void lineTo(final float xe, final float ye) { final int sideCode = (outcode0 & outcode1); // basic rejection criteria: - if (sideCode != 0) { + if (sideCode == 0) { + this.gOutCode = 0; + } else { + this.gOutCode &= sideCode; // keep last point coordinate before entering the clip again: this.outside = true; this.cx0 = xe; @@ -612,7 +631,6 @@ public void lineTo(final float xe, final float ye) { return; } if (outside) { - this.outside = false; finish(); } // clipping disabled: @@ -625,25 +643,25 @@ private void clip(final int sideCode, { // corner or cross-boundary on left or right side: if ((outcode0 != outcode1) - && ((sideCode & DHelpers.OUTCODE_MASK_T_B) != 0)) + && ((sideCode & MarlinConst.OUTCODE_MASK_T_B) != 0)) { // combine outcodes: final int mergeCode = (outcode0 | outcode1); - final int tbCode = mergeCode & DHelpers.OUTCODE_MASK_T_B; - final int lrCode = mergeCode & DHelpers.OUTCODE_MASK_L_R; - // add corners to outside stack: - final int off = (lrCode == DHelpers.OUTCODE_LEFT) ? 0 : 2; + final int tbCode = mergeCode & MarlinConst.OUTCODE_MASK_T_B; + final int lrCode = mergeCode & MarlinConst.OUTCODE_MASK_L_R; + final int off = (lrCode == MarlinConst.OUTCODE_LEFT) ? 0 : 2; + // add corners to outside stack: switch (tbCode) { - case DHelpers.OUTCODE_TOP: + case MarlinConst.OUTCODE_TOP: stack.push(off); // top return; - case DHelpers.OUTCODE_BOTTOM: + case MarlinConst.OUTCODE_BOTTOM: stack.push(off + 1); // bottom return; default: // both TOP / BOTTOM: - if ((outcode0 & DHelpers.OUTCODE_TOP) != 0) { + if ((outcode0 & MarlinConst.OUTCODE_TOP) != 0) { // top to bottom stack.push(off); // top stack.push(off + 1); // bottom @@ -667,9 +685,12 @@ public void curveTo(final float x1, final float y1, int sideCode = outcode0 & outcode3; - if (sideCode != 0) { + if (sideCode == 0) { + this.gOutCode = 0; + } else { sideCode &= Helpers.outcode(x1, y1, clipRect); sideCode &= Helpers.outcode(x2, y2, clipRect); + this.gOutCode &= sideCode; // basic rejection criteria: if (sideCode != 0) { @@ -683,7 +704,6 @@ public void curveTo(final float x1, final float y1, } } if (outside) { - this.outside = false; finish(); } // clipping disabled: @@ -700,8 +720,11 @@ public void quadTo(final float x1, final float y1, int sideCode = outcode0 & outcode2; - if (outcode2 != 0) { + if (sideCode == 0) { + this.gOutCode = 0; + } else { sideCode &= Helpers.outcode(x1, y1, clipRect); + this.gOutCode &= sideCode; // basic rejection criteria: if (sideCode != 0) { @@ -715,7 +738,6 @@ public void quadTo(final float x1, final float y1, } } if (outside) { - this.outside = false; finish(); } // clipping disabled: diff --git a/src/main/java/test/FillClipBugTest.java b/src/main/java/test/FillClipBugTest.java new file mode 100644 index 0000000..92eb113 --- /dev/null +++ b/src/main/java/test/FillClipBugTest.java @@ -0,0 +1,110 @@ +package test; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.Path2D; +import java.awt.image.BufferedImage; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.border.LineBorder; + +public class FillClipBugTest { + + public static void main(String[] args) { + + // First display which renderer is tested: + // JDK9 only: + System.setProperty("sun.java2d.renderer.verbose", "true"); + System.out.println("Testing renderer: "); + // Other JDK: + String renderer = "undefined"; + try { + renderer = sun.java2d.pipe.RenderingEngine.getInstance().getClass().getName(); + System.out.println(renderer); + } catch (Throwable th) { + // may fail with JDK9 jigsaw (jake) + if (false) { + System.err.println("Unable to get RenderingEngine.getInstance()"); + th.printStackTrace(); + } + } + + JFrame frame = new JFrame("JFrame Example"); + JPanel panel = new MyPanel(); + panel.setPreferredSize(new Dimension(MyPanel.max, MyPanel.max)); + panel.setBorder(new LineBorder(Color.BLUE)); + frame.add(panel); + frame.setLocationRelativeTo(null); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + frame.pack(); + frame.setVisible(true); + } + + static class MyPanel extends JPanel { + + final static int max = 250 - 1; + final static int decalX = 200; + final static int decalY = 200; + + private Path2D.Double rect1 = createRectOutsideBounds(400, 300); + private Path2D.Double rect2 = createRectOverBounds(); + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + final int w = getWidth(); + final int h = getHeight(); + + final BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); + final Graphics2D g2d = (Graphics2D) bi.getGraphics(); + + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, w, h); + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g2d.setColor(Color.blue); + g2d.fill(rect1); + + g2d.setColor(Color.red); + g2d.fill(rect2); + + g.drawImage(bi, 0, 0, this); + + g2d.dispose(); + } + + private static Path2D.Double createRectOutsideBounds(final int w, final int h) { + final Path2D.Double shape = new Path2D.Double(); + shape.moveTo(-decalX, -decalY); + shape.lineTo(w + decalX, -decalY); // go right + shape.lineTo(w + decalX, h + decalY); // go down + shape.lineTo(-decalX, h + decalY); // go left + + if (false) { + shape.closePath(); + } + return shape; + } + + private static Path2D.Double createRectOverBounds() { + final Path2D.Double shape = new Path2D.Double(); + shape.moveTo(0 + decalX, 50 + decalY); + shape.lineTo(0 + decalX, 0 + decalY); // go up + shape.lineTo(50 + decalX, 0 + decalY); // go right + shape.lineTo(50 + decalX, 50 + decalY); // go down + shape.lineTo(0 + decalX, 50 + decalY); // go left + + if (false) { + shape.closePath(); + } + return shape; + } + } + +}