Add assets PNG metadata tests

This commit is contained in:
2026-06-01 08:58:28 +02:00
parent 8ebb22325c
commit c62bc4d744
9 changed files with 314 additions and 8 deletions

View File

@@ -94,6 +94,7 @@ target_link_libraries(pp_foundation
add_library(pp_assets STATIC
src/assets/image_format.cpp
src/assets/image_metadata.cpp
src/assets/ppi_header.cpp
src/assets/settings_document.cpp)
target_include_directories(pp_assets

View File

@@ -86,9 +86,9 @@ Known local toolchain state:
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
including foundation event/logging/task queue coverage, PPI header, settings
document, paint brush/stroke coverage, UI color parsing, and layout XML
parse coverage.
including foundation event/logging/task queue coverage, PNG metadata, PPI
header, settings document, paint brush/stroke coverage, UI color parsing, and
layout XML parse coverage.
- `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes.

View File

@@ -309,8 +309,9 @@ input. A synchronous event dispatcher, structured logging facade, bounded FIFO
task queue, and deterministic `TraceRecorder` now record
component/name/thread/frame/stroke metadata with filtering, capacity, and
invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection,
PPI header recognition, and a pure typed settings document model, with
corrupt/truncated/unsupported and key/value limit tests.
PNG IHDR metadata parsing, PPI header recognition, and a pure typed settings
document model, with corrupt/truncated/unsupported, extreme-dimension, and
key/value limit tests.
`pp_paint` has started with pure brush parameter validation/stamp evaluation,
CPU reference math for the five current shader blend modes, and deterministic
stroke spacing/interpolation. `pp_document` has
@@ -528,7 +529,7 @@ Last verified on 2026-06-01:
```powershell
cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter
ctest --preset desktop-fast --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
@@ -552,6 +553,7 @@ Results:
- `pp_foundation_task_queue_tests` passed.
- `pp_foundation_trace_tests` passed.
- `pp_assets_image_format_tests` passed.
- `pp_assets_image_metadata_tests` passed.
- `pp_assets_ppi_header_tests` passed.
- `pp_assets_settings_document_tests` passed.
- `pp_paint_brush_tests` passed.

View File

@@ -1,7 +1,7 @@
[CmdletBinding()]
param(
[string[]]$Presets = @("android-arm64"),
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_image_metadata_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
)
$ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}"
shift || true
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
start="$(date +%s)"
cmake --preset "$preset"

View File

@@ -0,0 +1,146 @@
#include "assets/image_metadata.h"
#include <cstddef>
namespace pp::assets {
namespace {
constexpr std::byte png_signature[] {
std::byte { 0x89 },
std::byte { 0x50 },
std::byte { 0x4e },
std::byte { 0x47 },
std::byte { 0x0d },
std::byte { 0x0a },
std::byte { 0x1a },
std::byte { 0x0a },
};
[[nodiscard]] bool has_png_signature(std::span<const std::byte> bytes) noexcept
{
if (bytes.size() < 8U) {
return false;
}
for (std::size_t i = 0; i < 8U; ++i) {
if (bytes[i] != png_signature[i]) {
return false;
}
}
return true;
}
[[nodiscard]] std::uint32_t read_u32_be(std::span<const std::byte> bytes, std::size_t offset) noexcept
{
return (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset])) << 24U)
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 1U])) << 16U)
| (static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 2U])) << 8U)
| static_cast<std::uint32_t>(std::to_integer<std::uint8_t>(bytes[offset + 3U]));
}
[[nodiscard]] pp::foundation::Result<ImageColorType> parse_png_color_type(std::byte value) noexcept
{
switch (std::to_integer<std::uint8_t>(value)) {
case 0:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale);
case 2:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgb);
case 3:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::indexed);
case 4:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::grayscale_alpha);
case 6:
return pp::foundation::Result<ImageColorType>::success(ImageColorType::rgba);
default:
return pp::foundation::Result<ImageColorType>::failure(
pp::foundation::Status::invalid_argument("PNG color type is unsupported"));
}
}
[[nodiscard]] std::uint8_t component_count(ImageColorType color_type) noexcept
{
switch (color_type) {
case ImageColorType::grayscale:
case ImageColorType::indexed:
return 1;
case ImageColorType::grayscale_alpha:
return 2;
case ImageColorType::rgb:
return 3;
case ImageColorType::rgba:
return 4;
}
return 0;
}
}
pp::foundation::Result<ImageMetadata> parse_png_metadata(std::span<const std::byte> bytes) noexcept
{
constexpr std::size_t png_ihdr_end = 33;
if (bytes.size() < png_ihdr_end) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::out_of_range("PNG metadata is truncated"));
}
if (!has_png_signature(bytes)) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PNG signature is invalid"));
}
const auto ihdr_length = read_u32_be(bytes, 8);
if (ihdr_length != 13U || bytes[12] != std::byte { 'I' } || bytes[13] != std::byte { 'H' }
|| bytes[14] != std::byte { 'D' } || bytes[15] != std::byte { 'R' }) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PNG IHDR chunk is invalid"));
}
const auto width = read_u32_be(bytes, 16);
const auto height = read_u32_be(bytes, 20);
if (width == 0 || height == 0 || width > max_image_dimension || height > max_image_dimension) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::out_of_range("PNG dimensions are outside the configured range"));
}
const auto bit_depth = std::to_integer<std::uint8_t>(bytes[24]);
if (bit_depth == 0U) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PNG bit depth is invalid"));
}
const auto color_type = parse_png_color_type(bytes[25]);
if (!color_type) {
return pp::foundation::Result<ImageMetadata>::failure(color_type.status());
}
return pp::foundation::Result<ImageMetadata>::success(ImageMetadata {
.width = width,
.height = height,
.bit_depth = bit_depth,
.components = component_count(color_type.value()),
.color_type = color_type.value(),
});
}
const char* image_color_type_name(ImageColorType color_type) noexcept
{
switch (color_type) {
case ImageColorType::grayscale:
return "grayscale";
case ImageColorType::rgb:
return "rgb";
case ImageColorType::indexed:
return "indexed";
case ImageColorType::grayscale_alpha:
return "grayscale_alpha";
case ImageColorType::rgba:
return "rgba";
}
return "unknown";
}
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <span>
namespace pp::assets {
constexpr std::uint32_t max_image_dimension = 262144;
enum class ImageColorType : std::uint8_t {
grayscale,
rgb,
indexed,
grayscale_alpha,
rgba,
};
struct ImageMetadata {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint8_t bit_depth = 0;
std::uint8_t components = 0;
ImageColorType color_type = ImageColorType::rgba;
};
[[nodiscard]] pp::foundation::Result<ImageMetadata> parse_png_metadata(
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] const char* image_color_type_name(ImageColorType color_type) noexcept;
}

View File

@@ -76,6 +76,16 @@ add_test(NAME pp_assets_image_format_tests COMMAND pp_assets_image_format_tests)
set_tests_properties(pp_assets_image_format_tests PROPERTIES
LABELS "assets;desktop-fast")
add_executable(pp_assets_image_metadata_tests
assets/image_metadata_tests.cpp)
target_link_libraries(pp_assets_image_metadata_tests PRIVATE
pp_assets
pp_test_harness)
add_test(NAME pp_assets_image_metadata_tests COMMAND pp_assets_image_metadata_tests)
set_tests_properties(pp_assets_image_metadata_tests PROPERTIES
LABELS "assets;desktop-fast")
add_executable(pp_assets_ppi_header_tests
assets/ppi_header_tests.cpp)
target_link_libraries(pp_assets_ppi_header_tests PRIVATE

View File

@@ -0,0 +1,113 @@
#include "assets/image_metadata.h"
#include "test_harness.h"
#include <array>
#include <cstddef>
#include <string_view>
using pp::assets::ImageColorType;
using pp::assets::image_color_type_name;
using pp::assets::max_image_dimension;
using pp::assets::parse_png_metadata;
using pp::foundation::StatusCode;
namespace {
using PngHeader = std::array<std::byte, 33>;
PngHeader make_png_header(std::uint32_t width, std::uint32_t height, std::byte bit_depth, std::byte color_type)
{
PngHeader bytes {
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
std::byte { 'I' }, std::byte { 'H' }, std::byte { 'D' }, std::byte { 'R' },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
bit_depth, color_type, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
};
bytes[16] = static_cast<std::byte>((width >> 24U) & 0xffU);
bytes[17] = static_cast<std::byte>((width >> 16U) & 0xffU);
bytes[18] = static_cast<std::byte>((width >> 8U) & 0xffU);
bytes[19] = static_cast<std::byte>(width & 0xffU);
bytes[20] = static_cast<std::byte>((height >> 24U) & 0xffU);
bytes[21] = static_cast<std::byte>((height >> 16U) & 0xffU);
bytes[22] = static_cast<std::byte>((height >> 8U) & 0xffU);
bytes[23] = static_cast<std::byte>(height & 0xffU);
return bytes;
}
void parses_png_ihdr_metadata(pp::tests::Harness& h)
{
const auto rgba = make_png_header(320, 240, std::byte { 8 }, std::byte { 6 });
const auto rgb = make_png_header(17, 9, std::byte { 8 }, std::byte { 2 });
const auto rgba_result = parse_png_metadata(rgba);
const auto rgb_result = parse_png_metadata(rgb);
PP_EXPECT(h, rgba_result.ok());
PP_EXPECT(h, rgba_result.value().width == 320U);
PP_EXPECT(h, rgba_result.value().height == 240U);
PP_EXPECT(h, rgba_result.value().bit_depth == 8U);
PP_EXPECT(h, rgba_result.value().components == 4U);
PP_EXPECT(h, rgba_result.value().color_type == ImageColorType::rgba);
PP_EXPECT(h, image_color_type_name(rgba_result.value().color_type) == std::string_view("rgba"));
PP_EXPECT(h, rgb_result.ok());
PP_EXPECT(h, rgb_result.value().components == 3U);
PP_EXPECT(h, rgb_result.value().color_type == ImageColorType::rgb);
}
void maps_png_color_type_components(pp::tests::Harness& h)
{
const auto grayscale = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 0 }));
const auto indexed = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 3 }));
const auto gray_alpha = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 4 }));
PP_EXPECT(h, grayscale.ok());
PP_EXPECT(h, grayscale.value().components == 1U);
PP_EXPECT(h, grayscale.value().color_type == ImageColorType::grayscale);
PP_EXPECT(h, indexed.ok());
PP_EXPECT(h, indexed.value().components == 1U);
PP_EXPECT(h, indexed.value().color_type == ImageColorType::indexed);
PP_EXPECT(h, gray_alpha.ok());
PP_EXPECT(h, gray_alpha.value().components == 2U);
PP_EXPECT(h, gray_alpha.value().color_type == ImageColorType::grayscale_alpha);
}
void rejects_corrupt_or_extreme_png_metadata(pp::tests::Harness& h)
{
const std::array<std::byte, 8> truncated {
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
};
auto bad_signature = make_png_header(1, 1, std::byte { 8 }, std::byte { 6 });
bad_signature[0] = std::byte { 0x00 };
auto bad_ihdr = make_png_header(1, 1, std::byte { 8 }, std::byte { 6 });
bad_ihdr[15] = std::byte { 'X' };
const auto zero_width = make_png_header(0, 1, std::byte { 8 }, std::byte { 6 });
const auto too_large = make_png_header(max_image_dimension + 1U, 1, std::byte { 8 }, std::byte { 6 });
const auto bad_depth = make_png_header(1, 1, std::byte { 0 }, std::byte { 6 });
const auto bad_color = make_png_header(1, 1, std::byte { 8 }, std::byte { 5 });
PP_EXPECT(h, parse_png_metadata(truncated).status().code == StatusCode::out_of_range);
PP_EXPECT(h, parse_png_metadata(bad_signature).status().code == StatusCode::invalid_argument);
PP_EXPECT(h, parse_png_metadata(bad_ihdr).status().code == StatusCode::invalid_argument);
PP_EXPECT(h, parse_png_metadata(zero_width).status().code == StatusCode::out_of_range);
PP_EXPECT(h, parse_png_metadata(too_large).status().code == StatusCode::out_of_range);
PP_EXPECT(h, parse_png_metadata(bad_depth).status().code == StatusCode::invalid_argument);
PP_EXPECT(h, parse_png_metadata(bad_color).status().code == StatusCode::invalid_argument);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("parses_png_ihdr_metadata", parses_png_ihdr_metadata);
harness.run("maps_png_color_type_components", maps_png_color_type_components);
harness.run("rejects_corrupt_or_extreme_png_metadata", rejects_corrupt_or_extreme_png_metadata);
return harness.finish();
}