weisJ / jsvg

Java SVG renderer
MIT License
139 stars 8 forks source link

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

Closed stanio closed 5 months ago

stanio commented 8 months ago
clip-n-mask.svg: ```xml ``` ```java 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
weisJ commented 8 months ago

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.

stanio commented 8 months ago

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: ```xml ```
Actual Expected
gradient-actual gradient-expected
weisJ commented 8 months ago

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>
stanio commented 8 months ago

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.

stanio commented 8 months ago

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.

weisJ commented 8 months ago

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.

weisJ commented 5 months ago

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.

stanio commented 5 months ago

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.

weisJ commented 5 months ago

Version 1.5.0 has been released