Skip to content

Commit e1e0848

Browse files
committed
Add a sketch path filter.
1 parent f157c97 commit e1e0848

14 files changed

+265
-20
lines changed

lib/matplotlib/artist.py

+37
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def __init__(self):
101101
self._url = None
102102
self._gid = None
103103
self._snap = None
104+
self._sketch = rcParams['path.sketch']
104105

105106
def __getstate__(self):
106107
d = self.__dict__.copy()
@@ -456,6 +457,42 @@ def set_snap(self, snap):
456457
"""
457458
self._snap = snap
458459

460+
def get_sketch_params(self):
461+
"""
462+
Returns the sketch parameters, which is a tuple with three elements:
463+
464+
* *scale*: The amplitude of the wiggle perpendicular to the
465+
source line.
466+
467+
* *length*: The length of the wiggle along the line.
468+
469+
* *randomness*: The scale factor by which the length is
470+
shrunken or expanded.
471+
472+
May return `None` if no sketch parameters were set.
473+
"""
474+
return self._sketch
475+
476+
def set_sketch_params(self, scale=None, length=None, randomness=None):
477+
"""
478+
Sets the the sketch parameters:
479+
480+
* *scale*: The amplitude of the wiggle perpendicular to the
481+
source line, in pixels.
482+
483+
* *length*: The length of the wiggle along the line, in
484+
pixels (default 128.0)
485+
486+
* *randomness*: The scale factor by which the length is
487+
shrunken or expanded (default 16.0)
488+
489+
If *scale* is None, no wiggling will be set.
490+
"""
491+
if scale is None:
492+
self._sketch = None
493+
else:
494+
self._sketch = (scale, length or 128.0, randomness or 16.0)
495+
459496
def get_figure(self):
460497
"""
461498
Return the :class:`~matplotlib.figure.Figure` instance the

lib/matplotlib/backend_bases.py

+37
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,7 @@ def __init__(self):
701701
self._url = None
702702
self._gid = None
703703
self._snap = None
704+
self._sketch = None
704705

705706
def copy_properties(self, gc):
706707
'Copy properties from gc to self'
@@ -1003,6 +1004,42 @@ def get_hatch_path(self, density=6.0):
10031004
return None
10041005
return Path.hatch(self._hatch, density)
10051006

1007+
def get_sketch_params(self):
1008+
"""
1009+
Returns the sketch parameters, which is a tuple with three elements:
1010+
1011+
* *scale*: The amplitude of the wiggle perpendicular to the
1012+
source line.
1013+
1014+
* *length*: The length of the wiggle along the line.
1015+
1016+
* *randomness*: The scale factor by which the length is
1017+
shrunken or expanded.
1018+
1019+
May return `None` if no sketch parameters were set.
1020+
"""
1021+
return self._sketch
1022+
1023+
def set_sketch_params(self, scale=None, length=None, randomness=None):
1024+
"""
1025+
Sets the the sketch parameters:
1026+
1027+
* *scale*: The amplitude of the wiggle perpendicular to the
1028+
source line.
1029+
1030+
* *length*: The length of the wiggle along the line, in
1031+
pixels.
1032+
1033+
* *randomness*: The scale factor by which the length is
1034+
shrunken or expanded, in pixels.
1035+
1036+
If *scale* is None, no wiggling will be set.
1037+
"""
1038+
if scale is None:
1039+
self._sketch = None
1040+
else:
1041+
self._sketch = (scale, length or 128.0, randomness or 16.0)
1042+
10061043

10071044
class TimerBase(object):
10081045
'''

lib/matplotlib/backends/backend_pdf.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1313,11 +1313,12 @@ def writePathCollectionTemplates(self):
13131313
self.endStream()
13141314

13151315
@staticmethod
1316-
def pathOperations(path, transform, clip=None, simplify=None):
1316+
def pathOperations(path, transform, clip=None, simplify=None, sketch=None):
13171317
cmds = []
13181318
last_points = None
13191319
for points, code in path.iter_segments(transform, clip=clip,
1320-
simplify=simplify):
1320+
simplify=simplify,
1321+
sketch=sketch):
13211322
if code == Path.MOVETO:
13221323
# This is allowed anywhere in the path
13231324
cmds.extend(points)
@@ -1340,14 +1341,15 @@ def pathOperations(path, transform, clip=None, simplify=None):
13401341
last_points = points
13411342
return cmds
13421343

1343-
def writePath(self, path, transform, clip=False):
1344+
def writePath(self, path, transform, clip=False, sketch=None):
13441345
if clip:
13451346
clip = (0.0, 0.0, self.width * 72, self.height * 72)
13461347
simplify = path.should_simplify
13471348
else:
13481349
clip = None
13491350
simplify = False
1350-
cmds = self.pathOperations(path, transform, clip, simplify=simplify)
1351+
cmds = self.pathOperations(path, transform, clip, simplify=simplify,
1352+
sketch=sketch)
13511353
self.output(*cmds)
13521354

13531355
def reserveObject(self, name=''):
@@ -1526,7 +1528,8 @@ def draw_path(self, gc, path, transform, rgbFace=None):
15261528
self.check_gc(gc, rgbFace)
15271529
self.file.writePath(
15281530
path, transform,
1529-
rgbFace is None and gc.get_hatch_path() is None)
1531+
rgbFace is None and gc.get_hatch_path() is None,
1532+
gc.get_sketch_params())
15301533
self.file.output(self.gc.paint())
15311534

15321535
def draw_path_collection(self, gc, master_transform, paths, all_transforms,

lib/matplotlib/collections.py

+3
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ def draw(self, renderer):
260260
if self._hatch:
261261
gc.set_hatch(self._hatch)
262262

263+
if self.get_sketch_params() is not None:
264+
gc.set_sketch_params(*self.get_sketch_params())
265+
263266
if self.get_path_effects():
264267
#from patheffects import PathEffectsRenderer
265268
for pe in self.get_path_effects():

lib/matplotlib/lines.py

+2
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,8 @@ def draw(self, renderer):
545545
gc.set_joinstyle(join)
546546
gc.set_capstyle(cap)
547547
gc.set_snap(self.get_snap())
548+
if self.get_sketch_params() is not None:
549+
gc.set_sketch_params(*self.get_sketch_params())
548550

549551
funcname = self._lineStyles.get(self._linestyle, '_draw_nothing')
550552
if funcname != '_draw_nothing':

lib/matplotlib/patches.py

+6
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,9 @@ def draw(self, renderer):
426426
if self._hatch:
427427
gc.set_hatch(self._hatch)
428428

429+
if self.get_sketch_params() is not None:
430+
gc.set_sketch_params(*self.get_sketch_params())
431+
429432
path = self.get_path()
430433
transform = self.get_transform()
431434
tpath = transform.transform_path_non_affine(path)
@@ -4014,6 +4017,9 @@ def draw(self, renderer):
40144017
if self._hatch:
40154018
gc.set_hatch(self._hatch)
40164019

4020+
if self.get_sketch_params() is not None:
4021+
gc.set_sketch_params(*self.get_sketch_params())
4022+
40174023
# FIXME : dpi_cor is for the dpi-dependecy of the
40184024
# linewidth. There could be room for improvement.
40194025
#

lib/matplotlib/path.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def __len__(self):
199199

200200
def iter_segments(self, transform=None, remove_nans=True, clip=None,
201201
snap=False, stroke_width=1.0, simplify=None,
202-
curves=True):
202+
curves=True, sketch=None):
203203
"""
204204
Iterates over all of the curve segments in the path. Each
205205
iteration returns a 2-tuple (*vertices*, *code*), where
@@ -233,6 +233,10 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None,
233233
*curves*: If True, curve segments will be returned as curve
234234
segments. If False, all curves will be converted to line
235235
segments.
236+
237+
*sketch*: If not None, must be a 3-tuple of the form
238+
(scale, length, randomness), representing the sketch
239+
parameters.
236240
"""
237241
vertices = self.vertices
238242
if not len(vertices):
@@ -247,7 +251,8 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None,
247251
STOP = self.STOP
248252

249253
vertices, codes = cleanup_path(self, transform, remove_nans, clip,
250-
snap, stroke_width, simplify, curves)
254+
snap, stroke_width, simplify, curves,
255+
sketch)
251256
len_vertices = len(vertices)
252257

253258
i = 0

lib/matplotlib/rcsetup.py

+11
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,16 @@ def validate_bbox(s):
426426
return None
427427
raise ValueError("bbox should be 'tight' or 'standard'")
428428

429+
def validate_sketch(s):
430+
if s == 'None' or s is None:
431+
return None
432+
if isinstance(s, basestring):
433+
result = tuple([float(v.strip()) for v in s.split(',')])
434+
elif isinstance(s, (list, tuple)):
435+
result = tuple([float(v) for v in s])
436+
if len(result) != 3:
437+
raise ValueError("path.sketch must be a tuple (scale, length, randomness)")
438+
return result
429439

430440
class ValidateInterval:
431441
"""
@@ -741,6 +751,7 @@ def __call__(self, s):
741751
'path.simplify_threshold': [1.0 / 9.0, ValidateInterval(0.0, 1.0)],
742752
'path.snap': [True, validate_bool],
743753
'agg.path.chunksize': [0, validate_int], # 0 to disable chunking;
754+
'path.sketch': [None, validate_sketch],
744755
# recommend about 20000 to
745756
# enable. Experimental.
746757
# key-mappings (multi-character mappings should be a list/tuple)

matplotlibrc.template

+7
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,13 @@ text.hinting_factor : 8 # Specifies the amount of softness for hinting in the
358358
#path.snap : True # When True, rectilinear axis-aligned paths will be snapped to
359359
# the nearest pixel when certain criteria are met. When False,
360360
# paths will never be snapped.
361+
#path.sketch : None # May be none, or a 3-tuple of the form (scale, length,
362+
# randomness).
363+
# *scale* is the amplitude of the wiggle
364+
# perpendicular to the line (in pixels). *length*
365+
# is the length of the wiggle along the line (in
366+
# pixels). *randomness* is the factor by which
367+
# the length is randomly scaled.
361368

362369
# the default savefig params can be different from the display params
363370
# e.g., you may want a higher resolution, or to make the figure

src/_backend_agg.cpp

+22-2
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ GCAgg::GCAgg(const Py::Object &gc, double dpi) :
205205
_set_clip_path(gc);
206206
_set_snap(gc);
207207
_set_hatch_path(gc);
208+
_set_sketch_params(gc);
208209
}
209210

210211

@@ -375,6 +376,24 @@ GCAgg::_set_hatch_path(const Py::Object& gc)
375376
throw Py::Exception();
376377
}
377378

379+
void
380+
GCAgg::_set_sketch_params(const Py::Object& gc)
381+
{
382+
_VERBOSE("GCAgg::_get_sketch_params");
383+
384+
Py::Object method_obj = gc.getAttr("get_sketch_params");
385+
Py::Callable method(method_obj);
386+
Py::Object result = method.apply(Py::Tuple());
387+
if (result.ptr() == Py_None) {
388+
sketch_scale = 0.0;
389+
} else {
390+
Py::Tuple sketch_params(result);
391+
sketch_scale = Py::Float(sketch_params[0]);
392+
sketch_length = Py::Float(sketch_params[1]);
393+
sketch_randomness = Py::Float(sketch_params[2]);
394+
}
395+
}
396+
378397

379398
const size_t
380399
RendererAgg::PIXELS_PER_INCH(96);
@@ -1397,6 +1416,7 @@ RendererAgg::draw_path(const Py::Tuple& args)
13971416
typedef PathSnapper<clipped_t> snapped_t;
13981417
typedef PathSimplifier<snapped_t> simplify_t;
13991418
typedef agg::conv_curve<simplify_t> curve_t;
1419+
typedef Sketch<curve_t> sketch_t;
14001420

14011421
_VERBOSE("RendererAgg::draw_path");
14021422
args.verify_length(3, 4);
@@ -1430,10 +1450,11 @@ RendererAgg::draw_path(const Py::Tuple& args)
14301450
snapped_t snapped(clipped, gc.snap_mode, path.total_vertices(), snapping_linewidth);
14311451
simplify_t simplified(snapped, simplify, path.simplify_threshold());
14321452
curve_t curve(simplified);
1453+
sketch_t sketch(curve, gc.sketch_scale, gc.sketch_length, gc.sketch_randomness);
14331454

14341455
try
14351456
{
1436-
_draw_path(curve, has_clippath, face, gc);
1457+
_draw_path(sketch, has_clippath, face, gc);
14371458
}
14381459
catch (const char* e)
14391460
{
@@ -2102,7 +2123,6 @@ RendererAgg::write_rgba(const Py::Tuple& args)
21022123
if ((py_file = npy_PyFile_OpenFile(py_fileobj.ptr(), (char *)"wb")) == NULL) {
21032124
throw Py::Exception();
21042125
}
2105-
close_file = true;
21062126
}
21072127
else
21082128
{

src/_backend_agg.h

+5
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ class GCAgg
137137

138138
Py::Object hatchpath;
139139

140+
double sketch_scale;
141+
double sketch_length;
142+
double sketch_randomness;
143+
140144
protected:
141145
agg::rgba get_color(const Py::Object& gc);
142146
double points_to_pixels(const Py::Object& points);
@@ -148,6 +152,7 @@ class GCAgg
148152
void _set_antialiased(const Py::Object& gc);
149153
void _set_snap(const Py::Object& gc);
150154
void _set_hatch_path(const Py::Object& gc);
155+
void _set_sketch_params(const Py::Object& gc);
151156
};
152157

153158

0 commit comments

Comments
 (0)