##// END OF EJS Templates
make element_id kwarg only.
Matthias Bussonnier -
Show More
@@ -1,643 +1,643 b''
1 1 """Various display related classes.
2 2
3 3 Authors : MinRK, gregcaporaso, dannystaple
4 4 """
5 5 from html import escape as html_escape
6 6 from os.path import exists, isfile, splitext, abspath, join, isdir
7 7 from os import walk, sep, fsdecode
8 8
9 9 from IPython.core.display import DisplayObject, TextDisplayObject
10 10
11 11 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
12 12 'FileLink', 'FileLinks', 'Code']
13 13
14 14
15 15 class Audio(DisplayObject):
16 16 """Create an audio object.
17 17
18 18 When this object is returned by an input cell or passed to the
19 19 display function, it will result in Audio controls being displayed
20 20 in the frontend (only works in the notebook).
21 21
22 22 Parameters
23 23 ----------
24 24 data : numpy array, list, unicode, str or bytes
25 25 Can be one of
26 26
27 27 * Numpy 1d array containing the desired waveform (mono)
28 28 * Numpy 2d array containing waveforms for each channel.
29 29 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
30 30 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
31 31 * List of float or integer representing the waveform (mono)
32 32 * String containing the filename
33 33 * Bytestring containing raw PCM data or
34 34 * URL pointing to a file on the web.
35 35
36 36 If the array option is used, the waveform will be normalized.
37 37
38 38 If a filename or url is used, the format support will be browser
39 39 dependent.
40 40 url : unicode
41 41 A URL to download the data from.
42 42 filename : unicode
43 43 Path to a local file to load the data from.
44 44 embed : boolean
45 45 Should the audio data be embedded using a data URI (True) or should
46 46 the original source be referenced. Set this to True if you want the
47 47 audio to playable later with no internet connection in the notebook.
48 48
49 49 Default is `True`, unless the keyword argument `url` is set, then
50 50 default value is `False`.
51 51 rate : integer
52 52 The sampling rate of the raw data.
53 53 Only required when data parameter is being used as an array
54 54 autoplay : bool
55 55 Set to True if the audio should immediately start playing.
56 56 Default is `False`.
57 57 normalize : bool
58 58 Whether audio should be normalized (rescaled) to the maximum possible
59 59 range. Default is `True`. When set to `False`, `data` must be between
60 60 -1 and 1 (inclusive), otherwise an error is raised.
61 61 Applies only when `data` is a list or array of samples; other types of
62 62 audio are never normalized.
63 63
64 64 Examples
65 65 --------
66 66 ::
67 67
68 68 # Generate a sound
69 69 import numpy as np
70 70 framerate = 44100
71 71 t = np.linspace(0,5,framerate*5)
72 72 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
73 73 Audio(data,rate=framerate)
74 74
75 75 # Can also do stereo or more channels
76 76 dataleft = np.sin(2*np.pi*220*t)
77 77 dataright = np.sin(2*np.pi*224*t)
78 78 Audio([dataleft, dataright],rate=framerate)
79 79
80 80 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
81 81 Audio(url="http://www.w3schools.com/html/horse.ogg")
82 82
83 83 Audio('/path/to/sound.wav') # From file
84 84 Audio(filename='/path/to/sound.ogg')
85 85
86 86 Audio(b'RAW_WAV_DATA..) # From bytes
87 87 Audio(data=b'RAW_WAV_DATA..)
88 88
89 89 """
90 90 _read_flags = 'rb'
91 91
92 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True,
92 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True, *,
93 93 element_id=None):
94 94 if filename is None and url is None and data is None:
95 95 raise ValueError("No audio data found. Expecting filename, url, or data.")
96 96 if embed is False and url is None:
97 97 raise ValueError("No url found. Expecting url when embed=False")
98 98
99 99 if url is not None and embed is not True:
100 100 self.embed = False
101 101 else:
102 102 self.embed = True
103 103 self.autoplay = autoplay
104 104 self.element_id = element_id
105 105 super(Audio, self).__init__(data=data, url=url, filename=filename)
106 106
107 107 if self.data is not None and not isinstance(self.data, bytes):
108 108 if rate is None:
109 109 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
110 110 self.data = Audio._make_wav(data, rate, normalize)
111 111
112 112 def reload(self):
113 113 """Reload the raw data from file or URL."""
114 114 import mimetypes
115 115 if self.embed:
116 116 super(Audio, self).reload()
117 117
118 118 if self.filename is not None:
119 119 self.mimetype = mimetypes.guess_type(self.filename)[0]
120 120 elif self.url is not None:
121 121 self.mimetype = mimetypes.guess_type(self.url)[0]
122 122 else:
123 123 self.mimetype = "audio/wav"
124 124
125 125 @staticmethod
126 126 def _make_wav(data, rate, normalize):
127 127 """ Transform a numpy array to a PCM bytestring """
128 128 import struct
129 129 from io import BytesIO
130 130 import wave
131 131
132 132 try:
133 133 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
134 134 except ImportError:
135 135 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
136 136
137 137 fp = BytesIO()
138 138 waveobj = wave.open(fp,mode='wb')
139 139 waveobj.setnchannels(nchan)
140 140 waveobj.setframerate(rate)
141 141 waveobj.setsampwidth(2)
142 142 waveobj.setcomptype('NONE','NONE')
143 143 waveobj.writeframes(b''.join([struct.pack('<h',x) for x in scaled]))
144 144 val = fp.getvalue()
145 145 waveobj.close()
146 146
147 147 return val
148 148
149 149 @staticmethod
150 150 def _validate_and_normalize_with_numpy(data, normalize):
151 151 import numpy as np
152 152
153 153 data = np.array(data, dtype=float)
154 154 if len(data.shape) == 1:
155 155 nchan = 1
156 156 elif len(data.shape) == 2:
157 157 # In wave files,channels are interleaved. E.g.,
158 158 # "L1R1L2R2..." for stereo. See
159 159 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
160 160 # for channel ordering
161 161 nchan = data.shape[0]
162 162 data = data.T.ravel()
163 163 else:
164 164 raise ValueError('Array audio input must be a 1D or 2D array')
165 165
166 166 max_abs_value = np.max(np.abs(data))
167 167 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
168 168 scaled = np.int16(data / normalization_factor * 32767).tolist()
169 169 return scaled, nchan
170 170
171 171
172 172 @staticmethod
173 173 def _validate_and_normalize_without_numpy(data, normalize):
174 174 try:
175 175 max_abs_value = float(max([abs(x) for x in data]))
176 176 except TypeError:
177 177 raise TypeError('Only lists of mono audio are '
178 178 'supported if numpy is not installed')
179 179
180 180 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
181 181 scaled = [int(x / normalization_factor * 32767) for x in data]
182 182 nchan = 1
183 183 return scaled, nchan
184 184
185 185 @staticmethod
186 186 def _get_normalization_factor(max_abs_value, normalize):
187 187 if not normalize and max_abs_value > 1:
188 188 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
189 189 return max_abs_value if normalize else 1
190 190
191 191 def _data_and_metadata(self):
192 192 """shortcut for returning metadata with url information, if defined"""
193 193 md = {}
194 194 if self.url:
195 195 md['url'] = self.url
196 196 if md:
197 197 return self.data, md
198 198 else:
199 199 return self.data
200 200
201 201 def _repr_html_(self):
202 202 src = """
203 203 <audio {element_id} controls="controls" {autoplay}>
204 204 <source src="{src}" type="{type}" />
205 205 Your browser does not support the audio element.
206 206 </audio>
207 207 """
208 208 return src.format(src=self.src_attr(), type=self.mimetype, autoplay=self.autoplay_attr(),
209 209 element_id=self.element_id_attr())
210 210
211 211 def src_attr(self):
212 212 import base64
213 213 if self.embed and (self.data is not None):
214 214 data = base64=base64.b64encode(self.data).decode('ascii')
215 215 return """data:{type};base64,{base64}""".format(type=self.mimetype,
216 216 base64=data)
217 217 elif self.url is not None:
218 218 return self.url
219 219 else:
220 220 return ""
221 221
222 222 def autoplay_attr(self):
223 223 if(self.autoplay):
224 224 return 'autoplay="autoplay"'
225 225 else:
226 226 return ''
227 227
228 228 def element_id_attr(self):
229 229 if (self.element_id):
230 230 return 'id="{element_id}"'.format(element_id=self.element_id)
231 231 else:
232 232 return ''
233 233
234 234 class IFrame(object):
235 235 """
236 236 Generic class to embed an iframe in an IPython notebook
237 237 """
238 238
239 239 iframe = """
240 240 <iframe
241 241 width="{width}"
242 242 height="{height}"
243 243 src="{src}{params}"
244 244 frameborder="0"
245 245 allowfullscreen
246 246 ></iframe>
247 247 """
248 248
249 249 def __init__(self, src, width, height, **kwargs):
250 250 self.src = src
251 251 self.width = width
252 252 self.height = height
253 253 self.params = kwargs
254 254
255 255 def _repr_html_(self):
256 256 """return the embed iframe"""
257 257 if self.params:
258 258 try:
259 259 from urllib.parse import urlencode # Py 3
260 260 except ImportError:
261 261 from urllib import urlencode
262 262 params = "?" + urlencode(self.params)
263 263 else:
264 264 params = ""
265 265 return self.iframe.format(src=self.src,
266 266 width=self.width,
267 267 height=self.height,
268 268 params=params)
269 269
270 270 class YouTubeVideo(IFrame):
271 271 """Class for embedding a YouTube Video in an IPython session, based on its video id.
272 272
273 273 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
274 274 do::
275 275
276 276 vid = YouTubeVideo("foo")
277 277 display(vid)
278 278
279 279 To start from 30 seconds::
280 280
281 281 vid = YouTubeVideo("abc", start=30)
282 282 display(vid)
283 283
284 284 To calculate seconds from time as hours, minutes, seconds use
285 285 :class:`datetime.timedelta`::
286 286
287 287 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
288 288
289 289 Other parameters can be provided as documented at
290 290 https://developers.google.com/youtube/player_parameters#Parameters
291 291
292 292 When converting the notebook using nbconvert, a jpeg representation of the video
293 293 will be inserted in the document.
294 294 """
295 295
296 296 def __init__(self, id, width=400, height=300, **kwargs):
297 297 self.id=id
298 298 src = "https://www.youtube.com/embed/{0}".format(id)
299 299 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
300 300
301 301 def _repr_jpeg_(self):
302 302 # Deferred import
303 303 from urllib.request import urlopen
304 304
305 305 try:
306 306 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
307 307 except IOError:
308 308 return None
309 309
310 310 class VimeoVideo(IFrame):
311 311 """
312 312 Class for embedding a Vimeo video in an IPython session, based on its video id.
313 313 """
314 314
315 315 def __init__(self, id, width=400, height=300, **kwargs):
316 316 src="https://player.vimeo.com/video/{0}".format(id)
317 317 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
318 318
319 319 class ScribdDocument(IFrame):
320 320 """
321 321 Class for embedding a Scribd document in an IPython session
322 322
323 323 Use the start_page params to specify a starting point in the document
324 324 Use the view_mode params to specify display type one off scroll | slideshow | book
325 325
326 326 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
327 327
328 328 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
329 329 """
330 330
331 331 def __init__(self, id, width=400, height=300, **kwargs):
332 332 src="https://www.scribd.com/embeds/{0}/content".format(id)
333 333 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
334 334
335 335 class FileLink(object):
336 336 """Class for embedding a local file link in an IPython session, based on path
337 337
338 338 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
339 339
340 340 you would do::
341 341
342 342 local_file = FileLink("my/data.txt")
343 343 display(local_file)
344 344
345 345 or in the HTML notebook, just::
346 346
347 347 FileLink("my/data.txt")
348 348 """
349 349
350 350 html_link_str = "<a href='%s' target='_blank'>%s</a>"
351 351
352 352 def __init__(self,
353 353 path,
354 354 url_prefix='',
355 355 result_html_prefix='',
356 356 result_html_suffix='<br>'):
357 357 """
358 358 Parameters
359 359 ----------
360 360 path : str
361 361 path to the file or directory that should be formatted
362 362 url_prefix : str
363 363 prefix to be prepended to all files to form a working link [default:
364 364 '']
365 365 result_html_prefix : str
366 366 text to append to beginning to link [default: '']
367 367 result_html_suffix : str
368 368 text to append at the end of link [default: '<br>']
369 369 """
370 370 if isdir(path):
371 371 raise ValueError("Cannot display a directory using FileLink. "
372 372 "Use FileLinks to display '%s'." % path)
373 373 self.path = fsdecode(path)
374 374 self.url_prefix = url_prefix
375 375 self.result_html_prefix = result_html_prefix
376 376 self.result_html_suffix = result_html_suffix
377 377
378 378 def _format_path(self):
379 379 fp = ''.join([self.url_prefix, html_escape(self.path)])
380 380 return ''.join([self.result_html_prefix,
381 381 self.html_link_str % \
382 382 (fp, html_escape(self.path, quote=False)),
383 383 self.result_html_suffix])
384 384
385 385 def _repr_html_(self):
386 386 """return html link to file
387 387 """
388 388 if not exists(self.path):
389 389 return ("Path (<tt>%s</tt>) doesn't exist. "
390 390 "It may still be in the process of "
391 391 "being generated, or you may have the "
392 392 "incorrect path." % self.path)
393 393
394 394 return self._format_path()
395 395
396 396 def __repr__(self):
397 397 """return absolute path to file
398 398 """
399 399 return abspath(self.path)
400 400
401 401 class FileLinks(FileLink):
402 402 """Class for embedding local file links in an IPython session, based on path
403 403
404 404 e.g. to embed links to files that were generated in the IPython notebook
405 405 under ``my/data``, you would do::
406 406
407 407 local_files = FileLinks("my/data")
408 408 display(local_files)
409 409
410 410 or in the HTML notebook, just::
411 411
412 412 FileLinks("my/data")
413 413 """
414 414 def __init__(self,
415 415 path,
416 416 url_prefix='',
417 417 included_suffixes=None,
418 418 result_html_prefix='',
419 419 result_html_suffix='<br>',
420 420 notebook_display_formatter=None,
421 421 terminal_display_formatter=None,
422 422 recursive=True):
423 423 """
424 424 See :class:`FileLink` for the ``path``, ``url_prefix``,
425 425 ``result_html_prefix`` and ``result_html_suffix`` parameters.
426 426
427 427 included_suffixes : list
428 428 Filename suffixes to include when formatting output [default: include
429 429 all files]
430 430
431 431 notebook_display_formatter : function
432 432 Used to format links for display in the notebook. See discussion of
433 433 formatter functions below.
434 434
435 435 terminal_display_formatter : function
436 436 Used to format links for display in the terminal. See discussion of
437 437 formatter functions below.
438 438
439 439 Formatter functions must be of the form::
440 440
441 441 f(dirname, fnames, included_suffixes)
442 442
443 443 dirname : str
444 444 The name of a directory
445 445 fnames : list
446 446 The files in that directory
447 447 included_suffixes : list
448 448 The file suffixes that should be included in the output (passing None
449 449 meansto include all suffixes in the output in the built-in formatters)
450 450 recursive : boolean
451 451 Whether to recurse into subdirectories. Default is True.
452 452
453 453 The function should return a list of lines that will be printed in the
454 454 notebook (if passing notebook_display_formatter) or the terminal (if
455 455 passing terminal_display_formatter). This function is iterated over for
456 456 each directory in self.path. Default formatters are in place, can be
457 457 passed here to support alternative formatting.
458 458
459 459 """
460 460 if isfile(path):
461 461 raise ValueError("Cannot display a file using FileLinks. "
462 462 "Use FileLink to display '%s'." % path)
463 463 self.included_suffixes = included_suffixes
464 464 # remove trailing slashes for more consistent output formatting
465 465 path = path.rstrip('/')
466 466
467 467 self.path = path
468 468 self.url_prefix = url_prefix
469 469 self.result_html_prefix = result_html_prefix
470 470 self.result_html_suffix = result_html_suffix
471 471
472 472 self.notebook_display_formatter = \
473 473 notebook_display_formatter or self._get_notebook_display_formatter()
474 474 self.terminal_display_formatter = \
475 475 terminal_display_formatter or self._get_terminal_display_formatter()
476 476
477 477 self.recursive = recursive
478 478
479 479 def _get_display_formatter(self,
480 480 dirname_output_format,
481 481 fname_output_format,
482 482 fp_format,
483 483 fp_cleaner=None):
484 484 """ generate built-in formatter function
485 485
486 486 this is used to define both the notebook and terminal built-in
487 487 formatters as they only differ by some wrapper text for each entry
488 488
489 489 dirname_output_format: string to use for formatting directory
490 490 names, dirname will be substituted for a single "%s" which
491 491 must appear in this string
492 492 fname_output_format: string to use for formatting file names,
493 493 if a single "%s" appears in the string, fname will be substituted
494 494 if two "%s" appear in the string, the path to fname will be
495 495 substituted for the first and fname will be substituted for the
496 496 second
497 497 fp_format: string to use for formatting filepaths, must contain
498 498 exactly two "%s" and the dirname will be subsituted for the first
499 499 and fname will be substituted for the second
500 500 """
501 501 def f(dirname, fnames, included_suffixes=None):
502 502 result = []
503 503 # begin by figuring out which filenames, if any,
504 504 # are going to be displayed
505 505 display_fnames = []
506 506 for fname in fnames:
507 507 if (isfile(join(dirname,fname)) and
508 508 (included_suffixes is None or
509 509 splitext(fname)[1] in included_suffixes)):
510 510 display_fnames.append(fname)
511 511
512 512 if len(display_fnames) == 0:
513 513 # if there are no filenames to display, don't print anything
514 514 # (not even the directory name)
515 515 pass
516 516 else:
517 517 # otherwise print the formatted directory name followed by
518 518 # the formatted filenames
519 519 dirname_output_line = dirname_output_format % dirname
520 520 result.append(dirname_output_line)
521 521 for fname in display_fnames:
522 522 fp = fp_format % (dirname,fname)
523 523 if fp_cleaner is not None:
524 524 fp = fp_cleaner(fp)
525 525 try:
526 526 # output can include both a filepath and a filename...
527 527 fname_output_line = fname_output_format % (fp, fname)
528 528 except TypeError:
529 529 # ... or just a single filepath
530 530 fname_output_line = fname_output_format % fname
531 531 result.append(fname_output_line)
532 532 return result
533 533 return f
534 534
535 535 def _get_notebook_display_formatter(self,
536 536 spacer="&nbsp;&nbsp;"):
537 537 """ generate function to use for notebook formatting
538 538 """
539 539 dirname_output_format = \
540 540 self.result_html_prefix + "%s/" + self.result_html_suffix
541 541 fname_output_format = \
542 542 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
543 543 fp_format = self.url_prefix + '%s/%s'
544 544 if sep == "\\":
545 545 # Working on a platform where the path separator is "\", so
546 546 # must convert these to "/" for generating a URI
547 547 def fp_cleaner(fp):
548 548 # Replace all occurrences of backslash ("\") with a forward
549 549 # slash ("/") - this is necessary on windows when a path is
550 550 # provided as input, but we must link to a URI
551 551 return fp.replace('\\','/')
552 552 else:
553 553 fp_cleaner = None
554 554
555 555 return self._get_display_formatter(dirname_output_format,
556 556 fname_output_format,
557 557 fp_format,
558 558 fp_cleaner)
559 559
560 560 def _get_terminal_display_formatter(self,
561 561 spacer=" "):
562 562 """ generate function to use for terminal formatting
563 563 """
564 564 dirname_output_format = "%s/"
565 565 fname_output_format = spacer + "%s"
566 566 fp_format = '%s/%s'
567 567
568 568 return self._get_display_formatter(dirname_output_format,
569 569 fname_output_format,
570 570 fp_format)
571 571
572 572 def _format_path(self):
573 573 result_lines = []
574 574 if self.recursive:
575 575 walked_dir = list(walk(self.path))
576 576 else:
577 577 walked_dir = [next(walk(self.path))]
578 578 walked_dir.sort()
579 579 for dirname, subdirs, fnames in walked_dir:
580 580 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
581 581 return '\n'.join(result_lines)
582 582
583 583 def __repr__(self):
584 584 """return newline-separated absolute paths
585 585 """
586 586 result_lines = []
587 587 if self.recursive:
588 588 walked_dir = list(walk(self.path))
589 589 else:
590 590 walked_dir = [next(walk(self.path))]
591 591 walked_dir.sort()
592 592 for dirname, subdirs, fnames in walked_dir:
593 593 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
594 594 return '\n'.join(result_lines)
595 595
596 596
597 597 class Code(TextDisplayObject):
598 598 """Display syntax-highlighted source code.
599 599
600 600 This uses Pygments to highlight the code for HTML and Latex output.
601 601
602 602 Parameters
603 603 ----------
604 604 data : str
605 605 The code as a string
606 606 url : str
607 607 A URL to fetch the code from
608 608 filename : str
609 609 A local filename to load the code from
610 610 language : str
611 611 The short name of a Pygments lexer to use for highlighting.
612 612 If not specified, it will guess the lexer based on the filename
613 613 or the code. Available lexers: http://pygments.org/docs/lexers/
614 614 """
615 615 def __init__(self, data=None, url=None, filename=None, language=None):
616 616 self.language = language
617 617 super().__init__(data=data, url=url, filename=filename)
618 618
619 619 def _get_lexer(self):
620 620 if self.language:
621 621 from pygments.lexers import get_lexer_by_name
622 622 return get_lexer_by_name(self.language)
623 623 elif self.filename:
624 624 from pygments.lexers import get_lexer_for_filename
625 625 return get_lexer_for_filename(self.filename)
626 626 else:
627 627 from pygments.lexers import guess_lexer
628 628 return guess_lexer(self.data)
629 629
630 630 def __repr__(self):
631 631 return self.data
632 632
633 633 def _repr_html_(self):
634 634 from pygments import highlight
635 635 from pygments.formatters import HtmlFormatter
636 636 fmt = HtmlFormatter()
637 637 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
638 638 return style + highlight(self.data, self._get_lexer(), fmt)
639 639
640 640 def _repr_latex_(self):
641 641 from pygments import highlight
642 642 from pygments.formatters import LatexFormatter
643 643 return highlight(self.data, self._get_lexer(), LatexFormatter())
General Comments 0
You need to be logged in to leave comments. Login now