Skip to content

Commit 317a1b8

Browse files
ianhiclaude
andcommitted
Support all matplotlib formats in Download button
Remove artificial format restrictions - if matplotlib can generate it, we support downloading it. Use known MIME types where available, or fall back to application/octet-stream for unknown formats. The filename extension ensures proper OS handling regardless. This enables formats like PGF (LaTeX graphics), SVG compressed (svgz), and raw RGBA formats to download correctly. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c8e09a2 commit 317a1b8

3 files changed

Lines changed: 33 additions & 41 deletions

File tree

ipympl/backend_nbagg.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -359,24 +359,14 @@ def download(self):
359359

360360
def _send_save_buffer(self):
361361
"""Generate figure buffer respecting savefig rcParams and send to frontend."""
362-
# Get the format before calling savefig to properly warn about unsupported formats
363-
fmt = rcParams.get('savefig.format', 'png')
364-
365-
# Validate format is supported by the frontend
366-
supported_formats = {'png', 'jpg', 'jpeg', 'pdf', 'svg', 'eps', 'ps', 'tif', 'tiff'}
367-
if fmt not in supported_formats:
368-
warn(
369-
f"Download format '{fmt}' is not supported by the ipympl frontend, "
370-
f"falling back to PNG. Supported formats: {', '.join(sorted(supported_formats))}",
371-
UserWarning,
372-
stacklevel=3
373-
)
374-
375362
buf = io.BytesIO()
376363

377364
# Call savefig WITHOUT any parameters - fully respects all rcParams
378365
self.figure.savefig(buf)
379366

367+
# Get the format that was used
368+
fmt = rcParams.get('savefig.format', 'png')
369+
380370
# Get the buffer data
381371
data = buf.getvalue()
382372

src/mpl_widget.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -160,38 +160,36 @@ export class MPLCanvasModel extends DOMWidgetModel {
160160
// Get format from message (already parsed by on_comm_message)
161161
const format = msg.format || 'png';
162162

163-
// Map format to MIME type
163+
// Map format to MIME type - use known types where available
164164
const mimeTypes: { [key: string]: string } = {
165165
'png': 'image/png',
166166
'jpg': 'image/jpeg',
167167
'jpeg': 'image/jpeg',
168168
'pdf': 'application/pdf',
169169
'svg': 'image/svg+xml',
170+
'svgz': 'image/svg+xml',
170171
'eps': 'application/postscript',
171172
'ps': 'application/postscript',
172173
'tif': 'image/tiff',
173-
'tiff': 'image/tiff'
174+
'tiff': 'image/tiff',
175+
'pgf': 'application/x-latex',
176+
'raw': 'application/octet-stream',
177+
'rgba': 'application/octet-stream'
174178
};
175179

176-
const mimeType = mimeTypes[format];
177-
178-
// If format is unknown, fall back to canvas toDataURL method
179-
if (!mimeType) {
180-
console.warn(`Unknown save format '${format}', falling back to PNG`);
181-
blob_url = this.offscreen_canvas.toDataURL();
182-
filename = this.get('_figure_label') + '.png';
183-
} else {
184-
// Convert buffer to Uint8Array
185-
const buffer = new Uint8Array(
186-
ArrayBuffer.isView(buffers[0]) ? buffers[0].buffer : buffers[0]
187-
);
180+
// Use known MIME type or generic fallback
181+
const mimeType = mimeTypes[format] || 'application/octet-stream';
188182

189-
// Create blob with correct MIME type
190-
const blob = new Blob([buffer], { type: mimeType });
191-
blob_url = url_creator.createObjectURL(blob);
192-
filename = this.get('_figure_label') + '.' + format;
193-
should_revoke = true;
194-
}
183+
// Convert buffer to Uint8Array
184+
const buffer = new Uint8Array(
185+
ArrayBuffer.isView(buffers[0]) ? buffers[0].buffer : buffers[0]
186+
);
187+
188+
// Create blob with MIME type
189+
const blob = new Blob([buffer], { type: mimeType });
190+
blob_url = url_creator.createObjectURL(blob);
191+
filename = this.get('_figure_label') + '.' + format;
192+
should_revoke = true;
195193
} else {
196194
// Fallback to old behavior (use canvas toDataURL)
197195
blob_url = this.offscreen_canvas.toDataURL();

tests/test_download.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,24 +191,28 @@ def test_send_save_buffer_respects_transparent():
191191
plt.close(fig)
192192

193193

194-
def test_send_save_buffer_warns_on_unsupported_format():
195-
"""Test that _send_save_buffer warns about unsupported formats."""
194+
def test_send_save_buffer_with_pgf_format():
195+
"""Test that _send_save_buffer works with PGF format."""
196196
matplotlib.use('module://ipympl.backend_nbagg')
197197

198-
# Test with an unsupported format
199-
plt.rcParams['savefig.format'] = 'webp'
198+
# Test with PGF format (LaTeX graphics format)
199+
plt.rcParams['savefig.format'] = 'pgf'
200200

201201
fig, ax = plt.subplots()
202202
ax.plot([1, 2, 3], [1, 4, 2])
203203

204204
canvas = fig.canvas
205205
canvas.send = MagicMock()
206206

207-
# Should issue a warning
208-
with pytest.warns(UserWarning, match="Download format 'webp' is not supported"):
209-
canvas._send_save_buffer()
207+
# Should work without warnings
208+
canvas._send_save_buffer()
210209

211-
# Should still send the buffer (frontend will fall back to PNG)
210+
# Should send the buffer with format='pgf'
212211
assert canvas.send.called
212+
call_args = canvas.send.call_args
213+
assert 'data' in call_args[0][0]
214+
import json
215+
msg_data = json.loads(call_args[0][0]['data'])
216+
assert msg_data['format'] == 'pgf'
213217

214218
plt.close(fig)

0 commit comments

Comments
 (0)