drawsvg icon indicating copy to clipboard operation
drawsvg copied to clipboard

resize page to drawing feature?

Open greyltc opened this issue 4 years ago • 15 comments

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)

greyltc avatar Oct 07 '20 17:10 greyltc

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.

greyltc avatar Oct 07 '20 17:10 greyltc

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?

cduck avatar Oct 07 '20 17:10 cduck

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.

greyltc avatar Oct 07 '20 18:10 greyltc

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.

cduck avatar Oct 07 '20 19:10 cduck

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)

greyltc avatar Oct 07 '20 20:10 greyltc

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

greyltc avatar Oct 07 '20 20:10 greyltc

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

cduck avatar Oct 07 '20 21:10 cduck

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

image

cduck avatar Oct 08 '20 02:10 cduck

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.

greyltc avatar Oct 10 '20 15:10 greyltc

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!

greyltc avatar Oct 10 '20 16:10 greyltc

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.

cduck avatar Oct 10 '20 17:10 cduck

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.

greyltc avatar Oct 10 '20 18:10 greyltc

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.

cduck avatar Oct 10 '20 18:10 cduck

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.

shrx avatar Jun 04 '23 15:06 shrx

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.

cduck avatar Jun 06 '23 00:06 cduck