Anti-aliased `clip-path` and `mask` edges show tint of covered objects
stanio opened this issue · 9 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 |
---|---|
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 |
---|---|
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 applyfilter=dummy
on the same element which has themask
orclip-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