Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resize page to drawing feature? #25

Open
greyltc opened this issue Oct 7, 2020 · 15 comments
Open

resize page to drawing feature? #25

greyltc opened this issue Oct 7, 2020 · 15 comments

Comments

@greyltc
Copy link

greyltc commented Oct 7, 2020

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
Copy link
Author

greyltc commented Oct 7, 2020

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.

@cduck
Copy link
Owner

cduck commented Oct 7, 2020

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?

@greyltc
Copy link
Author

greyltc commented Oct 7, 2020

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.

@cduck
Copy link
Owner

cduck commented Oct 7, 2020

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.

@greyltc
Copy link
Author

greyltc commented Oct 7, 2020

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
Copy link
Author

greyltc commented Oct 7, 2020

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

@cduck
Copy link
Owner

cduck commented Oct 7, 2020

@cduck
Copy link
Owner

cduck commented Oct 8, 2020

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

@greyltc
Copy link
Author

greyltc commented Oct 10, 2020

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
Copy link
Author

greyltc commented Oct 10, 2020

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!

@cduck
Copy link
Owner

cduck commented Oct 10, 2020

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.

@greyltc
Copy link
Author

greyltc commented Oct 10, 2020

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.

@cduck
Copy link
Owner

cduck commented Oct 10, 2020

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.

@shrx
Copy link

shrx commented Jun 4, 2023

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.

@cduck
Copy link
Owner

cduck commented Jun 6, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants