An easy-to-use platform for EEG experimentation in the classroom
1import React, { Component } from 'react';
2import { toast } from 'react-toastify';
3import { Button } from './ui/button';
4import { storePyodideImageSvg, storePyodideImagePng } from '../utils/filesystem/storage';
5
6interface Props {
7 title: string;
8 imageTitle: string;
9 plotMIMEBundle: { [key: string]: string } | null | undefined;
10}
11
12function svgToPngArrayBuffer(svg: string): Promise<ArrayBuffer> {
13 return new Promise((resolve, reject) => {
14 const blob = new Blob([svg], { type: 'image/svg+xml' });
15 const url = URL.createObjectURL(blob);
16 const img = new Image();
17 img.onload = () => {
18 const canvas = document.createElement('canvas');
19 canvas.width = img.naturalWidth;
20 canvas.height = img.naturalHeight;
21 const ctx = canvas.getContext('2d');
22 if (!ctx) {
23 URL.revokeObjectURL(url);
24 reject(new Error('No 2d context'));
25 return;
26 }
27 ctx.drawImage(img, 0, 0);
28 URL.revokeObjectURL(url);
29 canvas.toBlob((pngBlob) => {
30 if (!pngBlob) { reject(new Error('Canvas toBlob failed')); return; }
31 pngBlob.arrayBuffer().then(resolve).catch(reject);
32 }, 'image/png');
33 };
34 img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('SVG load failed')); };
35 img.src = url;
36 });
37}
38
39export default class PyodidePlotWidget extends Component<Props> {
40 constructor(props: Props) {
41 super(props);
42 this.handleSaveSvg = this.handleSaveSvg.bind(this);
43 this.handleSavePng = this.handleSavePng.bind(this);
44 }
45
46 handleSaveSvg() {
47 const svg = this.props.plotMIMEBundle?.['image/svg+xml'];
48 if (!svg) return;
49 storePyodideImageSvg(this.props.title, this.props.imageTitle, svg)
50 .then(() => toast.success(`Saved ${this.props.imageTitle}.svg`))
51 .catch((err) => toast.error(`Failed to save SVG: ${err.message}`));
52 }
53
54 async handleSavePng() {
55 const svg = this.props.plotMIMEBundle?.['image/svg+xml'];
56 if (!svg) return;
57 try {
58 const arrayBuffer = await svgToPngArrayBuffer(svg);
59 await storePyodideImagePng(this.props.title, this.props.imageTitle, arrayBuffer);
60 toast.success(`Saved ${this.props.imageTitle}.png`);
61 } catch (err: unknown) {
62 toast.error(`Failed to save PNG: ${(err as Error).message}`);
63 }
64 }
65
66 render() {
67 const svg = this.props.plotMIMEBundle?.['image/svg+xml'];
68 if (!svg) return <div className="p-2" />;
69 return (
70 <div className="p-2">
71 <img
72 className="w-full h-auto"
73 src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`}
74 alt={this.props.imageTitle}
75 />
76 <div className="flex gap-2 mt-2">
77 <Button variant="outline" size="sm" onClick={this.handleSaveSvg}>
78 Save as SVG
79 </Button>
80 <Button variant="outline" size="sm" onClick={this.handleSavePng}>
81 Save as PNG
82 </Button>
83 </div>
84 </div>
85 );
86 }
87}