Skip to content

Commit

Permalink
Merge pull request #3485 from jkseppan/reduce-xobjects
Browse files Browse the repository at this point in the history
ENH : Reduce the use of XObjects in pdf backend
  • Loading branch information
tacaswell committed Oct 19, 2014
2 parents 917b9c1 + ef2c9fb commit c600130
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 5 deletions.
1 change: 1 addition & 0 deletions lib/matplotlib/__init__.py
Expand Up @@ -1401,6 +1401,7 @@ def tk_window_focus():
'matplotlib.tests.test_artist',
'matplotlib.tests.test_axes',
'matplotlib.tests.test_axes_grid1',
'matplotlib.tests.test_backend_bases',
'matplotlib.tests.test_backend_pdf',
'matplotlib.tests.test_backend_pgf',
'matplotlib.tests.test_backend_ps',
Expand Down
17 changes: 17 additions & 0 deletions lib/matplotlib/backend_bases.py
Expand Up @@ -384,6 +384,23 @@ def _iter_collection_raw_paths(self, master_transform, paths,
transform = Affine2D(all_transforms[i % Ntransforms])
yield path, transform + master_transform

def _iter_collection_uses_per_path(self, paths, all_transforms,
offsets, facecolors, edgecolors):
"""
Compute how many times each raw path object returned by
_iter_collection_raw_paths would be used when calling
_iter_collection. This is intended for the backend to decide
on the tradeoff between using the paths in-line and storing
them once and reusing. Rounds up in case the number of uses
is not the same for every path.
"""
Npaths = len(paths)
if Npaths == 0 or (len(facecolors) == 0 and len(edgecolors) == 0):
return 0
Npath_ids = max(Npaths, len(all_transforms))
N = max(Npath_ids, len(offsets))
return (N + Npath_ids - 1) // Npath_ids

def _iter_collection(self, gc, master_transform, all_transforms,
path_ids, offsets, offsetTrans, facecolors,
edgecolors, linewidths, linestyles,
Expand Down
23 changes: 18 additions & 5 deletions lib/matplotlib/backends/backend_pdf.py
Expand Up @@ -1616,12 +1616,24 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
if not len(edgecolors):
stroked = False
else:
if np.all(edgecolors[:, 3] == edgecolors[0, 3]):
if np.all(np.asarray(linewidths) == 0.0):
stroked = False
elif np.all(edgecolors[:, 3] == edgecolors[0, 3]):
stroked = edgecolors[0, 3] != 0.0
else:
can_do_optimization = False

if not can_do_optimization:
# Is the optimization worth it? Rough calculation:
# cost of emitting a path in-line is len_path * uses_per_path
# cost of XObject is len_path + 5 for the definition,
# uses_per_path for the uses
len_path = len(paths[0].vertices) if len(paths) > 0 else 0
uses_per_path = self._iter_collection_uses_per_path(
paths, all_transforms, offsets, facecolors, edgecolors)
should_do_optimization = \
len_path + uses_per_path + 5 < len_path * uses_per_path

if (not can_do_optimization) or (not should_do_optimization):
return RendererBase.draw_path_collection(
self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
Expand Down Expand Up @@ -1653,9 +1665,10 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,

def draw_markers(self, gc, marker_path, marker_trans, path, trans,
rgbFace=None):
# For simple paths or small numbers of markers, don't bother
# making an XObject
if len(path) * len(marker_path) <= 10:
# Same logic as in draw_path_collection
len_marker_path = len(marker_path)
uses = len(path)
if len_marker_path * uses < len_marker_path + uses + 5:
RendererBase.draw_markers(self, gc, marker_path, marker_trans,
path, trans, rgbFace)
return
Expand Down
17 changes: 17 additions & 0 deletions lib/matplotlib/backends/backend_ps.py
Expand Up @@ -633,6 +633,23 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
# Is the optimization worth it? Rough calculation:
# cost of emitting a path in-line is
# (len_path + 2) * uses_per_path
# cost of definition+use is
# (len_path + 3) + 3 * uses_per_path
len_path = len(paths[0].vertices) if len(paths) > 0 else 0
uses_per_path = self._iter_collection_uses_per_path(
paths, all_transforms, offsets, facecolors, edgecolors)
should_do_optimization = \
len_path + 3 * uses_per_path + 3 < (len_path + 2) * uses_per_path
if not should_do_optimization:
return RendererBase.draw_path_collection(
self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position)

write = self._pswriter.write

path_codes = []
Expand Down
17 changes: 17 additions & 0 deletions lib/matplotlib/backends/backend_svg.py
Expand Up @@ -607,6 +607,23 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
# Is the optimization worth it? Rough calculation:
# cost of emitting a path in-line is
# (len_path + 5) * uses_per_path
# cost of definition+use is
# (len_path + 3) + 9 * uses_per_path
len_path = len(paths[0].vertices) if len(paths) > 0 else 0
uses_per_path = self._iter_collection_uses_per_path(
paths, all_transforms, offsets, facecolors, edgecolors)
should_do_optimization = \
len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path
if not should_do_optimization:
return RendererBase.draw_path_collection(
self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position)

writer = self.writer
path_codes = []
writer.start('defs')
Expand Down
45 changes: 45 additions & 0 deletions lib/matplotlib/tests/test_backend_bases.py
@@ -0,0 +1,45 @@
from matplotlib.backend_bases import RendererBase
import matplotlib.transforms as transforms
import matplotlib.path as path
import numpy as np


def test_uses_per_path():
id = transforms.Affine2D()
paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)]
tforms = [id.rotate(i) for i in range(1, 5)]
offsets = np.arange(20).reshape((10, 2))
facecolors = ['red', 'green']
edgecolors = ['red', 'green']

def check(master_transform, paths, all_transforms,
offsets, facecolors, edgecolors):
rb = RendererBase()
raw_paths = list(rb._iter_collection_raw_paths(master_transform,
paths, all_transforms))
gc = rb.new_gc()
ids = [path_id for xo, yo, path_id, gc0, rgbFace in
rb._iter_collection(gc, master_transform, all_transforms,
range(len(raw_paths)), offsets,
transforms.IdentityTransform(),
facecolors, edgecolors, [], [], [False],
[], 'data')]
uses = rb._iter_collection_uses_per_path(
paths, all_transforms, offsets, facecolors, edgecolors)
seen = [0] * len(raw_paths)
for i in ids:
seen[i] += 1
for n in seen:
assert n in (uses-1, uses)

check(id, paths, tforms, offsets, facecolors, edgecolors)
check(id, paths[0:1], tforms, offsets, facecolors, edgecolors)
check(id, [], tforms, offsets, facecolors, edgecolors)
check(id, paths, tforms[0:1], offsets, facecolors, edgecolors)
check(id, paths, [], offsets, facecolors, edgecolors)
for n in range(0, offsets.shape[0]):
check(id, paths, tforms, offsets[0:n, :], facecolors, edgecolors)
check(id, paths, tforms, offsets, [], edgecolors)
check(id, paths, tforms, offsets, facecolors, [])
check(id, paths, tforms, offsets, [], [])
check(id, paths, tforms, offsets, facecolors[0:1], edgecolors)
Binary file not shown.
Binary file not shown.

0 comments on commit c600130

Please sign in to comment.