Index PPI project layers
This commit is contained in:
@@ -97,8 +97,9 @@ 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,
|
||||
body summary fields, and dirty-face PNG payload metadata, and is covered by
|
||||
`pano_cli_inspect_project_layout_smoke` with a minimal PPI fixture.
|
||||
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.
|
||||
- `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, thumbnail/body byte layout, body summary, 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` | 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` and `pano_cli inspect-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` | 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
|
||||
|
||||
@@ -313,8 +313,8 @@ 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/body-summary
|
||||
recognition, dirty-face PNG payload metadata validation, and a pure typed
|
||||
settings document model, with
|
||||
recognition, layer/frame indexing, dirty-face PNG payload metadata validation,
|
||||
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
|
||||
@@ -330,7 +330,7 @@ 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, and dirty-face PNG payload metadata, and
|
||||
body summary, layer/frame descriptors, and dirty-face PNG payload metadata, 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`
|
||||
@@ -570,7 +570,8 @@ Results:
|
||||
- `pp_assets_image_format_tests` passed.
|
||||
- `pp_assets_image_metadata_tests` passed.
|
||||
- `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout,
|
||||
body summary validation, and dirty-face PNG payload metadata validation.
|
||||
body summary validation, layer/frame indexing, and dirty-face PNG payload
|
||||
metadata validation.
|
||||
- `pp_assets_settings_document_tests` passed.
|
||||
- `pp_paint_brush_tests` passed.
|
||||
- `pp_paint_blend_tests` passed.
|
||||
@@ -591,8 +592,8 @@ 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, body summary, and dirty-face PNG payload metadata
|
||||
JSON.
|
||||
thumbnail/body byte layout, body summary, layer/frame descriptors, and
|
||||
dirty-face PNG payload metadata JSON.
|
||||
- `pano_cli_parse_layout_smoke` passed.
|
||||
- `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke
|
||||
sample counts/distances.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <bit>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
@@ -78,26 +79,28 @@ namespace {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_face_png_payload(
|
||||
[[nodiscard]] pp::foundation::Result<ImageMetadata> validate_face_png_payload(
|
||||
std::span<const std::byte> payload,
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) noexcept
|
||||
{
|
||||
const auto metadata = parse_png_metadata(payload);
|
||||
if (!metadata) {
|
||||
return metadata.status();
|
||||
return pp::foundation::Result<ImageMetadata>::failure(metadata.status());
|
||||
}
|
||||
|
||||
if (metadata.value().width != width || metadata.value().height != height) {
|
||||
return pp::foundation::Status::invalid_argument("PPI face PNG dimensions do not match the dirty box");
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI face PNG dimensions do not match the dirty box"));
|
||||
}
|
||||
|
||||
if (metadata.value().bit_depth != 8U || metadata.value().components != 4U
|
||||
|| metadata.value().color_type != ImageColorType::rgba) {
|
||||
return pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA");
|
||||
return pp::foundation::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA"));
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
return pp::foundation::Result<ImageMetadata>::success(metadata.value());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -221,10 +224,18 @@ pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(std::span<cons
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
namespace {
|
||||
|
||||
pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body) noexcept
|
||||
std::span<const std::byte> body,
|
||||
PpiBodyIndex* index) noexcept
|
||||
{
|
||||
if (index != nullptr) {
|
||||
index->summary = {};
|
||||
index->layers.clear();
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -251,6 +262,12 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
.declared_frame_count = 1,
|
||||
};
|
||||
|
||||
std::vector<bool> seen_orders;
|
||||
if (index != nullptr) {
|
||||
index->layers.resize(summary.layer_count);
|
||||
seen_orders.assign(summary.layer_count, false);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -278,6 +295,14 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
pp::foundation::Status::out_of_range("PPI layer order is outside the layer list"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
if (seen_orders[order.value()]) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer order is duplicated"));
|
||||
}
|
||||
seen_orders[order.value()] = true;
|
||||
}
|
||||
|
||||
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"));
|
||||
@@ -288,9 +313,19 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
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);
|
||||
const auto name_bytes = reader.read_bytes(name_length.value());
|
||||
if (!name_bytes) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(name_bytes.status());
|
||||
}
|
||||
|
||||
PpiLayerSummary layer_summary;
|
||||
if (index != nullptr) {
|
||||
layer_summary.stored_order = order.value();
|
||||
layer_summary.opacity = opacity.value();
|
||||
layer_summary.name.reserve(name_bytes.value().size());
|
||||
for (const auto byte : name_bytes.value()) {
|
||||
layer_summary.name.push_back(static_cast<char>(std::to_integer<unsigned char>(byte)));
|
||||
}
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 2U) {
|
||||
@@ -306,6 +341,12 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
layer_summary.blend_mode = blend_mode.value();
|
||||
layer_summary.alpha_locked = alpha_locked.value() != 0U;
|
||||
layer_summary.visible = visible.value() != 0U;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t layer_frames = 1;
|
||||
@@ -328,6 +369,10 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
}
|
||||
summary.total_layer_frames += layer_frames;
|
||||
|
||||
if (index != nullptr) {
|
||||
layer_summary.frames.resize(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");
|
||||
@@ -339,6 +384,10 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
layer_summary.frames[frame_index].duration_ms = duration.value();
|
||||
}
|
||||
}
|
||||
|
||||
for (std::uint32_t face = 0; face < 6U; ++face) {
|
||||
@@ -384,22 +433,40 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(byte_status);
|
||||
}
|
||||
|
||||
const auto payload_offset = reader.position();
|
||||
const auto payload = reader.read_bytes(data_size.value());
|
||||
if (!payload) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(payload.status());
|
||||
}
|
||||
|
||||
const auto png_status = validate_face_png_payload(
|
||||
const auto png_metadata = validate_face_png_payload(
|
||||
payload.value(),
|
||||
x1.value() - x0.value(),
|
||||
y1.value() - y0.value());
|
||||
if (!png_status.ok()) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(png_status);
|
||||
if (!png_metadata) {
|
||||
return pp::foundation::Result<PpiBodySummary>::failure(png_metadata.status());
|
||||
}
|
||||
|
||||
++summary.rgba_face_payload_count;
|
||||
if (index != nullptr) {
|
||||
layer_summary.frames[frame_index].faces[face] = PpiFacePayloadSummary {
|
||||
.has_data = true,
|
||||
.x0 = x0.value(),
|
||||
.y0 = y0.value(),
|
||||
.x1 = x1.value(),
|
||||
.y1 = y1.value(),
|
||||
.body_payload_offset = static_cast<std::uint32_t>(payload_offset),
|
||||
.payload_bytes = data_size.value(),
|
||||
.png_width = png_metadata.value().width,
|
||||
.png_height = png_metadata.value().height,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
index->layers[order.value()] = std::move(layer_summary);
|
||||
}
|
||||
}
|
||||
|
||||
if (header.document_version.minor >= 3U && summary.total_layer_frames != summary.declared_frame_count) {
|
||||
@@ -425,9 +492,35 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
pp::foundation::Status::invalid_argument("PPI body has trailing bytes"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
index->summary = summary;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiBodySummary>::success(summary);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body) noexcept
|
||||
{
|
||||
return parse_ppi_body_impl(header, body, nullptr);
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body)
|
||||
{
|
||||
PpiBodyIndex index;
|
||||
const auto summary = parse_ppi_body_impl(header, body, &index);
|
||||
if (!summary) {
|
||||
return pp::foundation::Result<PpiBodyIndex>::failure(summary.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiBodyIndex>::success(std::move(index));
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
@@ -448,4 +541,24 @@ pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(std::span<co
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(std::span<const std::byte> bytes)
|
||||
{
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
if (!layout) {
|
||||
return pp::foundation::Result<PpiProjectIndex>::failure(layout.status());
|
||||
}
|
||||
|
||||
const auto body = parse_ppi_body_index(
|
||||
layout.value().header,
|
||||
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
|
||||
if (!body) {
|
||||
return pp::foundation::Result<PpiProjectIndex>::failure(body.status());
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiProjectIndex>::success(PpiProjectIndex {
|
||||
.layout = layout.value(),
|
||||
.body = body.value(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
@@ -64,6 +67,43 @@ struct PpiProjectSummary {
|
||||
PpiBodySummary body;
|
||||
};
|
||||
|
||||
struct PpiFacePayloadSummary {
|
||||
bool has_data = false;
|
||||
std::uint32_t x0 = 0;
|
||||
std::uint32_t y0 = 0;
|
||||
std::uint32_t x1 = 0;
|
||||
std::uint32_t y1 = 0;
|
||||
std::uint32_t body_payload_offset = 0;
|
||||
std::uint32_t payload_bytes = 0;
|
||||
std::uint32_t png_width = 0;
|
||||
std::uint32_t png_height = 0;
|
||||
};
|
||||
|
||||
struct PpiFrameSummary {
|
||||
std::uint32_t duration_ms = 100;
|
||||
std::array<PpiFacePayloadSummary, 6> faces;
|
||||
};
|
||||
|
||||
struct PpiLayerSummary {
|
||||
std::uint32_t stored_order = 0;
|
||||
std::string name;
|
||||
float opacity = 1.0F;
|
||||
std::uint32_t blend_mode = 0;
|
||||
bool alpha_locked = false;
|
||||
bool visible = true;
|
||||
std::vector<PpiFrameSummary> frames;
|
||||
};
|
||||
|
||||
struct PpiBodyIndex {
|
||||
PpiBodySummary summary;
|
||||
std::vector<PpiLayerSummary> layers;
|
||||
};
|
||||
|
||||
struct PpiProjectIndex {
|
||||
PpiProjectLayout layout;
|
||||
PpiBodyIndex body;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
@@ -76,7 +116,14 @@ struct PpiProjectSummary {
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
|
||||
PpiHeaderInfo header,
|
||||
std::span<const std::byte> body);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
}
|
||||
|
||||
@@ -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\":73,\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"dirtyFaces\":0,\"rgbaFacePayloads\":0,\"compressedBytes\":0,\"infoBytes\":0\\}")
|
||||
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,\"rgbaFacePayloads\":0,\"compressedBytes\":0,\"infoBytes\":0\\}.*\"layers\":\\[\\{\"index\":0,\"storedOrder\":0,\"name\":\"Ink\"")
|
||||
|
||||
add_test(NAME pano_cli_parse_layout_smoke
|
||||
COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml")
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
|
||||
using pp::assets::parse_ppi_header;
|
||||
using pp::assets::parse_ppi_project_index;
|
||||
using pp::assets::parse_ppi_project_summary;
|
||||
using pp::assets::parse_ppi_project_layout;
|
||||
using pp::assets::ppi_header_size;
|
||||
@@ -257,6 +258,26 @@ void parses_minimal_project_body_summary(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, summary.value().body.info_bytes == 0U);
|
||||
}
|
||||
|
||||
void indexes_project_layers_frames_and_faces(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
||||
const auto index = parse_ppi_project_index(project);
|
||||
|
||||
PP_EXPECT(h, index.ok());
|
||||
PP_EXPECT(h, index.value().body.layers.size() == 1U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].stored_order == 0U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].name == "Ink");
|
||||
PP_EXPECT(h, index.value().body.layers[0].visible);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames.size() == 1U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 100U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].has_data);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x0 == 2U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x1 == 10U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].payload_bytes == 33U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].png_width == 8U);
|
||||
PP_EXPECT(h, !index.value().body.layers[0].frames[0].faces[1].has_data);
|
||||
}
|
||||
|
||||
void validates_dirty_face_png_payload_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
||||
@@ -322,6 +343,7 @@ int main()
|
||||
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("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("rejects_invalid_dirty_face_png_payloads", rejects_invalid_dirty_face_png_payloads);
|
||||
harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries);
|
||||
|
||||
@@ -57,6 +57,48 @@ void print_error(std::string_view command, std::string_view message)
|
||||
<< "\",\"error\":\"" << message << "\"}\n";
|
||||
}
|
||||
|
||||
std::string json_escape(std::string_view value)
|
||||
{
|
||||
constexpr char hex[] = "0123456789abcdef";
|
||||
std::string escaped;
|
||||
escaped.reserve(value.size());
|
||||
for (const unsigned char ch : value) {
|
||||
switch (ch) {
|
||||
case '"':
|
||||
escaped += "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
escaped += "\\\\";
|
||||
break;
|
||||
case '\b':
|
||||
escaped += "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
escaped += "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
escaped += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escaped += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escaped += "\\t";
|
||||
break;
|
||||
default:
|
||||
if (ch < 0x20U) {
|
||||
escaped += "\\u00";
|
||||
escaped.push_back(hex[(ch >> 4U) & 0x0fU]);
|
||||
escaped.push_back(hex[ch & 0x0fU]);
|
||||
} else {
|
||||
escaped.push_back(static_cast<char>(ch));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
void print_help()
|
||||
{
|
||||
std::cout
|
||||
@@ -287,34 +329,78 @@ int inspect_project(int argc, char** argv)
|
||||
std::istreambuf_iterator<char>()
|
||||
};
|
||||
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
|
||||
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);
|
||||
const auto project = pp::assets::parse_ppi_project_index(std::span<const std::byte>(data, chars.size()));
|
||||
if (!project) {
|
||||
print_error("inspect-project", project.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::cout << "{\"ok\":true,\"command\":\"inspect-project\""
|
||||
<< ",\"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
|
||||
<< ",\"rgbaFacePayloads\":" << summary.value().body.rgba_face_payload_count
|
||||
<< ",\"compressedBytes\":" << summary.value().body.compressed_face_bytes
|
||||
<< ",\"infoBytes\":" << summary.value().body.info_bytes
|
||||
<< "}}\n";
|
||||
<< ",\"documentVersion\":\"" << project.value().layout.header.document_version.major
|
||||
<< "." << project.value().layout.header.document_version.minor << "\""
|
||||
<< ",\"softwareVersion\":\"" << project.value().layout.header.software_version.major
|
||||
<< "." << project.value().layout.header.software_version.minor
|
||||
<< "." << project.value().layout.header.software_version.fix
|
||||
<< "." << project.value().layout.header.software_version.build << "\""
|
||||
<< ",\"thumbnail\":{\"width\":" << project.value().layout.header.thumbnail.width
|
||||
<< ",\"height\":" << project.value().layout.header.thumbnail.height
|
||||
<< ",\"components\":" << project.value().layout.header.thumbnail.components
|
||||
<< ",\"bytes\":" << project.value().layout.thumbnail_bytes
|
||||
<< "},\"body\":{\"offset\":" << project.value().layout.body_offset
|
||||
<< ",\"bytes\":" << project.value().layout.body_bytes
|
||||
<< ",\"width\":" << project.value().body.summary.width
|
||||
<< ",\"height\":" << project.value().body.summary.height
|
||||
<< ",\"layers\":" << project.value().body.summary.layer_count
|
||||
<< ",\"frames\":" << project.value().body.summary.declared_frame_count
|
||||
<< ",\"dirtyFaces\":" << project.value().body.summary.dirty_face_count
|
||||
<< ",\"rgbaFacePayloads\":" << project.value().body.summary.rgba_face_payload_count
|
||||
<< ",\"compressedBytes\":" << project.value().body.summary.compressed_face_bytes
|
||||
<< ",\"infoBytes\":" << project.value().body.summary.info_bytes
|
||||
<< "},\"layers\":[";
|
||||
for (std::size_t layer_index = 0; layer_index < project.value().body.layers.size(); ++layer_index) {
|
||||
const auto& layer = project.value().body.layers[layer_index];
|
||||
if (layer_index != 0U) {
|
||||
std::cout << ",";
|
||||
}
|
||||
std::cout << "{\"index\":" << layer_index
|
||||
<< ",\"storedOrder\":" << layer.stored_order
|
||||
<< ",\"name\":\"" << json_escape(layer.name) << "\""
|
||||
<< ",\"opacity\":" << layer.opacity
|
||||
<< ",\"blendMode\":" << layer.blend_mode
|
||||
<< ",\"alphaLocked\":" << (layer.alpha_locked ? "true" : "false")
|
||||
<< ",\"visible\":" << (layer.visible ? "true" : "false")
|
||||
<< ",\"frames\":[";
|
||||
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
|
||||
const auto& frame = layer.frames[frame_index];
|
||||
if (frame_index != 0U) {
|
||||
std::cout << ",";
|
||||
}
|
||||
std::cout << "{\"index\":" << frame_index
|
||||
<< ",\"durationMs\":" << frame.duration_ms
|
||||
<< ",\"dirtyFaces\":[";
|
||||
bool first_face = true;
|
||||
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 (!first_face) {
|
||||
std::cout << ",";
|
||||
}
|
||||
first_face = false;
|
||||
std::cout << "{\"face\":" << face_index
|
||||
<< ",\"box\":[" << face.x0 << "," << face.y0 << "," << face.x1 << "," << face.y1 << "]"
|
||||
<< ",\"bodyPayloadOffset\":" << face.body_payload_offset
|
||||
<< ",\"bytes\":" << face.payload_bytes
|
||||
<< ",\"png\":{\"width\":" << face.png_width
|
||||
<< ",\"height\":" << face.png_height
|
||||
<< "}}";
|
||||
}
|
||||
std::cout << "]}";
|
||||
}
|
||||
std::cout << "]}";
|
||||
}
|
||||
std::cout << "]}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user