Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce the use of XObjects in pdf backend [backport to 1.4.x] #3485

Merged
merged 7 commits into from Oct 19, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/matplotlib/__init__.py
Expand Up @@ -1321,6 +1321,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 @@ -382,6 +382,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 @@ -1617,12 +1617,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 @@ -1654,9 +1666,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 @@ -637,6 +637,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.