Skip to content

Commit

Permalink
Implement soft clipping for clip-path
Browse files Browse the repository at this point in the history
For now this isn't by default and has to be enabled using
SVGRenderingHints.KEY_SOFT_CLIPPING. Once I am certain the implementation
behaves nicely it will be enabled based on the value of regular antialiasing
rendering hint.
  • Loading branch information
weisJ committed Feb 12, 2024
1 parent 6e4bdec commit 347d1e9
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* MIT License
*
* Copyright (c) 2021-2023 Jannis Weis
* Copyright (c) 2021-2024 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
Expand Down Expand Up @@ -29,11 +29,16 @@ public final class SVGRenderingHints {
private SVGRenderingHints() {}

private static final int P_KEY_IMAGE_ANTIALIASING = 1;
private static final int P_KEY_SOFT_CLIPPING = 2;

public static final RenderingHints.Key KEY_IMAGE_ANTIALIASING = new Key(P_KEY_IMAGE_ANTIALIASING);
public static final Object VALUE_IMAGE_ANTIALIASING_ON = Value.ON;
public static final Object VALUE_IMAGE_ANTIALIASING_OFF = Value.OFF;

public static final RenderingHints.Key KEY_SOFT_CLIPPING = new Key(P_KEY_SOFT_CLIPPING);
public static final Object VALUE_SOFT_CLIPPING_ON = Value.ON;
public static final Object VALUE_SOFT_CLIPPING_OFF = Value.OFF;

private static final class Key extends RenderingHints.Key {
/**
* Construct a key using the indicated private key. Each
Expand Down
42 changes: 37 additions & 5 deletions jsvg/src/main/java/com/github/weisj/jsvg/nodes/ClipPath.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* MIT License
*
* Copyright (c) 2021-2023 Jannis Weis
* Copyright (c) 2021-2024 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
Expand All @@ -22,20 +22,26 @@
package com.github.weisj.jsvg.nodes;

import java.awt.*;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.awt.geom.*;

import org.jetbrains.annotations.NotNull;

import com.github.weisj.jsvg.attributes.UnitType;
import com.github.weisj.jsvg.attributes.paint.PaintParser;
import com.github.weisj.jsvg.geometry.util.GeometryUtil;
import com.github.weisj.jsvg.nodes.container.ContainerNode;
import com.github.weisj.jsvg.nodes.prototype.ShapedContainer;
import com.github.weisj.jsvg.nodes.prototype.spec.Category;
import com.github.weisj.jsvg.nodes.prototype.spec.ElementCategories;
import com.github.weisj.jsvg.nodes.prototype.spec.PermittedContent;
import com.github.weisj.jsvg.nodes.text.Text;
import com.github.weisj.jsvg.parser.AttributeNode;
import com.github.weisj.jsvg.renderer.MaskedPaint;
import com.github.weisj.jsvg.renderer.Output;
import com.github.weisj.jsvg.renderer.RenderContext;
import com.github.weisj.jsvg.util.BlittableImage;
import com.github.weisj.jsvg.util.ImageUtil;
import com.github.weisj.jsvg.util.ShapeUtil;

@ElementCategories({/* None */})
@PermittedContent(
Expand Down Expand Up @@ -76,10 +82,11 @@ private boolean checkIsValid() {
return true;
}

public @NotNull Shape clipShape(@NotNull RenderContext context, @NotNull Rectangle2D elementBounds) {
public @NotNull Shape clipShape(@NotNull RenderContext context, @NotNull Rectangle2D elementBounds,
boolean useSoftClip) {
// Todo: Handle bounding-box stuff as well (i.e. combined stroke etc.)
Shape shape = ShapedContainer.super.elementShape(context);
if (clipPathUnits == UnitType.ObjectBoundingBox) {
if (!useSoftClip && clipPathUnits == UnitType.ObjectBoundingBox) {
shape = clipPathUnits.viewTransform(elementBounds).createTransformedShape(shape);
}
Area areaShape = new Area(shape);
Expand All @@ -88,4 +95,29 @@ private boolean checkIsValid() {
}
return areaShape;
}

public @NotNull Paint createPaintForSoftClipping(@NotNull Output output, @NotNull RenderContext context,
@NotNull Rectangle2D objectBounds, @NotNull Shape clipShape) {
Rectangle2D transformedClipBounds = GeometryUtil.containingBoundsAfterTransform(
clipPathUnits.viewTransform(objectBounds),
clipShape.getBounds());
BlittableImage blitImage = BlittableImage.create(
ImageUtil::createLuminosityBuffer, context, output.clipBounds(),
transformedClipBounds, objectBounds, clipPathUnits);
Rectangle2D clipBoundsInUserSpace = blitImage.boundsInUserSpace();

if (ShapeUtil.isInvalidArea(clipBoundsInUserSpace)) return PaintParser.DEFAULT_COLOR;

blitImage.render(output, g -> {
g.setColor(Color.BLACK);
g.fillRect(0, 0, blitImage.image().getWidth(), blitImage.image().getHeight());
g.setColor(Color.WHITE);
g.fill(clipShape);
});

Point2D offset = new Point2D.Double(clipBoundsInUserSpace.getX(), clipBoundsInUserSpace.getY());
context.rootTransform().transform(offset, offset);

return new MaskedPaint(PaintParser.DEFAULT_COLOR, blitImage.image().getRaster(), offset);
}
}
8 changes: 2 additions & 6 deletions jsvg/src/main/java/com/github/weisj/jsvg/nodes/Mask.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.github.weisj.jsvg.renderer.RenderContext;
import com.github.weisj.jsvg.util.BlittableImage;
import com.github.weisj.jsvg.util.ImageUtil;
import com.github.weisj.jsvg.util.ShapeUtil;

@ElementCategories(Category.Container)
@PermittedContent(
Expand Down Expand Up @@ -95,11 +96,10 @@ public void build(@NotNull AttributeNode attributeNode) {
maskBounds.createIntersection(objectBounds), objectBounds, maskContentUnits);
Rectangle2D maskBoundsInUserSpace = blitImage.boundsInUserSpace();

if (isInvalidMaskingArea(maskBoundsInUserSpace)) return PaintParser.DEFAULT_COLOR;
if (ShapeUtil.isInvalidArea(maskBoundsInUserSpace)) return PaintParser.DEFAULT_COLOR;

blitImage.renderNode(output, this, this);


if (DEBUG) {
output.debugPaint(g -> {
g.setComposite(AlphaComposite.SrcOver.derive(0.5f));
Expand All @@ -112,10 +112,6 @@ public void build(@NotNull AttributeNode attributeNode) {
return new MaskedPaint(PaintParser.DEFAULT_COLOR, blitImage.image().getRaster(), offset);
}

private boolean isInvalidMaskingArea(@NotNull Rectangle2D area) {
return area.isEmpty() || Double.isNaN(area.getWidth()) || Double.isNaN(area.getHeight());
}

@Override
public boolean requiresInstantiation() {
return true;
Expand Down
30 changes: 20 additions & 10 deletions jsvg/src/main/java/com/github/weisj/jsvg/renderer/NodeRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,27 @@ public static void renderNode(@NotNull SVGNode node, @NotNull RenderContext cont
if (!childClip.isValid()) return null;
if (elementBounds == null) elementBounds = elementBounds(renderable, childContext);

Shape childClipShape = childClip.clipShape(childContext, elementBounds);

if (CLIP_DEBUG) {
childOutput.debugPaint(g -> {
g.setClip(null);
g.setPaint(Color.MAGENTA);
g.draw(childClipShape);
});
if (output.isSoftClippingEnabled()) {
if (!elementBounds.isEmpty()) {
Shape childClipShape = childClip.clipShape(childContext, elementBounds, true);

Rectangle2D bounds = elementBounds;
childOutput.setPaint(() -> childClip.createPaintForSoftClipping(
childOutput, childContext, bounds, childClipShape));
}
} else {
Shape childClipShape = childClip.clipShape(childContext, elementBounds, false);

if (CLIP_DEBUG) {
childOutput.debugPaint(g -> {
g.setClip(null);
g.setPaint(Color.MAGENTA);
g.draw(childClipShape);
});
}

childOutput.applyClip(childClipShape);
}

childOutput.applyClip(childClipShape);
}
}

Expand Down
5 changes: 5 additions & 0 deletions jsvg/src/main/java/com/github/weisj/jsvg/renderer/Output.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import com.github.weisj.jsvg.SVGRenderingHints;
import com.github.weisj.jsvg.util.Provider;

public interface Output {
Expand Down Expand Up @@ -107,6 +108,10 @@ public interface Output {

boolean supportsColors();

default boolean isSoftClippingEnabled() {
return renderingHint(SVGRenderingHints.KEY_SOFT_CLIPPING) == SVGRenderingHints.VALUE_SOFT_CLIPPING_ON;
}

interface SafeState {
void restore();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ public boolean supportsColors() {
return false;
}

@Override
public boolean isSoftClippingEnabled() {
// Not needed here. Always return false
return false;
}

private static class ShapeOutputSafeState implements SafeState {
private final @NotNull ShapeOutput shapeOutput;
private final @NotNull Stroke oldStroke;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.function.Consumer;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -146,6 +147,13 @@ public void renderNode(@NotNull Output parentOutput, @NotNull SVGNode node,
imgGraphics.dispose();
}

public void render(@NotNull Output output, @NotNull Consumer<Graphics2D> painter) {
Graphics2D imgGraphics = createGraphics();
imgGraphics.setRenderingHints(output.renderingHints());
painter.accept(imgGraphics);
imgGraphics.dispose();
}

public void prepareForBlitting(@NotNull Output output, @NotNull RenderContext parentContext) {
output.setTransform(parentContext.rootTransform());
output.translate(boundsInUserSpace.getX(), boundsInUserSpace.getY());
Expand Down
4 changes: 4 additions & 0 deletions jsvg/src/main/java/com/github/weisj/jsvg/util/ShapeUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public final class ShapeUtil {

private ShapeUtil() {}

public static boolean isInvalidArea(@NotNull Rectangle2D area) {
return area.isEmpty() || Double.isNaN(area.getWidth()) || Double.isNaN(area.getHeight());
}

/*
* Intersect two Shapes by the simplest method, attempting to produce a simplified result. The
* boolean arguments keep1 and keep2 specify whether or not the first or second shapes can be
Expand Down
9 changes: 8 additions & 1 deletion jsvg/src/test/java/com/github/weisj/jsvg/ClipPathTest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* MIT License
*
* Copyright (c) 2021-2022 Jannis Weis
* Copyright (c) 2021-2024 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
Expand Down Expand Up @@ -33,4 +33,11 @@ class ClipPathTest {
void tetClipPathUnits() {
assertEquals(SUCCESS, compareImages("clipPathUnits.svg"));
}

@Test
void tetClipPathUnitsSoftClip() {
ReferenceTest.SOFT_CLIPPING_VALUE = SVGRenderingHints.VALUE_SOFT_CLIPPING_ON;
assertEquals(SUCCESS, compareImages("clipPathUnits.svg"));
ReferenceTest.SOFT_CLIPPING_VALUE = SVGRenderingHints.VALUE_SOFT_CLIPPING_OFF;
}
}
4 changes: 3 additions & 1 deletion jsvg/src/test/java/com/github/weisj/jsvg/ReferenceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
public final class ReferenceTest {

private static final double DEFAULT_TOLERANCE = 0.5;
public static Object SOFT_CLIPPING_VALUE = SVGRenderingHints.VALUE_SOFT_CLIPPING_OFF;

@Test
void testIcons() {
Expand Down Expand Up @@ -163,7 +164,8 @@ public static BufferedImage render(@NotNull String path) {
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON,
RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE,
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON,
RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY,
SVGRenderingHints.KEY_SOFT_CLIPPING, SOFT_CLIPPING_VALUE));
}

private static BufferedImage render(@NotNull InputStream inputStream) {
Expand Down
17 changes: 16 additions & 1 deletion jsvg/src/test/java/com/github/weisj/jsvg/SVGViewer.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static void main(String[] args) {
JFrame frame = new JFrame("SVGViewer");

JComboBox<String> iconBox = new JComboBox<>(new DefaultComboBoxModel<>(findIcons()));
iconBox.setSelectedItem("test.svg");
iconBox.setSelectedItem("tmp.svg");

SVGPanel svgPanel = new SVGPanel((String) Objects.requireNonNull(iconBox.getSelectedItem()));
svgPanel.setPreferredSize(new Dimension(1000, 600));
Expand Down Expand Up @@ -85,6 +85,11 @@ public static void main(String[] args) {
JCheckBox paintShape = new JCheckBox("Paint SVG shape");
paintShape.addActionListener(e -> svgPanel.setPaintSVGShape(paintShape.isSelected()));
renderingMode.add(paintShape);
renderingMode.add(Box.createHorizontalStrut(5));

JCheckBox softClipping = new JCheckBox("Soft clipping");
softClipping.addActionListener(e -> svgPanel.setSoftClipping(softClipping.isSelected()));
renderingMode.add(softClipping);
renderingMode.add(Box.createHorizontalGlue());

JButton resourceInfo = new JButton("Print Memory");
Expand Down Expand Up @@ -135,6 +140,7 @@ public int getIconWidthIgnoreAutosize() {
};
private final JSVGCanvas jsvgCanvas = new JSVGCanvas();
private boolean paintShape;
private boolean softClipping;

public SVGPanel(@NotNull String iconName) {
selectIcon(iconName);
Expand Down Expand Up @@ -200,6 +206,11 @@ public void setPaintSVGShape(boolean paintShape) {
repaint();
}

public void setSoftClipping(boolean softClipping) {
this.softClipping = softClipping;
repaint();
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Expand All @@ -211,6 +222,10 @@ protected void paintComponent(Graphics g) {
switch (mode) {
case JSVG:
ViewBox viewBox = new ViewBox(0, 0, getWidth(), getHeight());
((Graphics2D) g).setRenderingHint(
SVGRenderingHints.KEY_SOFT_CLIPPING,
softClipping ? SVGRenderingHints.VALUE_SOFT_CLIPPING_ON
: SVGRenderingHints.VALUE_SOFT_CLIPPING_OFF);
if (paintShape) {
Shape shape = document.computeShape(viewBox);
g.setColor(Color.MAGENTA);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 347d1e9

Please sign in to comment.