AnimatedImage: Add class for reading APNGs

And probably GIFs in the future.
This commit is contained in:
Stenzek
2025-09-24 22:05:07 +10:00
parent 9d14a4a57f
commit 102af48b6b
8 changed files with 1405 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
add_executable(util-tests
animated_image_tests.cpp
image_tests.cpp
)

View File

@@ -0,0 +1,720 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "util/animated_image.h"
#include "common/error.h"
#include <gtest/gtest.h>
#include <vector>
namespace {
// Test fixture for AnimatedImage tests
class AnimatedImageTest : public ::testing::Test
{
protected:
// Helper method to create test images with a pattern
AnimatedImage CreateTestImage(u32 width, u32 height, u32 frames = 1)
{
AnimatedImage img(width, height, frames, {1, 10});
for (u32 f = 0; f < frames; f++)
{
for (u32 y = 0; y < height; y++)
{
for (u32 x = 0; x < width; x++)
{
img.GetPixels(f)[y * width + x] = (x + y + f) | 0xFF000000; // Make pixel opaque
}
}
}
return img;
}
};
} // namespace
// Constructor Tests
TEST_F(AnimatedImageTest, DefaultConstructor)
{
AnimatedImage img;
EXPECT_FALSE(img.IsValid());
EXPECT_EQ(img.GetWidth(), 0u);
EXPECT_EQ(img.GetHeight(), 0u);
EXPECT_EQ(img.GetFrames(), 0u);
}
TEST_F(AnimatedImageTest, ParameterizedConstructor)
{
const u32 width = 100;
const u32 height = 80;
const u32 frames = 5;
AnimatedImage::FrameDelay delay{1, 10};
AnimatedImage img(width, height, frames, delay);
EXPECT_TRUE(img.IsValid());
EXPECT_EQ(img.GetWidth(), width);
EXPECT_EQ(img.GetHeight(), height);
EXPECT_EQ(img.GetFrames(), frames);
EXPECT_EQ(img.GetFrameSize(), width * height);
// Check all frames have the same delay
for (u32 i = 0; i < frames; i++)
{
EXPECT_EQ(img.GetFrameDelay(i).numerator, delay.numerator);
EXPECT_EQ(img.GetFrameDelay(i).denominator, delay.denominator);
}
}
TEST_F(AnimatedImageTest, CopyConstructor)
{
auto original = CreateTestImage(50, 40, 2);
AnimatedImage copy(original);
EXPECT_EQ(copy.GetWidth(), original.GetWidth());
EXPECT_EQ(copy.GetHeight(), original.GetHeight());
EXPECT_EQ(copy.GetFrames(), original.GetFrames());
// Check pixels match
for (u32 f = 0; f < original.GetFrames(); f++)
{
for (u32 i = 0; i < original.GetFrameSize(); i++)
{
EXPECT_EQ(copy.GetPixels(f)[i], original.GetPixels(f)[i]);
}
}
}
TEST_F(AnimatedImageTest, MoveConstructor)
{
auto original = CreateTestImage(50, 40, 2);
const u32 width = original.GetWidth();
const u32 height = original.GetHeight();
const u32 frames = original.GetFrames();
// Store original pixel data to compare later
std::vector<std::vector<u32>> pixel_copies;
for (u32 f = 0; f < frames; f++)
{
pixel_copies.push_back(std::vector<u32>(original.GetPixels(f), original.GetPixels(f) + original.GetFrameSize()));
}
AnimatedImage moved(std::move(original));
EXPECT_FALSE(original.IsValid()); // Original should be invalid after move
EXPECT_EQ(moved.GetWidth(), width);
EXPECT_EQ(moved.GetHeight(), height);
EXPECT_EQ(moved.GetFrames(), frames);
// Check pixels were moved correctly
for (u32 f = 0; f < frames; f++)
{
for (u32 i = 0; i < width * height; i++)
{
EXPECT_EQ(moved.GetPixels(f)[i], pixel_copies[f][i]);
}
}
}
// Assignment Operator Tests
TEST_F(AnimatedImageTest, CopyAssignment)
{
auto original = CreateTestImage(60, 50, 3);
AnimatedImage copy;
copy = original;
EXPECT_EQ(copy.GetWidth(), original.GetWidth());
EXPECT_EQ(copy.GetHeight(), original.GetHeight());
EXPECT_EQ(copy.GetFrames(), original.GetFrames());
// Check pixels match
for (u32 f = 0; f < original.GetFrames(); f++)
{
for (u32 i = 0; i < original.GetFrameSize(); i++)
{
EXPECT_EQ(copy.GetPixels(f)[i], original.GetPixels(f)[i]);
}
}
}
TEST_F(AnimatedImageTest, MoveAssignment)
{
auto original = CreateTestImage(60, 50, 3);
const u32 width = original.GetWidth();
const u32 height = original.GetHeight();
const u32 frames = original.GetFrames();
std::vector<std::vector<u32>> pixel_copies;
for (u32 f = 0; f < frames; f++)
{
pixel_copies.push_back(std::vector<u32>(original.GetPixels(f), original.GetPixels(f) + original.GetFrameSize()));
}
AnimatedImage moved;
moved = std::move(original);
EXPECT_FALSE(original.IsValid()); // Original should be invalid after move
EXPECT_EQ(moved.GetWidth(), width);
EXPECT_EQ(moved.GetHeight(), height);
EXPECT_EQ(moved.GetFrames(), frames);
// Check pixels were moved correctly
for (u32 f = 0; f < frames; f++)
{
for (u32 i = 0; i < width * height; i++)
{
EXPECT_EQ(moved.GetPixels(f)[i], pixel_copies[f][i]);
}
}
}
// Pixel Access Tests
TEST_F(AnimatedImageTest, PixelAccess)
{
const u32 width = 10;
const u32 height = 8;
AnimatedImage img(width, height, 1, {1, 10});
// Test direct pixel access
for (u32 y = 0; y < height; y++)
{
for (u32 x = 0; x < width; x++)
{
img.GetPixels(0)[y * width + x] = 0xFF000000u | (x + y * width);
}
}
// Verify pixels
for (u32 y = 0; y < height; y++)
{
for (u32 x = 0; x < width; x++)
{
EXPECT_EQ(img.GetPixels(0)[y * width + x], 0xFF000000u | (x + y * width));
EXPECT_EQ(img.GetRowPixels(0, y)[x], 0xFF000000u | (x + y * width));
}
}
// Test GetPixelsSpan
auto span = img.GetPixelsSpan(0);
for (u32 i = 0; i < width * height; i++)
{
EXPECT_EQ(span[i], 0xFF000000u | i);
}
}
TEST_F(AnimatedImageTest, SetPixels)
{
const u32 width = 10;
const u32 height = 8;
AnimatedImage img(width, height, 1, {1, 10});
// Create source pixels
std::vector<u32> src_pixels(width * height);
for (u32 i = 0; i < width * height; i++)
{
src_pixels[i] = 0xFF000000 | i;
}
// Copy with SetPixels
img.SetPixels(0, src_pixels.data(), width * sizeof(u32));
// Verify pixels
for (u32 i = 0; i < width * height; i++)
{
EXPECT_EQ(img.GetPixels(0)[i], 0xFF000000u | i);
}
}
TEST_F(AnimatedImageTest, SetDelay)
{
AnimatedImage img(10, 10, 2, {1, 10});
AnimatedImage::FrameDelay delay{2, 20};
img.SetDelay(1, delay);
EXPECT_EQ(img.GetFrameDelay(1).numerator, 2);
EXPECT_EQ(img.GetFrameDelay(1).denominator, 20);
// First frame should be unchanged
EXPECT_EQ(img.GetFrameDelay(0).numerator, 1);
EXPECT_EQ(img.GetFrameDelay(0).denominator, 10);
}
// Image Manipulation Tests
TEST_F(AnimatedImageTest, Resize)
{
AnimatedImage img = CreateTestImage(10, 8, 2);
AnimatedImage img2 = CreateTestImage(10, 8, 2);
// Resize to larger dimensions, preserving content
img.Resize(20, 16, 3, {1, 10}, true);
EXPECT_EQ(img.GetWidth(), 20u);
EXPECT_EQ(img.GetHeight(), 16u);
EXPECT_EQ(img.GetFrames(), 3u);
// Check that original content is preserved
for (u32 f = 0; f < 2; f++)
{
for (u32 y = 0; y < 8; y++)
{
for (u32 x = 0; x < 10; x++)
{
EXPECT_EQ(img.GetRowPixels(f, y)[x] & 0xFFu, (x + y + f) & 0xFFu);
}
}
}
// Check that new areas are zeroed
for (u32 f = 0; f < 2; f++)
{
for (u32 y = 0; y < 8; y++)
{
for (u32 x = 10; x < 20; x++)
{
EXPECT_EQ(img.GetRowPixels(f, y)[x], 0u);
}
}
for (u32 y = 8; y < 16; y++)
{
for (u32 x = 0; x < 20; x++)
{
EXPECT_EQ(img.GetRowPixels(f, y)[x], 0u);
}
}
}
// Check third frame has the specified default delay
EXPECT_EQ(img.GetFrameDelay(2).numerator, 1u);
EXPECT_EQ(img.GetFrameDelay(2).denominator, 10u);
}
TEST_F(AnimatedImageTest, Clear)
{
AnimatedImage img = CreateTestImage(10, 8, 2);
img.Clear();
// Dimensions should remain the same
EXPECT_EQ(img.GetWidth(), 10u);
EXPECT_EQ(img.GetHeight(), 8u);
EXPECT_EQ(img.GetFrames(), 2u);
// All pixels should be zeroed
for (u32 f = 0; f < img.GetFrames(); f++)
{
for (u32 i = 0; i < img.GetFrameSize(); i++)
{
EXPECT_EQ(img.GetPixels(f)[i], 0u);
}
}
}
TEST_F(AnimatedImageTest, Invalidate)
{
AnimatedImage img = CreateTestImage(10, 8, 2);
img.Invalidate();
EXPECT_FALSE(img.IsValid());
EXPECT_EQ(img.GetWidth(), 0u);
EXPECT_EQ(img.GetHeight(), 0u);
EXPECT_EQ(img.GetFrames(), 0u);
}
TEST_F(AnimatedImageTest, TakePixels)
{
AnimatedImage img = CreateTestImage(10, 8, 2);
const u32 expected_size = 10 * 8 * 2;
auto pixels = img.TakePixels();
// Image should be invalidated
EXPECT_FALSE(img.IsValid());
EXPECT_EQ(img.GetWidth(), 0u);
EXPECT_EQ(img.GetHeight(), 0u);
EXPECT_EQ(img.GetFrames(), 0u);
// Pixel storage should have the expected size
EXPECT_EQ(pixels.size(), expected_size);
}
// File Operations Tests
TEST_F(AnimatedImageTest, LoadSavePNG)
{
// Create test image
AnimatedImage original = CreateTestImage(20, 16, 1);
// Set specific pixel patterns for verification
original.GetPixels(0)[0] = 0xFF0000FFu; // Blue
original.GetPixels(0)[1] = 0xFF00FF00u; // Green
original.GetPixels(0)[2] = 0xFFFF0000u; // Red
// Save to file
auto buffer = original.SaveToBuffer("test_image.png");
ASSERT_TRUE(buffer.has_value());
// Load the image back
AnimatedImage loaded;
ASSERT_TRUE(loaded.LoadFromBuffer("test_image.png", buffer.value()));
// Compare dimensions
EXPECT_EQ(loaded.GetWidth(), original.GetWidth());
EXPECT_EQ(loaded.GetHeight(), original.GetHeight());
EXPECT_EQ(loaded.GetFrames(), 1u);
// Compare specific pixel colors (ignoring alpha variations)
EXPECT_EQ(loaded.GetPixels(0)[0] & 0xFFFFFFu, 0x0000FFu); // Blue
EXPECT_EQ(loaded.GetPixels(0)[1] & 0xFFFFFFu, 0x00FF00u); // Green
EXPECT_EQ(loaded.GetPixels(0)[2] & 0xFFFFFFu, 0xFF0000u); // Red
}
TEST_F(AnimatedImageTest, LoadSaveMultiFramePNG)
{
// Create multi-frame test image
AnimatedImage original = CreateTestImage(20, 16, 2);
// Set different delays for frames
original.SetDelay(0, {1, 10});
original.SetDelay(1, {2, 20});
// Save to file
auto buffer = original.SaveToBuffer("test_anim.png");
ASSERT_TRUE(buffer.has_value());
// Load back
AnimatedImage loaded;
ASSERT_TRUE(loaded.LoadFromBuffer("test_anim.png", buffer.value()));
// Compare dimensions and frame count
EXPECT_EQ(loaded.GetWidth(), original.GetWidth());
EXPECT_EQ(loaded.GetHeight(), original.GetHeight());
EXPECT_EQ(loaded.GetFrames(), original.GetFrames());
// Compare frame delays
EXPECT_EQ(loaded.GetFrameDelay(0).numerator, 1u);
EXPECT_EQ(loaded.GetFrameDelay(0).denominator, 10u);
EXPECT_EQ(loaded.GetFrameDelay(1).numerator, 2u);
EXPECT_EQ(loaded.GetFrameDelay(1).denominator, 20u);
}
TEST_F(AnimatedImageTest, SaveLoadBuffer)
{
AnimatedImage original = CreateTestImage(20, 16, 1);
// Save to buffer
auto buffer = original.SaveToBuffer("test.png");
ASSERT_TRUE(buffer.has_value());
EXPECT_GT(buffer->size(), 0u);
// Load from buffer
AnimatedImage loaded;
ASSERT_TRUE(loaded.LoadFromBuffer("test.png", *buffer));
// Compare dimensions
EXPECT_EQ(loaded.GetWidth(), original.GetWidth());
EXPECT_EQ(loaded.GetHeight(), original.GetHeight());
// Compare some pixels (ignoring alpha)
for (u32 i = 0; i < std::min(10u, original.GetFrameSize()); i++)
{
EXPECT_EQ(loaded.GetPixels(0)[i] & 0xFFFFFF, original.GetPixels(0)[i] & 0xFFFFFF);
}
}
TEST_F(AnimatedImageTest, ErrorHandling)
{
AnimatedImage img;
Error err;
// Try loading non-existent file
EXPECT_FALSE(img.LoadFromFile("non_existent_file.png", &err));
EXPECT_TRUE(err.IsValid());
// Try loading file with invalid extension
EXPECT_FALSE(img.LoadFromFile("test.invalid", &err));
EXPECT_TRUE(err.IsValid());
}
TEST_F(AnimatedImageTest, CalculatePitch)
{
EXPECT_EQ(AnimatedImage::CalculatePitch(10, 5), 10 * sizeof(u32));
EXPECT_EQ(AnimatedImage::CalculatePitch(100, 200), 100 * sizeof(u32));
}
// Multiple frame handling and frame delay tests
TEST_F(AnimatedImageTest, MultipleFrameDelays)
{
const u32 width = 32;
const u32 height = 24;
const u32 frames = 5;
AnimatedImage img(width, height, frames, {1, 10});
// Set different delays for each frame
img.SetDelay(0, {1, 10});
img.SetDelay(1, {2, 20});
img.SetDelay(2, {3, 30});
img.SetDelay(3, {4, 40});
img.SetDelay(4, {5, 50});
// Verify each frame has the correct delay
EXPECT_EQ(img.GetFrameDelay(0).numerator, 1u);
EXPECT_EQ(img.GetFrameDelay(0).denominator, 10u);
EXPECT_EQ(img.GetFrameDelay(1).numerator, 2u);
EXPECT_EQ(img.GetFrameDelay(1).denominator, 20u);
EXPECT_EQ(img.GetFrameDelay(2).numerator, 3u);
EXPECT_EQ(img.GetFrameDelay(2).denominator, 30u);
EXPECT_EQ(img.GetFrameDelay(3).numerator, 4u);
EXPECT_EQ(img.GetFrameDelay(3).denominator, 40u);
EXPECT_EQ(img.GetFrameDelay(4).numerator, 5u);
EXPECT_EQ(img.GetFrameDelay(4).denominator, 50u);
}
TEST_F(AnimatedImageTest, PreserveFrameDelaysOnResize)
{
const u32 width = 16;
const u32 height = 16;
const u32 frames = 3;
AnimatedImage img(width, height, frames, {1, 10});
// Set unique delays for each frame
img.SetDelay(0, {5, 25});
img.SetDelay(1, {10, 50});
img.SetDelay(2, {15, 75});
// Resize with fewer frames - should preserve existing frame delays
img.Resize(32, 32, 2, {20, 100}, true);
EXPECT_EQ(img.GetWidth(), 32u);
EXPECT_EQ(img.GetHeight(), 32u);
EXPECT_EQ(img.GetFrames(), 2u);
// Original frame delays should be preserved
EXPECT_EQ(img.GetFrameDelay(0).numerator, 5u);
EXPECT_EQ(img.GetFrameDelay(0).denominator, 25u);
EXPECT_EQ(img.GetFrameDelay(1).numerator, 10u);
EXPECT_EQ(img.GetFrameDelay(1).denominator, 50u);
// Resize with more frames - new frames should use the default delay
img.Resize(32, 32, 4, {20, 100}, true);
EXPECT_EQ(img.GetFrames(), 4u);
// Original frame delays should still be preserved
EXPECT_EQ(img.GetFrameDelay(0).numerator, 5u);
EXPECT_EQ(img.GetFrameDelay(0).denominator, 25u);
EXPECT_EQ(img.GetFrameDelay(1).numerator, 10u);
EXPECT_EQ(img.GetFrameDelay(1).denominator, 50u);
// New frames should have the default delay
EXPECT_EQ(img.GetFrameDelay(2).numerator, 20u);
EXPECT_EQ(img.GetFrameDelay(2).denominator, 100u);
EXPECT_EQ(img.GetFrameDelay(3).numerator, 20u);
EXPECT_EQ(img.GetFrameDelay(3).denominator, 100u);
}
TEST_F(AnimatedImageTest, IndividualFrameModification)
{
const u32 width = 8;
const u32 height = 8;
const u32 frames = 3;
AnimatedImage img(width, height, frames, {1, 10});
// Set distinct patterns for each frame
for (u32 f = 0; f < frames; f++)
{
for (u32 y = 0; y < height; y++)
{
for (u32 x = 0; x < width; x++)
{
// Frame 0: all red, Frame 1: all green, Frame 2: all blue
u32 color = (f == 0) ? 0xFF0000FFu : ((f == 1) ? 0xFF00FF00u : 0xFFFF0000u);
img.GetPixels(f)[y * width + x] = color;
}
}
}
// Verify each frame has the correct pattern
for (u32 y = 0; y < height; y++)
{
for (u32 x = 0; x < width; x++)
{
EXPECT_EQ(img.GetPixels(0)[y * width + x], 0xFF0000FFu); // Red
EXPECT_EQ(img.GetPixels(1)[y * width + x], 0xFF00FF00u); // Green
EXPECT_EQ(img.GetPixels(2)[y * width + x], 0xFFFF0000u); // Blue
}
}
// Modify only the middle frame
for (u32 y = 0; y < height; y++)
{
for (u32 x = 0; x < width; x++)
{
img.GetPixels(1)[y * width + x] = 0xFFFFFFFFu; // White
}
}
// Verify only the middle frame was changed
for (u32 y = 0; y < height; y++)
{
for (u32 x = 0; x < width; x++)
{
EXPECT_EQ(img.GetPixels(0)[y * width + x], 0xFF0000FFu); // Still red
EXPECT_EQ(img.GetPixels(1)[y * width + x], 0xFFFFFFFFu); // Now white
EXPECT_EQ(img.GetPixels(2)[y * width + x], 0xFFFF0000u); // Still blue
}
}
}
TEST_F(AnimatedImageTest, MultiFrameAnimationRoundTrip)
{
const u32 width = 24;
const u32 height = 24;
const u32 frames = 4;
// Create a test animation with 4 frames, each with different content and timing
AnimatedImage original(width, height, frames, {1, 10});
// Frame 0: Red with delay 1/10
std::memset(original.GetPixels(0), 0, width * height * sizeof(u32));
for (u32 i = 0; i < width * height; i++)
{
original.GetPixels(0)[i] = 0xFF0000FFu; // Red
}
original.SetDelay(0, {1, 10});
// Frame 1: Green with delay 2/20
std::memset(original.GetPixels(1), 0, width * height * sizeof(u32));
for (u32 i = 0; i < width * height; i++)
{
original.GetPixels(1)[i] = 0xFF00FF00u; // Green
}
original.SetDelay(1, {2, 20});
// Frame 2: Blue with delay 3/30
std::memset(original.GetPixels(2), 0, width * height * sizeof(u32));
for (u32 i = 0; i < width * height; i++)
{
original.GetPixels(2)[i] = 0xFFFF0000u; // Blue
}
original.SetDelay(2, {3, 30});
// Frame 3: Yellow with delay 4/40
std::memset(original.GetPixels(3), 0, width * height * sizeof(u32));
for (u32 i = 0; i < width * height; i++)
{
original.GetPixels(3)[i] = 0xFF00FFFFu; // Yellow
}
original.SetDelay(3, {4, 40});
// Save to buffer
auto buffer = original.SaveToBuffer("test_animation.png");
ASSERT_TRUE(buffer.has_value());
// Load back from buffer
AnimatedImage loaded;
ASSERT_TRUE(loaded.LoadFromBuffer("test_animation.png", *buffer));
// Verify dimensions and frame count
EXPECT_EQ(loaded.GetWidth(), width);
EXPECT_EQ(loaded.GetHeight(), height);
EXPECT_EQ(loaded.GetFrames(), frames);
// Verify frame delays
EXPECT_EQ(loaded.GetFrameDelay(0).numerator, 1u);
EXPECT_EQ(loaded.GetFrameDelay(0).denominator, 10u);
EXPECT_EQ(loaded.GetFrameDelay(1).numerator, 2u);
EXPECT_EQ(loaded.GetFrameDelay(1).denominator, 20u);
EXPECT_EQ(loaded.GetFrameDelay(2).numerator, 3u);
EXPECT_EQ(loaded.GetFrameDelay(2).denominator, 30u);
EXPECT_EQ(loaded.GetFrameDelay(3).numerator, 4u);
EXPECT_EQ(loaded.GetFrameDelay(3).denominator, 40u);
// Verify frame contents (sampling first pixel of each frame)
EXPECT_EQ(loaded.GetPixels(0)[0] & 0xFFFFFFu, 0x0000FFu); // Red
EXPECT_EQ(loaded.GetPixels(1)[0] & 0xFFFFFFu, 0x00FF00u); // Green
EXPECT_EQ(loaded.GetPixels(2)[0] & 0xFFFFFFu, 0xFF0000u); // Blue
EXPECT_EQ(loaded.GetPixels(3)[0] & 0xFFFFFFu, 0x00FFFFu); // Yellow
}
TEST_F(AnimatedImageTest, MaximumAndZeroDelays)
{
const u32 width = 16;
const u32 height = 16;
const u32 frames = 4;
AnimatedImage img(width, height, frames, {1, 10});
// Set extreme delay values
img.SetDelay(0, {0, 1}); // Zero numerator (minimum)
img.SetDelay(1, {65535, 1}); // Maximum numerator (u16 max)
img.SetDelay(2, {1, 65535}); // Maximum denominator (u16 max)
img.SetDelay(3, {0, 65535}); // Both extreme
// Verify delay values were set correctly
EXPECT_EQ(img.GetFrameDelay(0).numerator, 0u);
EXPECT_EQ(img.GetFrameDelay(0).denominator, 1u);
EXPECT_EQ(img.GetFrameDelay(1).numerator, 65535u);
EXPECT_EQ(img.GetFrameDelay(1).denominator, 1u);
EXPECT_EQ(img.GetFrameDelay(2).numerator, 1u);
EXPECT_EQ(img.GetFrameDelay(2).denominator, 65535u);
EXPECT_EQ(img.GetFrameDelay(3).numerator, 0u);
EXPECT_EQ(img.GetFrameDelay(3).denominator, 65535u);
// Save to buffer and load back to verify these values are preserved
auto buffer = img.SaveToBuffer("test_delays.png");
ASSERT_TRUE(buffer.has_value());
AnimatedImage loaded;
ASSERT_TRUE(loaded.LoadFromBuffer("test_delays.png", *buffer));
// Verify delays are preserved
EXPECT_EQ(loaded.GetFrameDelay(0).numerator, 0u);
EXPECT_EQ(loaded.GetFrameDelay(0).denominator, 1u);
EXPECT_EQ(loaded.GetFrameDelay(1).numerator, 65535u);
EXPECT_EQ(loaded.GetFrameDelay(1).denominator, 1u);
EXPECT_EQ(loaded.GetFrameDelay(2).numerator, 1u);
EXPECT_EQ(loaded.GetFrameDelay(2).denominator, 65535u);
EXPECT_EQ(loaded.GetFrameDelay(3).numerator, 0u);
EXPECT_EQ(loaded.GetFrameDelay(3).denominator, 65535u);
}
TEST_F(AnimatedImageTest, ResizeBetweenSingleAndMultipleFrames)
{
// Start with a single frame
AnimatedImage img(16, 16, 1, {1, 10});
EXPECT_EQ(img.GetFrames(), 1u);
// Fill with a pattern
for (u32 i = 0; i < img.GetFrameSize(); i++)
{
img.GetPixels(0)[i] = 0xFF000000u | i;
}
// Resize to multiple frames
img.Resize(16, 16, 3, {2, 20}, true);
EXPECT_EQ(img.GetFrames(), 3u);
// Verify original frame is preserved
for (u32 i = 0; i < img.GetFrameSize(); i++)
{
EXPECT_EQ(img.GetPixels(0)[i], 0xFF000000u | i);
}
// Fill second frame with different pattern
for (u32 i = 0; i < img.GetFrameSize(); i++)
{
img.GetPixels(1)[i] = 0xFF000000u | (i * 2);
}
// Resize back to single frame
img.Resize(16, 16, 1, {3, 30}, true);
EXPECT_EQ(img.GetFrames(), 1u);
// Verify first frame is still preserved
for (u32 i = 0; i < img.GetFrameSize(); i++)
{
EXPECT_EQ(img.GetPixels(0)[i], 0xFF000000u | i);
}
}

View File

@@ -3,6 +3,7 @@
<Import Project="..\..\dep\msvc\vsprops\Configurations.props" />
<ItemGroup>
<ClCompile Include="..\..\dep\googletest\src\gtest_main.cc" />
<ClCompile Include="animated_image_tests.cpp" />
<ClCompile Include="image_tests.cpp" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,4 +1,6 @@
add_library(util
animated_image.cpp
animated_image.h
audio_stream.cpp
audio_stream.h
cd_image.cpp

588
src/util/animated_image.cpp Normal file
View File

@@ -0,0 +1,588 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "animated_image.h"
#include "common/assert.h"
#include "common/bitutils.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/scoped_guard.h"
#include "common/string_util.h"
#include <png.h>
// clang-format off
#ifdef _MSC_VER
#pragma warning(disable : 4611) // warning C4611: interaction between '_setjmp' and C++ object destruction is non-portable
#endif
// clang-format on
LOG_CHANNEL(Image);
static bool PNGBufferLoader(AnimatedImage* image, std::span<const u8> data, Error* error);
static bool PNGBufferSaver(const AnimatedImage& image, DynamicHeapArray<u8>* data, u8 quality, Error* error);
static bool PNGFileLoader(AnimatedImage* image, std::string_view filename, std::FILE* fp, Error* error);
static bool PNGFileSaver(const AnimatedImage& image, std::string_view filename, std::FILE* fp, u8 quality,
Error* error);
namespace {
struct FormatHandler
{
const char* extension;
bool (*buffer_loader)(AnimatedImage*, std::span<const u8>, Error*);
bool (*buffer_saver)(const AnimatedImage&, DynamicHeapArray<u8>*, u8, Error*);
bool (*file_loader)(AnimatedImage*, std::string_view, std::FILE*, Error*);
bool (*file_saver)(const AnimatedImage&, std::string_view, std::FILE*, u8, Error*);
};
} // namespace
static constexpr FormatHandler s_format_handlers[] = {
{"png", PNGBufferLoader, PNGBufferSaver, PNGFileLoader, PNGFileSaver},
};
static const FormatHandler* GetFormatHandler(std::string_view extension)
{
for (const FormatHandler& handler : s_format_handlers)
{
if (StringUtil::Strncasecmp(extension.data(), handler.extension, extension.size()) == 0)
return &handler;
}
return nullptr;
}
AnimatedImage::AnimatedImage() = default;
AnimatedImage::AnimatedImage(const AnimatedImage& copy)
: m_width(copy.m_width), m_height(copy.m_height), m_frame_size(copy.m_frame_size), m_frames(copy.m_frames),
m_pixels(copy.m_pixels), m_frame_delay(copy.m_frame_delay)
{
}
AnimatedImage::AnimatedImage(AnimatedImage&& move)
{
m_width = std::exchange(move.m_width, 0);
m_height = std::exchange(move.m_height, 0);
m_frame_size = std::exchange(move.m_frame_size, 0);
m_frames = std::exchange(move.m_frames, 0);
m_pixels = std::move(move.m_pixels);
m_frame_delay = std::move(move.m_frame_delay);
}
AnimatedImage::AnimatedImage(u32 width, u32 height, u32 frames, const FrameDelay& default_delay)
: m_width(width), m_height(height), m_frame_size(width * height), m_frames(frames), m_pixels(frames * width * height),
m_frame_delay(frames)
{
for (FrameDelay& delay : m_frame_delay)
delay = default_delay;
}
void AnimatedImage::Resize(u32 new_width, u32 new_height, u32 num_frames, const FrameDelay& default_delay,
bool preserve)
{
DebugAssert(new_width > 0 && new_height > 0 && num_frames > 0);
if (m_width == new_width && m_height == new_height && num_frames == m_frames)
return;
if (!preserve)
m_pixels.deallocate();
const u32 new_frame_size = new_width * new_height;
PixelStorage new_pixels;
new_pixels.resize(new_frame_size * num_frames);
std::memset(new_pixels.data(), 0, new_pixels.size() * sizeof(u32));
m_frame_delay.resize(num_frames);
if (preserve && !m_pixels.empty())
{
const u32 copy_frames = std::min(num_frames, m_frames);
for (u32 i = 0; i < copy_frames; i++)
{
StringUtil::StrideMemCpy(new_pixels.data() + i * new_frame_size, new_width * sizeof(u32),
m_pixels.data() + i * m_frame_size, m_width * sizeof(u32),
std::min(new_width, m_width) * sizeof(u32), std::min(new_height, m_height));
}
for (u32 i = m_frames; i < num_frames; i++)
m_frame_delay[i] = default_delay;
}
m_width = new_width;
m_height = new_height;
m_frame_size = new_frame_size;
m_frames = num_frames;
m_pixels = std::move(new_pixels);
}
AnimatedImage& AnimatedImage::operator=(const AnimatedImage& copy)
{
m_width = copy.m_width;
m_height = copy.m_height;
m_frame_size = copy.m_frame_size;
m_frames = copy.m_frames;
m_pixels = copy.m_pixels;
m_frame_delay = copy.m_frame_delay;
return *this;
}
AnimatedImage& AnimatedImage::operator=(AnimatedImage&& move)
{
m_width = std::exchange(move.m_width, 0);
m_height = std::exchange(move.m_height, 0);
m_frame_size = std::exchange(move.m_frame_size, 0);
m_frames = std::exchange(move.m_frames, 0);
m_pixels = std::move(move.m_pixels);
m_frame_delay = std::move(move.m_frame_delay);
return *this;
}
u32 AnimatedImage::CalculatePitch(u32 width, u32 height)
{
return width * sizeof(u32);
}
std::span<const AnimatedImage::PixelType> AnimatedImage::GetPixelsSpan(u32 frame) const
{
DebugAssert(frame < m_frames);
return m_pixels.cspan(frame * m_frame_size, m_frame_size);
}
std::span<AnimatedImage::PixelType> AnimatedImage::GetPixelsSpan(u32 frame)
{
DebugAssert(frame < m_frames);
return m_pixels.span(frame * m_frame_size, m_frame_size);
}
void AnimatedImage::Clear()
{
std::memset(m_pixels.data(), 0, m_pixels.size_bytes());
}
void AnimatedImage::Invalidate()
{
m_width = 0;
m_height = 0;
m_frame_size = 0;
m_frames = 0;
m_pixels.deallocate();
m_frame_delay.deallocate();
}
void AnimatedImage::SetPixels(u32 frame, const void* pixels, u32 pitch)
{
DebugAssert(frame < m_frames);
StringUtil::StrideMemCpy(GetPixels(frame), m_width * sizeof(u32), pixels, pitch, m_width * sizeof(u32), m_height);
}
void AnimatedImage::SetDelay(u32 frame, const FrameDelay& delay)
{
DebugAssert(frame < m_frames);
m_frame_delay[frame] = delay;
}
AnimatedImage::PixelStorage AnimatedImage::TakePixels()
{
m_width = 0;
m_height = 0;
m_frame_size = 0;
m_frames = 0;
return std::move(m_pixels);
}
bool AnimatedImage::LoadFromFile(const char* filename, Error* error /* = nullptr */)
{
auto fp = FileSystem::OpenManagedCFile(filename, "rb", error);
if (!fp)
return false;
return LoadFromFile(filename, fp.get(), error);
}
bool AnimatedImage::SaveToFile(const char* filename, u8 quality /* = DEFAULT_SAVE_QUALITY */,
Error* error /* = nullptr */) const
{
auto fp = FileSystem::OpenManagedCFile(filename, "wb", error);
if (!fp)
return false;
if (SaveToFile(filename, fp.get(), quality, error))
return true;
// save failed
fp.reset();
FileSystem::DeleteFile(filename);
return false;
}
bool AnimatedImage::LoadFromFile(std::string_view filename, std::FILE* fp, Error* error /* = nullptr */)
{
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->file_loader)
{
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
return false;
}
return handler->file_loader(this, filename, fp, error);
}
bool AnimatedImage::LoadFromBuffer(std::string_view filename, std::span<const u8> data, Error* error /* = nullptr */)
{
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->buffer_loader)
{
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
return false;
}
return handler->buffer_loader(this, data, error);
}
bool AnimatedImage::SaveToFile(std::string_view filename, std::FILE* fp, u8 quality /* = DEFAULT_SAVE_QUALITY */,
Error* error /* = nullptr */) const
{
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->file_saver)
{
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
return false;
}
if (!handler->file_saver(*this, filename, fp, quality, error))
return false;
if (std::fflush(fp) != 0)
{
Error::SetErrno(error, "fflush() failed: ", errno);
return false;
}
return true;
}
std::optional<DynamicHeapArray<u8>> AnimatedImage::SaveToBuffer(std::string_view filename,
u8 quality /* = DEFAULT_SAVE_QUALITY */,
Error* error /* = nullptr */) const
{
std::optional<DynamicHeapArray<u8>> ret;
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->file_saver)
{
Error::SetStringFmt(error, "Unknown extension '{}'", extension);
return ret;
}
ret = DynamicHeapArray<u8>();
if (!handler->buffer_saver(*this, &ret.value(), quality, error))
ret.reset();
return ret;
}
static void PNGSetErrorFunction(png_structp png_ptr, Error* error)
{
png_set_error_fn(
png_ptr, error,
[](png_structp png_ptr, png_const_charp message) {
Error::SetStringView(static_cast<Error*>(png_get_error_ptr(png_ptr)), message);
png_longjmp(png_ptr, 1);
},
[](png_structp png_ptr, png_const_charp message) { WARNING_LOG("libpng warning: {}", message); });
}
static bool PNGCommonLoader(AnimatedImage* image, png_structp png_ptr, png_infop info_ptr)
{
png_read_info(png_ptr, info_ptr);
const u32 width = png_get_image_width(png_ptr, info_ptr);
const u32 height = png_get_image_height(png_ptr, info_ptr);
const u32 num_frames = png_get_num_frames(png_ptr, info_ptr);
const png_byte color_type = png_get_color_type(png_ptr, info_ptr);
const png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
if (num_frames == 0)
png_error(png_ptr, "Image has zero frames");
// Read any color_type into 8bit depth, RGBA format.
// See http://www.libpng.org/pub/png/libpng-manual.txt
if (bit_depth == 16)
png_set_strip_16(png_ptr);
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png_ptr);
// PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png_ptr);
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png_ptr);
// These color_type don't have an alpha channel then fill it with 0xff.
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png_ptr);
png_read_update_info(png_ptr, info_ptr);
DebugAssert(num_frames > 0);
image->Resize(width, height, num_frames, {1, 10}, false);
if (num_frames > 1)
{
for (u32 i = 0; i < num_frames; i++)
{
png_read_frame_head(png_ptr, info_ptr);
const u32 frame_width = png_get_next_frame_width(png_ptr, info_ptr);
const u32 frame_height = png_get_next_frame_height(png_ptr, info_ptr);
if (frame_width != width || frame_height != height)
png_error(png_ptr, "Frame size does not match image size");
const u16 delay_num = static_cast<u16>(png_get_next_frame_delay_num(png_ptr, info_ptr));
const u16 delay_den = static_cast<u16>(png_get_next_frame_delay_den(png_ptr, info_ptr));
image->SetDelay(i, {delay_num, std::max<u16>(delay_den, 1)});
// TODO: blending/compose/etc.
const int num_passes = png_set_interlace_handling(png_ptr);
for (int pass = 0; pass < num_passes; pass++)
{
for (u32 y = 0; y < height; y++)
png_read_row(png_ptr, reinterpret_cast<png_bytep>(image->GetRowPixels(i, y)), nullptr);
}
}
}
else
{
const int num_passes = png_set_interlace_handling(png_ptr);
for (int pass = 0; pass < num_passes; pass++)
{
for (u32 y = 0; y < height; y++)
png_read_row(png_ptr, reinterpret_cast<png_bytep>(image->GetRowPixels(0, y)), nullptr);
}
}
return true;
}
bool PNGFileLoader(AnimatedImage* image, std::string_view filename, std::FILE* fp, Error* error)
{
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr)
{
Error::SetStringView(error, "png_create_read_struct() failed.");
return false;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
Error::SetStringView(error, "png_create_info_struct() failed.");
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
PNGSetErrorFunction(png_ptr, error);
if (setjmp(png_jmpbuf(png_ptr)))
{
image->Invalidate();
return false;
}
png_set_read_fn(png_ptr, fp, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
std::FILE* fp = static_cast<std::FILE*>(png_get_io_ptr(png_ptr));
if (std::fread(data_ptr, size, 1, fp) != 1)
png_error(png_ptr, "fread() failed");
});
return PNGCommonLoader(image, png_ptr, info_ptr);
}
bool PNGBufferLoader(AnimatedImage* image, std::span<const u8> data, Error* error)
{
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr)
{
Error::SetStringView(error, "png_create_read_struct() failed.");
return false;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
Error::SetStringView(error, "png_create_info_struct() failed.");
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
std::vector<png_bytep> row_pointers;
PNGSetErrorFunction(png_ptr, error);
if (setjmp(png_jmpbuf(png_ptr)))
{
image->Invalidate();
return false;
}
struct IOData
{
std::span<const u8> buffer;
size_t buffer_pos;
};
IOData iodata = {data, 0};
png_set_read_fn(png_ptr, &iodata, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
IOData* data = static_cast<IOData*>(png_get_io_ptr(png_ptr));
const size_t read_size = std::min<size_t>(data->buffer.size() - data->buffer_pos, size);
if (read_size > 0)
{
std::memcpy(data_ptr, &data->buffer[data->buffer_pos], read_size);
data->buffer_pos += read_size;
}
});
return PNGCommonLoader(image, png_ptr, info_ptr);
}
static void PNGSaveCommon(const AnimatedImage& image, png_structp png_ptr, png_infop info_ptr, u8 quality)
{
png_set_compression_level(png_ptr, std::clamp(quality / 10, 0, 9));
png_set_IHDR(png_ptr, info_ptr, image.GetWidth(), image.GetHeight(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
const u32 width = image.GetWidth();
const u32 height = image.GetHeight();
const u32 frames = image.GetFrames();
if (frames > 1)
{
if (!png_set_acTL(png_ptr, info_ptr, frames, 0))
png_error(png_ptr, "png_set_acTL() failed");
png_write_info(png_ptr, info_ptr);
for (u32 i = 0; i < frames; i++)
{
const AnimatedImage::FrameDelay& fd = image.GetFrameDelay(i);
png_write_frame_head(png_ptr, info_ptr, width, height, 0, 0, fd.numerator, fd.denominator, PNG_DISPOSE_OP_NONE,
PNG_BLEND_OP_SOURCE);
for (u32 y = 0; y < height; ++y)
png_write_row(png_ptr, (png_bytep)image.GetRowPixels(i, y));
png_write_frame_tail(png_ptr, info_ptr);
}
}
else
{
// only one frame
png_write_info(png_ptr, info_ptr);
for (u32 y = 0; y < height; ++y)
png_write_row(png_ptr, (png_bytep)image.GetRowPixels(0, y));
}
png_write_end(png_ptr, nullptr);
}
bool PNGFileSaver(const AnimatedImage& image, std::string_view filename, std::FILE* fp, u8 quality, Error* error)
{
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
png_infop info_ptr = nullptr;
if (!png_ptr)
{
Error::SetStringView(error, "png_create_write_struct() failed.");
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
if (png_ptr)
png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
});
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
Error::SetStringView(error, "png_create_info_struct() failed.");
return false;
}
PNGSetErrorFunction(png_ptr, error);
if (setjmp(png_jmpbuf(png_ptr)))
return false;
png_set_write_fn(
png_ptr, fp,
[](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
if (std::fwrite(data_ptr, size, 1, static_cast<std::FILE*>(png_get_io_ptr(png_ptr))) != 1)
png_error(png_ptr, "fwrite() failed");
},
[](png_structp png_ptr) {});
PNGSaveCommon(image, png_ptr, info_ptr, quality);
return true;
}
bool PNGBufferSaver(const AnimatedImage& image, DynamicHeapArray<u8>* data, u8 quality, Error* error)
{
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
png_infop info_ptr = nullptr;
if (!png_ptr)
{
Error::SetStringView(error, "png_create_write_struct() failed.");
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
if (png_ptr)
png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
});
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
Error::SetStringView(error, "png_create_info_struct() failed.");
return false;
}
struct IOData
{
DynamicHeapArray<u8>* buffer;
size_t buffer_pos;
};
IOData iodata = {data, 0};
data->resize(image.GetWidth() * image.GetHeight() * 2);
PNGSetErrorFunction(png_ptr, error);
if (setjmp(png_jmpbuf(png_ptr)))
return false;
png_set_write_fn(
png_ptr, &iodata,
[](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
IOData* iodata = static_cast<IOData*>(png_get_io_ptr(png_ptr));
const size_t new_pos = iodata->buffer_pos + size;
if (new_pos > iodata->buffer->size())
iodata->buffer->resize(std::max(new_pos, iodata->buffer->size() * 2));
std::memcpy(iodata->buffer->data() + iodata->buffer_pos, data_ptr, size);
iodata->buffer_pos += size;
},
[](png_structp png_ptr) {});
PNGSaveCommon(image, png_ptr, info_ptr, quality);
iodata.buffer->resize(iodata.buffer_pos);
return true;
}

89
src/util/animated_image.h Normal file
View File

@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "common/align.h"
#include "common/heap_array.h"
#include "common/types.h"
#include <cstdio>
#include <optional>
#include <span>
#include <string_view>
class Error;
class AnimatedImage
{
public:
struct FrameDelay
{
u16 numerator;
u16 denominator;
};
static constexpr u8 DEFAULT_SAVE_QUALITY = 85;
public:
using PixelType = u32;
using PixelStorage = DynamicHeapArray<PixelType>;
AnimatedImage();
AnimatedImage(u32 width, u32 height, u32 frames, const FrameDelay& default_delay);
AnimatedImage(const AnimatedImage& copy);
AnimatedImage(AnimatedImage&& move);
AnimatedImage& operator=(const AnimatedImage& copy);
AnimatedImage& operator=(AnimatedImage&& move);
static u32 CalculatePitch(u32 width, u32 height);
ALWAYS_INLINE bool IsValid() const { return (m_width > 0 && m_height > 0); }
ALWAYS_INLINE u32 GetWidth() const { return m_width; }
ALWAYS_INLINE u32 GetHeight() const { return m_height; }
ALWAYS_INLINE u32 GetPitch() const { return (m_width * sizeof(u32)); }
ALWAYS_INLINE u32 GetFrames() const { return m_frames; }
ALWAYS_INLINE u32 GetFrameSize() const { return m_frame_size; }
ALWAYS_INLINE const u32* GetPixels(u32 frame) const { return &m_pixels[frame * m_frame_size]; }
ALWAYS_INLINE PixelType* GetPixels(u32 frame) { return &m_pixels[frame * m_frame_size]; }
ALWAYS_INLINE const PixelType* GetRowPixels(u32 frame, u32 y) const
{
return &m_pixels[frame * m_frame_size + y * m_width];
}
ALWAYS_INLINE PixelType* GetRowPixels(u32 frame, u32 y) { return &m_pixels[frame * m_frame_size + y * m_width]; }
ALWAYS_INLINE const FrameDelay& GetFrameDelay(u32 frame) const { return m_frame_delay[frame]; }
std::span<const PixelType> GetPixelsSpan(u32 frame) const;
std::span<PixelType> GetPixelsSpan(u32 frame);
void Clear();
void Invalidate();
void Resize(u32 new_width, u32 new_height, u32 num_frames, const FrameDelay& default_delay, bool preserve);
void SetPixels(u32 frame, const void* pixels, u32 pitch);
void SetDelay(u32 frame, const FrameDelay& delay);
PixelStorage TakePixels();
bool LoadFromFile(const char* filename, Error* error = nullptr);
bool LoadFromFile(std::string_view filename, std::FILE* fp, Error* error = nullptr);
bool LoadFromBuffer(std::string_view filename, std::span<const u8> data, Error* error = nullptr);
bool SaveToFile(const char* filename, u8 quality = DEFAULT_SAVE_QUALITY, Error* error = nullptr) const;
bool SaveToFile(std::string_view filename, std::FILE* fp, u8 quality = DEFAULT_SAVE_QUALITY,
Error* error = nullptr) const;
std::optional<DynamicHeapArray<u8>> SaveToBuffer(std::string_view filename, u8 quality = DEFAULT_SAVE_QUALITY,
Error* error = nullptr) const;
protected:
using FrameDelayStorage = DynamicHeapArray<FrameDelay>;
u32 m_width = 0;
u32 m_height = 0;
u32 m_frame_size = 0;
u32 m_frames = 0;
PixelStorage m_pixels;
FrameDelayStorage m_frame_delay;
};

View File

@@ -2,6 +2,7 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\dep\msvc\vsprops\Configurations.props" />
<ItemGroup>
<ClInclude Include="animated_image.h" />
<ClInclude Include="compress_helpers.h" />
<ClInclude Include="elf_file.h" />
<ClInclude Include="image.h" />
@@ -110,6 +111,7 @@
<ClInclude Include="xinput_source.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="animated_image.cpp" />
<ClCompile Include="audio_stream.cpp" />
<ClCompile Include="cd_image.cpp" />
<ClCompile Include="cd_image_chd.cpp" />

View File

@@ -75,6 +75,7 @@
<ClInclude Include="opengl_context_egl_xlib.h" />
<ClInclude Include="texture_decompress.h" />
<ClInclude Include="opengl_context_sdl.h" />
<ClInclude Include="animated_image.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="state_wrapper.cpp" />
@@ -158,6 +159,7 @@
<ClCompile Include="opengl_context_egl_xlib.cpp" />
<ClCompile Include="texture_decompress.cpp" />
<ClCompile Include="opengl_context_sdl.cpp" />
<ClCompile Include="animated_image.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="metal_shaders.metal" />