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