Exemplo n.º 1
0
    def _gen_promotional_banner(total_width, params=None):
        """Generate promotion banner.

        This is the promotional banner for the storyboard package.

        Parameters
        ----------
        total_width : int
            Total width of the metadata sheet. Usually determined by the
            width of the bare storyboard.
        params : dict, optional
            Optional parameters enclosed in a dict. Default is
            ``None``. See the "Other Parameters" section for understood
            key/value pairs.

        Returns
        -------
        banner : PIL.Image.Image

        Other Parameters
        ----------------
        text_font : Font, optional
            Default is the font constructed by ``Font()`` without
            arguments.
        text_color: color, optional
            Default is 'black'.
        background_color: color, optional
            Default is 'white'.

        """

        if params is None:
            params = {}
        text_font = _read_param(params, 'text_font', Font())
        text_color = _read_param(params, 'text_color', 'black')
        background_color = _read_param(params, 'background_color', 'white')

        text = ("Generated by storyboard version %s. "
                "Fork me on GitHub: git.io/storyboard" % version.__version__)
        text_width, total_height = draw_text_block(None, (0, 0),
                                                   text,
                                                   params={
                                                       'font': text_font,
                                                       'dry_run': True,
                                                   })

        banner = Image.new('RGBA', (total_width, total_height),
                           background_color)
        # center the text -- calculate the x coordinate of its topleft
        # corner
        text_x = int((total_width - text_width) / 2)
        draw_text_block(banner, (text_x, 0),
                        text,
                        params={
                            'font': text_font,
                            'color': text_color,
                        })

        return banner
Exemplo n.º 2
0
    def __init__(self, video, params=None):
        """Initialize the StoryBoard class.

        See the module docstring for parameters and exceptions.

        """

        if params is None:
            params = {}
        if 'bins' in params and params['bins'] is not None:
            bins = params['bins']
            assert isinstance(bins, tuple) and len(bins) == 2
        else:
            bins = fflocate.guess_bins()
        frame_codec = _read_param(params, 'frame_codec', 'png')
        video_duration = _read_param(params, 'video_duration', None)
        print_progress = _read_param(params, 'print_progress', False)

        fflocate.check_bins(bins)

        # seek frame by frame if video duration is specially given
        # (indicating that normal input seeking may not work)
        self._seek_frame_by_frame = video_duration is not None

        self._bins = bins
        if isinstance(video, metadata.Video):
            self.video = video
        elif isinstance(video, str):
            self.video = metadata.Video(video,
                                        params={
                                            'ffprobe_bin': bins[1],
                                            'video_duration': video_duration,
                                            'print_progress': print_progress,
                                        })
        else:
            raise ValueError("expected str or storyboard.metadata.Video "
                             "for the video argument, got %s" %
                             type(video).__name__)
        self.frames = []
        self._frame_codec = frame_codec
Exemplo n.º 3
0
    def compute_sha1sum(self, params=None):
        """Computes the SHA-1 digest of the video file.

        Parameters
        ----------
        params : dict, optional
            Optional parameters enclosed in a dict. Default is ``None``.
            See the "Other Parameters" section for understood key/value
            pairs.

        Returns
        -------
        sha1sum : str
            The SHA-1 hex digest of the video file (40 character
            hexadecimal string).

        Other Parameters
        ----------------
        print_progress : bool, optional
            Whether to print progress information (to stderr). Default
            is False.

        Notes
        -----
        Since computing SHA-1 digest is an expensive operation, the
        digest is only calculated and set upon request, either through
        this method or `format_metadata` with the ``include_sha1sum``
        optional parameter set to ``True``. Further requests load the
        calculated value rather than repeat the computation.

        """

        self.__dp("entered StoryBoard.compute_sha1sum")
        if params is None:
            params = {}
        print_progress = _read_param(params, 'print_progress', False)

        self.__dp("left StoryBoard.compute_sha1sum")
        return self._get_sha1sum(print_progress=print_progress)
Exemplo n.º 4
0
    def format_metadata(self, params=None):
        """Return video metadata in one formatted string.

        Parameters
        ----------
        params : dict, optional
            Optional parameters enclosed in a dict. Default is ``None``.
            See the "Other Parameters" section for understood key/value
            pairs.

        Returns
        -------
        str
            A formatted string loaded with video and per-stream
            metadata, which can be printed directly. See the "Examples"
            section for a printed example.

        Other Parameters
        ----------------
        include_sha1sum : bool, optional
            Whether to include the SHA-1 hex digest. Default is
            False. Keep in mind that computing SHA-1 digest is an
            expensive operation, and hence is only performed upon
            request.
        print_progress : bool, optional
            Whether to print progress information (to stderr). Default
            is False.

        Examples
        --------
        >>> import os
        >>> import tempfile
        >>> import requests
        >>> video_uri = 'https://dl.bintray.com/zmwangx/pypi/sample-h264-aac-srt.mkv'
        >>> tempdir = tempfile.mkdtemp()
        >>> video_file = os.path.join(tempdir, 'sample-h264-aac-srt.mkv')
        >>> r = requests.get(video_uri, stream=True)
        >>> with open(video_file, 'wb') as fd:
        ...     for chunk in r.iter_content(65536):
        ...         bytes_written = fd.write(chunk)
        >>> print(Video(video_file).format_metadata({'include_sha1sum': True}))
        Title:                  Example video: H.264 + AAC + SRT in Matroska container
        Filename:               sample-h264-aac-srt.mkv
        File size:              4842 (4.73KiB)
        SHA-1 digest:           95e7d9f9359d8d7ba4ec441bc8cb3830a58ee102
        Container format:       Matroska
        Duration:               00:00:02.08
        Pixel dimensions:       128x72
        Display aspect ratio:   16:9
        Scan type:              Progressive scan
        Frame rate:             25 fps
        Streams:
            #0: Video, H.264 (High Profile level 1.0), 128x72 (DAR 16:9), 25 fps
            #1: Audio (und), AAC (Low-Complexity)
            #2: Subtitle, SubRip
        >>> os.remove(video_file)
        >>> os.rmdir(tempdir)
        """

        self.__dp("entered StoryBoard.format_metadata")
        if params is None:
            params = {}
        include_sha1sum = _read_param(params, 'include_sha1sum', False)
        print_progress = _read_param(params, 'print_progress', False)

        lines = []  # holds the lines that will be joined in the end
        # title
        if self.title:
            lines.append("Title:                  %s" % self.title)
        # filename
        lines.append("Filename:               %s" % self.filename)
        # size
        lines.append("File size:              %d (%s)" %
                     (self.size, self.size_text))
        # sha1sum
        if include_sha1sum:
            self._get_sha1sum(print_progress)
            lines.append("SHA-1 digest:           %s" % self.sha1sum)
        # container format
        lines.append("Container format:       %s" % self.format)
        # duration
        if self.duration_text:
            lines.append("Duration:               %s" % self.duration_text)
        else:
            lines.append("Duration:               Not available")
        # dimension
        if self.dimension_text:
            lines.append("Pixel dimensions:       %s" % self.dimension_text)
        # aspect ratio
        if self.dar_text:
            lines.append("Display aspect ratio:   %s" % self.dar_text)
        # scanning type
        if self.scan_type:
            lines.append("Scan type:              %s" % self.scan_type)
        # frame rate
        if self.frame_rate:
            lines.append("Frame rate:             %s" % self.frame_rate_text)
        # streams
        lines.append("Streams:")
        for stream in self.streams:
            lines.append("    #%d: %s" % (stream.index, stream.info_string))
        self.__dp("left StoryBoard.format_metadata")
        return '\n'.join(lines).strip()
Exemplo n.º 5
0
    def __init__(self, video, params=None):
        """Initialize the Video class.

        See class docstring for parameters of the constructor.

        """

        if params is None:
            params = {}
        if 'debug' in params and params['debug']:
            self.__debug = True
        self.__dp("entered StoryBoard.__init__")
        if 'ffprobe_bin' in params:
            ffprobe_bin = params['ffprobe_bin']
        else:
            _, ffprobe_bin = fflocate.guess_bins()
        video_duration = _read_param(params, 'video_duration', None)
        print_progress = _read_param(params, 'print_progress', False)

        self.path = os.path.abspath(video)
        if not os.path.exists(self.path):
            raise OSError("'" + video + "' does not exist")
        self.filename = os.path.basename(self.path)
        if hasattr(self.filename, 'decode'):
            # python2 str, need to be decoded to unicode for proper
            # printing
            self.filename = self.filename.decode('utf-8')

        if print_progress:
            sys.stderr.write("Processing %s\n" % self.filename)
            sys.stderr.write("Crunching metadata...\n")

        self._call_ffprobe(ffprobe_bin)

        self.title = self._get_title()
        self.format = self._get_format()
        self.size, self.size_text = self._get_size()
        if video_duration is None:
            self.duration, self.duration_text = self._get_duration()
        else:
            self.duration = video_duration
            self.duration_text = util.humantime(video_duration)
        self.sha1sum = None  # SHA-1 digest is generated upon request

        # the remaining attributes will be dynamically set when parsing
        # streams
        self.dimension = None
        self.dimension_text = None
        self.frame_rate = None
        self.frame_rate_text = None
        self.dar = None
        self.dar_text = None

        self._process_streams()

        # detect if the file contains any video streams at all and try
        # to extract scan type only if it does
        for stream in self.streams:
            if stream.type == 'video':
                break
        else:
            # no video stream
            self.scan_type = None
            self.__dp("left StoryBoard.__init__")
            return
        self.scan_type = self._get_scan_type(ffprobe_bin, print_progress)
        self.__dp("left StoryBoard.__init__")
Exemplo n.º 6
0
    def _gen_metadata_sheet(self, total_width, params=None):
        """Generate metadata sheet.

        Parameters
        ----------
        total_width : int
            Total width of the metadata sheet. Usually determined by the
            width of the bare storyboard.
        params : dict, optional
            Optional parameters enclosed in a dict. Default is
            ``None``. See the "Other Parameters" section for understood
            key/value pairs.

        Returns
        -------
        metadata_sheet : PIL.Image.Image

        Other Parameters
        ----------------
        text_font : Font, optional
            Default is the font constructed by ``Font()`` without
            arguments.
        text_color: color, optional
            Default is 'black'.
        line_spacing : float, optional
            Line spacing as a float. Default is 1.2.
        background_color: color, optional
            Default is 'white'.
        include_sha1sum: bool, optional
            See the `include_sha1sum` option of
            `storyboard.metadata.Video.format_metadata`. Beware that
            computing SHA-1 digest is an expensive operation.
        print_progress : bool, optional
            Whether to print progress information (to stderr). Default
            is False.

        """

        if params is None:
            params = {}
        text_font = _read_param(params, 'text_font', Font())
        text_color = _read_param(params, 'text_color', 'black')
        line_spacing = _read_param(params, 'line_spacing', 1.2)
        background_color = _read_param(params, 'background_color', 'white')
        include_sha1sum = _read_param(params, 'include_sha1sum', False)
        print_progress = _read_param(params, 'print_progress', False)

        text = self.video.format_metadata(params={
            'include_sha1sum': include_sha1sum,
            'print_progress': print_progress,
        })

        _, total_height = draw_text_block(None, (0, 0),
                                          text,
                                          params={
                                              'font': text_font,
                                              'spacing': line_spacing,
                                              'dry_run': True,
                                          })

        metadata_sheet = Image.new('RGBA', (total_width, total_height),
                                   background_color)
        draw_text_block(metadata_sheet, (0, 0),
                        text,
                        params={
                            'font': text_font,
                            'color': text_color,
                            'spacing': line_spacing,
                        })

        return metadata_sheet
Exemplo n.º 7
0
    def _gen_bare_storyboard(self, tile, thumbnail_width, params=None):
        """Generate bare storyboard (thumbnails only).

        Parameters
        ----------
        tile : tuple
            A tuple ``(cols, rows)`` specifying the number of columns
            and rows for the array of thumbnails.
        thumbnail_width : int
            Width of each thumbnail.
        params : dict, optional
            Optional parameters enclosed in a dict. Default is
            ``None``. See the "Other Parameters" section for understood
            key/value pairs.

        Returns
        -------
        base_storyboard : PIL.Image.Image

        Other Parameters
        ----------------
        tile_spacing : tuple, optional
            See the `tile_spacing` parameter of the `tile_images`
            function. Default is ``(0, 0)``.

        background_color : color, optional
            See the `canvas_color` paramter of the `tile_images`
            function. Default is ``'white'``.

        thumbnail_aspect_ratio : float, optional
            Aspect ratio of generated thumbnails. If ``None``, first try
            to use the display aspect ratio of the video
            (``self.video.dar``), then the aspect ratio of the frames if
            ``self.video.dar`` is not available. Default is ``None``.

        draw_timestamp : bool, optional
            See the `draw_timestamp` parameter of the `create_thumbnail`
            function. Default is ``False``.
        timestamp_font : Font, optional
            See the `timestamp_font` parameter of the `create_thumbnail`
            function. Default is the font constructed by ``Font()``
            without arguments.
        timestamp_align : {'right', 'center', 'left'}, optional
            See the `timestamp_align` parameter of the
            `create_thumbnail` function. Default is ``'right'``.

        print_progress : bool, optional
            Whether to print progress information (to stderr). Default
            is False.

        """

        if params is None:
            params = {}
        tile_spacing = _read_param(params, 'tile_spacing', (0, 0))
        background_color = _read_param(params, 'background_color', 'white')
        if (('thumbnail_aspect_ratio' in params
             and params['thumbnail_aspect_ratio'] is not None)):
            thumbnail_aspect_ratio = params['thumbnail_aspect_ratio']
        elif self.video.dar is not None:
            thumbnail_aspect_ratio = self.video.dar
        else:
            # defer calculation to after generating frames
            thumbnail_aspect_ratio = None
        draw_timestamp = _read_param(params, 'draw_timestamp', False)
        if draw_timestamp:
            timestamp_font = _read_param(params, 'timestamp_font', Font())
            timestamp_align = _read_param(params, 'timestamp_align', 'right')
        print_progress = _read_param(params, 'print_progress', False)

        cols, rows = tile
        if (not (isinstance(cols, int) and isinstance(rows, int) and cols > 0
                 and rows > 0)):
            raise ValueError('tile is not a tuple of positive integers')
        thumbnail_count = cols * rows
        self.gen_frames(cols * rows,
                        params={
                            'print_progress': print_progress,
                        })
        if thumbnail_aspect_ratio is None:
            frame_size = self.frames[0].image.size
            thumbnail_aspect_ratio = frame_size[0] / frame_size[1]

        thumbnails = []
        counter = 0
        for frame in self.frames:
            counter += 1
            if print_progress:
                sys.stderr.write("\rGenerating thumbnail %d/%d..." %
                                 (counter, thumbnail_count))
            thumbnails.append(
                create_thumbnail(frame,
                                 thumbnail_width,
                                 params={
                                     'aspect_ratio': thumbnail_aspect_ratio,
                                     'draw_timestamp': draw_timestamp,
                                     'timestamp_font': timestamp_font,
                                     'timestamp_align': timestamp_align,
                                 }))
        if print_progress:
            sys.stderr.write("\n")

        if print_progress:
            sys.stderr.write("Tiling thumbnails...\n")
        return tile_images(thumbnails,
                           tile,
                           params={
                               'tile_spacing': tile_spacing,
                               'canvas_color': background_color,
                               'close_separate_images': True,
                           })
Exemplo n.º 8
0
    def gen_frames(self, count, params=None):
        """Extract equally spaced frames from the video.

        When tasked with extracting N frames, this method extracts them
        at positions 1/2N, 3/2N, 5/2N, ... , (2N-1)/2N of the video. The
        extracted frames are stored in the `frames` attribute.

        Note that new frames are extracted only if the number of
        existing frames in the `frames` attribute doesn't match the
        specified `count` (0 at instantiation), in which case new frames
        are extracted to match the specification, and the `frames`
        attribute is overwritten.

        Parameters
        ----------
        count : int
            Number of (equally-spaced) frames to generate.
        params : dict, optional
            Optional parameters enclosed in a dict. Default is
            ``None``. See the "Other Parameters" section for understood
            key/value pairs.

        Returns
        -------
        None
            Access the generated frames through the `frames` attribute.

        RAISES
        ------
        OSError
            If frame extraction with FFmpeg fails.

        Other Parameters
        ----------------
        print_progress : bool, optional
            Whether to print progress information (to stderr). Default
            is False.

        """

        if params is None:
            params = {}
        print_progress = _read_param(params, 'print_progress', False)

        if len(self.frames) == count:
            return

        duration = self.video.duration
        interval = duration / count
        timestamps = [interval * (i + 1 / 2) for i in range(0, count)]
        counter = 0
        for timestamp in timestamps:
            counter += 1
            if print_progress:
                sys.stderr.write("\rExtracting frame %d/%d..." %
                                 (counter, count))
            try:
                frame = _extract_frame(self.video.path,
                                       timestamp,
                                       params={
                                           'ffmpeg_bin':
                                           self._bins[0],
                                           'codec':
                                           self._frame_codec,
                                           'frame_by_frame':
                                           self._seek_frame_by_frame,
                                       })
                self.frames.append(frame)
            except:
                # \rExtracting frame %d/%d... isn't terminated by
                # newline yet
                if print_progress:
                    sys.stderr.write("\n")
                raise
        if print_progress:
            sys.stderr.write("\n")
Exemplo n.º 9
0
    def gen_storyboard(self, params=None):
        """Generate full storyboard.

        A full storyboard has three sections, arranged vertically in the
        order listed: a metadata sheet, a bare storyboard, and a
        promotional banner.

        The metadata sheet consists of formatted metadata generated by
        ``storyboard.metadata.Video.format_metadata``; you may choose
        whether to include the SHA-1 hex digest of the video file (see
        `include_sha1sum` in "Other Parameters"). The bare storyboard is
        an array (usually 4x4) of thumbnails, generated from equally
        spaced frames from the video. The promotional banner briefly
        promotes this package (storyboard).

        The metadata sheet and promotional banner are optional and can
        be turned off individually. See `include_metadata_sheet` and
        `include_promotional_banner` in "Other Parameters".

        `Here <https://i.imgur.com/9T2zM8R.jpg>`_ is a basic example of
        a full storyboard.

        Parameters
        ----------
        params : dict, optional
            Optional parameters enclosed in a dict. Default is
            ``None``. See the "Other Parameters" section for understood
            key/value pairs.

        Returns
        -------
        full_storyboard : PIL.Image.Image

        Other Parameters
        ----------------
        include_metadata_sheet: bool, optional
            Whether to include a video metadata sheet in the
            storyboard. Default is ``True``.
        include_promotional_banner: bool, optional
            Whether to include a short promotional banner about this
            package (storyboard) at the bottom of the
            storyboard. Default is ``True``.

        background_color : color, optional
            Background color of the storyboard, in any color format
            recognized by Pillow. Default is ``'white'``.
        section_spacing : int, optional
            Vertical spacing between adjacent sections (metadata sheet
            and bare storyboard, bare storyboard and promotional
            banner). If ``None``, use the vertical tile spacing (see
            `tile_spacing`). Default is ``None``.
        margins : tuple, optional
            A tuple ``(hor, ver)`` specifying the horizontal and
            vertical margins (padding) around the entire storyboard (all
            sections included). Default is ``(10, 10)``.

        tile : tuple, optional
            A tuple ``(cols, rows)`` specifying the number of columns
            and rows for the array of thumbnails in the
            storyboard. Default is ``(4, 4)``.
        tile_spacing : tuple, optional
            A tuple ``(hor, ver)`` specifying the horizontal and
            vertical spacing between adjacent thumbnails. Default is
            ``(8, 6)``.

        thumbnail_width : int, optional
            Width of each thumbnail. Default is 480 (as in 480x270 for a
            16:9 video).
        thumbnail_aspect_ratio : float, optional
            Aspect ratio of generated thumbnails. If ``None``, first try
            to use the display aspect ratio of the video
            (``self.video.dar``), then the aspect ratio of the frames if
            ``self.video.dar`` is not available. Default is ``None``.

        draw_timestamp : bool, optional
            Whether to draw frame timestamps on top of thumbnails (as an
            overlay).  Default is ``True``.
        timestamp_font : Font, optional
            Font used for timestamps, if `draw_timestamp` is
            ``True``. Default is the font constructed by ``Font()``
            without arguments.
        timestamp_align : {'right', 'center', 'left'}, optional
            Horizontal alignment of timestamps over the thumbnails, if
            `draw_timestamp` is ``True``. Default is ``'right'``. Note
            that timestamps are always vertically aligned to the bottom.

        text_font: Font, optional
            Font used for metadata sheet and promotional banner. Default
            is the font constructed by ``Font()`` without arguments.
        text_color: color, optional
            Color of metadata and promotional text, in any format
            recognized by Pillow. Default is ``'black'``.
        line_spacing: float, optional
            Line spacing of metadata text as a float, e.g., 1.0 for
            single spacing, 1.5 for one-and-a-half spacing, and 2.0 for
            double spacing. Default is 1.2.

        include_sha1sum: bool, optional
            Whether to include the SHA-1 hex digest of the video file in
            the metadata fields. Default is ``False``. Be aware that
            computing SHA-1 digest is an expensive operation.

        print_progress : bool, optional
            Whether to print progress information (to stderr). Default
            is ``False``.

        """

        # process parameters -- a ton of them
        if params is None:
            params = {}
        include_metadata_sheet = _read_param(params, 'include_metadata_sheet',
                                             True)
        include_promotional_banner = _read_param(params,
                                                 'include_promotional_banner',
                                                 True)
        background_color = _read_param(params, 'background_color', 'white')
        margins = _read_param(params, 'margins', (10, 10))
        tile = _read_param(params, 'tile', (4, 4))
        tile_spacing = _read_param(params, 'tile_spacing', (8, 6))
        if (('section_spacing' in params
             and params['section_spacing'] is not None)):
            section_spacing = params['section_spacing']
        else:
            section_spacing = tile_spacing[1]
        thumbnail_width = _read_param(params, 'thumbnail_width', 480)
        thumbnail_aspect_ratio = _read_param(params, 'thumbnail_aspect_ratio',
                                             None)
        draw_timestamp = _read_param(params, 'draw_timestamp', True)
        timestamp_font = _read_param(params, 'timestamp_font', Font())
        timestamp_align = _read_param(params, 'timestamp_align', 'right')
        text_font = _read_param(params, 'text_font', Font())
        text_color = _read_param(params, 'text_color', 'black')
        line_spacing = _read_param(params, 'line_spacing', 1.2)
        include_sha1sum = _read_param(params, 'include_sha1sum', False)
        print_progress = _read_param(params, 'print_progress', False)

        # draw bare storyboard, metadata sheet, and promotional banner
        if print_progress:
            sys.stderr.write("Generating main storyboard...\n")
        bare_storyboard = self._gen_bare_storyboard(
            tile,
            thumbnail_width,
            params={
                'tile_spacing': tile_spacing,
                'background_color': background_color,
                'thumbnail_aspect_ratio': thumbnail_aspect_ratio,
                'draw_timestamp': draw_timestamp,
                'timestamp_font': timestamp_font,
                'timestamp_align': timestamp_align,
                'print_progress': print_progress,
            })
        total_width, _ = bare_storyboard.size

        if include_metadata_sheet:
            if print_progress:
                sys.stderr.write("Generating metadata sheet...\n")
            metadata_sheet = self._gen_metadata_sheet(total_width,
                                                      params={
                                                          'text_font':
                                                          text_font,
                                                          'text_color':
                                                          text_color,
                                                          'line_spacing':
                                                          line_spacing,
                                                          'background_color':
                                                          background_color,
                                                          'include_sha1sum':
                                                          include_sha1sum,
                                                          'print_progress':
                                                          print_progress,
                                                      })

        if include_promotional_banner:
            if print_progress:
                sys.stderr.write("Generating promotional banner...\n")
            banner = self._gen_promotional_banner(total_width,
                                                  params={
                                                      'text_font':
                                                      text_font,
                                                      'text_color':
                                                      text_color,
                                                      'background_color':
                                                      background_color,
                                                  })

        # combine different sections
        if print_progress:
            sys.stderr.write("Assembling pieces...\n")
        sections = []
        if include_metadata_sheet:
            sections.append(metadata_sheet)
        sections.append(bare_storyboard)
        if include_promotional_banner:
            sections.append(banner)
        storyboard = tile_images(sections, (1, len(sections)),
                                 params={
                                     'tile_spacing': (0, section_spacing),
                                     'margins': margins,
                                     'canvas_color': background_color,
                                     'close_separate_images': True,
                                 })

        return storyboard
Exemplo n.º 10
0
def tile_images(images, tile, params=None):
    """
    Combine images into a composite image through 2D tiling.

    For example, 16 thumbnails can be combined into an 4x4 array. As
    another example, three images of the same width (think of the
    metadata sheet, the bare storyboard, and the promotional banner) can
    be combined into a 1x3 array, i.e., assembled vertically.

    The image list is processed column-first.

    Note that except if you use the `tile_size` option (see "Other
    Parameters"), you should make sure that images passed into this
    function satisfy the condition that the widths of all images in each
    column and the heights of all images in each row match perfectly;
    otherwise, this function will give up and raise a ValueError.

    Parameters
    ----------
    images : list
        A list of PIL.Image.Image objects, satisfying the necessary
        height and width conditions (see explanation above).
    tile : tuple
        A tuple ``(m, n)`` indicating `m` columns and `n` rows. The
        product of `m` and `n` should be the length of `images`, or a
        `ValueError` will arise.
    params : dict, optional
        Optional parameters enclosed in a dict. Default is ``None``.
        See the "Other Parameters" section for understood key/value
        pairs.

    Returns
    -------
    PIL.Image.Image
        The composite image.

    Raises
    ------
    ValueError
        If the length of `images` does not match the product of columns
        and rows (as defined in `tile`), or the widths and heights of
        the images do not satisfy the necessary consistency conditions.

    Other Parameters
    ----------------
    tile_size : tuple, optional
        A tuple ``(width, height)``. If this parameter is specified,
        width and height consistency conditions won't be checked, and
        every image will be resized to (width, height) when
        combined. Default is ``None``.
    tile_spacing : tuple, optional
        A tuple ``(hor, ver)`` specifying the horizontal and vertical
        spacing between adjacent tiles. Default is ``(0, 0)``.
    margins : tuple, optional
        A tuple ``(hor, ver)`` specifying the horizontal and vertical
        margins (padding) around the combined image. Default is ``(0,
        0)``.
    canvas_color : color, optional
        A color in any format recognized by Pillow. This is only
        relevant if you have nonzero tile spacing or margins, when the
        background shines through the spacing or margins. Default is
        ``'white'``.
    close_separate_images : bool
        Whether to close the separate after combining. Closing the
        images will release the corresponding resources. Default is
        ``False``.

    """

    # pylint: disable=too-many-branches

    if params is None:
        params = {}
    cols, rows = tile
    if len(images) != cols * rows:
        msg = "{} images cannot fit into a {}x{}={} array".format(
            len(images), cols, rows, cols * rows)
        raise ValueError(msg)
    hor_spacing, ver_spacing = _read_param(params, 'tile_spacing', (0, 0))
    hor_margin, ver_margin = _read_param(params, 'margins', (0, 0))
    canvas_color = _read_param(params, 'canvas_color', 'white')
    close_separate_images = _read_param(params, 'close_separate_images', False)
    if 'tile_size' in params and params['tile_size'] is not None:
        tile_size = params['tile_size']
        tile_width, tile_height = tile_size
        canvas_width = (tile_width * cols + hor_spacing * (cols - 1) +
                        hor_margin * 2)
        canvas_height = (tile_height * rows + ver_spacing * (rows - 1) +
                         ver_margin * 2)
        resize = True
    else:
        # check column width consistency, bark if not
        # calculate total width along the way
        canvas_width = hor_spacing * (cols - 1) + hor_margin * 2
        for col in range(0, cols):
            # reference width set by the first image in the column
            ref_index = 0 * cols + col
            ref_width, ref_height = images[ref_index].size
            canvas_width += ref_width
            for row in range(1, rows):
                index = row * cols + col
                width, height = images[index].size
                if width != ref_width:
                    msg = ("the width of image #{} "
                           "(row #{}, col #{}, {}x{}) "
                           "does not agree with that of image #{} "
                           "(row #{}, col #{}, {}x{})".format(
                               index,
                               row,
                               col,
                               width,
                               height,
                               ref_index,
                               0,
                               col,
                               ref_width,
                               ref_height,
                           ))
                    raise ValueError(msg)
        # check row height consistency, bark if not
        # calculate total height along the way
        canvas_height = ver_spacing * (rows - 1) + ver_margin * 2
        for row in range(0, rows):
            # reference width set by the first image in the column
            ref_index = row * cols + 0
            ref_width, ref_height = images[ref_index].size
            canvas_height += ref_height
            for col in range(1, cols):
                index = row * cols + col
                width, height = images[index].size
                if height != ref_height:
                    msg = ("the height of image #{} "
                           "(row #{}, col #{}, {}x{}) "
                           "does not agree with that of image #{} "
                           "(row #{}, col #{}, {}x{})".format(
                               index,
                               row,
                               col,
                               width,
                               height,
                               ref_index,
                               0,
                               col,
                               ref_width,
                               ref_height,
                           ))
                    raise ValueError(msg)
        # passed tests, will assemble as is
        resize = False

    # start assembling images
    canvas = Image.new('RGB', (canvas_width, canvas_height), canvas_color)
    y = ver_margin
    for row in range(0, rows):
        x = hor_margin
        for col in range(0, cols):
            image = images[row * cols + col]

            if resize:
                canvas.paste(image.resize(tile_size, Image.LANCZOS), (x, y))
            else:
                canvas.paste(image, (x, y))

            # accumulate width of this column, as well as horizontal spacing
            x += image.size[0] + hor_spacing
        # accumulate height of this row, as well as vertical spacing
        y += images[row * cols + 0].size[1] + ver_spacing

    if close_separate_images:
        for image in images:
            image.close()

    return canvas
Exemplo n.º 11
0
def create_thumbnail(frame, width, params=None):
    """Create thumbnail of a video frame.

    The timestamp of the frame can be overlayed to the thumbnail. See
    "Other Parameters" to how to enable this feature and relevant
    options.

    Parameters
    ----------
    frame : storyboard.frame.Frame
        The video frame to be thumbnailed.
    width : int
        Width of the thumbnail.
    params : dict, optional
        Optional parameters enclosed in a dict. Default is ``None``.
        See the "Other Parameters" section for understood key/value
        pairs.

    Returns
    -------
    thumbnail : PIL.Image.Image

    Other Parameters
    ----------------
    aspect_ratio : float, optional
        Aspect ratio of the thumbnail. If ``None``, use the aspect ratio
        (only considering the pixel dimensions) of the frame. Default is
        ``None``.
    draw_timestamp : bool, optional
        Whether to draw frame timestamp over the timestamp. Default is
        ``False``.
    timestamp_font : Font, optional
        Font for the timestamp, if `draw_timestamp` is ``True``.
        Default is the font constructed by ``Font()`` without arguments.
    timestamp_align : {'right', 'center', 'left'}, optional
        Horizontal alignment of the timestamp over the thumbnail, if
        `draw_timestamp` is ``True``. Default is ``'right'``. Note that
        the timestamp is always vertically aligned towards the bottom of
        the thumbnail.

    """

    if params is None:
        params = {}
    if 'aspect_ratio' in params:
        aspect_ratio = params['aspect_ratio']
    else:
        image_width, image_height = frame.image.size
        aspect_ratio = image_width / image_height
    height = int(round(width / aspect_ratio))
    size = (width, height)
    draw_timestamp = _read_param(params, 'draw_timestamp', False)
    if draw_timestamp:
        timestamp_font = _read_param(params, 'timestamp_font', Font())
        timestamp_align = _read_param(params, 'timestamp_align', 'right')

    thumbnail = frame.image.resize(size, Image.LANCZOS)

    if draw_timestamp:
        draw = ImageDraw.Draw(thumbnail)

        timestamp_text = util.humantime(frame.timestamp, ndigits=0)
        timestamp_width, timestamp_height = \
            draw.textsize(timestamp_text, timestamp_font.obj)

        # calculate upperleft corner of the timestamp overlay
        # we hard code a margin of 5 pixels
        timestamp_y = height - 5 - timestamp_height
        if timestamp_align == 'right':
            timestamp_x = width - 5 - timestamp_width
        elif timestamp_align == 'left':
            timestamp_x = 5
        elif timestamp_align == 'center':
            timestamp_x = int((width - timestamp_width) / 2)
        else:
            raise ValueError("timestamp alignment option '%s' not recognized" %
                             timestamp_align)

        # draw white timestamp with 1px thick black border
        for x_offset in range(-1, 2):
            for y_offset in range(-1, 2):
                draw.text((timestamp_x + x_offset, timestamp_y + y_offset),
                          timestamp_text,
                          fill='black',
                          font=timestamp_font.obj)
        draw.text((timestamp_x, timestamp_y),
                  timestamp_text,
                  fill='white',
                  font=timestamp_font.obj)

    return thumbnail
Exemplo n.º 12
0
def draw_text_block(canvas, xy, text, params=None):
    """Draw a block of text.

    You need to specify a canvas to draw upon. If you are not sure about
    the size of the canvas, there is a `dry_run` option (see "Other
    Parameters" that help determine the size of the text block before
    creating the canvas.

    Parameters
    ----------
    canvas : PIL.ImageDraw.Image
        The canvas to draw the text block upon. If the `dry_run` option
        is on (see "Other Parameters"), `canvas` can be ``None``.
    xy : tuple
        Tuple ``(x, y)`` consisting of x and y coordinates of the
        topleft corner of the text block.
    text : str
        Text to be drawn, can be multiline.
    params : dict, optional
        Optional parameters enclosed in a dict. Default is ``None``.
        See the "Other Parameters" section for understood key/value
        pairs.

    Returns
    -------
    (width, height)
        Size of text block.

    Other Parameters
    ----------------
    font : Font, optional
        Default is the font constructed by ``Font()`` without arguments.
    color : color, optional
        Color of text; can be in any color format accepted by Pillow
        (used for the ``fill`` argument of
        ``PIL.ImageDraw.Draw.text``). Default is ``'black'``.
    spacing : float, optional
        Line spacing as a float. Default is 1.2.
    dry_run : bool, optional
        If ``True``, do not draw anything, only return the size of the
        text block. Default is ``False``.

    """

    if params is None:
        params = {}
    x, y = xy
    font = _read_param(params, 'font', Font())
    color = _read_param(params, 'color', 'black')
    spacing = _read_param(params, 'spacing', 1.2)
    dry_run = _read_param(params, 'dry_run', False)

    if not dry_run:
        draw = ImageDraw.Draw(canvas)

    line_height = int(round(font.size * spacing))
    width = 0
    height = 0
    for line in text.splitlines():
        w, _ = font.obj.getsize(line)
        if not dry_run:
            draw.text((x, y), line, fill=color, font=font.obj)
        if w > width:
            width = w  # update width to that of the current widest line
        height += line_height
        y += line_height
    return (width, height)
Exemplo n.º 13
0
def extract_frame(video_path, timestamp, params=None):
    """Extract a video frame from a given timestamp.

    Use FFmpeg to seek to the specified timestamp and decode the
    corresponding frame.

    Parameters
    ----------
    video_path : str
        Path to the video file.
    timestamp : float
        Timestamp in seconds (as a nonnegative float).
    params : dict, optional
        Optional parameters enclosed in a dict. Default is ``None``.
        See the "Other Parameters" section for understood key/value
        pairs.

    Returns
    -------
    frame : Frame

    Raises
    ------
    OSError
        If video file doesn't exist, ffmpeg binary doesn't exist or
        fails to run, or ffmpeg runs but generates no output (possibly
        due to an out of range timestamp).

    Other Parameters
    ----------------
    ffmpeg_bin : str, optional
        Name or path of FFmpeg binary. If ``None``, make educated guess
        using ``storyboard.fflocate.guess_bins``. Default is ``None``.
    codec : str, optional
        Image codec used by FFmpeg when outputing the frame. Default is
        ``'png'``. There is no need to touch this option unless your
        FFmpeg cannot encode PNG, which is very unlikely.
    frame_by_frame : bool, optional
        Whether to seek frame by frame, i.e., whether to use output
        seeking (see https://trac.ffmpeg.org/wiki/Seeking). Default is
        ``False``. Note that seeking frame by frame is *extremely* slow,
        but accurate. Only use this when the container metadata is wrong
        or missing, so that input seeking produces wrong image.

    """

    if params is None:
        params = {}
    if 'ffmpeg_bin' in params and params['ffmpeg_bin'] is not None:
        ffmpeg_bin = params['ffmpeg_bin']
    else:
        ffmpeg_bin, _ = fflocate.guess_bins()
    codec = _read_param(params, 'codec', 'png')
    frame_by_frame = (params['frame_by_frame'] if 'frame_by_frame' in params
                      else False)

    if not os.path.exists(video_path):
        raise OSError("video file '%s' does not exist" % video_path)

    ffmpeg_args = [ffmpeg_bin]
    if frame_by_frame:
        # output seeking
        ffmpeg_args += [
            '-i', video_path,
            '-ss', str(timestamp),
        ]
    else:
        # input seeking
        ffmpeg_args += [
            '-ss', str(timestamp),
            '-i', video_path,
        ]
    ffmpeg_args += [
        '-f', 'image2',
        '-vcodec', codec,
        '-vframes', '1',
        '-hide_banner',
        '-',
    ]
    proc = subprocess.Popen(ffmpeg_args,
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    frame_bytes, ffmpeg_err = proc.communicate()
    if proc.returncode != 0:
        msg = (("ffmpeg failed to extract frame at time %.2f\n"
                "ffmpeg error message:\n%s") %
               (timestamp, ffmpeg_err.strip().decode('utf-8')))
        raise OSError(msg)

    if not frame_bytes:
        # empty output, no frame generated
        msg = ("ffmpeg generated no output "
               "(timestamp %.2f might be out of range)"
               "ffmpeg error message:\n%s" %
               (timestamp, ffmpeg_err.strip().decode('utf-8')))
        raise OSError(msg)

    try:
        frame_image = Image.open(io.BytesIO(frame_bytes))
    except IOError:
        raise OSError("failed to open frame with PIL.Image.open")

    return Frame(timestamp, frame_image)