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