drawsvg
drawsvg copied to clipboard
resize page to drawing feature?
Is it possible to add a feature that shrinks the canvas so that it's just big enough to hold the objects in it?
I was thinking of a workflow of something like this:
d = draw.Drawing(200, 100, origin='center', displayInline=False)
d.append(some stuff)
d.append(some more stuff)
d.shrink_to_fit()
svg_data = d.to_svg()
The ability to initialize the drawing's width and height to None and have the canvas size be autocalculated on output would be cool:
d = draw.Drawing(None, None, origin='center', displayInline=False)
This would solve a problem I have now where I draw a bunch of geometry, then I rotate it and now it's annoying to figure out how big the canvas should be to just fit everything in.
This could be solved for simple geometry but I don't think drawSvg is well suited to solve this for many cases. The main problem is you are allowed to add arbitrary SVG attributes that this package doesn't understand (like transform=""
and probably other features I'm unaware of) that change the size and position of shapes. A "fit page to geometry" feature would be better suited to a higher-level library with a complete understanding of the geometry it represents. (I've started thinking about higher-level representations in https://github.com/cduck/hyperbolic but there are likely better libraries out there for this.)
Are there specific use cases you think could be solved well in drawSvg? Maybe shrink-to-fit a specific top-level Rectangle
or Lines
with only a transform attribute?
Maybe you're right that this must be solved at a higher level. But maybe also my canvas size calculation problem can be solved by understanding the SVG spec better. Do you know if there's any way around having to specify those fixed x and y canvas dimensions at the beginning?
As for solving this at a higher level, I'm trying that route too.
Right now, I'm sending the bytes I get from .to_svg()
to Rsvg like that:
svg_handle = Rsvg.Handle.new_from_data(d.asSvg().encode())
The problem I have is that the rsvg drawing output I get always seems to have the x and y dims that I give to draw.Drawing(
on initialization. Rsvg has some tools that I think might be useful for me. I can do svg_handle.get_geometry_for_element(None)
and I think that can give me the outer limits for arbitrary transformations on drawing objects that I might do... but I still don't yet have a complete working solution.
You have a good point that an SVG rendering library could be used to determine the bounding box. After a quick search, I found the cairosvg.bounding_box
module that might be helpful. CairoSVG is already a dependency of drawSvg so this could be a good way to implement a shrink-to-fit method. If you find a good solution, I would definitely add this to the package.
Hm. Not really so useful maybe? It doesn't seem to understand rotations.
$ cat bbtest.py
import cairosvg
import drawSvg as draw
d = draw.Drawing(1000, 1000, origin='center', displayInline=False)
angle = 45
rot = f"rotate({angle},0,0)"
g = draw.Group(**{"transform":rot})
g.append(draw.Rectangle(0,0,40,50))
d.append(g)
t = cairosvg.parser.Tree(bytestring=d.asSvg().encode())
s = cairosvg.surface.SVGSurface(t, None, 96)
get_bbg = cairosvg.bounding_box.bounding_box_group
print(get_bbg(s, t))
$ python bbtest.py
(0.0, -50.0, 40.0, 50.0)
I do get a useful result from Rsvg though:
$ cat bbtest_rsvg.py
import gi
gi.require_version('Rsvg', '2.0')
from gi.repository import Rsvg
import drawSvg as draw
d = draw.Drawing(1000, 1000, origin='center', displayInline=False)
angle = 45
rot = f"rotate({angle},0,0)"
g = draw.Group(**{"transform":rot})
g.append(draw.Rectangle(0,0,40,50))
d.append(g)
svgh = Rsvg.Handle.new_from_data(d.asSvg().encode())
vb = svgh.get_intrinsic_dimensions().out_viewbox
r = svgh.get_geometry_for_layer(None, vb).out_ink_rect
print(f"Width = {r.width}, Height = {r.height}")
$ python bbtest_rsvg.py
Width = 127.28125, Height = 127.28125
It looks like Cairo can do this too but it may not be supported in CairoSVG. https://pycairo.readthedocs.io/en/latest/reference/surfaces.html#class-recordingsurface-surface https://stackoverflow.com/questions/53838231/cairo-pdf-bounding-box
Solution using Cairo
Now you got me interested in the problem... Here is a mostly complete solution. The constants may need to be tweaked to get good results for different drawing sizes.
import cairosvg, cairocffi
import drawSvg as draw
# Contribute this to CairoSVG? This wrapper was missing.
class RecordingSurface(cairosvg.surface.Surface):
"""A surface that records draw commands."""
def _create_surface(self, width, height):
cairo_surface = cairocffi.RecordingSurface(
cairocffi.CONTENT_COLOR_ALPHA, None)
return cairo_surface, width, height
def get_bounding_box(d, pad=0, resolution=1/256, max_size=10000):
rbox = (-max_size, -max_size, 2*max_size, 2*max_size)
# Hack, add an argument to asSvg instead
svg_lines = d.asSvg().split('\n')
svg_lines[2] = f'viewBox="{rbox[0]}, {rbox[1]}, {rbox[2]}, {rbox[3]}">'
svg_code = '\n'.join(svg_lines)
t = cairosvg.parser.Tree(bytestring=svg_code)
s = RecordingSurface(t, None, 72, scale=1/resolution)
b = s.cairo.ink_extents()
return (
rbox[0] + b[0]*resolution - pad,
-(rbox[1]+b[1]*resolution)-b[3]*resolution - pad,
b[2]*resolution + pad*2,
b[3]*resolution + pad*2,
)
def fit_to_contents(d, pad=0, resolution=1/256, max_size=10000):
bb = get_bounding_box(d, pad=pad, resolution=resolution, max_size=max_size)
d.viewBox = (bb[0], -bb[1]-bb[3], bb[2], bb[3])
d.width, d.height = bb[2], bb[3]
# Debug: Draw bounding rectangle
d.append(draw.Rectangle(*bb, fill='none', stroke_width=2,
stroke='red', stroke_dasharray='5 2'))
d = draw.Drawing(0, 0, displayInline=False)
angle = 35
rot = f"rotate({angle},0,0)"
d.append(draw.Rectangle(0,0,40,70,transform=rot), z=1)
fit_to_contents(d, pad=0)
d
I'm a little dissappointed that all the solutions we have look like they require the vector art to be rendered and then something comes along and counts some pixels in the rendered image. This means all the results we get are approximations which depend on the details of the how the rasterizaion was done. I was kind of hoping for a pure math/geometry solution, but I think maybe there is nothing that keeps track of the geometry to be able to answer the geometry extents quesion for us.
That said, I think having the ability to auto-size the canvas to the content is a cool feature to add even though it might be an approximation!
Would you still like to add the feature to do the auto-resize automatically? It should be easy to replace the current resize solution if you find a better one in the future.
auto-resize automatically
I don't think any of this should be automatic. I'd say the user should need to express their desire to have this type of auto canvas sizing done for them somehow. explicitly calling fit_to_contents
and/or some special way to initializing the Drawing.
I'd say pretty much exactly what you have a few comments up would already be quite useful, though the only thing I'm a bit unsure of is that 72
you have in there.
I agree the user should express it. I liked your idea of doing it automatically when the user doesn't specify dimensions (or maybe a special flag like 'auto') when creating the drawing.
The 72 is to cancel out a unit conversion CairoSVG does for some reason.
The solution proposed by @greyltc using Rsvg
produces a very wrong (too small) bounding box for my SVG, and the solution proposed by @cduck using cairosvg
took so long that I didn't even wait for it to finish.
I appreciate the feedback @shrx. I currently have no plans to work on a feature like this unless someone finds a more accurate and faster method.