Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets

Add Drawing.as_spritesheet and save_spritesheet

+69 -6
+2 -1
README.md
··· 140 140 #d.display_image() # Display SVG as an image (will not be interactive) 141 141 #d.display_iframe() # Display as interactive SVG (alternative) 142 142 #d.as_gif('orbit.gif', fps=10) # Render as a GIF image, optionally save to file 143 - #d.as_mp4('orbig.mp4', fps=60) # Render as an MP4 video, optionally save to file 143 + #d.as_mp4('orbig.mp4', fps=60, verbose=True) # Render as an MP4 video, optionally save to file 144 + #d.as_spritesheet('orbit-spritesheet.png', row_length=10, fps=3) # Render as a spritesheet 144 145 d.display_inline() # Display as interactive SVG 145 146 ``` 146 147
+12
drawsvg/drawing.py
··· 365 365 self.as_mp4( 366 366 fname, fps=fps, duration=duration, context=context, 367 367 verbose=verbose) 368 + def save_spritesheet(self, fname, fps=10, duration=None, context=None, 369 + row_length=None, verbose=False): 370 + self.as_spritesheet( 371 + fname, fps=fps, duration=duration, context=context, 372 + row_length=row_length, verbose=verbose) 368 373 def as_video(self, to_file=None, fps=10, duration=None, 369 374 mime_type=None, file_type=None, context=None, verbose=False): 370 375 if file_type is None and mime_type is None: ··· 391 396 return self.as_video( 392 397 to_file=to_file, fps=fps, duration=duration, context=context, 393 398 mime_type='video/mp4', file_type='mp4', verbose=verbose) 399 + def as_spritesheet(self, to_file=None, fps=10, duration=None, context=None, 400 + row_length=None, verbose=False): 401 + frames = self.as_animation_frames( 402 + fps=fps, duration=duration, context=context) 403 + sheet = video.render_spritesheet( 404 + frames, row_length=row_length, verbose=verbose) 405 + return raster.Raster.from_arr(sheet, out_file=to_file) 394 406 def _repr_svg_(self): 395 407 '''Display in Jupyter notebook.''' 396 408 return self.as_svg(randomize_ids=True)
+24
drawsvg/raster.py
··· 25 25 ) from e 26 26 return cairosvg 27 27 28 + def delay_import_imageio(): 29 + try: 30 + import imageio 31 + except ImportError as e: 32 + raise ImportError( 33 + 'Optional dependencies not installed. ' 34 + 'Install with `python3 -m pip install "drawsvg[all]"` ' 35 + 'or `python3 -m pip install "drawsvg[raster]"`. ' 36 + 'See https://github.com/cduck/drawsvg#full-feature-install ' 37 + 'for more details.' 38 + ) from e 39 + return imageio 40 + 28 41 29 42 class Raster: 30 43 def __init__(self, png_data=None, png_file=None): ··· 43 56 cairosvg = delay_import_cairo() 44 57 cairosvg.svg2png(bytestring=svg_data, write_to=out_file) 45 58 return Raster(None, png_file=out_file) 59 + @staticmethod 60 + def from_arr(arr, out_file=None): 61 + imageio = delay_import_imageio() 62 + if out_file is None: 63 + with io.BytesIO() as f: 64 + imageio.imwrite(f, arr, format='png') 65 + f.seek(0) 66 + return Raster(f.read()) 67 + else: 68 + imageio.imwrite(out_file, arr, format='png') 69 + return Raster(None, png_file=out_file) 46 70 def _repr_png_(self): 47 71 if self.png_data: 48 72 return self.png_data
+31 -5
drawsvg/video.py
··· 149 149 print(f'Converting to video') 150 150 imageio.mimsave(file, frames, **kwargs) 151 151 152 - def save_spritesheet(frames, file, row_length=None, verbose=False, **kwargs): 152 + def render_spritesheet(frames, row_length=None, verbose=False, **kwargs): 153 153 ''' 154 154 Save a series of drawings as a bitmap spritesheet 155 155 156 156 Arguments: 157 157 frames: A list of `Drawing`s or a list of `numpy.array`s. 158 - file: File name or file like object to write the spritesheet to. The 159 - extension determines the output format. 160 158 row_length: The length (in frames) of one row in the spritesheet. 161 159 If not provided, all frames go on one row. 162 160 align_bottom: If frames are different sizes, align the bottoms of each ··· 168 166 **kwargs: Other arguments to imageio.imsave(). 169 167 170 168 ''' 171 - np, imageio = delay_import_np_imageio() 169 + np, _ = delay_import_np_imageio() 172 170 if not isinstance(frames[0], np.ndarray): 173 171 frames = render_svg_frames(frames, verbose=verbose, **kwargs) 174 172 kwargs.pop('align_bottom', None) 175 173 kwargs.pop('align_right', None) 176 - kwargs.pop('bg', None) 174 + bg = kwargs.pop('bg', (255, 255, 255, 255)) 177 175 178 176 cols = row_length if row_length is not None else len(frames) 179 177 rows = (len(frames) - 1) // cols + 1 180 178 181 179 if rows * cols > len(frames): # Unfilled final row 182 180 empty_frame = np.zeros(frames[0].shape, dtype=frames[0].dtype) 181 + empty_frame[..., :] = bg[:empty_frame.shape[-1]] 183 182 frames.extend([empty_frame] * (rows * cols - len(frames))) 184 183 185 184 block_arrangement = [] ··· 190 189 ]) 191 190 192 191 spritesheet = np.block(block_arrangement) 192 + return spritesheet 193 + 194 + def save_spritesheet(frames, file, row_length=None, verbose=False, **kwargs): 195 + ''' 196 + Save a series of drawings as a bitmap spritesheet 197 + 198 + Arguments: 199 + frames: A list of `Drawing`s or a list of `numpy.array`s. 200 + file: File name or file like object to write the spritesheet to. The 201 + extension determines the output format. 202 + row_length: The length (in frames) of one row in the spritesheet. 203 + If not provided, all frames go on one row. 204 + align_bottom: If frames are different sizes, align the bottoms of each 205 + frame in the video. 206 + align_right: If frames are different sizes, align the right edge of each 207 + frame in the video. 208 + bg: If frames are different sizes, fill the background with this color. 209 + (default is white: (255, 255, 255, 255)) 210 + **kwargs: Other arguments to imageio.imsave(). 211 + 212 + ''' 213 + _, imageio = delay_import_np_imageio() 214 + spritesheet = render_spritesheet( 215 + frames, row_length=row_length, verbose=verbose, **kwargs) 216 + kwargs.pop('align_bottom', None) 217 + kwargs.pop('align_right', None) 218 + kwargs.pop('bg', None) 193 219 imageio.imsave(file, spritesheet, **kwargs)
examples/orbit-spritesheet.png

This is a binary file and will not be displayed.