Summarize PPI project bodies
This commit is contained in:
@@ -97,8 +97,8 @@ Known local toolchain state:
|
||||
- `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
|
||||
and is covered by `pano_cli_inspect_project_layout_smoke` with a minimal PPI
|
||||
fixture.
|
||||
plus body summary fields, and is covered by
|
||||
`pano_cli_inspect_project_layout_smoke` with a minimal PPI fixture.
|
||||
- `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and
|
||||
is covered by `pano_cli_create_animation_document_smoke`.
|
||||
- `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted
|
||||
|
||||
@@ -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 but is not yet wired to legacy `Canvas`, PPI 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` | 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` and `pano_cli inspect-project` validate the fixed PPI header and thumbnail/body byte layout, but do not yet deserialize the project body 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` | Full PPI load/save fixtures cover thumbnail, layers, frames, metadata, corrupt payloads, and round-trip compatibility |
|
||||
| DEBT-0013 | Open | Modernization | `pp_assets` and `pano_cli inspect-project` validate the fixed PPI header, thumbnail/body byte layout, and body summary, but do not yet deserialize layer face PNG payloads into document/layer/frame pixel 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` | Full PPI load/save fixtures cover thumbnail, decoded layer face payloads, 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
|
||||
|
||||
@@ -312,9 +312,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,
|
||||
PNG IHDR metadata parsing, PPI header/project byte-layout recognition, and a
|
||||
pure typed settings document model, with corrupt/truncated/unsupported,
|
||||
extreme-dimension, and key/value limit tests.
|
||||
PNG IHDR metadata parsing, PPI header/project byte-layout/body-summary
|
||||
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 plus a pure text stroke-script parser.
|
||||
@@ -328,8 +328,8 @@ the paint blend reference. `pp_ui_core` has started with XML-layout-facing
|
||||
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,
|
||||
and
|
||||
`pano_cli inspect-project` reports validated PPI thumbnail/body byte layout and
|
||||
body summary, and
|
||||
`pano_cli create-document` can create simple animation documents with explicit
|
||||
frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke
|
||||
sampler for scripted-stroke automation. `pano_cli simulate-stroke-script`
|
||||
@@ -568,8 +568,8 @@ Results:
|
||||
- `pp_foundation_trace_tests` passed.
|
||||
- `pp_assets_image_format_tests` passed.
|
||||
- `pp_assets_image_metadata_tests` passed.
|
||||
- `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout
|
||||
validation.
|
||||
- `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout and
|
||||
body summary validation.
|
||||
- `pp_assets_settings_document_tests` passed.
|
||||
- `pp_paint_brush_tests` passed.
|
||||
- `pp_paint_blend_tests` passed.
|
||||
@@ -590,7 +590,7 @@ Results:
|
||||
- `pano_cli_inspect_png_metadata_smoke` passed and reports PNG metadata JSON
|
||||
for the tiny IHDR fixture.
|
||||
- `pano_cli_inspect_project_layout_smoke` passed and reports PPI
|
||||
thumbnail/body byte layout JSON.
|
||||
thumbnail/body byte layout and body summary JSON.
|
||||
- `pano_cli_parse_layout_smoke` passed.
|
||||
- `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke
|
||||
sample counts/distances.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "foundation/binary_stream.h"
|
||||
|
||||
#include <bit>
|
||||
#include <limits>
|
||||
|
||||
namespace pp::assets {
|
||||
@@ -13,6 +14,69 @@ namespace {
|
||||
return reader.read_u32_le();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_positive_i32(
|
||||
pp::foundation::ByteReader& reader,
|
||||
const char* message) noexcept
|
||||
{
|
||||
const auto value = reader.read_u32_le();
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.value() > static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max())) {
|
||||
return pp::foundation::Result<std::uint32_t>::failure(
|
||||
pp::foundation::Status::out_of_range(message));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<float> read_f32(pp::foundation::ByteReader& reader) noexcept
|
||||
{
|
||||
const auto bits = reader.read_u32_le();
|
||||
if (!bits) {
|
||||
return pp::foundation::Result<float>::failure(bits.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<float>::success(std::bit_cast<float>(bits.value()));
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status skip_bytes(
|
||||
pp::foundation::ByteReader& reader,
|
||||
std::size_t bytes) noexcept
|
||||
{
|
||||
const auto skipped = reader.read_bytes(bytes);
|
||||
if (!skipped) {
|
||||
return skipped.status();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_canvas_size(std::uint32_t width, std::uint32_t height) noexcept
|
||||
{
|
||||
if (width == 0 || height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("PPI canvas dimensions must be greater than zero");
|
||||
}
|
||||
|
||||
if (width > max_ppi_canvas_dimension || height > max_ppi_canvas_dimension) {
|
||||
return pp::foundation::Status::out_of_range("PPI canvas dimensions exceed the configured limit");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept
|
||||
{
|
||||
const auto next = summary.compressed_face_bytes + static_cast<std::uint64_t>(bytes);
|
||||
if (next > max_ppi_face_payload_bytes) {
|
||||
return pp::foundation::Status::out_of_range("PPI compressed face payload exceeds the configured limit");
|
||||
}
|
||||
|
||||
summary.compressed_face_bytes = next;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte> bytes) noexcept
|
||||
@@ -134,4 +198,221 @@ pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(std::span<cons
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body) noexcept
|
||||
{
|
||||
pp::foundation::ByteReader reader(body);
|
||||
const auto width = read_positive_i32(reader, "PPI canvas width is outside the supported range");
|
||||
const auto height = read_positive_i32(reader, "PPI canvas height is outside the supported range");
|
||||
const auto layer_count = read_positive_i32(reader, "PPI layer count is outside the supported range");
|
||||
if (!width || !height || !layer_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!width ? width.status() : (!height ? height.status() : layer_count.status()));
|
||||
}
|
||||
|
||||
const auto canvas_status = validate_canvas_size(width.value(), height.value());
|
||||
if (!canvas_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(canvas_status);
|
||||
}
|
||||
|
||||
if (layer_count.value() == 0 || layer_count.value() > max_ppi_layer_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
|
||||
}
|
||||
|
||||
PpiBodySummary summary {
|
||||
.width = width.value(),
|
||||
.height = height.value(),
|
||||
.layer_count = layer_count.value(),
|
||||
.declared_frame_count = 1,
|
||||
};
|
||||
|
||||
if (header.document_version.minor >= 3U) {
|
||||
const auto declared_frames = read_positive_i32(reader, "PPI declared frame count is outside the supported range");
|
||||
if (!declared_frames) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(declared_frames.status());
|
||||
}
|
||||
|
||||
if (declared_frames.value() == 0 || declared_frames.value() > max_ppi_frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI declared frame count is outside the configured range"));
|
||||
}
|
||||
summary.declared_frame_count = declared_frames.value();
|
||||
}
|
||||
|
||||
for (std::uint32_t layer_index = 0; layer_index < summary.layer_count; ++layer_index) {
|
||||
const auto order = read_positive_i32(reader, "PPI layer order is outside the supported range");
|
||||
const auto opacity = read_f32(reader);
|
||||
const auto name_length = read_positive_i32(reader, "PPI layer name length is outside the supported range");
|
||||
if (!order || !opacity || !name_length) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!order ? order.status() : (!opacity ? opacity.status() : name_length.status()));
|
||||
}
|
||||
|
||||
if (order.value() >= summary.layer_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer order is outside the layer list"));
|
||||
}
|
||||
|
||||
if (opacity.value() < 0.0F || opacity.value() > 1.0F) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
|
||||
}
|
||||
|
||||
if (name_length.value() > max_ppi_layer_name_length) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
|
||||
}
|
||||
|
||||
const auto name_status = skip_bytes(reader, name_length.value());
|
||||
if (!name_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(name_status);
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 2U) {
|
||||
const auto blend_mode = read_positive_i32(reader, "PPI layer blend mode is outside the supported range");
|
||||
const auto alpha_locked = reader.read_u8();
|
||||
const auto visible = reader.read_u8();
|
||||
if (!blend_mode || !alpha_locked || !visible) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!blend_mode ? blend_mode.status() : (!alpha_locked ? alpha_locked.status() : visible.status()));
|
||||
}
|
||||
|
||||
if (alpha_locked.value() > 1U || visible.value() > 1U) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t layer_frames = 1;
|
||||
if (header.document_version.minor >= 3U) {
|
||||
const auto frame_count = read_positive_i32(reader, "PPI layer frame count is outside the supported range");
|
||||
if (!frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(frame_count.status());
|
||||
}
|
||||
|
||||
if (frame_count.value() == 0 || frame_count.value() > max_ppi_frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
|
||||
}
|
||||
layer_frames = frame_count.value();
|
||||
}
|
||||
|
||||
if (summary.total_layer_frames > max_ppi_frame_count - layer_frames) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI total frame count exceeds the configured limit"));
|
||||
}
|
||||
summary.total_layer_frames += layer_frames;
|
||||
|
||||
for (std::uint32_t frame_index = 0; frame_index < layer_frames; ++frame_index) {
|
||||
if (header.document_version.minor >= 3U) {
|
||||
const auto duration = read_positive_i32(reader, "PPI frame duration is outside the supported range");
|
||||
if (!duration) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(duration.status());
|
||||
}
|
||||
|
||||
if (duration.value() == 0) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
|
||||
}
|
||||
}
|
||||
|
||||
for (std::uint32_t face = 0; face < 6U; ++face) {
|
||||
const auto has_data = read_positive_i32(reader, "PPI face data flag is outside the supported range");
|
||||
if (!has_data) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(has_data.status());
|
||||
}
|
||||
|
||||
if (has_data.value() > 1U) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI face data flag is invalid"));
|
||||
}
|
||||
|
||||
if (has_data.value() == 0U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++summary.dirty_face_count;
|
||||
const auto x0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto y0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto x1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto y1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
|
||||
const auto data_size = read_positive_i32(reader, "PPI compressed face data size is outside the supported range");
|
||||
if (!x0 || !y0 || !x1 || !y1 || !data_size) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
!x0 ? x0.status()
|
||||
: (!y0 ? y0.status() : (!x1 ? x1.status() : (!y1 ? y1.status() : data_size.status()))));
|
||||
}
|
||||
|
||||
if (x0.value() >= x1.value() || y0.value() >= y1.value() || x1.value() > summary.width
|
||||
|| y1.value() > summary.height) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI dirty box is outside the canvas"));
|
||||
}
|
||||
|
||||
if (data_size.value() == 0U) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI compressed face payload must not be empty"));
|
||||
}
|
||||
|
||||
const auto payload_status = add_payload_bytes(summary, data_size.value());
|
||||
if (!payload_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(payload_status);
|
||||
}
|
||||
|
||||
const auto skip_status = skip_bytes(reader, data_size.value());
|
||||
if (!skip_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(skip_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 3U && summary.total_layer_frames != summary.declared_frame_count) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI declared frame count does not match layer frames"));
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 4U) {
|
||||
const auto info_bytes = read_positive_i32(reader, "PPI info block size is outside the supported range");
|
||||
if (!info_bytes) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(info_bytes.status());
|
||||
}
|
||||
|
||||
summary.info_bytes = info_bytes.value();
|
||||
const auto info_status = skip_bytes(reader, summary.info_bytes);
|
||||
if (!info_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(info_status);
|
||||
}
|
||||
}
|
||||
|
||||
if (!reader.empty()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI body has trailing bytes"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiBodySummary>::success(summary);
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
if (!layout) {
|
||||
return pp::foundation::Result<PpiProjectSummary>::failure(layout.status());
|
||||
}
|
||||
|
||||
const auto body = parse_ppi_body_summary(
|
||||
layout.value().header,
|
||||
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
|
||||
if (!body) {
|
||||
return pp::foundation::Result<PpiProjectSummary>::failure(body.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiProjectSummary>::success(PpiProjectSummary {
|
||||
.layout = layout.value(),
|
||||
.body = body.value(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::size_t ppi_header_size = 40;
|
||||
constexpr std::uint32_t max_ppi_canvas_dimension = 131072;
|
||||
constexpr std::uint32_t max_ppi_layer_count = 1024;
|
||||
constexpr std::uint32_t max_ppi_frame_count = 100000;
|
||||
constexpr std::size_t max_ppi_layer_name_length = 128;
|
||||
constexpr std::uint64_t max_ppi_face_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||
|
||||
struct PpiVersion {
|
||||
std::uint32_t major = 0;
|
||||
@@ -42,6 +47,22 @@ struct PpiProjectLayout {
|
||||
std::size_t body_bytes = 0;
|
||||
};
|
||||
|
||||
struct PpiBodySummary {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t layer_count = 0;
|
||||
std::uint32_t declared_frame_count = 0;
|
||||
std::uint32_t total_layer_frames = 0;
|
||||
std::uint32_t dirty_face_count = 0;
|
||||
std::uint64_t compressed_face_bytes = 0;
|
||||
std::uint32_t info_bytes = 0;
|
||||
};
|
||||
|
||||
struct PpiProjectSummary {
|
||||
PpiProjectLayout layout;
|
||||
PpiBodySummary body;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
@@ -50,4 +71,11 @@ struct PpiProjectLayout {
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ if(TARGET pano_cli)
|
||||
COMMAND pano_cli inspect-project --path "${CMAKE_CURRENT_SOURCE_DIR}/data/projects/minimal-project.ppi")
|
||||
set_tests_properties(pano_cli_inspect_project_layout_smoke PROPERTIES
|
||||
LABELS "assets;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"thumbnail\":\\{\"width\":128,\"height\":128,\"components\":4,\"bytes\":65536\\}.*\"body\":\\{\"offset\":65576,\"bytes\":7\\}")
|
||||
PASS_REGULAR_EXPRESSION "\"thumbnail\":\\{\"width\":128,\"height\":128,\"components\":4,\"bytes\":65536\\}.*\"body\":\\{\"offset\":65576,\"bytes\":73,\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"dirtyFaces\":0,\"compressedBytes\":0,\"infoBytes\":0\\}")
|
||||
|
||||
add_test(NAME pano_cli_parse_layout_smoke
|
||||
COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml")
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using pp::assets::parse_ppi_header;
|
||||
using pp::assets::parse_ppi_project_summary;
|
||||
using pp::assets::parse_ppi_project_layout;
|
||||
using pp::assets::ppi_header_size;
|
||||
using pp::assets::ppi_thumbnail_byte_size;
|
||||
@@ -22,6 +25,18 @@ void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
|
||||
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||
}
|
||||
|
||||
void append_f32(std::vector<std::byte>& bytes, float value)
|
||||
{
|
||||
append_u32(bytes, std::bit_cast<std::uint32_t>(value));
|
||||
}
|
||||
|
||||
void append_ascii(std::vector<std::byte>& bytes, std::string_view value)
|
||||
{
|
||||
for (const auto ch : value) {
|
||||
bytes.push_back(static_cast<std::byte>(ch));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::byte> valid_header()
|
||||
{
|
||||
std::vector<std::byte> bytes {
|
||||
@@ -42,6 +57,35 @@ std::vector<std::byte> valid_header()
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void append_minimal_body(std::vector<std::byte>& bytes)
|
||||
{
|
||||
append_u32(bytes, 64);
|
||||
append_u32(bytes, 32);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 0);
|
||||
append_f32(bytes, 1.0F);
|
||||
append_u32(bytes, 3);
|
||||
append_ascii(bytes, "Ink");
|
||||
append_u32(bytes, 0);
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 1 });
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 100);
|
||||
for (std::uint32_t i = 0; i < 6U; ++i) {
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
|
||||
std::vector<std::byte> minimal_project()
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
||||
append_minimal_body(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void parses_legacy_ppi_header(pp::tests::Harness& h)
|
||||
{
|
||||
const auto bytes = valid_header();
|
||||
@@ -100,10 +144,7 @@ void rejects_unsupported_document_versions(pp::tests::Harness& h)
|
||||
|
||||
void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h)
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
const auto thumbnail_size = ppi_thumbnail_byte_size(parse_ppi_header(bytes).value().thumbnail);
|
||||
PP_EXPECT(h, thumbnail_size.ok());
|
||||
bytes.resize(ppi_header_size + thumbnail_size.value() + 7U, std::byte { 0xaa });
|
||||
const auto bytes = minimal_project();
|
||||
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
|
||||
@@ -111,7 +152,7 @@ void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, layout.value().thumbnail_offset == ppi_header_size);
|
||||
PP_EXPECT(h, layout.value().thumbnail_bytes == 128U * 128U * 4U);
|
||||
PP_EXPECT(h, layout.value().body_offset == ppi_header_size + (128U * 128U * 4U));
|
||||
PP_EXPECT(h, layout.value().body_bytes == 7U);
|
||||
PP_EXPECT(h, layout.value().body_bytes == 73U);
|
||||
}
|
||||
|
||||
void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h)
|
||||
@@ -125,6 +166,45 @@ void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, layout.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void parses_minimal_project_body_summary(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = minimal_project();
|
||||
const auto summary = parse_ppi_project_summary(project);
|
||||
|
||||
PP_EXPECT(h, summary.ok());
|
||||
PP_EXPECT(h, summary.value().body.width == 64U);
|
||||
PP_EXPECT(h, summary.value().body.height == 32U);
|
||||
PP_EXPECT(h, summary.value().body.layer_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.declared_frame_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.total_layer_frames == 1U);
|
||||
PP_EXPECT(h, summary.value().body.dirty_face_count == 0U);
|
||||
PP_EXPECT(h, summary.value().body.compressed_face_bytes == 0U);
|
||||
PP_EXPECT(h, summary.value().body.info_bytes == 0U);
|
||||
}
|
||||
|
||||
void rejects_invalid_project_body_summaries(pp::tests::Harness& h)
|
||||
{
|
||||
auto truncated = minimal_project();
|
||||
truncated.pop_back();
|
||||
|
||||
auto mismatched_frames = minimal_project();
|
||||
mismatched_frames[ppi_header_size + (128U * 128U * 4U) + 12U] = std::byte { 2 };
|
||||
|
||||
auto bad_layer_name = minimal_project();
|
||||
bad_layer_name[ppi_header_size + (128U * 128U * 4U) + 24U] = std::byte { 255 };
|
||||
|
||||
const auto truncated_result = parse_ppi_project_summary(truncated);
|
||||
const auto mismatched_frames_result = parse_ppi_project_summary(mismatched_frames);
|
||||
const auto bad_layer_name_result = parse_ppi_project_summary(bad_layer_name);
|
||||
|
||||
PP_EXPECT(h, !truncated_result.ok());
|
||||
PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !mismatched_frames_result.ok());
|
||||
PP_EXPECT(h, mismatched_frames_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_layer_name_result.ok());
|
||||
PP_EXPECT(h, bad_layer_name_result.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -135,5 +215,7 @@ int main()
|
||||
harness.run("rejects_unsupported_document_versions", rejects_unsupported_document_versions);
|
||||
harness.run("parses_project_layout_with_thumbnail_and_body", parses_project_layout_with_thumbnail_and_body);
|
||||
harness.run("rejects_project_layout_with_truncated_thumbnail", rejects_project_layout_with_truncated_thumbnail);
|
||||
harness.run("parses_minimal_project_body_summary", parses_minimal_project_body_summary);
|
||||
harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries);
|
||||
return harness.finish();
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -287,25 +287,32 @@ int inspect_project(int argc, char** argv)
|
||||
std::istreambuf_iterator<char>()
|
||||
};
|
||||
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
|
||||
const auto layout = pp::assets::parse_ppi_project_layout(std::span<const std::byte>(data, chars.size()));
|
||||
if (!layout) {
|
||||
print_error("inspect-project", layout.status().message);
|
||||
const auto summary = pp::assets::parse_ppi_project_summary(std::span<const std::byte>(data, chars.size()));
|
||||
if (!summary) {
|
||||
print_error("inspect-project", summary.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::cout << "{\"ok\":true,\"command\":\"inspect-project\""
|
||||
<< ",\"documentVersion\":\"" << layout.value().header.document_version.major
|
||||
<< "." << layout.value().header.document_version.minor << "\""
|
||||
<< ",\"softwareVersion\":\"" << layout.value().header.software_version.major
|
||||
<< "." << layout.value().header.software_version.minor
|
||||
<< "." << layout.value().header.software_version.fix
|
||||
<< "." << layout.value().header.software_version.build << "\""
|
||||
<< ",\"thumbnail\":{\"width\":" << layout.value().header.thumbnail.width
|
||||
<< ",\"height\":" << layout.value().header.thumbnail.height
|
||||
<< ",\"components\":" << layout.value().header.thumbnail.components
|
||||
<< ",\"bytes\":" << layout.value().thumbnail_bytes
|
||||
<< "},\"body\":{\"offset\":" << layout.value().body_offset
|
||||
<< ",\"bytes\":" << layout.value().body_bytes
|
||||
<< ",\"documentVersion\":\"" << summary.value().layout.header.document_version.major
|
||||
<< "." << summary.value().layout.header.document_version.minor << "\""
|
||||
<< ",\"softwareVersion\":\"" << summary.value().layout.header.software_version.major
|
||||
<< "." << summary.value().layout.header.software_version.minor
|
||||
<< "." << summary.value().layout.header.software_version.fix
|
||||
<< "." << summary.value().layout.header.software_version.build << "\""
|
||||
<< ",\"thumbnail\":{\"width\":" << summary.value().layout.header.thumbnail.width
|
||||
<< ",\"height\":" << summary.value().layout.header.thumbnail.height
|
||||
<< ",\"components\":" << summary.value().layout.header.thumbnail.components
|
||||
<< ",\"bytes\":" << summary.value().layout.thumbnail_bytes
|
||||
<< "},\"body\":{\"offset\":" << summary.value().layout.body_offset
|
||||
<< ",\"bytes\":" << summary.value().layout.body_bytes
|
||||
<< ",\"width\":" << summary.value().body.width
|
||||
<< ",\"height\":" << summary.value().body.height
|
||||
<< ",\"layers\":" << summary.value().body.layer_count
|
||||
<< ",\"frames\":" << summary.value().body.declared_frame_count
|
||||
<< ",\"dirtyFaces\":" << summary.value().body.dirty_face_count
|
||||
<< ",\"compressedBytes\":" << summary.value().body.compressed_face_bytes
|
||||
<< ",\"infoBytes\":" << summary.value().body.info_bytes
|
||||
<< "}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user