weisJ / jsvg

Java SVG renderer

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Anti-aliased `clip-path` and `mask` edges show tint of covered objects

stanio opened this issue · comments

clip-n-mask.svg:
<svg xmlns="http://www.w3.org/2000/svg"
    width="100" height="100" viewBox="0 0 100 100">

  <defs>
    <mask id="myMask" maskContentUnits="objectBoundingBox"
        x="-50%" y="-50%" width="200%" height="200%">
      <rect x="-0.5" y="-0.5" width="2" height="2" fill="white" />
      <circle cx="0.5" cy="0.5" r="0.4" fill="black" />
    </mask>
    <clipPath id="myClip" clipPathUnits="objectBoundingBox"
        shape-rendering="geometricPrecision">
      <path d="M -0.5,-0.5 h 2 v 2 h -2 z
               M 0.5,0.1 a 0.4,0.4 0 0 0 0,0.8
                         a 0.4,0.4 0 0 0 0,-0.8 z" />
    </clipPath>
  </defs>

  <g clip-path="url(#myClip)">
    <rect x="10" y="10" width="50" height="50" fill="red" />
    <rect x="10" y="10" width="80" height="80"
        fill="white" stroke="black" stroke-width="2" />
  </g>

  <rect x="40" y="40" width="50" height="50" fill="white" mask="url(#myMask)"
      stroke="blue" stroke-width="16" paint-order="stroke fill" />

</svg>
import java.io.File;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Dimension2D;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;

import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.SVGRenderingHints;
import com.github.weisj.jsvg.parser.SVGLoader;

public class JSVGTest {

    static boolean softClip = true;
    static boolean whiteBkg = true;

    public static void main(String[] args) throws Exception {
        String inputName = "clip-n-mask";
        SVGDocument svg = new SVGLoader()
                .load(new File(inputName + ".svg").toURI().toURL());
        Dimension2D size = svg.size();
        BufferedImage image = new BufferedImage((int) size.getWidth(),
                                                (int) size.getHeight(),
                                                whiteBkg ? BufferedImage.TYPE_INT_RGB
                                                         : BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_RENDERING,
                           RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                           RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                           RenderingHints.VALUE_STROKE_PURE);
        if (softClip) {
            g.setRenderingHint(SVGRenderingHints.KEY_SOFT_CLIPPING,
                               SVGRenderingHints.VALUE_SOFT_CLIPPING_ON);
        }
        if (whiteBkg) {
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, image.getWidth(), image.getHeight());
        }
        svg.render(null, g);
        g.dispose();
        ImageIO.write(image, "png", new File(inputName + ".png"));
        System.out.println("Done.");
    }

}

The anti-aliased edges of clip-path and mask regions on groups of stacked objects, or single shapes with paint-order="stroke fill", "bleed" / "shine-through" tint of otherwise fully covered objects:

Actual Expected
aa-bleed expected

There really is no easy fix for the first case as it is very difficult to track which parts of an element are actually obstructed by another. I have pushed a fix for the second case. As no additional mask/clip can be introduced between painting the stroke and fill it is relatively easy to detect if the stroke shape needs to be adjusted to alleviate bleeding.

In the first case, and if a mask is applied to the group (vs. clip-path) – shouldn't the group be first fully rendered (that should result in no covered objects shown) before applying the mask?

mask-gradient.svg:
<svg xmlns="http://www.w3.org/2000/svg"
    width="100" height="100" viewBox="0 0 100 100">

  <defs>
    <linearGradient id="myGradient">
      <stop offset="0" stop-color="black" />
      <stop offset="1" stop-color="white" />
    </linearGradient>
    <mask id="myMask" maskContentUnits="objectBoundingBox">
      <rect x="0" y="0" width="1" height="1" fill="url(#myGradient)" />
    </mask>
  </defs>

  <g mask="url(#myMask)">
    <rect x="10" y="10" width="50" height="50" fill="red" />
    <rect x="10" y="10" width="80" height="80"
        fill="white" stroke="black" stroke-width="2" />
  </g>

</svg>
Actual Expected
gradient-actual gradient-expected

Currently masks are implemented in a way that avoids an additional offscreen image, which I want to keep for the cases where the "isolation" isn't necessary (offscreen images are the most expensive part during rendering).
Until I have figured out how to reliably detect when the offscreen image can be avoided the following is a cheap fix to enforce isolation="isolate".

Simply apply filter=dummy on the same element which has the mask or clip-path.

<filter id="dummy">
    <feMerge>
        <feMergeNode in="SourceGraphic" /> 
    </feMerge>
</filter>

cheap fix to enforce isolation="isolate"... Simply apply filter=dummy on the same element which has the mask or clip-path.

Nice – thank you! Just learned about isolation: isolate.

I have always considered SVG masks expensive, and I opt to use clip-path where I need just a "shape mask". In my particular use case, I need to render at smaller sizes/resolutions and I don't expect a noticeable performance hit because of additional offscreen composition, while I need the best possible quality. Thank you, again.

Tried with current 1.5.0-SNAPSHOT, the second case of "stroke below fill" (paint-order="stroke fill") is now good using mask or clip-path, without using an extra filter.

FWIW, just noticed Edge and Firefox exhibit the same issue when using clip-path, but not with mask (likely they eagerly compose offscreen with mask). The extra filter workaround is applicable there, too.

I have noticed this as well. It at least indicates that most SVGs in the wild don’t make use of this feature.

I think for a first solution I will add an SVGRenderingHint.MASK_CLIP_RENDERING with values fast, default=fast and accuracy, wherein accuracy will enforce isolation of the subtree to which the clip/mask is applied to.

of course the accuracy setting could still optimise for the case where a the mask/clip is applied to a single leaf element.

Finally got around to fixing this "properly". You can set SVGRenderingHints.KEY_MASK_CLIP_RENDERING to SVGRenderingHints.VALUE_MASK_CLIP_RENDERING_ACCURACY to enforce the proper isolated behaviour.

It is available in the latest snapshot.

Verified with current 1.5.0-SNAPSHOT and SVGRenderingHints.KEY_MASK_CLIP_RENDERING = SVGRenderingHints.VALUE_MASK_CLIP_RENDERING_ACCURACY (and no extra filter) – the result is now as expected in full.

Version 1.5.0 has been released