Skip to content

endless looping GIFs with PillowWriter #11789

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 16, 2018
Merged

endless looping GIFs with PillowWriter #11789

merged 8 commits into from
Oct 16, 2018

Conversation

t-makaro
Copy link
Contributor

@t-makaro t-makaro commented Jul 28, 2018

PR Summary

Closes #11787 . Adds an optional parameter to PillowWriter that will set the number of times that a GIF will loop for. Defaults to 0 meaning endless looping. Previously defaulted to 1 (never looping) without a way to change it. API change removed.

Makes PillowWriter produce endless looping gifs like ImageMagickWriter and ImageMagickFileWriter.

PR Checklist

  • [ ] Has Pytest style unit tests
  • Code is PEP 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • [ ] Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way
  • API consistency

@t-makaro
Copy link
Contributor Author

t-makaro commented Jul 28, 2018

Checklist 1: I have no idea how I would create a pytest for this.

Checklist 3,4: I want to add the loop parameter to the PillowWrite.__init__ docstring, but I believe that I would have to copy the entire docstring from MovieWriter.__init__, and then add the extra parameter, so that the sphinx-autodoc will work properly. That's that best way I can think of doing it, is it?

Checklist 5: It's a minor new feature. So, I shouldn't add this?
Checklist 6: It is an api change, but it shouldn't break anything? Like who relied on a gif that only looped once?

@jklymak
Copy link
Member

jklymak commented Jul 28, 2018

  1. can you interrogate the gif header for the loop parameter? Probably not so important for a test here.

3/4 Maybe just in the MovieWriter doc string say that loop exists if the writer is Pillow. For triple bonus points you could look into the other writers and see if there is a loop method for those.
5/6 I think a whats new entry is good, but no need for an API entry - thats generally only if API is changed, not if some is added.

@WeatherGod
Copy link
Member

WeatherGod commented Jul 28, 2018 via email

@jklymak
Copy link
Member

jklymak commented Jul 28, 2018

Oh, well in that case it’d be good to quickly go through the writers and document their behaviour.

@t-makaro
Copy link
Contributor Author

I thought that I'd take a crack a setting the number of loops with ImageMagickWriter. I attempted to modify this line, but the gif still looped infinitely despite this doc. I'll stick with just modifying PillowWriter.

@t-makaro
Copy link
Contributor Author

Warning, treated as error:
/home/circleci/project/doc/users/next_whats_new/endless_gif.rst:document isn't included in any toctree

I think this is just since the next_whats_new toctree is commented out in whats_new.rst

Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyone can dismiss once comments handled

@@ -557,6 +557,7 @@ def isAvailable(cls):
def __init__(self, *args, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__init__(self, *args, loop=False, **kwargs)
or at least make it a boolean.

Argument needs to be documented in some docstring.

Please check for API consistency across writers; if the behavior of other writers is to loop then the default should be changed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Pillow image.save function takes the loop parameter as an integer, so a gif could be make to loop a set number of times. Also, loop=False might make sense in the docstring, but loop=self.loop where self.loop is False is an endless loop since False==0. I suppose I could change it to loop=not self.loop, but should we not allow setting to loop say 5 times?

I'll change it to a Boolean if you still want. (I suppose no one needs a gif to loop exactly 137 times!)

I like the move to the loop kwarg in the __init__ function definition. I'll do that for sure.

HTMLWriter and ImageMagick both default to looping I believe, but the ffmpegwriter writing to a .mp4 wouldn't loop. Whether defaulting to loop or not really depends on the format. Gif's should loop, mp4's can't. Different formats have different looping properties (HTMLWriter has a 'reflect' option, so it has a default_mode parameter that takes a string instead of a loop parameter.

I'll default it to looping (unless you object).

I know that I need to put it in a docstring, but I'm having difficulty figuring out where. I'm not familiar with sphinx-autodoc. I really could use help here. I could write an entire new docstring based off the base class and add the parameter. Or I could do as @jklymak suggested, but then other writers would have a "loop parameter only available in PillowWriter".

@@ -0,0 +1,7 @@
Looping GIFs with PillowWriter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something went wrong with the formatting.

Copy link
Contributor Author

@t-makaro t-makaro Jul 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the file has windows line endings. I will change that. Other than that I don't know.

@jklymak
Copy link
Member

jklymak commented Jul 28, 2018

I think you could accept a Boolean and convert to an integer internally; just check if a Boolean. OTOH it’s a bit confusing that 0 is loop and 1 is loop only once. Agree that specifying the number loops is desiarable option

@t-makaro
Copy link
Contributor Author

t-makaro commented Jul 28, 2018

I suppose I could do

self.loop = not loop if isintance(loop, bool) else loop

then set the default to loop=True. Then in whatever docstring say that it accepts a Boolean or an integer that specifies number of loops. This way it is clear that True is endless, False is once, and an integer >=1 is an integer.

What do people think?

@jklymak
Copy link
Member

jklymak commented Jul 28, 2018

More that I think about it maybe just make it an integer and value error on anything else so the Boolean doesn’t get passed by accident. Maybe just lay out the options here and folks can vote 🗳 (I’d do it but am on phone). Sorry to drag out a simple change but may as well get it right.

@t-makaro
Copy link
Contributor Author

t-makaro commented Jul 28, 2018

Option 1:
Booleans only.
Pros:

  • Simple.

Cons:

  • can't set a finite >= 1 integer of loops to display.

Implemented as

self.loop = not loop

Option 2:
ints (this is what it currently is, but without the ValueError)
Pros:

  • Can set a finite >= 1 integer of loops to display.

Cons:

  • 0 == infinity

Implemented as

# can't use isinstance since bools are ints
if type(loop) == int:
    self.loop = loop
else:
    raise ValueError("loop must be an int")

Option 3:
Both bools and ints
Pros:

  • simple True is endless and False plays once
  • finite number of loops is possible

Cons:

  • We have to explain that both are excepted.

Implemented as

self.loop = not loop if isinstance(loop, bool) else loop

Documentation will be updated to reflect whatever choice.
I'm in favour of option 2 or 3.

@tacaswell tacaswell added this to the v3.1 milestone Jul 28, 2018
@jklymak
Copy link
Member

jklymak commented Jul 28, 2018

I vote 2 + ValueError, just because I think loop=False will actually pass loop=0 if its not caught, but I may be wrong.

@t-makaro
Copy link
Contributor Author

I added the ValueError. If we did want to support both bools and ints in the future, then it is easier to add that, then it is to remove support for bools after the fact.

dopplershift
dopplershift previously approved these changes Jul 31, 2018
Copy link
Contributor

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 on the functionality. Minor tweak to the int check if you're interested.

@@ -554,7 +554,38 @@ def isAvailable(cls):
return False
return True

def __init__(self, *args, **kwargs):
def __init__(self, *args, loop=0, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So much easier to do this now that we're Python 3 only.

output file. Some keys that may be of use include:
title, artist, genre, subject, copyright, srcform, comment.
'''
if type(loop) == int:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not isinstance(loop, int):
    raise ValueError(...)
self.loop = loop

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bools are ints. So not isinstance(True, int) will return false. Which will result in passing a value of True aka 1 to loop which won't loop, but one would expect loop=True to loop. See my option 3 for a way to implement support for both bools and ints, but this will force just ints.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I suppose

if type(loop) != int:
    raise ValueError(...)
self.loop=loop

But it only saves one of code, and I don't really feel like pushing another commit to this unless I have too 😛.

@dopplershift
Copy link
Contributor

One other question on further thought: since this is not a boolean flag, but a number of loops, should the parameter be loops rather than loop?

@t-makaro
Copy link
Contributor Author

That's not a bad point. But I think that the plural loops makes the fact that a value of 0 results in endless looping make even less sense. Pillow uses the parameter name loop, maybe we should just stick with that.

@fredrik-1
Copy link
Contributor

I am looking through the animation module at the moment and based on the available options in all writers at the moment I would say that we could just add unlimited looping also in the pillow writer without any option to change it. Most people want it anyway (but I am not against the option).

I think that a refractoring or at least change in documentation about kwargs should be done in the animation module. At the moment the parameters to the pillow writer are something like,
codec, fps, bitrate, extra_args, metadata but the only one that are not ignored is fps.

So a new API or documentation that clearly show what options that are used in a writer and adding new relevant options should be good.

I have started to change some of the documentation to show which kwargs that are actually used and how.

I would suggest to just add the unlimited looping functionality in a pr and the rest in other prs. Do this really need a test?

loop: int
The number of times that the gif will loop.
A value of 0 is endless.
codec: string or None, optional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of the options are just ignored in the pillow writer so I don't think that they should be documented.

@fredrik-1
Copy link
Contributor

fredrik-1 commented Aug 12, 2018

I thought that I'd take a crack a setting the number of loops with ImageMagickWriter. I attempted to modify this line, but the gif still looped infinitely despite this doc. I'll stick with just modifying PillowWriter.

Changing that line worked for me. I think that both the pillow- and imagemagick writer API should be the same.

One possibility that are not perfect but is in accordance with the current implementation is to put the new kwarg in moviewriter and just put better documentation in the subclasses which kwargs that are used in that writer.

When both imagemagick and pillow seems to use ints for number of loops with 0 as infinity I think that we should stick to that notation.

Edit:
I would even say that this pr and the extra kwargs in the HTMLWriter breaks the API. It is with those changes not possible to get the same functionality from anim.save(name, writer=writer_obj, ...) and anim.save(name, writer=string_name_of_writer, ...).

So I believe that all new kwargs should be in MovieWriter and animation.save

@t-makaro
Copy link
Contributor Author

t-makaro commented Sep 18, 2018

Sorry it's been a while.

In the interest in getting this bug fix done (and leaving me time for my thousand other projects), I've removed all API changes. This PR now does 2 things:

  1. Makes gifs with PillowWriter loop endlessly. Consistent with ImageMagickWriter and ImageMagickFileWriter.
  2. Adds a simple docstring to PillowWriter.

I'll leave any refractoring/API changes to someone more familiar with this API than me. This PR is now the minimum needed to fix #11789. The suggested loop keyword argument can be added later.

----------
fps: int
Framerate for the gif.
'''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kill the docstring as well for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

Endless Looping GIFs with PillowWriter
--------------------------------------

We acknowledge that most people want to watch a gif more than once. Saving an animatation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo (animatation)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silly me. Fixed.

Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modulo typo in whatsnew

@anntzer anntzer dismissed dopplershift’s stale review September 19, 2018 08:54

@dopplershift I dismissed your review as the scope of the PR has changed; can you check it again?

@t-makaro
Copy link
Contributor Author

Ok, I fixed the typo. I believe the doc failures are due to sphinx 1.8.0.

@QuLogic
Copy link
Member

QuLogic commented Sep 29, 2018

We should have fixed the doc builds now; can you rebase to see if everything's working again?

@t-makaro
Copy link
Contributor Author

t-makaro commented Oct 3, 2018

Last time I tried to rebase a fork, I really messed up the commit history. Any advice?

@jklymak
Copy link
Member

jklymak commented Oct 3, 2018

You should have branched your fork before submitting this PR. Right now your PR is based on your master branch, and that will make your life hard.

After that, you'd best give https://matplotlib.org/devel/gitwash/index.html a thorough look. In particular pay attention to backing up your branch.

With your setup, I would do:

git checkout master
git checkout -b backup-of-master  # this will save your bacon if you mess up.
git checkout master    # get back to the changes you made in this PR
git fetch upstream   # if you haven't set your 'upstream' remote, then you need to do that first.  
git rebase upstream/master  # you don't have any conflicts so this should be smooth
git push origin master --force   

If you mess up:

git checkout master
git reset --hard backup-of-master

will get you locally back to where you were.

@QuLogic
Copy link
Member

QuLogic commented Oct 3, 2018

This is some redundant branch switching:

git checkout -b backup-of-master # this will save your bacon if you mess up.
git checkout master # get back to the changes you made in this PR

git branch backup-of-master will create a branch and not switch to it.

@t-makaro
Copy link
Contributor Author

t-makaro commented Oct 9, 2018

@jklymak Your advice seems to have worked. Thank you. I've rebased.

@dstansby
Copy link
Member

Can whoever merges this squash-merge it?

@jklymak jklymak merged commit 566bd8b into matplotlib:master Oct 16, 2018
thoo added a commit to thoo/matplotlib that referenced this pull request Oct 18, 2018
* master: (51 commits)
  Disable sticky edge accumulation if no autoscaling.
  Avoid quadratic behavior when accumulating stickies.
  Rectified plot error (matplotlib#12501)
  endless looping GIFs with PillowWriter (matplotlib#11789)
  TST: add test for re-normalizing image
  FIX: colorbar re-check norm before draw for autolabels
  Fix search for sphinx >=1.8 (matplotlib#12216)
  Fix some flake8 issues
  Don't handle impossible values for `align` in hist()
  DOC: add section about test / doc dependencies
  DOC: clarify time windows for support
  TST: test colorbar tick labels correct
  FIX: make minor ticks formatted with science formatter as well
  DOC: clarify what 'minor version' means
  Adjust the widths of the messages during the build.
  Simplify radar_chart example.
  Fix duplicate condition in pathpatch3d example (matplotlib#12495)
  Fix typo in documentation
  FIX: make unused spines invisible
  Kill FontManager.update_fonts.
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Looping gifs with PillowWriter
10 participants