deeplook / svglib

Read SVG files and convert them to other formats.
GNU Lesser General Public License v3.0
323 stars 79 forks source link

SVG with clipPath forces stroked border #238

Closed donkirkby closed 4 years ago

donkirkby commented 4 years ago

Thanks for publishing this library, it's very helpful for including matplotlib plots in PDF reports.

I've run into trouble when my SVG needs to be clipped to its bounding rectangle. I'd like to just clip it, but it also draws the the bounding rectangle.

Here's a small example of what I'm trying to do. The circle is drawn half outside the top boundary of the SVG, so I want to see a semicircle.

import svgwrite
from io import BytesIO

from reportlab.platypus import SimpleDocTemplate
from svglib.svglib import svg2rlg

def go():
    doc = SimpleDocTemplate("example.pdf")
    story = []
    svg_drawing = svgwrite.Drawing(size=("400px", "200px"))
    clip_path = svg_drawing.defs.add(svg_drawing.clipPath(id='border_clip'))
    clip_path.add(svg_drawing.rect(size=(400, 200), fill='red', stroke='blue'))
    svg_drawing.add(svg_drawing.circle((200, 0),
                                       60,
                                       clip_path='url(#border_clip)'))
    svg_text = svg_drawing.tostring()
    svg_bytes = svg_text.encode()
    pdf_drawing = svg2rlg(BytesIO(svg_bytes))

    story.append(pdf_drawing)
    doc.build(story)

go()

The PDF looks like this:

Screenshot from 2020-03-28 14-31-38

Here's what I want the PDF to look like, without drawing the bounding rectangle:

Screenshot from 2020-03-28 14-48-16

Analysis

I did some investigation, and the problem appears to be that Canvas.clipPath() is receiving stroke=1. That tells it to stroke the clip path. It's hard for me to tell exactly how that stroke value is decided, but it seems to be left as the default when calling convertRect() on the rectangle in the clipping path. Perhaps it should copy the stroke from the rectangle instead of using the default. As you can see in my snippet, I set the stroke, but it was ignored.

Workaround

It's ugly, but I worked around this by monkeypatching Canvas.clipPath(). That just changes it from always drawing the clip path to never drawing it. Some users might want to choose the stroke for each clip path.

Here's my example with the monkeypatch. It produces the second PDF, above.

import svgwrite
from io import BytesIO
from reportlab.pdfgen.canvas import Canvas

from reportlab.platypus import SimpleDocTemplate
from svglib.svglib import svg2rlg

original_clip_path = Canvas.clipPath

# noinspection PyUnusedLocal,PyPep8Naming
def patched_clip_path(canvas,
                      aPath,
                      stroke=1,
                      fill=0,
                      fillMode=None):
    stroke = 0
    # noinspection PyArgumentList
    return original_clip_path(canvas, aPath, stroke, fill, fillMode)

def go():
    Canvas.clipPath = patched_clip_path
    doc = SimpleDocTemplate("example.pdf")
    story = []
    svg_drawing = svgwrite.Drawing(size=("400px", "200px"))
    clip_path = svg_drawing.defs.add(svg_drawing.clipPath(id='border_clip'))
    clip_path.add(svg_drawing.rect(size=(400, 200), fill='red', stroke='blue'))
    svg_drawing.add(svg_drawing.circle((200, 0),
                                       60,
                                       clip_path='url(#border_clip)'))
    svg_text = svg_drawing.tostring()
    svg_bytes = svg_text.encode()
    pdf_drawing = svg2rlg(BytesIO(svg_bytes))

    story.append(pdf_drawing)
    doc.build(story)

go()

I'm using these versions: