Decode PPI face payloads
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
94
src/assets/image_pixels.cpp
Normal file
94
src/assets/image_pixels.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "assets/image_pixels.h"
|
||||
|
||||
#include "assets/image_metadata.h"
|
||||
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#define STB_IMAGE_STATIC
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include <stb/stb_image.h>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> rgba_byte_size(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) noexcept
|
||||
{
|
||||
const auto pixels = static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
|
||||
constexpr auto channels = 4ULL;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / channels) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("RGBA byte size overflows"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * channels;
|
||||
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("RGBA byte size exceeds addressable memory"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> bytes)
|
||||
{
|
||||
const auto metadata = parse_png_metadata(bytes);
|
||||
if (!metadata) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(metadata.status());
|
||||
}
|
||||
|
||||
if (bytes.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
return pp::foundation::Result<Rgba8Image>::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<const stbi_uc*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
&width,
|
||||
&height,
|
||||
&source_components,
|
||||
4);
|
||||
if (decoded == nullptr) {
|
||||
return pp::foundation::Result<Rgba8Image>::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<std::uint32_t>(width) != metadata.value().width
|
||||
|| static_cast<std::uint32_t>(height) != metadata.value().height) {
|
||||
cleanup();
|
||||
return pp::foundation::Result<Rgba8Image>::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<Rgba8Image>::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<Rgba8Image>::success(std::move(image));
|
||||
}
|
||||
|
||||
}
|
||||
21
src/assets/image_pixels.h
Normal file
21
src/assets/image_pixels.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
struct Rgba8Image {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<Rgba8Image> decode_png_rgba8(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
}
|
||||
@@ -561,4 +561,59 @@ pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(std::span<const
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::span<const std::byte> bytes)
|
||||
{
|
||||
auto project = parse_ppi_project_index(bytes);
|
||||
if (!project) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::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<PpiDecodedProjectImages>::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<PpiDecodedProjectImages>::failure(image.status());
|
||||
}
|
||||
|
||||
if (image.value().width != face.png_width || image.value().height != face.png_height) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
|
||||
pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed"));
|
||||
}
|
||||
|
||||
decoded.faces.push_back(PpiDecodedFacePayload {
|
||||
.layer_index = static_cast<std::uint32_t>(layer_index),
|
||||
.frame_index = static_cast<std::uint32_t>(frame_index),
|
||||
.face_index = static_cast<std::uint32_t>(face_index),
|
||||
.descriptor = face,
|
||||
.image = image.value(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::success(std::move(decoded));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "assets/image_pixels.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
@@ -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<PpiDecodedFacePayload> faces;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
@@ -126,4 +140,7 @@ struct PpiProjectIndex {
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
67
tests/assets/image_pixels_tests.cpp
Normal file
67
tests/assets/image_pixels_tests.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "assets/image_pixels.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
using pp::assets::decode_png_rgba8;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::byte, 68> 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();
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
|
||||
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<std::byte> png_ihdr_payload(
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::byte> 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<std::byte> minimal_project()
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
@@ -127,7 +151,10 @@ std::vector<std::byte> minimal_project()
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::byte> project_with_single_face_payload(std::vector<std::byte> payload)
|
||||
std::vector<std::byte> project_with_single_face_payload(
|
||||
std::vector<std::byte> 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<std::byte> project_with_single_face_payload(std::vector<std::byte> 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<std::uint32_t>(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();
|
||||
|
||||
Reference in New Issue
Block a user