PS backend wish list

Here is my near term wish list for the PS backend:

    > - implement draw_markers and draw_lines with the new API
    > (transform is done in backend). There are comments in
    > backend_bases and in backend_ps to get you started

    > I started looking into this tonight, but I am pretty much
    > lost. The comments are a little too abstract for me right
    > now, I cant find a footing. Could you offer some more
    > details?

Sure, maybe more than you had bargained for <wink>. I'm CC-ing the
dev list in case any of this information is useful to others. [BTW,
Darren is tentatively offering to take on some of the work to keep the
PS backend up to snuff]

There are several motivations to change backend renderer API, most of
them based on limitations or inefficiencies of the current API

  * The renderer interface is based on the GTK drawing model, which
    doesn't have a path concept, and is thus a bit behind most drawing
    APIs: ps, pdf, svg, cairo, agg, libart, etc...

  * Once you have a draw path method, many of the other methods
    (draw_rectangle, draw_polygon) become superfluous since they are
    just special cases of draw_path. [ There is some debate about
    whether it is useful to keep these redundant methods around for
    efficiency or convenience. ]

  * Many backends (svg, ps, agg) have transformation support built-in
    (at least for affine transformations). I initially did the
    transformations in the front-end for convenience to backend
    writers (backends always work in display coords) but this caused
    several problems, inefficiency being one, and the new API moves
    the transformation to the backend. Among other things, it allows
    the backend to fail gracefully when transforming on a per-element
    basis (log of non-positive data) w/o a mask or w/o an extra pass
    through the data. For large numbers of points, the savings can be
    appreciable. So the new backend methods are passed a
    Transformation instance.

  * We needed a draw_markers method. draw_markers is a special case
    where the same path is repeatedly drawn at many places. In the
    old API, we would do something like this for draw_plus in the
    Line2D class

            for (x,y) in zip(xt, yt):
                renderer.draw_line(gc, x-offset, y, x+offset, y)
                renderer.draw_line(gc, x, y-offset, x, y+offset)

    This is enormously inefficient, because of all the extra function
    calls and because of all the gc state setting that must be done on
    each call to draw_line in the inner loop. In the new API, we do

            path = agg.path_storage()
            path.move_to(-offset, 0)
            path.line_to( offset, 0)
            path.move_to( 0, -offset)
            path.line_to( 0, offset)
            renderer.draw_markers(gc, path, None, xt, yt, self._transform)

    and the backend only has to set the gc state once. Also, agg can
    cache the rasterized path and display it at many locations which
    is fast.

So those are the motivations. There are three new methods that have
been introduced thus far. The plan is introduce these three new
methods and then remove many of the redundant methods, so the overall
number of renderer methods will decrease.

  draw_markers - draw the same path at many locations
  draw_path - draw an agg path (details later)
  draw_lines - already exists but new method has trans in backend

The signatures of these three methods are

  draw_markers(self, gc, path, rgbFace, x, y, trans):
  draw_path(self, gc, rgbFace, path, trans)
  draw_lines(self, gc, x, y, trans)

These should be documented in backend_bases, but gc is a backend
GraphicsContext, rgbFace is an rgbTuple or None, x and y are numerix
arrays, path is an agg.path_storage and trans is a
matplotlib.transforms.Transformation instance. Details on these
latter two to follow.

path is an agg.path_storage instance. In the first implementation of
draw_markers in backend_ps, path was simply a list of (code
vertices...) where code was one of STOP, MOVETO, LINETO, CURVE3,
CURVE4, ENDPOLY and vertices were a bunch of x,y verts. I
subsequently decided to just use the agg path class for this (wrapped
by SWIG) because it is more generally useful (the code in backend_ps
_draw_markers is thus stale). Here is a script that illustrates the
path_storage class from matplotlib.agg import path_storage

  p = path_storage()
  p.move_to(10,10)
  p.line_rel(100,100)
  p.line_rel(0,-100)
  p.line_to(30,30)
  p.curve3(20,30,40,50)

  for i in range(p.total_vertices()):
      cmd, x, y = p.vertex(i)
      print cmd, x, y

This script outputs

  peds-pc311:~/python/projects/matplotlib/unit> python path_storage.py
  1 10.0 10.0
  2 110.0 110.0
  2 110.0 10.0
  2 30.0 30.0
  3 20.0 30.0
  3 40.0 50.0

Note that there are more vertices than commands used to create the
path, because there are two vertices generated by the curve3 call.
The 1,2,3 command codes are from an agg ENUM, and are found in
agg22/include/agg_basics.h

    enum path_commands_e
    {
        path_cmd_stop = 0, //----path_cmd_stop
        path_cmd_move_to = 1, //----path_cmd_move_to
        path_cmd_line_to = 2, //----path_cmd_line_to
        path_cmd_curve3 = 3, //----path_cmd_curve3
        path_cmd_curve4 = 4, //----path_cmd_curve4
        path_cmd_end_poly = 6, //----path_cmd_end_poly
        path_cmd_mask = 0x0F //----path_cmd_mask
    };

See agg22/include/agg_basics.h, agg22/include/agg_path_storage.h and
swig/agg_path_storage.i for more information on available methods of
the agg path_storage class.

You will need to translate these path primitives into the basic
postscript moveto, lineto, etc commands. For the curve3 you would use
a cubic spline. I don't know if postscript has a quartic spline...

The Transformation class is fairly well documented in transforms.py
and in the _draw_markers prototype method I wrote in backend_ps. Here
is an example usage

        if trans.need_nonlinear():
            x,y = trans.nonlinear_only_numerix(x, y)

        # the a,b,c,d,tx,ty affine which transforms x and y
        vec6 = trans.as_vec6_val()

vec6 is a standard length 6 vector containing the information needed
to make an affine transformation. Note the call to
transform.nonlinear_only_numerix(x, y) can fail (eg log of nonpositive
data). I may provide some helper function in extension code to
support this. What you want is a function that returns the
transformed data with a mask indicating the points to be skipped. I
suggest you not worry about this right now -- if the transformation
fails because the user has illegal data that is OK for the time being.
It is easier in the agg extension code because I to the transformation
element-by-element in a c++ loop and drop points on which the
transformation fails. This would probably be prohibitively slow in
python.

Note that I hid the _draw_markers prototype method in backend_ps with
a prefix underscore because it is incomplete and because I am using
the existence of that method in Line2D as a sentinel for whether a
backend as implemented the new API. For example, in lines.py

  self._newstyle = hasattr(renderer, 'draw_markers')

So once you implement draw_markers, you need to implement draw_lines
with the new signature. draw_path isn't utilized yet by the
front-end, but it will be nice to expose a path primitive for people
who want to make splines, etc.

I'll try and take this email and turn it into something more formal,
or use it to rewrite backend_bases and backend_template. So far, the
only backend besides agg to be ported to the new API is cairo -- I
guess as long as the old API is still working there is little
incentive to do it. I've been holding off *requiring* the new API
because it would irreparably break some backends that don't support
paths (gtk, wx, gd). Some of these (gtk, wx) have been essential for
some people because they support unicode. But now that agg and ps
support unicode, this is no longer so important. We can also provide
a helper method that converts simple paths (those comprised of moveto,
lineto and endpoly) into draw_line and draw_polygon methods if we want
to keep these backends on board. Also, Steve thinks GTK may be
getting paths in the near future as they move to a cairo renderer,
which suggests that waiting may be the right move.

OK, that should be enough to get you started. Sorry for the
incomplete set of documentation or guidelines. There has been a lot
of discussion on where the backends should be going, and since I've
been mulling all the options I've been slow to offer clear guidance in
the backend documentation. I think your first objective should be to
figure out how to translate an agg.path_storage into a postscript
path -- the rest should be easy :slight_smile:

Let me know if you have any more questions!

JDH

[..snip..]

I made a first (and second) attempt at implementing draw_markers and
draw_lines in the postscript backend. The changes are in CVS, although I left
draw_markers masked as _draw_markers, it needs to be unmasked if you want to
try it out.

I found some places for speed/memory/ps-filesize improvements. With
draw_markers masked, the script below took 2.43 seconds to generate and write
the 1.5MB eps file. With draw_markers unmasked, it took 0.69 seconds to make
a 350KB eps file.

Some comments:

1) Circles are being drawn with draw_markers, but agg.path_storage has no
curve information in it? Circles are faithfully reproduced in ps output, but
it takes 50 line segments to draw each circle in plot(arange(10000),'-o').

2) I think each tickmark is listed in agg.path_storage twice, and therefore
gets rendered twice in PS.

3) I expected marker paths to be terminated with the agg.path_cmd_end_poly
code. This is not the case. What is the purpose of path_cmd_end_poly?

4) I am getting an unrecognized agg.path_commands_e code. They should be one
of 0,1,2,3,4,6,0x0F, and I am getting a value of 70. ?? I just ignore it and
PS seems to render fine.

5) Im not doing anything with vec6 = transform.as_vec6_val(). I'm not sure
what it is used for.

6) draw_lines is getting a long pathlist from agg. Rather than draw a straight
line between two points, it is doing something like

50.106 249.850 moveto
53.826 249.850 lineto
57.546 249.850 lineto
61.266 249.850 lineto

and thats just for the line in the legend! The straight line in the actual
plot has many, many intermediate points.

Feedback appreciated!

from pylab import *
from time import clock

figure(1)
plot(arange(10000),'-s')
l=legend(('1e4 markers',))
d = clock()
savefig('temp.eps')
print clock()-d

···

On Wednesday 30 March 2005 10:39 pm, John Hunter wrote:

    > - implement draw_markers and draw_lines with the new API
    > (transform is done in backend).

--

Darren