diff --git a/data/pref.xml b/data/pref.xml
index 6133b7baf..a6847b35f 100644
--- a/data/pref.xml
+++ b/data/pref.xml
@@ -359,6 +359,7 @@
+
diff --git a/data/strings/en.ini b/data/strings/en.ini
index 3dc537e9e..023b0d408 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -463,6 +463,11 @@ layers = Layers:
frames = Frames:
anidir = Animation Direction:
pixel_ratio = Apply pixel ratio
+for_twitter = Export for Twitter
+for_twitter_tooltip = <<
+
+
diff --git a/src/app/commands/cmd_save_file.cpp b/src/app/commands/cmd_save_file.cpp
index 1b733353b..513a1f4d5 100644
--- a/src/app/commands/cmd_save_file.cpp
+++ b/src/app/commands/cmd_save_file.cpp
@@ -17,6 +17,7 @@
#include "app/console.h"
#include "app/context_access.h"
#include "app/file/file.h"
+#include "app/file/gif_format.h"
#include "app/file_selector.h"
#include "app/i18n/strings.h"
#include "app/job.h"
@@ -386,6 +387,8 @@ again:;
case doc::AniDir::PING_PONG: m_aniDir = "ping-pong"; break;
}
+ GifEncoderDurationFix fix(win.isForTwitter());
+
saveDocumentInBackground(
context, doc, outputFilename, false);
diff --git a/src/app/file/gif_format.cpp b/src/app/file/gif_format.cpp
index d0f8f0e4a..d796bf86d 100644
--- a/src/app/file/gif_format.cpp
+++ b/src/app/file/gif_format.cpp
@@ -14,6 +14,7 @@
#include "app/file/file.h"
#include "app/file/file_format.h"
#include "app/file/format_options.h"
+#include "app/file/gif_format.h"
#include "app/file/gif_options.h"
#include "app/modules/gui.h"
#include "app/pref/preferences.h"
@@ -102,6 +103,21 @@ FileFormat* CreateGifFormat()
static int interlaced_offset[] = { 0, 4, 2, 1 };
static int interlaced_jumps[] = { 8, 8, 4, 2 };
+// True if the GifEncoder should save the animation for Twitter:
+// * Frames duration >= 2, and
+// * Last frame 1/4 of its duration
+static bool fix_last_frame_duration = false;
+
+GifEncoderDurationFix::GifEncoderDurationFix(bool state)
+{
+ fix_last_frame_duration = state;
+}
+
+GifEncoderDurationFix::~GifEncoderDurationFix()
+{
+ fix_last_frame_duration = false;
+}
+
struct GifFilePtr {
public:
#if GIFLIB_MAJOR >= 5
@@ -967,7 +983,9 @@ public:
if (frameBounds.isEmpty())
frameBounds = gfx::Rect(0, 0, 1, 1);
- writeImage(gifFrame, frame, frameBounds, disposal);
+ writeImage(gifFrame, frame, frameBounds, disposal,
+ // Only the last frame in the animation needs the fix
+ (fix_last_frame_duration && gifFrame == nframes-1));
// Dispose/clear frame content
process_disposal_method(m_previousImage,
@@ -1035,10 +1053,22 @@ private:
// Writes graphics extension record (to save the duration of the
// frame and maybe the transparency index).
- void writeExtension(gifframe_t gifFrame, frame_t frame, int transparentIndex, DisposalMethod disposalMethod) {
+ void writeExtension(const gifframe_t gifFrame,
+ const frame_t frame,
+ const int transparentIndex,
+ const DisposalMethod disposalMethod,
+ const bool fixDuration) {
unsigned char extension_bytes[5];
int frameDelay = m_sprite->frameDuration(frame) / 10;
+ // Fix duration for Twitter. It looks like the last frame must be
+ // 1/4 of its duration for some strange reason in the Twitter
+ // conversion from GIF to video.
+ if (fixDuration)
+ frameDelay = MAX(2, frameDelay/4);
+ if (fix_last_frame_duration)
+ frameDelay = MAX(2, frameDelay);
+
extension_bytes[0] = (((int(disposalMethod) & 7) << 2) |
(transparentIndex >= 0 ? 1: 0));
extension_bytes[1] = (frameDelay & 0xff);
@@ -1106,7 +1136,11 @@ private:
}
}
- void writeImage(gifframe_t gifFrame, frame_t frame, const gfx::Rect& frameBounds, DisposalMethod disposal) {
+ void writeImage(const gifframe_t gifFrame,
+ const frame_t frame,
+ const gfx::Rect& frameBounds,
+ const DisposalMethod disposal,
+ const bool fixDuration) {
UniquePtr framePaletteRef;
UniquePtr rgbmapRef;
Palette* framePalette = m_sprite->palette(frame);
@@ -1227,7 +1261,8 @@ private:
remap.map(m_transparentIndex, localTransparent);
// Write extension record.
- writeExtension(gifFrame, frame, localTransparent, disposal);
+ writeExtension(gifFrame, frame, localTransparent,
+ disposal, fixDuration);
// Write the image record.
if (EGifPutImageDesc(m_gifFile,
diff --git a/src/app/file/gif_format.h b/src/app/file/gif_format.h
new file mode 100644
index 000000000..91cb0d15d
--- /dev/null
+++ b/src/app/file/gif_format.h
@@ -0,0 +1,21 @@
+// Aseprite
+// Copyright (C) 2018 David Capello
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_FILE_GIF_FORMAT_H_INCLUDED
+#define APP_FILE_GIF_FORMAT_H_INCLUDED
+#pragma once
+
+namespace app {
+
+ class GifEncoderDurationFix {
+ public:
+ GifEncoderDurationFix(bool state);
+ ~GifEncoderDurationFix();
+ };
+
+} // namespace app
+
+#endif
diff --git a/src/app/ui/export_file_window.cpp b/src/app/ui/export_file_window.cpp
index 310058fbf..0d90ffb6b 100644
--- a/src/app/ui/export_file_window.cpp
+++ b/src/app/ui/export_file_window.cpp
@@ -49,6 +49,7 @@ ExportFileWindow::ExportFileWindow(const Document* doc)
fill_frames_combobox(m_doc->sprite(), frames(), m_docPref.saveCopy.frameTag());
fill_anidir_combobox(anidir(), m_docPref.saveCopy.aniDir());
pixelRatio()->setSelected(m_docPref.saveCopy.applyPixelRatio());
+ forTwitter()->setSelected(m_docPref.saveCopy.forTwitter());
updateAniDir();
@@ -83,6 +84,7 @@ void ExportFileWindow::savePref()
m_docPref.saveCopy.layer(layersValue());
m_docPref.saveCopy.frameTag(framesValue());
m_docPref.saveCopy.applyPixelRatio(applyPixelRatio());
+ m_docPref.saveCopy.forTwitter(isForTwitter());
}
std::string ExportFileWindow::outputFilenameValue() const
@@ -116,6 +118,11 @@ bool ExportFileWindow::applyPixelRatio() const
return pixelRatio()->isSelected();
}
+bool ExportFileWindow::isForTwitter() const
+{
+ return forTwitter()->isSelected();
+}
+
void ExportFileWindow::setOutputFilename(const std::string& pathAndFilename)
{
m_outputPath = base::get_file_path(pathAndFilename);
diff --git a/src/app/ui/export_file_window.h b/src/app/ui/export_file_window.h
index 1d47afc9b..3ce0e05a2 100644
--- a/src/app/ui/export_file_window.h
+++ b/src/app/ui/export_file_window.h
@@ -31,6 +31,7 @@ namespace app {
std::string framesValue() const;
doc::AniDir aniDirValue() const;
bool applyPixelRatio() const;
+ bool isForTwitter() const;
obs::signal SelectOutputFile;