aggdraw 1.3.9 API change?
karimbahgat opened this issue · comments
I recently upgraded my old legacy aggdraw to the newest 1.3.9 PyPI version produced by this project (super happy about that and the precompiled wheels btw, a simple pip install did the trick).
One thing I noticed was that my existing code using the Draw.path() method failed. It seems that in this new version, the args to path() have switched from what was previously:
Draw.path(xy, pathobj, pen, brush)
...to the new 1.3.9:
Draw.path(pathobj, xy, (pen, brush))
The path() docstring still refers to the old way of doing it. Was this API change intentional / is it documented anywhere, or was it an inadvertent mistake from some of the recent AGG version portings?
@karimbahgat Do you have a full example that shows some error message? As far as I can tell from the code the interface hasn't changed: https://github.com/pytroll/aggdraw/blob/master/aggdraw.cxx#L1355
Seems like I was mistaken about the brush and pen being bundled together, but still seems like the first xy arg was dropped from the API. The function takes max 3 args now instead of 4. Here's what happens to me on aggdraw 1.3.9 (from PyPI) in Python 2.7.
First set it up:
import aggdraw
import PIL, PIL.Image
print aggdraw.__version__, aggdraw
im = PIL.Image.new('RGB', (100,100), 'green')
d = aggdraw.Draw(im)
Define the path:
poly = [(0,0),(80,10),(80,80)]
brush, pen = aggdraw.Brush('red'), None
path = aggdraw.Path()
path.moveto(*poly[0])
for p in poly:
path.lineto(*p)
path.close()
Now let's try adding the path using the conventional args:
d.path((0,0), path, brush, pen)
Giving us this error:
Traceback (most recent call last):
File "C:/Users/kimok/Desktop/testagg.py", line 16, in
d.path((0,0), path, brush, pen)
TypeError: path() takes at most 3 arguments (4 given)
There's no args hints for this function in interactive mode, and the docstring still lists the old 4 args, so I just had to trial and error what the correct args were. If we drop the first xy arg it works and shows it drawed correctly:
d.path(path, brush, pen)
d.flush()
im.show()
Could you try this code and see what you get?
You are correct that it seems the definition changed, but this happened quite a while ago (2015).
This was added to one of the old forks of aggdraw by @ejeschke (c7cac6c#diff-ee3b67c4a4d123b356ce520be532b34c) which was actually copied from Alex Toney with https://github.com/bluemoon/crayons/blob/master/ext/aggdraw/patches/03-aggdraw-path-fix.patch. But Alex doesn't seem to have an active github account anymore (can't click on it).
Drawing seems like it was originally based on draw_symbol
(Draw.symbol and Draw.path use to point to the same draw_symbol
in the C++ code). I copied the documentation for it based on the old docs here. However, it looks like both draw_symbol
(Draw.symbol) and draw_path
(Draw.path) have been changed to use the coordinates from the Path object provided to them and have removed the "xy" argument.
So I would say this issue marks a need to update the documentation on my part, but I don't think I/we will be moving back to the API as, from what I can tell, it seems redundant to pass x/y and a Path...right?
Note: I just researched all of this over the last 15 minutes so I could have missed something.
👍 for just updating the doc.
I see, that makes sense because until recently I've only used the old stable pypi version from 2005.
But it seems this change only affected path(), symbol() still follows the old API using xy as the first arg. Try:
sym = ''
sym += 'M%s,%s ' % poly[0]
for p in poly[1:]:
sym += 'L%s,%s ' % p
sym = aggdraw.Symbol(sym)
d.symbol((0,0), path, brush, pen)
In terms of what should be the ideal API I'm not sure, but I think there was a use for the xy arg. Specifically if your symbol or path only used relative coordinates, you could construct some type of marker once (say, a cross or a triangle), and then place however many you like at different xy locations. In fact, it seems it even works with absolute coordinates (with slightly different results), but the end result always being that the xy coordinates is used in a sense to offset the path or symbol you're drawing. This should be faster (but have not tested) than reconstructing the same symbols/paths over and over again.
Try:
from random import randrange
for _ in range(100):
d.symbol((randrange(0,100),randrange(0,100)), sym, brush, pen)
Also seems like the error message for symbol() is wrong: if you give it the wrong second arg, it says the second arg should be a Path object (rather than Symbol). It seems to accept and be able to handle both Path and Symbol objects (with correct results).
@karimbahgat Interesting. Looks like internally the Symbol object is actually just a Path object which is why this works: https://github.com/pytroll/aggdraw/blob/master/aggdraw.cxx#L2098-L2105
Creating a symbol is creating a Path with some extra handling of some of the operators.
As for the (x, y) coordinates, I'm not an aggdraw expert (I maintain it, I don't use it heavily) but is it possible to use the moveto
and rmoveto
to accomplish something similar? You'd still end up doing a for loop in python and modifying the original Path object, but it should allow for the same/similar functionality.
If you'd like a feature like this I'd suggest adding a separate method to Draw that allows it and making a pull request. If I had to guess I would say the aggdraw API was changed to more closely match the C++ interfaces. I'm not saying that is the best approach, but I think there are pros and cons to both.
I'm curious if @dov has any opinion on this. I didn't realize that symbols still took the x/y coordinates. It looks like the logic was originally commented out for draw_path
, but I'm not sure if it should be supported. It may have been a copy/paste error on an experimental feature or an unfinished implementation.
Couldn't we take the for loop from https://github.com/pytroll/aggdraw/blob/master/aggdraw.cxx#L1403-L1410 and apply it here https://github.com/pytroll/aggdraw/blob/master/aggdraw.cxx#L1359-L1364?
@djhoese I'm not really sure what you are asking but a symbol is indeed just a path . In draw_symbol
this path is being copied and pasted at multiple xy-locations. There is obviously no reason why the draw_path
method could not receive a symbol as well and then paste it at each vertex. But why? Isn't it just as easy to call both of these method in succession, and let each of them keep their simpler semantics?
What would be nice though, is if draw_symbol in addition of accepting a path (agg::path_storage
), would also be overloaded to also accept a agg::svg::path_renderer
type. The we can load svg files (symbols) into aggdraw and paste them in multiple locations.
Ok so you're saying it makes sense that draw_path
doesn't take x/y locations because you could pass the path to draw_symbol instead? Or rather make a symbol from a path and then pass that to draw_symbol...right?
Sorry, none of my code uses paths.
Right. I think it makes sense that draw_path
doesn't take x,y locations. It's drawing a path, duh..., and a path contains its own x,y locations. draw_symbol
on the other hand treats a predefined path as a symbol, and stamps it the list of x,y locations. I think the use cases for these two methods are different enough to justify having them separate. Again, a symbol, could be abstracted to be richer than just a path, and then reside in its own symbol class. This would probably make these functions easier to understand.
Yes, noticed the same thing. In fact, it appears every drawing operation from ellipse to rectangle and polygon, actually end up just creating a Path object in the C++ code.
Regarding the xy coordinates, as someone who uses paths and symbols often (eg only way to create polygons with holes), here are some of the benefits I have noticed of the xy arg:
- Creating multiple Path objects with different starting coordinates, or using multiple moveto() on the same Path object seems to have some overhead to it. Based on some simple testing I did, multiple Path creation can be ca 35% slower than simply creating one symbol and passing it multiple times with draw.symbol().
- Another use of the xy arg is that it can take a series of xy coordinates and draw multiple symbols in one call (as you pointed out too), which is fast if all the symbols are to be drawn with the same colors. In my testing this was a total of 80% or almost twice as fast as creating multiple Paths. This seems to be limited to symbol() right now.
Regarding symbol vs path, I always thought their main difference was how you specified the coordinates (SVG string for Symbol, and method calls for Path).
Not sure I fully understand how Symbol is inherintly more independent than Path, could you elaborate? They all have the same drawing primitives, eg moveto, curveto, relative coordinates, etc.
But I agree and could see the benefit of changing them towards and making it clearer that they have different use cases.
Maybe since the main benefit of the xy arg is speed, we could instead of it add some type of Batch class and .batch() method that simply collects a series of drawing instructions + pens/brushes and then draws them all in one efficient sweep. At a later date ofcourse and as a separate issue.
As the maintainer and based on this discussion, I think I will:
- keep the interface as is for now
- update the docstring for
Draw.path
(draw_path) to reflect x/y not being passed - Release as 1.3.10
- Merge #50
- Play with it in master for a while (a week or two?)
- Release 1.4 (not sure it needs a 2.0 since aggdraw hasn't changed, just agg)
- Change interfaces if needed, but prefer to add new ones or add keyword arguments. Lean towards taking advantage of existing agg interfaces, if any, for specifying multiple paths/symbols.
- Release 1.5.
- Replace .cxx with cython.
- Test performance, binary sizes, etc.
- Release 2.0
Let's imagine that 9 and 10 aren't going to take a year to complete...but probably will. @mraspaud thoughts?
@karimbahgat wrote:
Not sure I fully understand how Symbol is inherintly more independent than Path, could you elaborate? They all have the same drawing primitives, eg moveto, curveto, relative coordinates, etc.
The main difference is that a path only describes geometry, but a symbol can contain a whole "drawing", i.e. paths, colors, strokes, gradients, etc. Of course if the symbol is more than a path, then the brush and the pen should be ignored in draw_symbol()
. This actually makes me think that the draw_symbol()
should not take the pen and brush at all, but instead there should be a function like Symbol(path=path,pen=pen,brush=brush)
that creates a "symbol" from a path.
That's pretty interesting, so the SVG-style symbol string can also implement colors, gradients, etc? Is that implemented yet in aggdraw or just in the SVG spec?
@djhoese regarding point 2, maybe also time to remove the "Experimental" notes for symbol(), Symbol(), path(), and Path()? Or maybe they're actually fitting now that they may be changed around..
@karimbahgat wrote:
That's pretty interesting, so the SVG-style symbol string can also implement colors, gradients, etc? Is that implemented yet in aggdraw or just in the SVG spec?
SVG can contain anything you can draw with an SVG editor, e.g. inkscape . There is no such thing as an SVG symbol, but only SVG documents. There is an SVG path though, which is a sub language within the SVG spec.
Currently there is no support for svg in aggdraw, but it should be pretty straightforward to implement. I have used it in my C++ Qt interactive image, vector, and svg, viewer widget http://github.com/dov/qviv (which is badly lacking documentation...).
Well aggdraw does have limited support for SVG "path strings", which is what the Symbol() class does, parsing them and turning them into Path objects. But as far as I understand SVG "path strings", and therefore aggdraw.symbol(), they only specify coordinates/geometry and not eg multiple colors within the same path string, right?
When you say you would like to see full SVG support, do you mean full support of parsing and drawing an entire SVG XML-style document/string, including changing colors, gradients, etc?
Maybe since the main benefit of the xy arg is speed, we could instead of it add some type of Batch class and .batch() method that simply collects a series of drawing instructions + pens/brushes and then draws them all in one efficient sweep. At a later date ofcourse and as a separate issue.
Yes, IMO something like this is badly needed. I have played around with modifying the code to be able to draw a list of objects. This speeded up my python use cases tremendously.
At first I was looking to do something like draw an array of ellipses with a single pen and brush, but with separate coordinates. But really it would be much nicer to be able to specify different attributes for each (color, fill, etc). This got me wondering if this should take the form of passing in matching arrays of pens, brushes, etc.
In the end of this thought process, I'm kind of convinced it would be the most generally useful to be able to pass in a disparate list of objects with attributes. So this would be like a batch operation, where you could have polygons, circles, ellipses, etc all in a large argument with different pen/brush attributes, and it would render the whole thing in one go. Since almost everything is converted into a path internally anyway, this is probably not such a big task.
@ejeschke Thanks for the input. I think you make some really good points. Your mention of arrays and the way you describe this really makes me want to migrate to a cython interface where it would be much easier to take advantage of numpy arrays. It also makes me think we should have a real interface for this (versus some hacked together thing) like a "collections" set of classes for each object type. I have some experience with this working on the vispy library (OpenGL wrapper). Making these types of interfaces would also benefit from the higher level cython and/or python interface rather than trying to code it in C++.
Absolutely agree regarding batch operations, would be hugely helpful and shouldn't in theory be difficult to implement. Probably good to raise this as a separate issue for future discussions.
So I finally got a chance to look at this tonight and found something interesting that I think others should know if they don't already: it doesn't matter (right now) which order you specify the brush or the pen when drawing objects. The low-level draw
function checks which is which.
I noticed this after noticing that the docstring from the original aggdraw docs didn't match the PyArgs_ParseTuple
variables. So...interesting, but doesn't mean there is anything to change right now. Might be something to straighten out in future interface changes (explicit keyword arguments).