An easy-to-use platform for EEG experimentation in the classroom
at main 87 lines 2.9 kB view raw
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}