Exemplo n.º 1
0
def die_unless_writer(writer_name: str) -> None:
    '''
    Raise an exception unless a matplotlib animation writer class registered
    with the passed name is recognized by both BETSE and matplotlib, implying
    the external command required by this class to be installed on the current
    system.

    Parameters
    ----------
    writer_name : str
        Alphanumeric lowercase name of the writer to validate.

    See Also
    ----------
    :func:`is_writer`
        Further details.
    '''

    # For human-readable granularity in exception messages, call granular
    # testers rather than the catch-all is_writer() tester.
    #
    # If this writer is unrecognized by BETSE, raise an exception.
    die_unless_writer_betse(writer_name)

    # If this writer is unrecognized by matplotlib...
    if not is_writer_mpl(writer_name):
        # Basename of this writer's command.
        writer_basename = WRITER_NAME_TO_COMMAND_BASENAME[writer_name]

        # Raise this exception.
        raise BetseMatplotlibException(
            'Matplotlib animation video writer "{}" '
            'not registered with matplotlib '
            '(i.e., command "{}" not found).'.format(writer_name,
                                                     writer_basename))
Exemplo n.º 2
0
def get_first_writer_name(writer_names: SequenceTypes) -> str:
    '''
    First name (e.g., ``imagemagick``) of the matplotlib animation writer class
    (e.g., :class:`ImageMagickWriter`) in the passed list recognized by both
    this application and :mod:`matplotlib` if any *or* raise an exception
    otherwise.

    This function iteratively searches for external commands in the same order
    as the passed list lists names.

    Parameters
    ----------
    writer_names : SequenceTypes
        List of the alphanumeric lowercase names of all writers to search for.

    Returns
    ----------
    str
        Alphanumeric lowercase name of the first such writer.

    Raises
    ----------
    BetseMatplotlibException
        If either:

        * Any writer in the passed list is unrecognized by this application.
        * No such writer is registered with :mod:`matplotlib`.
    '''

    # For the name of each passed writer...
    for writer_name in writer_names:
        # If this writer is unrecognized by BETSE, raise an exception. This
        # prevents BETSE from silently ignoring newly added writers not yet
        # recognized by BETSE.
        die_unless_writer_betse(writer_name)

        # If this writer is recognized by matplotlib, cease searching.
        if is_writer_mpl(writer_name):
            return writer_name

    # Else, no such command is in the ${PATH}. Raise an exception.
    raise BetseMatplotlibException(
        'Matplotlib animation video writers {} not found.'.format(
            strjoin.join_as_conjunction_double_quoted(*writer_names)))
Exemplo n.º 3
0
def die_unless_figure() -> bool:
    '''
    Raise an exception
    ``True`` only if one or more figures are currently open with the
    :mod:`matplotlib.pyplot` GCF API.

    Raises
    -----------
    BetseMatplotlibException
        If no figures are currently open with this API.

    See Also
    -----------
    :func:`is_figure`
        Further details.
    '''

    if not is_figure():
        raise BetseMatplotlibException('No matplotlib figures currently open.')
Exemplo n.º 4
0
def die_unless_writer_betse(writer_name: str) -> None:
    '''
    Raise an exception unless a matplotlib animation writer class registered
    with the passed name is recognized by BETSE.

    If an exception is *not* raised, this does *not* imply the external command
    required by this class to be runnable or even installed on this system.

    Parameters
    ----------
    writer_name : str
        Alphanumeric lowercase name of the writer to validate.

    See Also
    ----------
    is_writer_betse
        Further details.
    '''

    if not is_writer_betse(writer_name):
        raise BetseMatplotlibException(
            'Matplotlib animation video writer "{}" '
            'unrecognized by BETSE.'.format(writer_name))
Exemplo n.º 5
0
def die_unless_writer_command(writer_basename: str) -> None:
    '''
    Raise an exception unless at least one matplotlib animation writer class
    running the external command with the passed basename is recognized by both
    BETSE and matplotlib, implying this command to be installed on the current
    system.

    Parameters
    ----------
    writer_basename : str
        Basename of the external command of the writer to validate.

    See Also
    ----------
    is_writer_command
        Further details.
    '''

    if not is_writer_command(writer_basename):
        raise BetseMatplotlibException(
            'Matplotlib animation video writer command "{basename}" '
            'unrecognized by BETSE or not registered with matplotlib '
            '(i.e., command "{basename}" not found).'.format(
                basename=writer_basename))
Exemplo n.º 6
0
def get_first_codec_name(
    writer_name: str,
    container_filetype: str,
    codec_names: SequenceTypes,
) -> (str, NoneType):
    '''
    Name of the first video codec (e.g., ``libx264``) in the passed list
    supported by both the encoder with the passed matplotlib-specific name
    (e.g., ``ffmpeg``) and the container format with the passed filetype (e.g.,
    ``mkv``, ``mp4``) if that writer supports codecs (as most do), ``None`` if
    this writer supports no codecs (e.g., ``imagemagick``) and the passed list
    contains ``None``, *or* raise an exception otherwise (i.e., if no passed
    codecs are supported by both this writer and container format).

    Algorithm
    ----------
    This function iteratively searches for video codecs in the same order as
    listed in the passed list as follows:

    * If there are no remaining video codecs in this list to be examined, an
      exception is raised.
    * If the current video codec has the application-specific name ``auto``,
      the name of an intelligently selected codec supported by both this
      encoder and container if any is returned *or* an exception is raised
      otherwise (i.e., if no codecs are supported by both this encoder and
      container). Note that this codec's name rather than the
      application-specific name ``auto`` is returned. See this function's body
      for further commentary.
    * Else if the current video codec is supported by both this encoder and
      container, this codec's name is returned.
    * Else the next video codec in this list is examined.

    Parameters
    ----------
    writer_name : str
        Matplotlib-specific alphanumeric lowercase name of the video encoder to
        search for the passed codecs.
    container_filetype: str
        Filetype of the video container format to constrain this search to.
    codec_names: SequenceTypes
        Sequence of the encoder-specific names of all codecs to search for (in
        descending order of preference).

    Returns
    ----------
    str
        Name of the first codec in the passed list supported by both this
        encoder and container.

    Raises
    ----------
    BetseMatplotlibException
        If any of the following errors arise:

        * This writer is either:

          * Unrecognized by this application or :mod:`matplotlib`.
          * Not found as an external command in the current ``${PATH}``.

        * This container format is unsupported by this writer.
        * No codec whose name is in the passed list is supported by both this
          writer and this container format.

    See Also
    ----------
    :func:`is_writer`
        Tester validating this writer.
    '''

    # If this writer is unrecognized, raise an exception.
    die_unless_writer(writer_name)

    # Basename of this writer's command.
    writer_basename = WRITER_NAME_TO_COMMAND_BASENAME[writer_name]

    # Dictionary mapping from the filetype of each video container format to a
    # list of the names of all video codecs supported by this writer.
    container_filetype_to_codec_names = (
        WRITER_BASENAME_TO_CONTAINER_FILETYPE_TO_CODEC_NAMES[writer_basename])

    # If the passed container is unsupported by this writer, raise an exception.
    if container_filetype not in container_filetype_to_codec_names:
        raise BetseMatplotlibException(
            'Video container format "{}" unsupported by '
            'matplotlib animation video writer "{}".'.format(
                container_filetype, writer_name))

    # List of the names of all candidate codecs supported by both this encoder
    # and this container.
    codec_names_supported = container_filetype_to_codec_names[
        container_filetype]

    # List of the names of all candidate codecs to be detected below, with each
    # instance of "auto" in the original list of these names replaced by the
    # list of all codecs supported by both this encoder and container.
    codec_names_candidate = []

    # For the name of each candidate codec...
    for codec_name in codec_names:
        # If this is the BETSE-specific name "auto", append the names of all
        # codecs supported by both this encoder and container.
        if codec_name == 'auto':
            codec_names_candidate.extend(codec_names_supported)
        # Else, append only the name of this codec.
        else:
            codec_names_candidate.append(codec_name)

    # Log this detection attempt.
    logs.log_debug('Detecting encoder "%s" codec from candidates: %r',
                   writer_name, codec_names_candidate)

    # For the name of each preferred codec...
    for codec_name in codec_names_candidate:
        # If this encoder supports this codec *AND*...
        if is_writer_command_codec(writer_basename, codec_name):
            # Log this detection result.
            logs.log_debug('Detected encoder "%s" codec "%s".', writer_name,
                           codec_name)

            # If this container is not known to support this codec, log a
            # non-fatal warning. Since what this application thinks it knows
            # and what reality actually is need not coincide, this container
            # could actually still support this codec. Hence, this edge case
            # does *NOT* constitute a hard, fatal error.
            if codec_name not in codec_names_supported:
                logs.log_warning(
                    'Encoder "%s" container "%s" '
                    'not known to support codec "%s".', writer_name,
                    container_filetype, codec_name)

            # Return the name of this codec.
            return codec_name

    # Else, no passed codecs are supported by this combination of writer and
    # container format. Raise an exception.
    raise BetseMatplotlibException(
        'Codec(s) {} unsupported by '
        'encoder "{}" and/or container "{}".'.format(
            strjoin.join_as_conjunction_double_quoted(*codec_names),
            writer_name, container_filetype))
Exemplo n.º 7
0
def is_writer_command_codec(writer_basename: str,
                            codec_name: StrOrNoneTypes) -> bool:
    '''
    ``True`` only if the matplotlib animation writer class running the external
    command with the passed basename (e.g., ``ffmpeg``) supports the video
    codec with the passed encoder-specific name (e.g., ``libx264``).

    Specifically, this function returns ``True`` only if this basename is:

    * ``ffmpeg`` and the ``ffmpeg -help encoder={codec_name}`` command
      succeeds.
    * ``avconv`` and the ``avconv -help encoder={codec_name}`` command
      succeeds.
    * ``mencoder``, the ``mencoder -ovc help`` command lists the
      Mencoder-specific ``lavc`` video codec and either:

      * ``ffmpeg`` is in the current ``${PATH}`` and recursively calling this
        function as ``is_writer_codec('ffmpeg', codec_name)`` returns ``True``.
      * ``ffmpeg`` is *not* in the current ``${PATH}``, in which case this
        function assumes the passed codec to be supported and returns ``True``.

    * Any other passed basename (e.g., ``convert``, implying ImageMagick) *and*
      the passed codec is ``None``. These basenames are assumed to *not*
      actually be video encoders and thus support no video codecs.

    Parameters
    ----------
    writer_basename : str
        Basename of the external command of the video encoder to test.
    codec_name : str
        Encoder-specific name of the codec to be tested for.

    Returns
    ----------
    bool
        ``True`` only if this encoder supports this codec.

    Raises
    ----------
    BetseMatplotlibException
        If this basename is either:

        * Unrecognized by this application.
        * Unregistered with :mod:`matplotlib`, implying
        * Not found as an external command in the current ``${PATH}``.
        * Mencoder and the ``mencoder -ovc help`` command fails to list the
          Mencoder-specific ``lavc`` video codec required by Matplotlib.
    '''

    # Log this detection attempt.
    logs.log_debug('Detecting encoder "%s" codec "%s"...', writer_basename,
                   codec_name)

    # Absolute path of this command.
    writer_filename = get_writer_command_filename(writer_basename)

    # For FFmpeg, detect this codec by capturing help documentation output by
    # the "ffmpeg" command for this codec and grepping this output for a string
    # stating this codec to be unrecognized. Sadly, this command reports
    # success rather than failure when this codec is unrecognized. (wut,
    # FFmpeg?)
    if writer_basename == 'ffmpeg':
        # List of all shell words comprising the command to be tested.
        ffmpeg_command_words = (
            writer_filename,
            '-help',
            'encoder=' + shellstr.shell_quote(codec_name),
        )

        # Help documentation for this codec captured from "ffmpeg".
        ffmpeg_codec_help = cmdrun.get_stdout_or_die(ffmpeg_command_words)

        # Return whether this documentation is suffixed by a string implying
        # this codec to be unrecognized or not. If this codec is unrecognized,
        # this documentation ends with the following line:
        #
        #     Codec '${codec_name}' is not recognized by FFmpeg.
        return not ffmpeg_codec_help.endswith("' is not recognized by FFmpeg.")
    # For Libav, detect this codec in the same exact manner as for FFmpeg.
    elif writer_basename == 'avconv':
        # List of all shell words comprising the command to be tested.
        avconv_command_words = (
            writer_filename,
            '-help',
            'encoder=' + shellstr.shell_quote(codec_name),
        )

        # Help documentation for this codec captured from "avconv".
        avconv_codec_help = cmdrun.get_stdout_or_die(avconv_command_words)
        # print('avconv_command_words: {}'.format(avconv_command_words))
        # print('avconv_codec_help: {}'.format(avconv_codec_help))

        # Return whether this documentation is suffixed by an indicative
        # string.
        return not avconv_codec_help.endswith("' is not recognized by Libav.")
    # For Mencoder, detect this codec by capturing help documentation output by
    # the "mencoder" command for *ALL* video codecs, grepping this output for
    # the "lavc" video codec required by matplotlib, and, if found, repeating
    # the above FFmpeg-specific logic to specifically detect this codec.
    elif writer_basename == 'mencoder':
        # Help documentation for all codecs captured from "mencoder".
        mencoder_codecs_help = cmdrun.get_stdout_or_die(
            (writer_filename, '-ovc', 'help'))
        # print('mencoder codecs help: ' + mencoder_codecs_help)

        # If this output contains a line resembling the following, this
        # installation of Mencoder supports the requisite "lavc" codec:
        #     lavc     - libavcodec codecs - best quality!
        if regexes.is_match_line(text=mencoder_codecs_help,
                                 regex=r'^\s+lavc\s+'):
            # If the "ffmpeg" command is installed on the current system, query
            # that command for whether or not the passed codec is supported.
            # Note that the recursion bottoms out with this call, as the above
            # logic handling the FFmpeg writer does *NOT* recall this function.
            if is_writer_command('ffmpeg'):
                return is_writer_command_codec('ffmpeg', codec_name)
            # Else, "ffmpeg" is *NOT* in the ${PATH}. Since Mencoder implements
            # "lavc" codec support by linking against the "libavcodec" shared
            # library rather than calling the "ffmpeg" command, it's
            # technically permissible (albeit uncommon) for the "mencoder" but
            # not "ffmpeg" command to be in the ${PATH}. Hence, this does *NOT*
            # indicate a fatal error. This does, however, prevent us from
            # querying whether or not the passed codec is supported. In lieu of
            # sensible alternatives...
            else:
                # Log a non-fatal warning.
                logs.log_warning(
                    'Mencoder "libavcodec"-based video codec "{}" '
                    'possibly unavailable. Consider installing FFmpeg to '
                    'resolve this warning.'.format(codec_name))

                # Assume the passed codec to be supported.
                return True
        # Else, Mencoder fails to support the "lavc" codec. Raise an exception.
        else:
            raise BetseMatplotlibException(
                'Mencoder video codec "lavc" unavailable.')

    # For any other writer (e.g., ImageMagick), assume this writer to *NOT* be
    # a video encoder and hence support *NO* video codecs. In this case, return
    # True only if the passed codec is "None" -- signifying "no video codec."
    return codec_name is None
Exemplo n.º 8
0
    def setup(self, *args, **kwargs) -> None:
        '''
        Prepare to write animation frames.

        This method is implicitly called by the superclass :meth:`saving`
        method implicitly called by the :meth:`Anim.save` method. Note that,
        unfortunately, the design of both methods prohibits this method from
        accepting subclass-specific parameters.

        Parameters
        -----------
        outfile : str
            :meth:`str.format`-formatted template describing the filenames of
            the resulting frame images. This template is subject to the
            following :meth:`str.format` formatting:

            * The first ``{``- and ``}``-delimited substring (e.g., ``{:07d}``)
              will be replaced by the 0-based index of the current frame. If
              this substring does *not* exist, an exception is raised.

        See the superclass :meth:`setup` method for details on all other
        parameters.

        See Also
        -----------
        https://www.python.org/dev/peps/pep-3101
            For details on format specifiers.
        '''

        super().setup(*args, **kwargs)

        # Frame number of the next frame to be written.
        self._frame_number = 0

        # If this filename template is malformed, raise an exception.
        if not ('{' in self.outfile and '}' in self.outfile):
            raise BetseMatplotlibException(
                'Frame filename template "{}" contains no "{{"- and "}}"-'
                'delimited format specifier.'.format(self.outfile))

        # Output filetype. Override the superclass' awkward choice of "rgba" as
        # output filetype default.
        self.frame_format = pathnames.get_filetype_undotted_or_none(
            self.outfile)

        # List of all output filetypes supported by this class.
        #
        # Since this class serializes frames by calling the savefig() function
        # *AND* since that function supports all image filetypes supported by
        # the current backend, this syllogistically follows. (Q.E.D.)
        out_filetypes_supported = self.fig.canvas.get_supported_filetypes()

        # If this filetype is unsupported, raise an exception.
        if self.frame_format not in out_filetypes_supported:
            raise BetseMatplotlibException(
                'Frame filetype "{}" unsupported by the '
                'current Matplotlib backend (i.e., not in "{}").'.format(
                    self.frame_format, str(out_filetypes_supported)))

        # Parent directory of all output files.
        out_dirname = pathnames.get_dirname(self.outfile)

        # Create this directory if needed.
        dirs.make_parent_unless_dir(out_dirname)