BenT
BenT

Reputation: 3200

ImageMagick - Making 2 GIFs into side by side GIFs using IM convert

I have 2 GIFs that are the same length.

I want to put the GIFs beside each other to have 1 GIF with both playing at the same time. I have tried to use the convert tool with:

convert +append 1.gif1 2.gif output.gif

However, this seems to blend all the images together and changes the size to be extremely small.

I was thinking that I could append each image together and then create a GIF out of those already combined images. However it did not work when I tried:

convert -delay 15 -loop 0 1*.png 2*.png +append output.gif

I have a lot of images with long names and I do not want to have to go through individually and append each figure with new naming conventions.

Upvotes: 16

Views: 8083

Answers (2)

ffmpeg "one-liner" without intermediate files

ffmpeg can also handle GIFs nowadays, so we can use the answers from the following "video side-by-die" question: https://unix.stackexchange.com/questions/233832/merge-two-video-clips-into-one-placing-them-next-to-each-other to solve this questionas well. The following command joins two GIFs side-by-side fixing height 300 px for each:

ffmpeg \
  -i 1.gif \
  -i 2.gif \
  -filter_complex '
  [0]scale=-1:300[a];
  [1]scale=-1:300[b];
  [a][b]hstack
  ' \
  12.gif

enter image description here

I've created my test inputs 1.gif (480x, blue background, 2 seconds) and 2.gif (640x, green background, 3 seconds) with:

ffmpeg -y -f lavfi -i "
color=blue:480x480:d=2,
drawtext=
  fontcolor=black:
  fontsize=600:
  text='%{eif\:t\:d}':
  x=(w-text_w)/2:
  y=(h-text_h)/2
" 1.gif
ffmpeg -y -f lavfi -i "
color=green:640x640:d=3,
drawtext=
  fontcolor=black:
  fontsize=800:
  text='%{eif\:t\:d}':
  x=(w-text_w)/2:
  y=(h-text_h)/2
" 2.gif

The command creates the following processing graph:

1.gif --> [0] --> scale --> [a] --+
                                  |
                                  v
                                  hstack --> 12.gif
                                  ^
                                  |
2.gif --> [1] --> scale --> [b] --+
  • first the scale filters modifies the videos to set them to the same height
  • then the hstack filter takes those scaled intermediate videos and "horizontally stacks" them

As we can see, if one GIF is longer than the other, that command stops the shortest GIF at the last frame.

To instead cut off the video to runtime of the shortest video we can add the shortest=1 option of hstack as in:

ffmpeg -y -i 1.gif -i 2.gif -filter_complex '[0]scale=-1:300[a];
  [1]scale=-1:300[b];[a][b]hstack=shortest=1' 12-short.gif

enter image description here

Alternatively, to stack vertically we can use the vstack filter, and fix the video widths rather than heights:

ffmpeg -y -i 1.gif -i 2.gif -filter_complex '[0]scale=300:-1[a];
  [1]scale=300:-1[b];[a][b]vstack' 12-vert.gif

enter image description here

When I view the generated videos with eog locally, the very first loop has some graphical artifacts. I don't know if this is a bug of eog or problem with the GIF itself. I don't observe that on the browser however.

Related: https://unix.stackexchange.com/questions/40159/running-two-animations-side-by-side-using-imagemagick

Tested on Ubuntu 23.10, ffmpeg 6.0.

Upvotes: 2

Mark Setchell
Mark Setchell

Reputation: 207455

I don't have 2 animated GIFs of the same length, so I'll just use two copies of this one:

enter image description here

Let's look at the frames in there, with this:

identify 1.gif
1.gif[0] GIF 500x339 500x339+0+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[1] GIF 449x339 500x339+51+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[2] GIF 449x339 500x339+51+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[3] GIF 449x339 500x339+51+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[4] GIF 448x339 500x339+52+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[5] GIF 449x339 500x339+51+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[6] GIF 448x339 500x339+52+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[7] GIF 448x339 500x339+52+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[8] GIF 448x339 500x339+52+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[9] GIF 448x339 500x339+52+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[10] GIF 448x339 500x339+52+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[11] GIF 500x339 500x339+0+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[12] GIF 500x339 500x339+0+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[13] GIF 500x339 500x339+0+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[14] GIF 500x339 500x339+0+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[15] GIF 448x339 500x339+52+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[16] GIF 500x339 500x339+0+0 8-bit sRGB 32c 508KB 0.000u 0:00.000
1.gif[17] GIF 500x339 500x339+0+0 8-bit sRGB 32c 508KB 0.000u 0:00.000

Mmmm, 18 frames with different sizes, that means we need to use -coalesce to rebuild partial frames into full ones.

Let's copy that and make 2.gif

cp 1.gif 2.gif

Now we can split the two gifs into their component frames, like this:

convert 1.gif -coalesce a-%04d.gif     # split frames of 1.gif into a-0001.gif, a-0002.gif etc
convert 2.gif -coalesce b-%04d.gif     # split frames of 2.gif into b-0001.gif, b-0002.gif etc

Now let's join the individual frames side-by-side:

for f in a-*.gif; do convert $f ${f/a/b} +append $f; done

Note that ${f/a/b} is a bash-ism meaning "take the value of f and replace the letter 'a' with 'b'".

And put them back together again:

convert -loop 0 -delay 20 a-*.gif result.gif

That looks longer, and harder, than it is because I tried to explain it all, but it looks like this really:

convert 1.gif -coalesce a-%04d.gif                         # separate frames of 1.gif
convert 2.gif -coalesce b-%04d.gif                         # separate frames of 2.gif
for f in a-*.gif; do convert $f ${f/a/b} +append $f; done  # append frames side-by-side
convert -loop 0 -delay 20 a-*.gif result.gif               # rejoin frames

enter image description here

Note that this conceptual code, not production quality. It doesn't remove the temporary files it creates, nor does it carry the inter-frame time forward from the original GIFs. If you want to get the original frame rate you could get them like this and save them into an array and feed the delays back into the re-animation command at the end:

identify -format "%f[%s] %T\n" 1.gif
1.gif[0] 8
1.gif[1] 8
1.gif[2] 8
1.gif[3] 8
1.gif[4] 8
1.gif[5] 8
1.gif[6] 8
1.gif[7] 8
1.gif[8] 8
1.gif[9] 8
1.gif[10] 11
1.gif[11] 11
1.gif[12] 11
1.gif[13] 11
1.gif[14] 11
1.gif[15] 11
1.gif[16] 11
1.gif[17] 26

Also, you may want a spacer between the two animations, say 10 pixels, which you can do by replacing the convert command inside the for loop with this one:

convert $f -size 10x xc:none ${f/a/b} +append $f

enter image description here

Upvotes: 31

Related Questions