From 88507df90ec1f2f456b457949fbf206e92219cfb Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 13:35:03 +0200 Subject: [PATCH] Decode PPI face payloads --- CMakeLists.txt | 9 +++ docs/modernization/build-inventory.md | 11 ++-- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 14 ++-- scripts/automation/platform-build.ps1 | 2 +- scripts/automation/platform-build.sh | 2 +- src/assets/image_pixels.cpp | 94 +++++++++++++++++++++++++++ src/assets/image_pixels.h | 21 ++++++ src/assets/ppi_header.cpp | 55 ++++++++++++++++ src/assets/ppi_header.h | 17 +++++ tests/CMakeLists.txt | 10 +++ tests/assets/image_pixels_tests.cpp | 67 +++++++++++++++++++ tests/assets/ppi_header_tests.cpp | 60 ++++++++++++++++- 13 files changed, 349 insertions(+), 15 deletions(-) create mode 100644 src/assets/image_pixels.cpp create mode 100644 src/assets/image_pixels.h create mode 100644 tests/assets/image_pixels_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ff0ac6..42ead08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,11 +98,20 @@ target_link_libraries(pp_foundation add_library(pp_assets STATIC src/assets/image_format.cpp src/assets/image_metadata.cpp + src/assets/image_pixels.cpp src/assets/ppi_header.cpp src/assets/settings_document.cpp) target_include_directories(pp_assets PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_include_directories(pp_assets + SYSTEM PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/libs/stb") +if(MSVC) + set_source_files_properties(src/assets/image_pixels.cpp + PROPERTIES + COMPILE_OPTIONS "/analyze-") +endif() target_link_libraries(pp_assets PUBLIC pp_foundation diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index ec391da..d9d06ea 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -90,16 +90,19 @@ 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, PNG metadata, PPI - header/layout, settings document, document snapshot/per-layer-frame/move/duration - coverage, paint brush/stroke/stroke-script coverage, renderer shader descriptor - coverage, UI color parsing, and layout XML parse coverage. + including foundation event/logging/task queue coverage, PNG metadata and + decode, PPI header/layout, settings document, document + snapshot/per-layer-frame/move/duration coverage, paint + brush/stroke/stroke-script coverage, renderer shader descriptor coverage, + UI color parsing, and layout XML parse coverage. - `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by `pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture. - `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout, body summary fields, layer/frame descriptors, and dirty-face PNG payload metadata, and is covered by `pano_cli_inspect_project_layout_smoke` with a minimal PPI fixture. +- `pp_assets_image_pixels_tests` decodes PNG payloads to RGBA8 and rejects + corrupt image payloads. - `pano_cli load-project` creates a metadata-only `pp_document` projection with per-layer frame counts and durations, and is covered by `pano_cli_load_project_metadata_smoke`. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 011a57a..1c13005 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -31,7 +31,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, and per-layer frame metadata, but it is not yet wired to legacy `Canvas`, PPI pixel load/save, selection masks, or legacy action commands | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | -| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, and metadata-only `pano_cli load-project` validate the fixed PPI header, thumbnail/body byte layout, layer/frame index, dirty-face descriptors, and dirty-face PNG payload metadata, but do not yet deserialize layer face PNG pixels into document/layer/frame data | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_ppi_header_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke` | Full PPI load/save fixtures cover thumbnail, decoded layer face payloads, frames, metadata, corrupt payloads, and round-trip compatibility | +| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, and metadata-only `pano_cli load-project` validate the fixed PPI header, thumbnail/body byte layout, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, and asset-level RGBA PNG payload decoding, but do not yet attach decoded pixels to `pp_document` layer/frame/face storage | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke` | Full PPI load/save fixtures cover thumbnail, decoded layer face payloads attached to documents, frames, metadata, corrupt payloads, and round-trip compatibility | | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index eaa9cc1..6bca35b 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -314,7 +314,8 @@ component/name/thread/frame/stroke metadata with filtering, capacity, and invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection, PNG IHDR metadata parsing, PPI header/project byte-layout/body-summary recognition, layer/frame indexing, dirty-face PNG payload metadata validation, -and a pure typed settings document model, with +asset-level RGBA PNG payload decoding, 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 @@ -331,7 +332,8 @@ length parsing, color parsing, tinyxml-backed layout XML parsing, and invalid input tests. `pano_cli inspect-image` exposes PNG IHDR metadata as JSON, `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout, -body summary, layer/frame descriptors, and dirty-face PNG payload metadata, and +body summary, layer/frame descriptors, dirty-face PNG payload metadata, and +asset-level decode coverage, and `pano_cli load-project` creates a metadata-only `pp_document` projection with per-layer frame counts and durations. `pano_cli create-document` can create simple animation documents with explicit @@ -546,7 +548,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_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 +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_image_pixels_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 @@ -572,9 +574,11 @@ Results: - `pp_foundation_trace_tests` passed. - `pp_assets_image_format_tests` passed. - `pp_assets_image_metadata_tests` passed. +- `pp_assets_image_pixels_tests` passed, including RGBA8 PNG decode and corrupt + payload rejection. - `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout, - body summary validation, layer/frame indexing, and dirty-face PNG payload - metadata validation. + body summary validation, layer/frame indexing, dirty-face PNG payload + metadata validation, and decoded dirty-face payload coverage. - `pp_assets_settings_document_tests` passed. - `pp_paint_brush_tests` passed. - `pp_paint_blend_tests` passed. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index a961546..401caf0 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -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_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_paint_stroke_script_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_image_pixels_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_paint_stroke_script_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" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index f3a8408..be4d948 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -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_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_paint_stroke_script_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_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_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" diff --git a/src/assets/image_pixels.cpp b/src/assets/image_pixels.cpp new file mode 100644 index 0000000..aedbe7a --- /dev/null +++ b/src/assets/image_pixels.cpp @@ -0,0 +1,94 @@ +#include "assets/image_pixels.h" + +#include "assets/image_metadata.h" + +#include +#include + +#define STB_IMAGE_STATIC +#define STB_IMAGE_IMPLEMENTATION +#include + +namespace pp::assets { + +namespace { + +[[nodiscard]] pp::foundation::Result rgba_byte_size( + std::uint32_t width, + std::uint32_t height) noexcept +{ + const auto pixels = static_cast(width) * static_cast(height); + constexpr auto channels = 4ULL; + if (pixels > std::numeric_limits::max() / channels) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("RGBA byte size overflows")); + } + + const auto bytes = pixels * channels; + if (bytes > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("RGBA byte size exceeds addressable memory")); + } + + return pp::foundation::Result::success(static_cast(bytes)); +} + +} + +pp::foundation::Result decode_png_rgba8(std::span bytes) +{ + const auto metadata = parse_png_metadata(bytes); + if (!metadata) { + return pp::foundation::Result::failure(metadata.status()); + } + + if (bytes.size() > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PNG payload is too large for the decoder")); + } + + int width = 0; + int height = 0; + int source_components = 0; + auto* decoded = stbi_load_from_memory( + reinterpret_cast(bytes.data()), + static_cast(bytes.size()), + &width, + &height, + &source_components, + 4); + if (decoded == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PNG payload could not be decoded")); + } + + const auto cleanup = [decoded]() noexcept { + stbi_image_free(decoded); + }; + + if (width <= 0 || height <= 0 + || static_cast(width) != metadata.value().width + || static_cast(height) != metadata.value().height) { + cleanup(); + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("decoded PNG dimensions are inconsistent")); + } + + const auto byte_count = rgba_byte_size(metadata.value().width, metadata.value().height); + if (!byte_count) { + cleanup(); + return pp::foundation::Result::failure(byte_count.status()); + } + + Rgba8Image image { + .width = metadata.value().width, + .height = metadata.value().height, + .pixels = {}, + }; + image.pixels.assign(decoded, decoded + byte_count.value()); + cleanup(); + + return pp::foundation::Result::success(std::move(image)); +} + +} diff --git a/src/assets/image_pixels.h b/src/assets/image_pixels.h new file mode 100644 index 0000000..42b67ed --- /dev/null +++ b/src/assets/image_pixels.h @@ -0,0 +1,21 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include +#include + +namespace pp::assets { + +struct Rgba8Image { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::vector pixels; +}; + +[[nodiscard]] pp::foundation::Result decode_png_rgba8( + std::span bytes); + +} diff --git a/src/assets/ppi_header.cpp b/src/assets/ppi_header.cpp index 2017be3..cf5069d 100644 --- a/src/assets/ppi_header.cpp +++ b/src/assets/ppi_header.cpp @@ -561,4 +561,59 @@ pp::foundation::Result parse_ppi_project_index(std::span decode_ppi_project_images(std::span bytes) +{ + auto project = parse_ppi_project_index(bytes); + if (!project) { + return pp::foundation::Result::failure(project.status()); + } + + PpiDecodedProjectImages decoded { + .project = project.value(), + .faces = {}, + }; + decoded.faces.reserve(decoded.project.body.summary.rgba_face_payload_count); + + const auto body = bytes.subspan(decoded.project.layout.body_offset, decoded.project.layout.body_bytes); + for (std::size_t layer_index = 0; layer_index < decoded.project.body.layers.size(); ++layer_index) { + const auto& layer = decoded.project.body.layers[layer_index]; + for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) { + const auto& frame = layer.frames[frame_index]; + for (std::size_t face_index = 0; face_index < frame.faces.size(); ++face_index) { + const auto& face = frame.faces[face_index]; + if (!face.has_data) { + continue; + } + + if (face.body_payload_offset > body.size() + || face.payload_bytes > body.size() - face.body_payload_offset) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI face payload range is outside the body")); + } + + const auto image = decode_png_rgba8( + body.subspan(face.body_payload_offset, face.payload_bytes)); + if (!image) { + return pp::foundation::Result::failure(image.status()); + } + + if (image.value().width != face.png_width || image.value().height != face.png_height) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed")); + } + + decoded.faces.push_back(PpiDecodedFacePayload { + .layer_index = static_cast(layer_index), + .frame_index = static_cast(frame_index), + .face_index = static_cast(face_index), + .descriptor = face, + .image = image.value(), + }); + } + } + } + + return pp::foundation::Result::success(std::move(decoded)); +} + } diff --git a/src/assets/ppi_header.h b/src/assets/ppi_header.h index fd98976..4321bd1 100644 --- a/src/assets/ppi_header.h +++ b/src/assets/ppi_header.h @@ -1,5 +1,6 @@ #pragma once +#include "assets/image_pixels.h" #include "foundation/result.h" #include @@ -104,6 +105,19 @@ struct PpiProjectIndex { PpiBodyIndex body; }; +struct PpiDecodedFacePayload { + std::uint32_t layer_index = 0; + std::uint32_t frame_index = 0; + std::uint32_t face_index = 0; + PpiFacePayloadSummary descriptor; + Rgba8Image image; +}; + +struct PpiDecodedProjectImages { + PpiProjectIndex project; + std::vector faces; +}; + [[nodiscard]] pp::foundation::Result parse_ppi_header( std::span bytes) noexcept; @@ -126,4 +140,7 @@ struct PpiProjectIndex { [[nodiscard]] pp::foundation::Result parse_ppi_project_index( std::span bytes); +[[nodiscard]] pp::foundation::Result decode_ppi_project_images( + std::span bytes); + } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9f28755..7ac518a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -86,6 +86,16 @@ add_test(NAME pp_assets_image_metadata_tests COMMAND pp_assets_image_metadata_te set_tests_properties(pp_assets_image_metadata_tests PROPERTIES LABELS "assets;desktop-fast") +add_executable(pp_assets_image_pixels_tests + assets/image_pixels_tests.cpp) +target_link_libraries(pp_assets_image_pixels_tests PRIVATE + pp_assets + pp_test_harness) + +add_test(NAME pp_assets_image_pixels_tests COMMAND pp_assets_image_pixels_tests) +set_tests_properties(pp_assets_image_pixels_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 diff --git a/tests/assets/image_pixels_tests.cpp b/tests/assets/image_pixels_tests.cpp new file mode 100644 index 0000000..d1b9e03 --- /dev/null +++ b/tests/assets/image_pixels_tests.cpp @@ -0,0 +1,67 @@ +#include "assets/image_pixels.h" +#include "test_harness.h" + +#include +#include +#include +#include + +using pp::assets::decode_png_rgba8; +using pp::foundation::StatusCode; + +namespace { + +constexpr std::array transparent_png_1x1 { + 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 { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 }, + std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 }, + std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 }, + std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 }, + std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 }, + std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, + std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 }, + std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 }, + std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 }, + std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 }, + std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f }, + std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, + std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 }, + std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 }, +}; + +void decodes_png_to_rgba8_pixels(pp::tests::Harness& h) +{ + const auto image = decode_png_rgba8(transparent_png_1x1); + + PP_EXPECT(h, image.ok()); + PP_EXPECT(h, image.value().width == 1U); + PP_EXPECT(h, image.value().height == 1U); + PP_EXPECT(h, image.value().pixels.size() == 4U); + PP_EXPECT(h, image.value().pixels[0] == 0U); + PP_EXPECT(h, image.value().pixels[1] == 0U); + PP_EXPECT(h, image.value().pixels[2] == 0U); + PP_EXPECT(h, image.value().pixels[3] == 0U); +} + +void rejects_corrupt_png_payload(pp::tests::Harness& h) +{ + auto corrupt = transparent_png_1x1; + corrupt[0] = std::byte { 0x00 }; + + const auto image = decode_png_rgba8(corrupt); + + PP_EXPECT(h, !image.ok()); + PP_EXPECT(h, image.status().code == StatusCode::invalid_argument); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("decodes_png_to_rgba8_pixels", decodes_png_to_rgba8_pixels); + harness.run("rejects_corrupt_png_payload", rejects_corrupt_png_payload); + return harness.finish(); +} diff --git a/tests/assets/ppi_header_tests.cpp b/tests/assets/ppi_header_tests.cpp index a5a2c4d..0c06d31 100644 --- a/tests/assets/ppi_header_tests.cpp +++ b/tests/assets/ppi_header_tests.cpp @@ -9,6 +9,7 @@ #include using pp::assets::parse_ppi_header; +using pp::assets::decode_ppi_project_images; using pp::assets::parse_ppi_project_index; using pp::assets::parse_ppi_project_summary; using pp::assets::parse_ppi_project_layout; @@ -119,6 +120,29 @@ std::vector png_ihdr_payload( return bytes; } +std::vector transparent_png_1x1() +{ + return { + 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 { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 }, + std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 }, + std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 }, + std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 }, + std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 }, + std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, + std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 }, + std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 }, + std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 }, + std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 }, + std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f }, + std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, + std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 }, + std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 }, + }; +} + std::vector minimal_project() { auto bytes = valid_header(); @@ -127,7 +151,10 @@ std::vector minimal_project() return bytes; } -std::vector project_with_single_face_payload(std::vector payload) +std::vector project_with_single_face_payload( + std::vector payload, + std::uint32_t dirty_width = 8, + std::uint32_t dirty_height = 4) { auto bytes = valid_header(); bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 }); @@ -149,8 +176,8 @@ std::vector project_with_single_face_payload(std::vector p append_u32(bytes, 1); append_u32(bytes, 2); append_u32(bytes, 3); - append_u32(bytes, 10); - append_u32(bytes, 7); + append_u32(bytes, 2 + dirty_width); + append_u32(bytes, 3 + dirty_height); append_u32(bytes, static_cast(payload.size())); bytes.insert(bytes.end(), payload.begin(), payload.end()); @@ -289,6 +316,31 @@ void validates_dirty_face_png_payload_metadata(pp::tests::Harness& h) PP_EXPECT(h, summary.value().body.compressed_face_bytes == 33U); } +void decodes_dirty_face_png_payloads(pp::tests::Harness& h) +{ + const auto project = project_with_single_face_payload(transparent_png_1x1(), 1, 1); + const auto decoded = decode_ppi_project_images(project); + + PP_EXPECT(h, decoded.ok()); + PP_EXPECT(h, decoded.value().faces.size() == 1U); + PP_EXPECT(h, decoded.value().faces[0].layer_index == 0U); + PP_EXPECT(h, decoded.value().faces[0].frame_index == 0U); + PP_EXPECT(h, decoded.value().faces[0].face_index == 0U); + PP_EXPECT(h, decoded.value().faces[0].image.width == 1U); + PP_EXPECT(h, decoded.value().faces[0].image.height == 1U); + PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U); + PP_EXPECT(h, decoded.value().faces[0].image.pixels[3] == 0U); +} + +void rejects_metadata_only_payload_when_decoding_pixels(pp::tests::Harness& h) +{ + const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4)); + const auto decoded = decode_ppi_project_images(project); + + PP_EXPECT(h, !decoded.ok()); + PP_EXPECT(h, decoded.status().code == StatusCode::invalid_argument); +} + void rejects_invalid_dirty_face_png_payloads(pp::tests::Harness& h) { auto mismatched_dimensions = project_with_single_face_payload(png_ihdr_payload(7, 4)); @@ -345,6 +397,8 @@ int main() harness.run("parses_minimal_project_body_summary", parses_minimal_project_body_summary); harness.run("indexes_project_layers_frames_and_faces", indexes_project_layers_frames_and_faces); harness.run("validates_dirty_face_png_payload_metadata", validates_dirty_face_png_payload_metadata); + harness.run("decodes_dirty_face_png_payloads", decodes_dirty_face_png_payloads); + harness.run("rejects_metadata_only_payload_when_decoding_pixels", rejects_metadata_only_payload_when_decoding_pixels); harness.run("rejects_invalid_dirty_face_png_payloads", rejects_invalid_dirty_face_png_payloads); harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries); return harness.finish();