|
import io |
|
|
|
import numpy as np |
|
from numpy.testing import assert_array_almost_equal |
|
from PIL import Image, TiffTags |
|
import pytest |
|
|
|
|
|
from matplotlib import ( |
|
collections, patheffects, pyplot as plt, transforms as mtransforms, |
|
rcParams, rc_context) |
|
from matplotlib.backends.backend_agg import RendererAgg |
|
from matplotlib.figure import Figure |
|
from matplotlib.image import imread |
|
from matplotlib.path import Path |
|
from matplotlib.testing.decorators import image_comparison |
|
from matplotlib.transforms import IdentityTransform |
|
|
|
|
|
def test_repeated_save_with_alpha(): |
|
|
|
|
|
|
|
fig = Figure([1, 0.4]) |
|
fig.set_facecolor((0, 1, 0.4)) |
|
fig.patch.set_alpha(0.25) |
|
|
|
|
|
|
|
buf = io.BytesIO() |
|
|
|
fig.savefig(buf, |
|
facecolor=fig.get_facecolor(), |
|
edgecolor='none') |
|
|
|
|
|
|
|
buf.seek(0) |
|
fig.savefig(buf, |
|
facecolor=fig.get_facecolor(), |
|
edgecolor='none') |
|
|
|
|
|
|
|
buf.seek(0) |
|
assert_array_almost_equal(tuple(imread(buf)[0, 0]), |
|
(0.0, 1.0, 0.4, 0.250), |
|
decimal=3) |
|
|
|
|
|
def test_large_single_path_collection(): |
|
buff = io.BytesIO() |
|
|
|
|
|
|
|
|
|
f, ax = plt.subplots() |
|
collection = collections.PathCollection( |
|
[Path([[-10, 5], [10, 5], [10, -5], [-10, -5], [-10, 5]])]) |
|
ax.add_artist(collection) |
|
ax.set_xlim(10**-3, 1) |
|
plt.savefig(buff) |
|
|
|
|
|
def test_marker_with_nan(): |
|
|
|
|
|
fig, ax = plt.subplots(1) |
|
steps = 1000 |
|
data = np.arange(steps) |
|
ax.semilogx(data) |
|
ax.fill_between(data, data*0.8, data*1.2) |
|
buf = io.BytesIO() |
|
fig.savefig(buf, format='png') |
|
|
|
|
|
def test_long_path(): |
|
buff = io.BytesIO() |
|
fig = Figure() |
|
ax = fig.subplots() |
|
points = np.ones(100_000) |
|
points[::2] *= -1 |
|
ax.plot(points) |
|
fig.savefig(buff, format='png') |
|
|
|
|
|
@image_comparison(['agg_filter.png'], remove_text=True) |
|
def test_agg_filter(): |
|
def smooth1d(x, window_len): |
|
|
|
s = np.r_[ |
|
2*x[0] - x[window_len:1:-1], x, 2*x[-1] - x[-1:-window_len:-1]] |
|
w = np.hanning(window_len) |
|
y = np.convolve(w/w.sum(), s, mode='same') |
|
return y[window_len-1:-window_len+1] |
|
|
|
def smooth2d(A, sigma=3): |
|
window_len = max(int(sigma), 3) * 2 + 1 |
|
A = np.apply_along_axis(smooth1d, 0, A, window_len) |
|
A = np.apply_along_axis(smooth1d, 1, A, window_len) |
|
return A |
|
|
|
class BaseFilter: |
|
|
|
def get_pad(self, dpi): |
|
return 0 |
|
|
|
def process_image(self, padded_src, dpi): |
|
raise NotImplementedError("Should be overridden by subclasses") |
|
|
|
def __call__(self, im, dpi): |
|
pad = self.get_pad(dpi) |
|
padded_src = np.pad(im, [(pad, pad), (pad, pad), (0, 0)], |
|
"constant") |
|
tgt_image = self.process_image(padded_src, dpi) |
|
return tgt_image, -pad, -pad |
|
|
|
class OffsetFilter(BaseFilter): |
|
|
|
def __init__(self, offsets=(0, 0)): |
|
self.offsets = offsets |
|
|
|
def get_pad(self, dpi): |
|
return int(max(self.offsets) / 72 * dpi) |
|
|
|
def process_image(self, padded_src, dpi): |
|
ox, oy = self.offsets |
|
a1 = np.roll(padded_src, int(ox / 72 * dpi), axis=1) |
|
a2 = np.roll(a1, -int(oy / 72 * dpi), axis=0) |
|
return a2 |
|
|
|
class GaussianFilter(BaseFilter): |
|
"""Simple Gaussian filter.""" |
|
|
|
def __init__(self, sigma, alpha=0.5, color=(0, 0, 0)): |
|
self.sigma = sigma |
|
self.alpha = alpha |
|
self.color = color |
|
|
|
def get_pad(self, dpi): |
|
return int(self.sigma*3 / 72 * dpi) |
|
|
|
def process_image(self, padded_src, dpi): |
|
tgt_image = np.empty_like(padded_src) |
|
tgt_image[:, :, :3] = self.color |
|
tgt_image[:, :, 3] = smooth2d(padded_src[:, :, 3] * self.alpha, |
|
self.sigma / 72 * dpi) |
|
return tgt_image |
|
|
|
class DropShadowFilter(BaseFilter): |
|
|
|
def __init__(self, sigma, alpha=0.3, color=(0, 0, 0), offsets=(0, 0)): |
|
self.gauss_filter = GaussianFilter(sigma, alpha, color) |
|
self.offset_filter = OffsetFilter(offsets) |
|
|
|
def get_pad(self, dpi): |
|
return max(self.gauss_filter.get_pad(dpi), |
|
self.offset_filter.get_pad(dpi)) |
|
|
|
def process_image(self, padded_src, dpi): |
|
t1 = self.gauss_filter.process_image(padded_src, dpi) |
|
t2 = self.offset_filter.process_image(t1, dpi) |
|
return t2 |
|
|
|
fig, ax = plt.subplots() |
|
|
|
|
|
line1, = ax.plot([0.1, 0.5, 0.9], [0.1, 0.9, 0.5], "bo-", |
|
mec="b", mfc="w", lw=5, mew=3, ms=10, label="Line 1") |
|
line2, = ax.plot([0.1, 0.5, 0.9], [0.5, 0.2, 0.7], "ro-", |
|
mec="r", mfc="w", lw=5, mew=3, ms=10, label="Line 1") |
|
|
|
gauss = DropShadowFilter(4) |
|
|
|
for line in [line1, line2]: |
|
|
|
|
|
xx = line.get_xdata() |
|
yy = line.get_ydata() |
|
shadow, = ax.plot(xx, yy) |
|
shadow.update_from(line) |
|
|
|
|
|
transform = mtransforms.offset_copy(line.get_transform(), ax.figure, |
|
x=4.0, y=-6.0, units='points') |
|
shadow.set_transform(transform) |
|
|
|
|
|
|
|
shadow.set_zorder(line.get_zorder() - 0.5) |
|
shadow.set_agg_filter(gauss) |
|
shadow.set_rasterized(True) |
|
|
|
ax.set_xlim(0., 1.) |
|
ax.set_ylim(0., 1.) |
|
|
|
ax.xaxis.set_visible(False) |
|
ax.yaxis.set_visible(False) |
|
|
|
|
|
def test_too_large_image(): |
|
fig = plt.figure(figsize=(300, 1000)) |
|
buff = io.BytesIO() |
|
with pytest.raises(ValueError): |
|
fig.savefig(buff) |
|
|
|
|
|
def test_chunksize(): |
|
x = range(200) |
|
|
|
|
|
fig, ax = plt.subplots() |
|
ax.plot(x, np.sin(x)) |
|
fig.canvas.draw() |
|
|
|
|
|
fig, ax = plt.subplots() |
|
rcParams['agg.path.chunksize'] = 105 |
|
ax.plot(x, np.sin(x)) |
|
fig.canvas.draw() |
|
|
|
|
|
@pytest.mark.backend('Agg') |
|
def test_jpeg_dpi(): |
|
|
|
plt.plot([0, 1, 2], [0, 1, 0]) |
|
buf = io.BytesIO() |
|
plt.savefig(buf, format="jpg", dpi=200) |
|
im = Image.open(buf) |
|
assert im.info['dpi'] == (200, 200) |
|
|
|
|
|
def test_pil_kwargs_png(): |
|
from PIL.PngImagePlugin import PngInfo |
|
buf = io.BytesIO() |
|
pnginfo = PngInfo() |
|
pnginfo.add_text("Software", "test") |
|
plt.figure().savefig(buf, format="png", pil_kwargs={"pnginfo": pnginfo}) |
|
im = Image.open(buf) |
|
assert im.info["Software"] == "test" |
|
|
|
|
|
def test_pil_kwargs_tiff(): |
|
buf = io.BytesIO() |
|
pil_kwargs = {"description": "test image"} |
|
plt.figure().savefig(buf, format="tiff", pil_kwargs=pil_kwargs) |
|
im = Image.open(buf) |
|
tags = {TiffTags.TAGS_V2[k].name: v for k, v in im.tag_v2.items()} |
|
assert tags["ImageDescription"] == "test image" |
|
|
|
|
|
def test_pil_kwargs_webp(): |
|
plt.plot([0, 1, 2], [0, 1, 0]) |
|
buf_small = io.BytesIO() |
|
pil_kwargs_low = {"quality": 1} |
|
plt.savefig(buf_small, format="webp", pil_kwargs=pil_kwargs_low) |
|
assert len(pil_kwargs_low) == 1 |
|
buf_large = io.BytesIO() |
|
pil_kwargs_high = {"quality": 100} |
|
plt.savefig(buf_large, format="webp", pil_kwargs=pil_kwargs_high) |
|
assert len(pil_kwargs_high) == 1 |
|
assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes |
|
|
|
|
|
def test_webp_alpha(): |
|
plt.plot([0, 1, 2], [0, 1, 0]) |
|
buf = io.BytesIO() |
|
plt.savefig(buf, format="webp", transparent=True) |
|
im = Image.open(buf) |
|
assert im.mode == "RGBA" |
|
|
|
|
|
def test_draw_path_collection_error_handling(): |
|
fig, ax = plt.subplots() |
|
ax.scatter([1], [1]).set_paths(Path([(0, 1), (2, 3)])) |
|
with pytest.raises(TypeError): |
|
fig.canvas.draw() |
|
|
|
|
|
def test_chunksize_fails(): |
|
|
|
|
|
|
|
|
|
|
|
N = 100_000 |
|
dpi = 500 |
|
w = 5*dpi |
|
h = 6*dpi |
|
|
|
|
|
x = np.linspace(0, w, N) |
|
y = np.ones(N) * h |
|
y[::2] = 0 |
|
path = Path(np.vstack((x, y)).T) |
|
|
|
path.simplify_threshold = 0 |
|
|
|
|
|
ra = RendererAgg(w, h, dpi) |
|
gc = ra.new_gc() |
|
gc.set_linewidth(1) |
|
gc.set_foreground('r') |
|
|
|
gc.set_hatch('/') |
|
with pytest.raises(OverflowError, match='cannot split hatched path'): |
|
ra.draw_path(gc, path, IdentityTransform()) |
|
gc.set_hatch(None) |
|
|
|
with pytest.raises(OverflowError, match='cannot split filled path'): |
|
ra.draw_path(gc, path, IdentityTransform(), (1, 0, 0)) |
|
|
|
|
|
with rc_context({'agg.path.chunksize': 0}): |
|
with pytest.raises(OverflowError, match='Please set'): |
|
ra.draw_path(gc, path, IdentityTransform()) |
|
|
|
|
|
with rc_context({'agg.path.chunksize': 1_000_000}): |
|
with pytest.raises(OverflowError, match='Please reduce'): |
|
ra.draw_path(gc, path, IdentityTransform()) |
|
|
|
|
|
with rc_context({'agg.path.chunksize': 90_000}): |
|
with pytest.raises(OverflowError, match='Please reduce'): |
|
ra.draw_path(gc, path, IdentityTransform()) |
|
|
|
path.should_simplify = False |
|
with pytest.raises(OverflowError, match="should_simplify is False"): |
|
ra.draw_path(gc, path, IdentityTransform()) |
|
|
|
|
|
def test_non_tuple_rgbaface(): |
|
|
|
fig = plt.figure() |
|
fig.add_subplot(projection="3d").scatter( |
|
[0, 1, 2], [0, 1, 2], path_effects=[patheffects.Stroke(linewidth=4)]) |
|
fig.canvas.draw() |
|
|