mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-14 02:14:35 +00:00
AnimatedImage: Add class for reading APNGs
And probably GIFs in the future.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
add_executable(util-tests
|
||||
animated_image_tests.cpp
|
||||
image_tests.cpp
|
||||
)
|
||||
|
||||
|
||||
720
src/util-tests/animated_image_tests.cpp
Normal file
720
src/util-tests/animated_image_tests.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
588
src/util/animated_image.cpp
Normal 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
89
src/util/animated_image.h
Normal 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;
|
||||
};
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user