diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index e03ab01ade93..9ba4f4e7d9be 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -37,7 +37,6 @@ from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.cbook import Bunch, is_string_like, \ get_realpath_and_stat, is_writable_file_like, maxdict -from matplotlib.mlab import quad2cubic from matplotlib.figure import Figure from matplotlib.font_manager import findfont, is_opentype_cff_font from matplotlib.afm import AFM @@ -48,6 +47,7 @@ from matplotlib.mathtext import MathTextParser from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path +from matplotlib import _path from matplotlib import ttconv # Overview @@ -287,6 +287,17 @@ def __repr__(self): def pdfRepr(self): return self.op + +class Verbatim(object): + """Store verbatim PDF command content for later inclusion in the + stream.""" + def __init__(self, x): + self._x = x + + def pdfRepr(self): + return self._x + + # PDF operators (not an exhaustive list) _pdfops = dict( close_fill_stroke=b'b', fill_stroke=b'B', fill=b'f', closepath=b'h', @@ -1388,32 +1399,11 @@ def writePathCollectionTemplates(self): @staticmethod def pathOperations(path, transform, clip=None, simplify=None, sketch=None): - cmds = [] - last_points = None - for points, code in path.iter_segments(transform, clip=clip, - simplify=simplify, - sketch=sketch): - if code == Path.MOVETO: - # This is allowed anywhere in the path - cmds.extend(points) - cmds.append(Op.moveto) - elif code == Path.CLOSEPOLY: - cmds.append(Op.closepath) - elif last_points is None: - # The other operations require a previous point - raise ValueError('Path lacks initial MOVETO') - elif code == Path.LINETO: - cmds.extend(points) - cmds.append(Op.lineto) - elif code == Path.CURVE3: - points = quad2cubic(*(list(last_points[-2:]) + list(points))) - cmds.extend(points[2:]) - cmds.append(Op.curveto) - elif code == Path.CURVE4: - cmds.extend(points) - cmds.append(Op.curveto) - last_points = points - return cmds + return [Verbatim(_path.convert_to_string( + path, transform, clip, simplify, sketch, + 6, + [Op.moveto.op, Op.lineto.op, b'', Op.curveto.op, Op.closepath.op], + True))] def writePath(self, path, transform, clip=False, sketch=None): if clip: diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 1474d95d9c26..8b1af0aebff5 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -26,7 +26,6 @@ def _fn_name(): return sys._getframe(1).f_code.co_name from matplotlib.cbook import is_string_like, get_realpath_and_stat, \ is_writable_file_like, maxdict, file_requires_unicode -from matplotlib.mlab import quad2cubic from matplotlib.figure import Figure from matplotlib.font_manager import findfont, is_opentype_cff_font @@ -36,6 +35,7 @@ def _fn_name(): return sys._getframe(1).f_code.co_name from matplotlib._mathtext_data import uni2type1 from matplotlib.text import Text from matplotlib.path import Path +from matplotlib import _path from matplotlib.transforms import Affine2D from matplotlib.backends.backend_mixed import MixedModeRenderer @@ -531,27 +531,9 @@ def _convert_path(self, path, transform, clip=False, simplify=None): self.height * 72.0) else: clip = None - for points, code in path.iter_segments(transform, clip=clip, - simplify=simplify): - if code == Path.MOVETO: - ps.append("%g %g m" % tuple(points)) - elif code == Path.CLOSEPOLY: - ps.append("cl") - elif last_points is None: - # The other operations require a previous point - raise ValueError('Path lacks initial MOVETO') - elif code == Path.LINETO: - ps.append("%g %g l" % tuple(points)) - elif code == Path.CURVE3: - points = quad2cubic(*(list(last_points[-2:]) + list(points))) - ps.append("%g %g %g %g %g %g c" % - tuple(points[2:])) - elif code == Path.CURVE4: - ps.append("%g %g %g %g %g %g c" % tuple(points)) - last_points = points - - ps = "\n".join(ps) - return ps + return _path.convert_to_string( + path, transform, clip, simplify, None, + 6, [b'm', b'l', b'', b'c', b'cl'], True).decode('ascii') def _get_clip_path(self, clippath, clippath_transform): key = (clippath, id(clippath_transform)) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 865acf66620e..53e7176cff96 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -538,7 +538,9 @@ def _convert_path(self, path, transform=None, clip=None, simplify=None): clip = (0.0, 0.0, self.width, self.height) else: clip = None - return _path.convert_to_svg(path, transform, clip, simplify, 6) + return _path.convert_to_string( + path, transform, clip, simplify, None, 6, + [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii') def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index 2baedbb952aa..af2aba90e86f 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -4007,6 +4007,8 @@ def quad2cubic(q0x, q0y, q1x, q1y, q2x, q2y): points of a quadratic curve, and the output is a tuple of *x* and *y* coordinates of the four control points of the cubic curve. """ + # TODO: Candidate for deprecation -- no longer used internally + # c0x, c0y = q0x, q0y c1x, c1y = q0x + 2./3. * (q1x - q0x), q0y + 2./3. * (q1y - q0y) c2x, c2y = c1x + 1./3. * (q2x - q0x), c1y + 1./3. * (q2y - q0y) diff --git a/src/_path.h b/src/_path.h index 52873cedcf29..c7b0d683d4f6 100644 --- a/src/_path.h +++ b/src/_path.h @@ -939,79 +939,166 @@ void cleanup_path(PathIterator &path, } } +void quad2cubic(double x0, double y0, + double x1, double y1, + double x2, double y2, + double *outx, double *outy) +{ + + outx[0] = x0 + 2./3. * (x1 - x0); + outy[0] = y0 + 2./3. * (y1 - y0); + outx[1] = outx[0] + 1./3. * (x2 - x0); + outy[1] = outy[0] + 1./3. * (y2 - y0); + outx[2] = x2; + outy[2] = y2; +} + +char *__append_to_string(char *p, char *buffer, size_t buffersize, + const char *content) +{ + int buffersize_int = (int)buffersize; + + for (const char *i = content; *i; ++i) { + if (p < buffer || p - buffer >= buffersize_int) { + return NULL; + } + + *p++ = *i; + } + + return p; +} + template -void convert_to_svg(PathIterator &path, - agg::trans_affine &trans, - agg::rect_d &clip_rect, - bool simplify, - int precision, - char *buffer, - size_t *buffersize) +int __convert_to_string(PathIterator &path, + int precision, + char **codes, + bool postfix, + char *buffer, + size_t *buffersize) { #if PY_VERSION_HEX < 0x02070000 char format[64]; snprintf(format, 64, "%s.%dg", "%", precision); #endif - typedef agg::conv_transform transformed_path_t; - typedef PathNanRemover nan_removal_t; - typedef PathClipper clipped_t; - typedef PathSimplifier simplify_t; - - bool do_clip = (clip_rect.x1 < clip_rect.x2 && clip_rect.y1 < clip_rect.y2); - - transformed_path_t tpath(path, trans); - nan_removal_t nan_removed(tpath, true, path.has_curves()); - clipped_t clipped(nan_removed, do_clip, clip_rect); - simplify_t simplified(clipped, simplify, path.simplify_threshold()); - char *p = buffer; + double x[3]; + double y[3]; + double last_x = 0.0; + double last_y = 0.0; - const char codes[] = { 'M', 'L', 'Q', 'C' }; - const int waits[] = { 1, 1, 2, 3 }; - - int wait = 0; + const int sizes[] = { 1, 1, 2, 3 }; + int size = 0; unsigned code; - double x = 0, y = 0; - while ((code = simplified.vertex(&x, &y)) != agg::path_cmd_stop) { - if (wait == 0) { - *p++ = '\n'; - if (code == 0x4f) { - *p++ = 'z'; - *p++ = '\n'; - continue; + while ((code = path.vertex(&x[0], &y[0])) != agg::path_cmd_stop) { + if (code == 0x4f) { + if ((p = __append_to_string(p, buffer, *buffersize, codes[4])) == NULL) return 1; + } else if (code < 5) { + size = sizes[code - 1]; + + for (int i = 1; i < size; ++i) { + unsigned subcode = path.vertex(&x[i], &y[i]); + if (subcode != code) { + return 2; + } } - *p++ = codes[code - 1]; - wait = waits[code - 1]; - } else { - *p++ = ' '; - } + /* For formats that don't support quad curves, convert to + cubic curves */ + if (code == CURVE3 && codes[code - 1][0] == '\0') { + quad2cubic(last_x, last_y, x[0], y[0], x[1], y[1], x, y); + code++; + size = 3; + } + if (!postfix) { + if ((p = __append_to_string(p, buffer, *buffersize, codes[code - 1])) == NULL) return 1; + if ((p = __append_to_string(p, buffer, *buffersize, " ")) == NULL) return 1; + } + + for (int i = 0; i < size; ++i) { #if PY_VERSION_HEX >= 0x02070000 - char *str; - str = PyOS_double_to_string(x, 'g', precision, 0, NULL); - p += snprintf(p, *buffersize - (p - buffer), "%s", str); - PyMem_Free(str); - *p++ = ' '; - str = PyOS_double_to_string(y, 'g', precision, 0, NULL); - p += snprintf(p, *buffersize - (p - buffer), "%s", str); - PyMem_Free(str); + char *str; + str = PyOS_double_to_string(x[i], 'g', precision, 0, NULL); + if ((p = __append_to_string(p, buffer, *buffersize, str)) == NULL) { + PyMem_Free(str); + return 1; + } + PyMem_Free(str); + if ((p = __append_to_string(p, buffer, *buffersize, " ")) == NULL) return 1; + str = PyOS_double_to_string(y[i], 'g', precision, 0, NULL); + if ((p = __append_to_string(p, buffer, *buffersize, str)) == NULL) { + PyMem_Free(str); + return 1; + } + PyMem_Free(str); + if ((p = __append_to_string(p, buffer, *buffersize, " ")) == NULL) return 1; #else - char str[64]; - PyOS_ascii_formatd(str, 64, format, x); - p += snprintf(p, *buffersize - (p - buffer), "%s", str); - *p++ = ' '; - PyOS_ascii_formatd(str, 64, format, y); - p += snprintf(p, *buffersize - (p - buffer), "%s", str); + char str[64]; + PyOS_ascii_formatd(str, 64, format, x[i]); + if ((p = __append_to_string(p, buffer, *buffersize, str)) == NULL) return 1; + p = __append_to_string(p, buffer, *buffersize, " "); + PyOS_ascii_formatd(str, 64, format, y[i]); + if ((p = __append_to_string(p, buffer, *buffersize, str)) == NULL) return 1; + if ((p = __append_to_string(p, buffer, *buffersize, " ")) == NULL) return 1; #endif + } + + if (postfix) { + if ((p = __append_to_string(p, buffer, *buffersize, codes[code - 1])) == NULL) return 1; + } + + last_x = x[size - 1]; + last_y = y[size - 1]; + } else { + // Unknown code value + return 2; + } - --wait; + if ((p = __append_to_string(p, buffer, *buffersize, "\n")) == NULL) return 1; } - *p = '\0'; *buffersize = p - buffer; + + return 0; +} + +template +int convert_to_string(PathIterator &path, + agg::trans_affine &trans, + agg::rect_d &clip_rect, + bool simplify, + SketchParams sketch_params, + int precision, + char **codes, + bool postfix, + char *buffer, + size_t *buffersize) +{ + typedef agg::conv_transform transformed_path_t; + typedef PathNanRemover nan_removal_t; + typedef PathClipper clipped_t; + typedef PathSimplifier simplify_t; + typedef agg::conv_curve curve_t; + typedef Sketch sketch_t; + + bool do_clip = (clip_rect.x1 < clip_rect.x2 && clip_rect.y1 < clip_rect.y2); + + transformed_path_t tpath(path, trans); + nan_removal_t nan_removed(tpath, true, path.has_curves()); + clipped_t clipped(nan_removed, do_clip, clip_rect); + simplify_t simplified(clipped, simplify, path.simplify_threshold()); + + if (sketch_params.scale == 0.0) { + return __convert_to_string(simplified, precision, codes, postfix, buffer, buffersize); + } else { + curve_t curve(simplified); + sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness); + return __convert_to_string(sketch, precision, codes, postfix, buffer, buffersize); + } + } #endif diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index bc27ec081d47..c7ce991e94ea 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -613,19 +613,25 @@ static PyObject *Py_cleanup_path(PyObject *self, PyObject *args, PyObject *kwds) return Py_BuildValue("NN", pyvertices.pyobj(), pycodes.pyobj()); } -const char *Py_convert_to_svg__doc__ = "convert_to_svg(path, trans, cliprect, simplify, precision)"; +const char *Py_convert_to_string__doc__ = "convert_to_string(path, trans, " + "clip_rect, simplify, sketch, precision, codes, postfix)"; -static PyObject *Py_convert_to_svg(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_convert_to_string(PyObject *self, PyObject *args, PyObject *kwds) { py::PathIterator path; agg::trans_affine trans; agg::rect_d cliprect; PyObject *simplifyobj; bool simplify = false; + SketchParams sketch; int precision; + PyObject *codesobj; + char *codes[5]; + int postfix; + int status; if (!PyArg_ParseTuple(args, - "O&O&O&Oi:convert_to_svg", + "O&O&O&OO&iOi:convert_to_string", &convert_path, &path, &convert_trans_affine, @@ -633,7 +639,11 @@ static PyObject *Py_convert_to_svg(PyObject *self, PyObject *args, PyObject *kwd &convert_rect, &cliprect, &simplifyobj, - &precision)) { + &convert_sketch_params, + &sketch, + &precision, + &codesobj, + &postfix)) { return NULL; } @@ -643,14 +653,58 @@ static PyObject *Py_convert_to_svg(PyObject *self, PyObject *args, PyObject *kwd simplify = true; } + if (!PySequence_Check(codesobj)) { + return NULL; + } + if (PySequence_Size(codesobj) != 5) { + PyErr_SetString( + PyExc_ValueError, + "codes must be a 5-length sequence of byte strings"); + return NULL; + } + for (int i = 0; i < 5; ++i) { + PyObject *item = PySequence_GetItem(codesobj, i); + if (item == NULL) { + return NULL; + } + codes[i] = PyBytes_AsString(item); + if (codes[i] == NULL) { + return NULL; + } + } + size_t buffersize = path.total_vertices() * (precision + 5) * 4; - std::string buffer; - buffer.reserve(buffersize); + if (buffersize == 0) { + return PyBytes_FromString(""); + } + + PyObject *bufferobj = PyBytes_FromStringAndSize(NULL, buffersize); + if (bufferobj == NULL) { + return NULL; + } + char *buffer = PyBytes_AsString(bufferobj); + + CALL_CPP("convert_to_string", + (status = convert_to_string( + path, trans, cliprect, simplify, sketch, + precision, codes, (bool)postfix, buffer, + &buffersize))); - CALL_CPP("convert_to_svg", - (convert_to_svg(path, trans, cliprect, simplify, precision, &buffer[0], &buffersize))); + if (status) { + Py_DECREF(bufferobj); + if (status == 1) { + PyErr_SetString(PyExc_MemoryError, "Buffer overflow"); + } else if (status == 2) { + PyErr_SetString(PyExc_ValueError, "Malformed path codes"); + } + return NULL; + } + + if (_PyBytes_Resize(&bufferobj, buffersize)) { + return NULL; + } - return PyUnicode_DecodeASCII(&buffer[0], buffersize, ""); + return bufferobj; } extern "C" { @@ -671,7 +725,7 @@ extern "C" { {"path_intersects_path", (PyCFunction)Py_path_intersects_path, METH_VARARGS|METH_KEYWORDS, Py_path_intersects_path__doc__}, {"convert_path_to_polygons", (PyCFunction)Py_convert_path_to_polygons, METH_VARARGS, Py_convert_path_to_polygons__doc__}, {"cleanup_path", (PyCFunction)Py_cleanup_path, METH_VARARGS, Py_cleanup_path__doc__}, - {"convert_to_svg", (PyCFunction)Py_convert_to_svg, METH_VARARGS, Py_convert_to_svg__doc__}, + {"convert_to_string", (PyCFunction)Py_convert_to_string, METH_VARARGS, Py_convert_to_string__doc__}, {NULL} };