Skip to content

Commit 6b2263e

Browse files
committed
Support for GIF emojis
1 parent 3123d6b commit 6b2263e

7 files changed

Lines changed: 227 additions & 30 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ add_library(${PROJECT_NAME} OBJECT
241241
src/image_xyz.h
242242
src/image_webp.cpp
243243
src/image_webp.h
244+
src/image_gif.cpp
245+
src/image_gif.h
244246
src/input_buttons_desktop.cpp
245247
src/input_buttons.h
246248
src/input.cpp

CMakePresets.json

Lines changed: 11 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/image_gif.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#include "image_gif.h"
2+
#include <gif_lib.h>
3+
#include "output.h"
4+
5+
static int read_data(GifFileType* gifFile, GifByteType* buffer, int size) {
6+
auto* bufp = reinterpret_cast<Filesystem_Stream::InputStream*>(gifFile->UserData);
7+
if (bufp && *bufp) {
8+
bufp->read(reinterpret_cast<char*>(buffer), size);
9+
if (bufp->fail()) return 0; // Read failed
10+
return size; // Return number of bytes read
11+
}
12+
return 0; // No data available
13+
}
14+
15+
ImageGif::Decoder::Decoder(Filesystem_Stream::InputStream& is) noexcept : ImageGif::Decoder() {
16+
int error = D_GIF_SUCCEEDED;
17+
gifFile = DGifOpen(&is, read_data, &error);
18+
19+
if (!gifFile || error != D_GIF_SUCCEEDED) {
20+
Output::Warning("ImageGif: Failed to open GIF file: {} ({})", is.GetName(), GifErrorString(error));
21+
return;
22+
}
23+
24+
if (DGifSlurp(gifFile) != GIF_OK) {
25+
Output::Warning("ImageGif: Failed to read GIF data: {} ({})", is.GetName());
26+
return;
27+
}
28+
29+
currentFrame = 0;
30+
}
31+
32+
ImageGif::Decoder::~Decoder() noexcept {
33+
if (gifFile) DGifCloseFile(gifFile, nullptr);
34+
}
35+
36+
bool ImageGif::Decoder::ReadNext(ImageOut& output, GifTimingInfo& timing) {
37+
memset(&output, 0, sizeof(ImageOut));
38+
memset(&timing, 0, sizeof(GifTimingInfo));
39+
40+
if (!gifFile || currentFrame >= gifFile->ImageCount) {
41+
output.pixels = nullptr;
42+
return false;
43+
}
44+
45+
const SavedImage* frame = &gifFile->SavedImages[currentFrame];
46+
const GifImageDesc& image = frame->ImageDesc;
47+
48+
ColorMapObject* colorMap = image.ColorMap ? image.ColorMap : gifFile->SColorMap;
49+
if (!colorMap) {
50+
Output::Warning("ImageGif: No color map found for frame {}", currentFrame);
51+
output.pixels = nullptr;
52+
return false;
53+
}
54+
55+
output.width = image.Width;
56+
output.height = image.Height;
57+
output.bpp = 32;
58+
output.pixels = new uint32_t[image.Width * image.Height];
59+
60+
const int left = image.Left;
61+
const int top = image.Top;
62+
const int width = image.Width;
63+
const int height = image.Height;
64+
const int bg = gifFile->SBackGroundColor;
65+
66+
const GifPixelType* src = frame->RasterBits;
67+
// const GifPixelType* srcPrev = currentFrame > 0 ? gifFile->SavedImages[currentFrame - 1].RasterBits : nullptr;
68+
69+
int transparentIndex = -1;
70+
int disposalMethod = 0;
71+
72+
for (int i = 0; i < frame->ExtensionBlockCount; ++i) {
73+
if (frame->ExtensionBlocks[i].Function == GRAPHICS_EXT_FUNC_CODE) {
74+
uint8_t fields = frame->ExtensionBlocks[i].Bytes[0];
75+
disposalMethod = (fields & 0x1C) >> 2; // 3 bits for disposal method
76+
bool transparency = (fields & 0x01) != 0; // 1 bit for transparency
77+
if (transparency) {
78+
transparentIndex = frame->ExtensionBlocks[i].Bytes[3]; // Transparency index
79+
}
80+
timing.delay = ((int)frame->ExtensionBlocks[i].Bytes[1] | ((int)frame->ExtensionBlocks[i].Bytes[2] << 8)) * 10; // Convert to milliseconds
81+
break;
82+
}
83+
}
84+
85+
size_t srcIndex = 0;
86+
for (int y = 0; y < height; ++y) {
87+
for (int x = 0; x < width; ++x) {
88+
GifPixelType index = src[srcIndex++];
89+
if (index == transparentIndex) {
90+
((uint32_t*)output.pixels)[(top + y) * output.width + (left + x)] = 0; // Transparent pixel
91+
continue;
92+
}
93+
// if (disposalMethod == 2)
94+
// ((uint32_t*)output.pixels)[(top + y) * output.width + (left + x)] = bg;
95+
if (index < colorMap->ColorCount) {
96+
const GifColorType& color = colorMap->Colors[index];
97+
int dstX = left + x;
98+
int dstY = top + y;
99+
if (dstX < output.width && dstY < output.height) {
100+
((uint32_t*)output.pixels)[dstY * output.width + dstX] =
101+
color.Red | (color.Green << 8) | (color.Blue << 16) | 0xFF000000;
102+
}
103+
}
104+
}
105+
}
106+
107+
currentFrame++;
108+
return true;
109+
}

src/image_gif.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* This file is part of EasyRPG Player.
3+
*
4+
* EasyRPG Player is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* EasyRPG Player is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with EasyRPG Player. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#ifndef EP_IMAGE_GIF_H
19+
#define EP_IMAGE_GIF_H
20+
21+
#include <cstdint>
22+
#include <optional>
23+
#include "bitmap.h"
24+
#include "filesystem_stream.h"
25+
#include <gif_lib.h>
26+
27+
struct GifTimingInfo {
28+
int delay;
29+
};
30+
31+
namespace ImageGif {
32+
struct Decoder {
33+
~Decoder() noexcept;
34+
35+
bool ReadNext(ImageOut& output, GifTimingInfo& timing);
36+
Decoder(Filesystem_Stream::InputStream& is) noexcept;
37+
38+
inline operator bool() const noexcept { return gifFile != nullptr; }
39+
private:
40+
explicit Decoder() noexcept = default;
41+
42+
GifFileType* gifFile;
43+
int currentFrame{};
44+
};
45+
46+
}
47+
48+
#endif

src/image_webp.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ bool ImageWebP::Decoder::ReadNext(ImageOut& output, TimingInfo& timing) {
6666
Output::Warning("ImageWebP: Failed to decode next frame");
6767
return false;
6868
}
69-
output.pixels = new uint32_t[animData.canvas_width * animData.canvas_height * 4];
70-
memcpy(output.pixels, pixels, animData.canvas_width * animData.canvas_height * 4);
69+
output.pixels = new uint32_t[animData.canvas_width * animData.canvas_height];
70+
memcpy(output.pixels, pixels, animData.canvas_width * animData.canvas_height * sizeof(uint32_t));
7171
output.bpp = 32; // WebP always uses 32 bits per pixel (RGBA)
7272
output.height = animData.canvas_height;
7373
output.width = animData.canvas_width;

src/multiplayer/chat_overlay.cpp

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ using json = nlohmann::json;
4141
#include "icons.h"
4242
#include "overlay_utils.h"
4343
#include "image_webp.h"
44+
#include "image_gif.h"
4445

4546
namespace {
4647
Point ChatTextSquare() {
@@ -201,17 +202,18 @@ void ChatOverlay::Draw(Bitmap& dst) {
201202
// semitransparent bg
202203
int yidx = lidx + 1 + input_row_offset;
203204
int line_height = text_height;
205+
bool emojis_only = false;
204206

205207
// begin override line height for components
206208

207209
// expand messages with only emojis
208-
if (std::all_of(line->cbegin(), line->cend(), [](const std::shared_ptr<ChatComponent>& comp) { return bool(comp->Downcast<ChatComponents::Emoji>()); })) {
209-
line_height = OverlayUtils::LargeScreen() ? 56 : 18;
210-
}
211-
212210
if (std::any_of(line->cbegin(), line->cend(), [](const std::shared_ptr<ChatComponent>& comp) { return bool(comp->Downcast<ChatComponents::Screenshot>()); })) {
213211
line_height = ChatScreenshot::sizer().y;
214212
}
213+
else if (std::all_of(line->cbegin(), line->cend(), [](const std::shared_ptr<ChatComponent>& comp) { return bool(comp->Downcast<ChatComponents::Emoji>()); })) {
214+
emojis_only = true;
215+
line_height = OverlayUtils::LargeScreen() ? 56 : 18;
216+
}
215217

216218
// end override line height
217219

@@ -247,22 +249,21 @@ void ChatOverlay::Draw(Bitmap& dst) {
247249
}
248250
else if (auto emoji = span->Downcast<ChatComponents::Emoji>()) {
249251
Point dims = emoji->GetSize();
252+
if (emojis_only) dims.x = dims.y = line_height;
250253
int comp_y = y + (baseline ? baseline - text_height : 0);
251254
if (emoji->bitmap) {
252-
double zoom = emoji->bitmap->GetRect().y / (double)text_height;
253-
bitmap->StretchBlit({offset, comp_y, line_height, line_height}, *emoji->bitmap, emoji->bitmap->GetRect(), 255);
255+
bitmap->StretchBlit({offset, comp_y, dims.x, dims.y}, *emoji->bitmap, emoji->bitmap->GetRect(), 255);
254256
}
255-
else {
257+
else if (!emoji->HasAnimation()) {
256258
// the emoji is still live, request it now
257259
emoji->RequestBitmap(this);
258-
bitmap->FillRect({ offset, comp_y, line_height, line_height }, offwhite);
260+
bitmap->FillRect({ offset, comp_y, dims.y, dims.y }, offwhite);
259261
}
260-
offset += line_height;
262+
offset += dims.x;
261263
}
262264
else if (auto screenshot = span->Downcast<ChatComponents::Screenshot>()) {
263265
Point dims = screenshot->GetSize();
264266
if (screenshot->bitmap) {
265-
double zoom = screenshot->bitmap->GetRect().y / (double)text_height;
266267
bitmap->StretchBlit({offset, y, dims.x, dims.y}, *screenshot->bitmap, screenshot->bitmap->GetRect(), 255);
267268
}
268269
else {
@@ -608,6 +609,8 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) {
608609
if (emoji.empty()) return;
609610
parent = parent_;
610611

612+
if (request) return;
613+
611614
auto req = AsyncHandler::RequestFile("../images/ynomoji", emoji);
612615
req->SetGraphicFile(true);
613616
req->SetParentScope(true);
@@ -627,6 +630,7 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) {
627630
}
628631

629632
request = req->Bind([this, extension](FileRequestResult* result) {
633+
request.reset(); // Clear the request after processing
630634
if (!result->success) {
631635
emoji.clear();
632636
Output::Debug("failed: {}", result->file);
@@ -650,25 +654,51 @@ void ChatEmoji::RequestBitmap(ChatOverlay* parent_) {
650654
}
651655

652656
void ChatEmoji::DecodeGif(const std::string& filePath) {
653-
// Use a GIF decoding library to load frames and delays
654-
// Example: giflib or similar library
655-
// Pseudo-code:
657+
constexpr int minimum_frame_delay = 64;
656658
frames.clear();
657659
frameDelays.clear();
658-
// Load GIF file and extract frames and delays
659-
// for each frame in GIF:
660-
// Convert frame to Bitmap and store in gifFrames
661-
// Store delay in frameDelays
662-
// Set currentFrame to 0
660+
auto fs = FileFinder::OpenImage("../images/ynomoji", filePath);
661+
if (!fs) return;
662+
663+
ImageGif::Decoder dec(fs);
664+
if (!dec) return;
665+
666+
ImageOut image{};
667+
GifTimingInfo timing{};
668+
while (dec.ReadNext(image, timing)) {
669+
auto bitmap = Bitmap::Create(image.pixels, image.width, image.height, 0, format_R8G8B8A8_a().format());
670+
if (!bitmap) {
671+
if (image.pixels) delete[] image.pixels;
672+
continue;
673+
}
674+
bitmap->SetBilinear();
675+
frames.push_back(std::move(bitmap));
676+
677+
if (!timing.delay)
678+
timing.delay = 100;
679+
680+
int delay = std::min(minimum_frame_delay, timing.delay);
681+
frameDelays.push_back(delay);
682+
loopLength += frameDelays.back();
683+
}
684+
663685
currentFrame = 0;
664-
lastFrameTime = std::chrono::steady_clock::now();
686+
if (loopLength && frames.size() > 1) {
687+
lastFrameTime = std::chrono::steady_clock::now();
688+
int loopDelta = std::chrono::duration_cast<std::chrono::milliseconds>(lastFrameTime.time_since_epoch()).count() % loopLength;
689+
while (currentFrame < frameDelays.size() && loopDelta >= frameDelays[currentFrame]) {
690+
loopDelta -= frameDelays[currentFrame];
691+
currentFrame = (currentFrame + 1) % frames.size();
692+
}
693+
}
665694
}
666695

667696
void ChatEmoji::DecodeWebP(const std::string& filePath) {
668697
// Use a WebP decoding library to load the image
669698
// Example: libwebp or similar library
670699
// Pseudo-code:
671700
frames.clear();
701+
frameDelays.clear();
672702
auto fs = FileFinder::OpenImage("../images/ynomoji", filePath);
673703
if (!fs) return;
674704

@@ -714,7 +744,8 @@ void ChatEmoji::DecodeWebP(const std::string& filePath) {
714744
}
715745

716746
void ChatEmoji::UpdateAnimation() {
717-
if (frames.size() < 2 || frameDelays.empty()) return; // No animation to update
747+
if (frames.size() == 1) bitmap = frames[0]; // If there's only one frame, just display it
748+
else if (frames.size() < 2 || frameDelays.empty()) return; // No animation to update
718749

719750
auto now = std::chrono::steady_clock::now();
720751
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastFrameTime).count();

src/multiplayer/chat_overlay.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <lcf/span.h>
2424
#include <chrono>
2525
#include <uv.h>
26+
#include <mutex>
2627

2728
#include "drawable.h"
2829
#include "bitmap.h"
@@ -183,6 +184,9 @@ class ChatEmoji : public ChatComponent {
183184
void UpdateAnimation();
184185
static std::shared_ptr<ChatEmoji> GetOrCreate(const std::string& emojiKey, ChatOverlay* parent);
185186

187+
inline bool HasAnimation() const noexcept {
188+
return frames.size() > 1;
189+
}
186190
private:
187191
std::vector<std::shared_ptr<Bitmap>> frames; // Store frames for GIFs
188192
std::vector<int> frameDelays; // Store delays for each frame in milliseconds

0 commit comments

Comments
 (0)