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 add_library(pp_assets STATIC
src/assets/image_format.cpp src/assets/image_format.cpp
src/assets/image_metadata.cpp
src/assets/ppi_header.cpp src/assets/ppi_header.cpp
src/assets/settings_document.cpp) src/assets/settings_document.cpp)
target_include_directories(pp_assets target_include_directories(pp_assets

View File

@@ -86,9 +86,9 @@ Known local toolchain state:
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries, `pp_ui_core`, `pano_cli`, and their current headless test binaries,
including foundation event/logging/task queue coverage, PPI header, settings including foundation event/logging/task queue coverage, PNG metadata, PPI
document, paint brush/stroke coverage, UI color parsing, and layout XML header, settings document, paint brush/stroke coverage, UI color parsing, and
parse coverage. layout XML parse coverage.
- `panopainter_validate_shaders` validates the current combined GLSL shader - `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes. 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 task queue, and deterministic `TraceRecorder` now record
component/name/thread/frame/stroke metadata with filtering, capacity, and component/name/thread/frame/stroke metadata with filtering, capacity, and
invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection, invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection,
PPI header recognition, and a pure typed settings document model, with PNG IHDR metadata parsing, PPI header recognition, and a pure typed settings
corrupt/truncated/unsupported and key/value limit tests. document model, with corrupt/truncated/unsupported, extreme-dimension, and
key/value limit tests.
`pp_paint` has started with pure brush parameter validation/stamp evaluation, `pp_paint` has started with pure brush parameter validation/stamp evaluation,
CPU reference math for the five current shader blend modes, and deterministic CPU reference math for the five current shader blend modes, and deterministic
stroke spacing/interpolation. `pp_document` has stroke spacing/interpolation. `pp_document` has
@@ -528,7 +529,7 @@ Last verified on 2026-06-01:
```powershell ```powershell
cmake --preset windows-msvc-default 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 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\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli 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_task_queue_tests` passed.
- `pp_foundation_trace_tests` passed. - `pp_foundation_trace_tests` passed.
- `pp_assets_image_format_tests` passed. - `pp_assets_image_format_tests` passed.
- `pp_assets_image_metadata_tests` passed.
- `pp_assets_ppi_header_tests` passed. - `pp_assets_ppi_header_tests` passed.
- `pp_assets_settings_document_tests` passed. - `pp_assets_settings_document_tests` passed.
- `pp_paint_brush_tests` passed. - `pp_paint_brush_tests` passed.

View File

@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string[]]$Presets = @("android-arm64"), [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" $ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}" preset="${1:-android-arm64}"
shift || true 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)" start="$(date +%s)"
cmake --preset "$preset" 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 set_tests_properties(pp_assets_image_format_tests PROPERTIES
LABELS "assets;desktop-fast") 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 add_executable(pp_assets_ppi_header_tests
assets/ppi_header_tests.cpp) assets/ppi_header_tests.cpp)
target_link_libraries(pp_assets_ppi_header_tests PRIVATE 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();
}