Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util/animated_image.cpp
4802 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "animated_image.h"
5
6
#include "common/assert.h"
7
#include "common/bitutils.h"
8
#include "common/error.h"
9
#include "common/file_system.h"
10
#include "common/log.h"
11
#include "common/path.h"
12
#include "common/scoped_guard.h"
13
#include "common/string_util.h"
14
15
#include <png.h>
16
17
// clang-format off
18
#ifdef _MSC_VER
19
#pragma warning(disable : 4611) // warning C4611: interaction between '_setjmp' and C++ object destruction is non-portable
20
#endif
21
// clang-format on
22
23
LOG_CHANNEL(Image);
24
25
static bool PNGBufferLoader(AnimatedImage* image, std::span<const u8> data, Error* error);
26
static bool PNGBufferSaver(const AnimatedImage& image, DynamicHeapArray<u8>* data, u8 quality, Error* error);
27
static bool PNGFileLoader(AnimatedImage* image, std::string_view filename, std::FILE* fp, Error* error);
28
static bool PNGFileSaver(const AnimatedImage& image, std::string_view filename, std::FILE* fp, u8 quality,
29
Error* error);
30
31
namespace {
32
struct FormatHandler
33
{
34
const char* extension;
35
bool (*buffer_loader)(AnimatedImage*, std::span<const u8>, Error*);
36
bool (*buffer_saver)(const AnimatedImage&, DynamicHeapArray<u8>*, u8, Error*);
37
bool (*file_loader)(AnimatedImage*, std::string_view, std::FILE*, Error*);
38
bool (*file_saver)(const AnimatedImage&, std::string_view, std::FILE*, u8, Error*);
39
};
40
} // namespace
41
42
static constexpr FormatHandler s_format_handlers[] = {
43
{"png", PNGBufferLoader, PNGBufferSaver, PNGFileLoader, PNGFileSaver},
44
};
45
46
static const FormatHandler* GetFormatHandler(std::string_view extension)
47
{
48
for (const FormatHandler& handler : s_format_handlers)
49
{
50
if (StringUtil::Strncasecmp(extension.data(), handler.extension, extension.size()) == 0)
51
return &handler;
52
}
53
54
return nullptr;
55
}
56
57
AnimatedImage::AnimatedImage() = default;
58
59
AnimatedImage::AnimatedImage(const AnimatedImage& copy)
60
: m_width(copy.m_width), m_height(copy.m_height), m_frame_size(copy.m_frame_size), m_frames(copy.m_frames),
61
m_pixels(copy.m_pixels), m_frame_delay(copy.m_frame_delay)
62
{
63
}
64
65
AnimatedImage::AnimatedImage(AnimatedImage&& move)
66
{
67
m_width = std::exchange(move.m_width, 0);
68
m_height = std::exchange(move.m_height, 0);
69
m_frame_size = std::exchange(move.m_frame_size, 0);
70
m_frames = std::exchange(move.m_frames, 0);
71
m_pixels = std::move(move.m_pixels);
72
m_frame_delay = std::move(move.m_frame_delay);
73
}
74
75
AnimatedImage::AnimatedImage(u32 width, u32 height, u32 frames, const FrameDelay& default_delay)
76
: m_width(width), m_height(height), m_frame_size(width * height), m_frames(frames), m_pixels(frames * width * height),
77
m_frame_delay(frames)
78
{
79
for (FrameDelay& delay : m_frame_delay)
80
delay = default_delay;
81
}
82
83
void AnimatedImage::Resize(u32 new_width, u32 new_height, u32 num_frames, const FrameDelay& default_delay,
84
bool preserve)
85
{
86
DebugAssert(new_width > 0 && new_height > 0 && num_frames > 0);
87
if (m_width == new_width && m_height == new_height && num_frames == m_frames)
88
return;
89
90
if (!preserve)
91
m_pixels.deallocate();
92
93
const u32 new_frame_size = new_width * new_height;
94
95
PixelStorage new_pixels;
96
new_pixels.resize(new_frame_size * num_frames);
97
std::memset(new_pixels.data(), 0, new_pixels.size() * sizeof(u32));
98
m_frame_delay.resize(num_frames);
99
if (preserve && !m_pixels.empty())
100
{
101
const u32 copy_frames = std::min(num_frames, m_frames);
102
for (u32 i = 0; i < copy_frames; i++)
103
{
104
StringUtil::StrideMemCpy(new_pixels.data() + i * new_frame_size, new_width * sizeof(u32),
105
m_pixels.data() + i * m_frame_size, m_width * sizeof(u32),
106
std::min(new_width, m_width) * sizeof(u32), std::min(new_height, m_height));
107
}
108
109
for (u32 i = m_frames; i < num_frames; i++)
110
m_frame_delay[i] = default_delay;
111
}
112
113
m_width = new_width;
114
m_height = new_height;
115
m_frame_size = new_frame_size;
116
m_frames = num_frames;
117
m_pixels = std::move(new_pixels);
118
}
119
120
AnimatedImage& AnimatedImage::operator=(const AnimatedImage& copy)
121
{
122
m_width = copy.m_width;
123
m_height = copy.m_height;
124
m_frame_size = copy.m_frame_size;
125
m_frames = copy.m_frames;
126
m_pixels = copy.m_pixels;
127
m_frame_delay = copy.m_frame_delay;
128
return *this;
129
}
130
131
AnimatedImage& AnimatedImage::operator=(AnimatedImage&& move)
132
{
133
m_width = std::exchange(move.m_width, 0);
134
m_height = std::exchange(move.m_height, 0);
135
m_frame_size = std::exchange(move.m_frame_size, 0);
136
m_frames = std::exchange(move.m_frames, 0);
137
m_pixels = std::move(move.m_pixels);
138
m_frame_delay = std::move(move.m_frame_delay);
139
return *this;
140
}
141
142
u32 AnimatedImage::CalculatePitch(u32 width, u32 height)
143
{
144
return width * sizeof(u32);
145
}
146
147
std::span<const AnimatedImage::PixelType> AnimatedImage::GetPixelsSpan(u32 frame) const
148
{
149
DebugAssert(frame < m_frames);
150
return m_pixels.cspan(frame * m_frame_size, m_frame_size);
151
}
152
153
std::span<AnimatedImage::PixelType> AnimatedImage::GetPixelsSpan(u32 frame)
154
{
155
DebugAssert(frame < m_frames);
156
return m_pixels.span(frame * m_frame_size, m_frame_size);
157
}
158
159
void AnimatedImage::Clear()
160
{
161
std::memset(m_pixels.data(), 0, m_pixels.size_bytes());
162
}
163
164
void AnimatedImage::Invalidate()
165
{
166
m_width = 0;
167
m_height = 0;
168
m_frame_size = 0;
169
m_frames = 0;
170
m_pixels.deallocate();
171
m_frame_delay.deallocate();
172
}
173
174
void AnimatedImage::SetPixels(u32 frame, const void* pixels, u32 pitch)
175
{
176
DebugAssert(frame < m_frames);
177
StringUtil::StrideMemCpy(GetPixels(frame), m_width * sizeof(u32), pixels, pitch, m_width * sizeof(u32), m_height);
178
}
179
180
void AnimatedImage::SetDelay(u32 frame, const FrameDelay& delay)
181
{
182
DebugAssert(frame < m_frames);
183
m_frame_delay[frame] = delay;
184
}
185
186
AnimatedImage::PixelStorage AnimatedImage::TakePixels()
187
{
188
m_width = 0;
189
m_height = 0;
190
m_frame_size = 0;
191
m_frames = 0;
192
return std::move(m_pixels);
193
}
194
195
bool AnimatedImage::LoadFromFile(const char* filename, Error* error /* = nullptr */)
196
{
197
auto fp = FileSystem::OpenManagedCFile(filename, "rb", error);
198
if (!fp)
199
return false;
200
201
return LoadFromFile(filename, fp.get(), error);
202
}
203
204
bool AnimatedImage::SaveToFile(const char* filename, u8 quality /* = DEFAULT_SAVE_QUALITY */,
205
Error* error /* = nullptr */) const
206
{
207
auto fp = FileSystem::OpenManagedCFile(filename, "wb", error);
208
if (!fp)
209
return false;
210
211
if (SaveToFile(filename, fp.get(), quality, error))
212
return true;
213
214
// save failed
215
fp.reset();
216
FileSystem::DeleteFile(filename);
217
return false;
218
}
219
220
bool AnimatedImage::LoadFromFile(std::string_view filename, std::FILE* fp, Error* error /* = nullptr */)
221
{
222
const std::string_view extension(Path::GetExtension(filename));
223
const FormatHandler* handler = GetFormatHandler(extension);
224
if (!handler || !handler->file_loader)
225
{
226
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
227
return false;
228
}
229
230
return handler->file_loader(this, filename, fp, error);
231
}
232
233
bool AnimatedImage::LoadFromBuffer(std::string_view filename, std::span<const u8> data, Error* error /* = nullptr */)
234
{
235
const std::string_view extension(Path::GetExtension(filename));
236
const FormatHandler* handler = GetFormatHandler(extension);
237
if (!handler || !handler->buffer_loader)
238
{
239
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
240
return false;
241
}
242
243
return handler->buffer_loader(this, data, error);
244
}
245
246
bool AnimatedImage::SaveToFile(std::string_view filename, std::FILE* fp, u8 quality /* = DEFAULT_SAVE_QUALITY */,
247
Error* error /* = nullptr */) const
248
{
249
const std::string_view extension(Path::GetExtension(filename));
250
const FormatHandler* handler = GetFormatHandler(extension);
251
if (!handler || !handler->file_saver)
252
{
253
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
254
return false;
255
}
256
257
if (!handler->file_saver(*this, filename, fp, quality, error))
258
return false;
259
260
if (std::fflush(fp) != 0)
261
{
262
Error::SetErrno(error, "fflush() failed: ", errno);
263
return false;
264
}
265
266
return true;
267
}
268
269
std::optional<DynamicHeapArray<u8>> AnimatedImage::SaveToBuffer(std::string_view filename,
270
u8 quality /* = DEFAULT_SAVE_QUALITY */,
271
Error* error /* = nullptr */) const
272
{
273
std::optional<DynamicHeapArray<u8>> ret;
274
275
const std::string_view extension(Path::GetExtension(filename));
276
const FormatHandler* handler = GetFormatHandler(extension);
277
if (!handler || !handler->file_saver)
278
{
279
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
280
return ret;
281
}
282
283
ret = DynamicHeapArray<u8>();
284
if (!handler->buffer_saver(*this, &ret.value(), quality, error))
285
ret.reset();
286
287
return ret;
288
}
289
290
static void PNGSetErrorFunction(png_structp png_ptr, Error* error)
291
{
292
png_set_error_fn(
293
png_ptr, error,
294
[](png_structp png_ptr, png_const_charp message) {
295
Error::SetStringView(static_cast<Error*>(png_get_error_ptr(png_ptr)), message);
296
png_longjmp(png_ptr, 1);
297
},
298
[](png_structp png_ptr, png_const_charp message) { WARNING_LOG("libpng warning: {}", message); });
299
}
300
301
static bool PNGCommonLoader(AnimatedImage* image, png_structp png_ptr, png_infop info_ptr)
302
{
303
png_read_info(png_ptr, info_ptr);
304
305
const u32 width = png_get_image_width(png_ptr, info_ptr);
306
const u32 height = png_get_image_height(png_ptr, info_ptr);
307
const u32 num_frames = png_get_num_frames(png_ptr, info_ptr);
308
const png_byte color_type = png_get_color_type(png_ptr, info_ptr);
309
const png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
310
311
if (num_frames == 0)
312
png_error(png_ptr, "Image has zero frames");
313
314
// Read any color_type into 8bit depth, RGBA format.
315
// See http://www.libpng.org/pub/png/libpng-manual.txt
316
317
if (bit_depth == 16)
318
png_set_strip_16(png_ptr);
319
320
if (color_type == PNG_COLOR_TYPE_PALETTE)
321
png_set_palette_to_rgb(png_ptr);
322
323
// PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
324
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
325
png_set_expand_gray_1_2_4_to_8(png_ptr);
326
327
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
328
png_set_tRNS_to_alpha(png_ptr);
329
330
// These color_type don't have an alpha channel then fill it with 0xff.
331
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)
332
png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
333
334
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
335
png_set_gray_to_rgb(png_ptr);
336
337
png_read_update_info(png_ptr, info_ptr);
338
339
DebugAssert(num_frames > 0);
340
image->Resize(width, height, num_frames, {1, 10}, false);
341
if (num_frames > 1)
342
{
343
for (u32 i = 0; i < num_frames; i++)
344
{
345
png_read_frame_head(png_ptr, info_ptr);
346
347
const u32 frame_width = png_get_next_frame_width(png_ptr, info_ptr);
348
const u32 frame_height = png_get_next_frame_height(png_ptr, info_ptr);
349
if (frame_width != width || frame_height != height)
350
png_error(png_ptr, "Frame size does not match image size");
351
352
const u16 delay_num = static_cast<u16>(png_get_next_frame_delay_num(png_ptr, info_ptr));
353
const u16 delay_den = static_cast<u16>(png_get_next_frame_delay_den(png_ptr, info_ptr));
354
image->SetDelay(i, {delay_num, std::max<u16>(delay_den, 1)});
355
356
// TODO: blending/compose/etc.
357
const int num_passes = png_set_interlace_handling(png_ptr);
358
for (int pass = 0; pass < num_passes; pass++)
359
{
360
for (u32 y = 0; y < height; y++)
361
png_read_row(png_ptr, reinterpret_cast<png_bytep>(image->GetRowPixels(i, y)), nullptr);
362
}
363
}
364
}
365
else
366
{
367
const int num_passes = png_set_interlace_handling(png_ptr);
368
for (int pass = 0; pass < num_passes; pass++)
369
{
370
for (u32 y = 0; y < height; y++)
371
png_read_row(png_ptr, reinterpret_cast<png_bytep>(image->GetRowPixels(0, y)), nullptr);
372
}
373
}
374
375
return true;
376
}
377
378
bool PNGFileLoader(AnimatedImage* image, std::string_view filename, std::FILE* fp, Error* error)
379
{
380
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
381
if (!png_ptr)
382
{
383
Error::SetStringView(error, "png_create_read_struct() failed.");
384
return false;
385
}
386
387
png_infop info_ptr = png_create_info_struct(png_ptr);
388
if (!info_ptr)
389
{
390
Error::SetStringView(error, "png_create_info_struct() failed.");
391
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
392
return false;
393
}
394
395
ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
396
397
PNGSetErrorFunction(png_ptr, error);
398
if (setjmp(png_jmpbuf(png_ptr)))
399
{
400
image->Invalidate();
401
return false;
402
}
403
404
png_set_read_fn(png_ptr, fp, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
405
std::FILE* fp = static_cast<std::FILE*>(png_get_io_ptr(png_ptr));
406
if (std::fread(data_ptr, size, 1, fp) != 1)
407
png_error(png_ptr, "fread() failed");
408
});
409
410
return PNGCommonLoader(image, png_ptr, info_ptr);
411
}
412
413
bool PNGBufferLoader(AnimatedImage* image, std::span<const u8> data, Error* error)
414
{
415
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
416
if (!png_ptr)
417
{
418
Error::SetStringView(error, "png_create_read_struct() failed.");
419
return false;
420
}
421
422
png_infop info_ptr = png_create_info_struct(png_ptr);
423
if (!info_ptr)
424
{
425
Error::SetStringView(error, "png_create_info_struct() failed.");
426
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
427
return false;
428
}
429
430
ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
431
432
std::vector<png_bytep> row_pointers;
433
434
PNGSetErrorFunction(png_ptr, error);
435
if (setjmp(png_jmpbuf(png_ptr)))
436
{
437
image->Invalidate();
438
return false;
439
}
440
441
struct IOData
442
{
443
std::span<const u8> buffer;
444
size_t buffer_pos;
445
};
446
IOData iodata = {data, 0};
447
448
png_set_read_fn(png_ptr, &iodata, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
449
IOData* data = static_cast<IOData*>(png_get_io_ptr(png_ptr));
450
const size_t read_size = std::min<size_t>(data->buffer.size() - data->buffer_pos, size);
451
if (read_size > 0)
452
{
453
std::memcpy(data_ptr, &data->buffer[data->buffer_pos], read_size);
454
data->buffer_pos += read_size;
455
}
456
});
457
458
return PNGCommonLoader(image, png_ptr, info_ptr);
459
}
460
461
static void PNGSaveCommon(const AnimatedImage& image, png_structp png_ptr, png_infop info_ptr, u8 quality)
462
{
463
png_set_compression_level(png_ptr, std::clamp(quality / 10, 0, 9));
464
png_set_IHDR(png_ptr, info_ptr, image.GetWidth(), image.GetHeight(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
465
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
466
467
const u32 width = image.GetWidth();
468
const u32 height = image.GetHeight();
469
const u32 frames = image.GetFrames();
470
if (frames > 1)
471
{
472
if (!png_set_acTL(png_ptr, info_ptr, frames, 0))
473
png_error(png_ptr, "png_set_acTL() failed");
474
475
png_write_info(png_ptr, info_ptr);
476
477
for (u32 i = 0; i < frames; i++)
478
{
479
const AnimatedImage::FrameDelay& fd = image.GetFrameDelay(i);
480
png_write_frame_head(png_ptr, info_ptr, width, height, 0, 0, fd.numerator, fd.denominator, PNG_DISPOSE_OP_NONE,
481
PNG_BLEND_OP_SOURCE);
482
483
for (u32 y = 0; y < height; ++y)
484
png_write_row(png_ptr, (png_bytep)image.GetRowPixels(i, y));
485
486
png_write_frame_tail(png_ptr, info_ptr);
487
}
488
}
489
else
490
{
491
// only one frame
492
png_write_info(png_ptr, info_ptr);
493
for (u32 y = 0; y < height; ++y)
494
png_write_row(png_ptr, (png_bytep)image.GetRowPixels(0, y));
495
}
496
497
png_write_end(png_ptr, nullptr);
498
}
499
500
bool PNGFileSaver(const AnimatedImage& image, std::string_view filename, std::FILE* fp, u8 quality, Error* error)
501
{
502
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
503
png_infop info_ptr = nullptr;
504
if (!png_ptr)
505
{
506
Error::SetStringView(error, "png_create_write_struct() failed.");
507
return false;
508
}
509
510
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
511
if (png_ptr)
512
png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
513
});
514
515
info_ptr = png_create_info_struct(png_ptr);
516
if (!info_ptr)
517
{
518
Error::SetStringView(error, "png_create_info_struct() failed.");
519
return false;
520
}
521
522
PNGSetErrorFunction(png_ptr, error);
523
if (setjmp(png_jmpbuf(png_ptr)))
524
return false;
525
526
png_set_write_fn(
527
png_ptr, fp,
528
[](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
529
if (std::fwrite(data_ptr, size, 1, static_cast<std::FILE*>(png_get_io_ptr(png_ptr))) != 1)
530
png_error(png_ptr, "fwrite() failed");
531
},
532
[](png_structp png_ptr) {});
533
534
PNGSaveCommon(image, png_ptr, info_ptr, quality);
535
return true;
536
}
537
538
bool PNGBufferSaver(const AnimatedImage& image, DynamicHeapArray<u8>* data, u8 quality, Error* error)
539
{
540
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
541
png_infop info_ptr = nullptr;
542
if (!png_ptr)
543
{
544
Error::SetStringView(error, "png_create_write_struct() failed.");
545
return false;
546
}
547
548
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
549
if (png_ptr)
550
png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
551
});
552
553
info_ptr = png_create_info_struct(png_ptr);
554
if (!info_ptr)
555
{
556
Error::SetStringView(error, "png_create_info_struct() failed.");
557
return false;
558
}
559
560
struct IOData
561
{
562
DynamicHeapArray<u8>* buffer;
563
size_t buffer_pos;
564
};
565
IOData iodata = {data, 0};
566
567
data->resize(image.GetWidth() * image.GetHeight() * 2);
568
569
PNGSetErrorFunction(png_ptr, error);
570
if (setjmp(png_jmpbuf(png_ptr)))
571
return false;
572
573
png_set_write_fn(
574
png_ptr, &iodata,
575
[](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
576
IOData* iodata = static_cast<IOData*>(png_get_io_ptr(png_ptr));
577
const size_t new_pos = iodata->buffer_pos + size;
578
if (new_pos > iodata->buffer->size())
579
iodata->buffer->resize(std::max(new_pos, iodata->buffer->size() * 2));
580
std::memcpy(iodata->buffer->data() + iodata->buffer_pos, data_ptr, size);
581
iodata->buffer_pos += size;
582
},
583
[](png_structp png_ptr) {});
584
585
PNGSaveCommon(image, png_ptr, info_ptr, quality);
586
iodata.buffer->resize(iodata.buffer_pos);
587
return true;
588
}
589
590