Attach PPI pixels to documents
This commit is contained in:
@@ -135,13 +135,15 @@ target_link_libraries(pp_paint
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_document STATIC
|
||||
src/document/document.cpp)
|
||||
src/document/document.cpp
|
||||
src/document/ppi_import.cpp)
|
||||
target_include_directories(pp_document
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(pp_document
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
pp_assets
|
||||
pp_paint
|
||||
pp_project_options
|
||||
PRIVATE
|
||||
|
||||
@@ -92,7 +92,7 @@ Known local toolchain state:
|
||||
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
|
||||
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
|
||||
snapshot/per-layer-frame/move/duration/face-pixel 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
|
||||
@@ -103,8 +103,12 @@ Known local toolchain state:
|
||||
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
|
||||
- `pp_document_ppi_import_tests` attaches decoded PPI dirty-face payloads to
|
||||
`pp_document` layer/frame storage and rejects payloads outside document
|
||||
layers.
|
||||
- `pano_cli load-project` creates a `pp_document` projection with per-layer
|
||||
frame counts, durations, and decoded face-pixel payloads when present; the
|
||||
metadata-only minimal fixture remains covered by
|
||||
`pano_cli_load_project_metadata_smoke`.
|
||||
- `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and
|
||||
is covered by `pano_cli_create_animation_document_smoke`.
|
||||
|
||||
@@ -28,10 +28,10 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions |
|
||||
| DEBT-0008 | Open | Modernization | `windows-msvc-default` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset |
|
||||
| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands |
|
||||
| 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-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, and renderer-free RGBA8 face payload storage, but it is not yet wired to legacy `Canvas`, selection masks, save, 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`; `pp_document_ppi_import_tests` | 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, 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-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, and `pano_cli load-project` validate the fixed PPI header, thumbnail/body byte layout, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, and decoded pixel attachment to `pp_document`, but full PPI save/round-trip fixtures are not yet extracted | 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`; `pp_document_ppi_import_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
|
||||
|
||||
@@ -323,7 +323,8 @@ stroke spacing/interpolation plus a pure text stroke-script parser.
|
||||
`pp_document` has
|
||||
started with a pure canvas/layer/frame model, alpha-lock metadata, snapshot
|
||||
construction, per-layer frame metadata, layer metadata operations, frame
|
||||
move/duration queries, and layer/frame/undo-redo history invariant tests.
|
||||
move/duration queries, renderer-free RGBA8 cube-face payload storage, PPI image
|
||||
import, and layer/frame/undo-redo history invariant tests.
|
||||
`pp_renderer_api` has started with renderer-neutral
|
||||
texture/readback descriptors and validation tests. `pp_paint_renderer` has
|
||||
started with deterministic CPU layer compositing over renderer extents using
|
||||
@@ -334,8 +335,9 @@ input tests.
|
||||
`pano_cli inspect-project` reports validated PPI thumbnail/body byte layout,
|
||||
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 load-project` creates a `pp_document` projection with per-layer frame
|
||||
counts, durations, and decoded face-pixel payload attachment when PPI image
|
||||
payloads are present.
|
||||
`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`
|
||||
@@ -548,7 +550,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_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
|
||||
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_document_ppi_import_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
|
||||
@@ -585,8 +587,11 @@ Results:
|
||||
- `pp_paint_stroke_tests` passed.
|
||||
- `pp_paint_stroke_script_tests` passed.
|
||||
- `pp_document_tests` passed, including snapshot construction, alpha-lock
|
||||
metadata, per-layer frame metadata, frame move, duration, and history
|
||||
invariants.
|
||||
metadata, per-layer frame metadata, frame move, duration, face-pixel payload
|
||||
storage/replacement/rejection, and history invariants.
|
||||
- `pp_document_ppi_import_tests` passed, including decoded PPI dirty-face
|
||||
payload attachment to `pp_document` layer/frame storage and out-of-range
|
||||
payload rejection.
|
||||
- `pp_renderer_api_tests` passed, including shader descriptor validation.
|
||||
- `pp_paint_renderer_compositor_tests` passed.
|
||||
- `pp_ui_core_color_tests` passed.
|
||||
@@ -602,9 +607,9 @@ Results:
|
||||
- `pano_cli_inspect_project_layout_smoke` passed and reports PPI
|
||||
thumbnail/body byte layout, body summary, layer/frame descriptors, and
|
||||
dirty-face PNG payload metadata JSON.
|
||||
- `pano_cli_load_project_metadata_smoke` passed and reports a metadata-only
|
||||
`pp_document` projection with per-layer frame counts and durations for the
|
||||
minimal PPI fixture.
|
||||
- `pano_cli_load_project_metadata_smoke` passed and reports a `pp_document`
|
||||
projection with per-layer frame counts, durations, and zero loaded face
|
||||
payloads for the minimal PPI fixture.
|
||||
- `pano_cli_parse_layout_smoke` passed.
|
||||
- `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke
|
||||
sample counts/distances.
|
||||
|
||||
@@ -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_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")
|
||||
[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_document_ppi_import_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_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}"
|
||||
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_document_ppi_import_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"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
@@ -105,6 +107,67 @@ namespace {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) noexcept
|
||||
{
|
||||
const auto width64 = static_cast<std::uint64_t>(width);
|
||||
const auto height64 = static_cast<std::uint64_t>(height);
|
||||
if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("face pixel dimensions overflow"));
|
||||
}
|
||||
|
||||
const auto pixels = width64 * height64;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / rgba8_components) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("face pixel byte size overflows"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * rgba8_components;
|
||||
if (bytes > max_face_pixel_payload_bytes) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("face pixel payload exceeds the configured limit"));
|
||||
}
|
||||
|
||||
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("face pixel payload exceeds addressable memory"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_face_pixels(
|
||||
LayerFacePixels pixels,
|
||||
std::uint32_t document_width,
|
||||
std::uint32_t document_height) noexcept
|
||||
{
|
||||
if (pixels.face_index >= cube_face_count) {
|
||||
return pp::foundation::Status::out_of_range("cube face index is outside the document");
|
||||
}
|
||||
|
||||
if (pixels.width == 0 || pixels.height == 0) {
|
||||
return pp::foundation::Status::invalid_argument("face pixel dimensions must be greater than zero");
|
||||
}
|
||||
|
||||
if (pixels.x > document_width || pixels.width > document_width - pixels.x
|
||||
|| pixels.y > document_height || pixels.height > document_height - pixels.y) {
|
||||
return pp::foundation::Status::out_of_range("face pixel rectangle is outside the document");
|
||||
}
|
||||
|
||||
const auto expected_bytes = rgba8_byte_size(pixels.width, pixels.height);
|
||||
if (!expected_bytes) {
|
||||
return expected_bytes.status();
|
||||
}
|
||||
|
||||
if (pixels.rgba8.size() != expected_bytes.value()) {
|
||||
return pp::foundation::Status::invalid_argument("face pixel payload byte size does not match dimensions");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
|
||||
@@ -251,6 +314,17 @@ pp::foundation::Result<std::uint64_t> CanvasDocument::layer_animation_duration_m
|
||||
return pp::foundation::Result<std::uint64_t>::success(frame_duration_sum(layers_[index].frames));
|
||||
}
|
||||
|
||||
std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const auto& layer : layers_) {
|
||||
for (const auto& frame : layer.frames) {
|
||||
count += frame.face_pixels.size();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::span<const Layer> CanvasDocument::layers() const noexcept
|
||||
{
|
||||
return layers_;
|
||||
@@ -423,9 +497,9 @@ pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t dura
|
||||
duration_status);
|
||||
}
|
||||
|
||||
frames_.push_back(AnimationFrame { .duration_ms = duration_ms });
|
||||
frames_.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
|
||||
for (auto& layer : layers_) {
|
||||
layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms });
|
||||
layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
|
||||
}
|
||||
active_frame_index_ = frames_.size() - 1U;
|
||||
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
||||
@@ -547,6 +621,41 @@ pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexc
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
|
||||
std::size_t layer_index,
|
||||
std::size_t frame_index,
|
||||
LayerFacePixels pixels)
|
||||
{
|
||||
const auto layer_status = validate_layer_index(layer_index, layers_.size());
|
||||
if (!layer_status.ok()) {
|
||||
return layer_status;
|
||||
}
|
||||
|
||||
const auto frame_status = validate_frame_index(frame_index, layers_[layer_index].frames.size());
|
||||
if (!frame_status.ok()) {
|
||||
return frame_status;
|
||||
}
|
||||
|
||||
const auto pixels_status = validate_face_pixels(pixels, width_, height_);
|
||||
if (!pixels_status.ok()) {
|
||||
return pixels_status;
|
||||
}
|
||||
|
||||
auto& faces = layers_[layer_index].frames[frame_index].face_pixels;
|
||||
const auto existing = std::find_if(
|
||||
faces.begin(),
|
||||
faces.end(),
|
||||
[face_index = pixels.face_index](const LayerFacePixels& face) {
|
||||
return face.face_index == face_index;
|
||||
});
|
||||
if (existing == faces.end()) {
|
||||
faces.push_back(std::move(pixels));
|
||||
} else {
|
||||
*existing = std::move(pixels);
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
|
||||
CanvasDocument initial_document,
|
||||
std::size_t max_entries)
|
||||
|
||||
@@ -18,6 +18,9 @@ constexpr std::uint32_t min_frame_duration_ms = 1;
|
||||
constexpr std::size_t min_document_history_entries = 2;
|
||||
constexpr std::size_t max_document_history_entries = 10000;
|
||||
constexpr std::size_t max_layer_name_length = 128;
|
||||
constexpr std::uint32_t cube_face_count = 6;
|
||||
constexpr std::uint32_t rgba8_components = 4;
|
||||
constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
|
||||
|
||||
struct DocumentConfig {
|
||||
std::uint32_t width = 0;
|
||||
@@ -25,8 +28,18 @@ struct DocumentConfig {
|
||||
std::uint32_t layer_count = 1;
|
||||
};
|
||||
|
||||
struct LayerFacePixels {
|
||||
std::uint32_t face_index = 0;
|
||||
std::uint32_t x = 0;
|
||||
std::uint32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> rgba8;
|
||||
};
|
||||
|
||||
struct AnimationFrame {
|
||||
std::uint32_t duration_ms = 100;
|
||||
std::vector<LayerFacePixels> face_pixels;
|
||||
};
|
||||
|
||||
struct Layer {
|
||||
@@ -65,6 +78,7 @@ public:
|
||||
[[nodiscard]] std::size_t active_frame_index() const noexcept;
|
||||
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> layer_animation_duration_ms(std::size_t index) const noexcept;
|
||||
[[nodiscard]] std::size_t face_pixel_payload_count() const noexcept;
|
||||
[[nodiscard]] std::span<const Layer> layers() const noexcept;
|
||||
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
|
||||
|
||||
@@ -84,6 +98,10 @@ public:
|
||||
[[nodiscard]] pp::foundation::Status move_frame(std::size_t from, std::size_t to);
|
||||
[[nodiscard]] pp::foundation::Status set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_active_frame(std::size_t index) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_frame_face_pixels(
|
||||
std::size_t layer_index,
|
||||
std::size_t frame_index,
|
||||
LayerFacePixels pixels);
|
||||
|
||||
private:
|
||||
std::uint32_t width_ = 0;
|
||||
|
||||
116
src/document/ppi_import.cpp
Normal file
116
src/document/ppi_import.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "document/ppi_import.h"
|
||||
|
||||
#include <utility>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<pp::paint::BlendMode> ppi_layer_blend_mode(
|
||||
std::uint32_t blend_mode) noexcept
|
||||
{
|
||||
switch (blend_mode) {
|
||||
case 0:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::normal);
|
||||
case 1:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::multiply);
|
||||
case 2:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::screen);
|
||||
case 3:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::color_dodge);
|
||||
case 4:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::overlay);
|
||||
default:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document"));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<CanvasDocument> document_from_ppi_index(
|
||||
const pp::assets::PpiProjectIndex& project)
|
||||
{
|
||||
if (project.body.layers.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI project has no layers"));
|
||||
}
|
||||
|
||||
const auto& reference_frames = project.body.layers.front().frames;
|
||||
if (reference_frames.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI project has no frames"));
|
||||
}
|
||||
|
||||
std::vector<AnimationFrame> frames;
|
||||
frames.reserve(reference_frames.size());
|
||||
for (const auto& frame : reference_frames) {
|
||||
frames.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
|
||||
}
|
||||
|
||||
std::vector<std::vector<AnimationFrame>> layer_frames;
|
||||
layer_frames.reserve(project.body.layers.size());
|
||||
std::vector<DocumentLayerConfig> layers;
|
||||
layers.reserve(project.body.layers.size());
|
||||
for (const auto& layer : project.body.layers) {
|
||||
const auto blend_mode = ppi_layer_blend_mode(layer.blend_mode);
|
||||
if (!blend_mode) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(blend_mode.status());
|
||||
}
|
||||
|
||||
auto& frame_list = layer_frames.emplace_back();
|
||||
frame_list.reserve(layer.frames.size());
|
||||
for (const auto& frame : layer.frames) {
|
||||
frame_list.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
|
||||
}
|
||||
|
||||
layers.push_back(DocumentLayerConfig {
|
||||
.name = layer.name,
|
||||
.visible = layer.visible,
|
||||
.alpha_locked = layer.alpha_locked,
|
||||
.opacity = layer.opacity,
|
||||
.blend_mode = blend_mode.value(),
|
||||
.frames = std::span<const AnimationFrame>(frame_list.data(), frame_list.size()),
|
||||
});
|
||||
}
|
||||
|
||||
return CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = project.body.summary.width,
|
||||
.height = project.body.summary.height,
|
||||
.layers = layers,
|
||||
.frames = frames,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<CanvasDocument> import_ppi_project_document(
|
||||
const pp::assets::PpiDecodedProjectImages& project)
|
||||
{
|
||||
auto document = document_from_ppi_index(project.project);
|
||||
if (!document) {
|
||||
return document;
|
||||
}
|
||||
|
||||
auto value = document.value();
|
||||
for (const auto& face : project.faces) {
|
||||
const auto status = value.set_layer_frame_face_pixels(
|
||||
face.layer_index,
|
||||
face.frame_index,
|
||||
LayerFacePixels {
|
||||
.face_index = face.face_index,
|
||||
.x = face.descriptor.x0,
|
||||
.y = face.descriptor.y0,
|
||||
.width = face.image.width,
|
||||
.height = face.image.height,
|
||||
.rgba8 = face.image.pixels,
|
||||
});
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasDocument>::success(std::move(value));
|
||||
}
|
||||
|
||||
}
|
||||
11
src/document/ppi_import.h
Normal file
11
src/document/ppi_import.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "assets/ppi_header.h"
|
||||
#include "document/document.h"
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<CanvasDocument> import_ppi_project_document(
|
||||
const pp::assets::PpiDecodedProjectImages& project);
|
||||
|
||||
}
|
||||
@@ -166,6 +166,16 @@ add_test(NAME pp_document_tests COMMAND pp_document_tests)
|
||||
set_tests_properties(pp_document_tests PROPERTIES
|
||||
LABELS "document;desktop-fast")
|
||||
|
||||
add_executable(pp_document_ppi_import_tests
|
||||
document/ppi_import_tests.cpp)
|
||||
target_link_libraries(pp_document_ppi_import_tests PRIVATE
|
||||
pp_document
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_document_ppi_import_tests COMMAND pp_document_ppi_import_tests)
|
||||
set_tests_properties(pp_document_ppi_import_tests PROPERTIES
|
||||
LABELS "assets;document;integration;desktop-fast")
|
||||
|
||||
add_executable(pp_renderer_api_tests
|
||||
renderer_api/renderer_api_tests.cpp)
|
||||
target_link_libraries(pp_renderer_api_tests PRIVATE
|
||||
@@ -250,7 +260,7 @@ if(TARGET pano_cli)
|
||||
COMMAND pano_cli load-project --path "${CMAKE_CURRENT_SOURCE_DIR}/data/projects/minimal-project.ppi")
|
||||
set_tests_properties(pano_cli_load_project_metadata_smoke PROPERTIES
|
||||
LABELS "assets;document;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"load-project\".*\"pixelDataLoaded\":false.*\"document\":\\{\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"animationDurationMs\":100,\"layerNames\":\\[\"Ink\"\\],\"layerFrameCounts\":\\[1\\],\"layerDurationsMs\":\\[100\\]")
|
||||
PASS_REGULAR_EXPRESSION "\"command\":\"load-project\".*\"pixelDataLoaded\":false.*\"facePayloads\":0.*\"document\":\\{\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"animationDurationMs\":100,\"layerNames\":\\[\"Ink\"\\],\"layerFrameCounts\":\\[1\\],\"layerDurationsMs\":\\[100\\]")
|
||||
|
||||
add_test(NAME pano_cli_parse_layout_smoke
|
||||
COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml")
|
||||
|
||||
@@ -11,6 +11,7 @@ using pp::document::DocumentConfig;
|
||||
using pp::document::DocumentLayerConfig;
|
||||
using pp::document::DocumentSnapshotConfig;
|
||||
using pp::document::AnimationFrame;
|
||||
using pp::document::LayerFacePixels;
|
||||
using pp::document::max_document_history_entries;
|
||||
using pp::document::max_canvas_dimension;
|
||||
using pp::document::max_frame_count;
|
||||
@@ -186,8 +187,8 @@ void creates_document_from_snapshot_metadata(pp::tests::Harness& h)
|
||||
},
|
||||
};
|
||||
const AnimationFrame frames[] {
|
||||
{ .duration_ms = 100 },
|
||||
{ .duration_ms = 250 },
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
{ .duration_ms = 250, .face_pixels = {} },
|
||||
};
|
||||
|
||||
const auto document_result = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
@@ -212,14 +213,14 @@ void creates_document_from_snapshot_metadata(pp::tests::Harness& h)
|
||||
void preserves_per_layer_snapshot_timelines(pp::tests::Harness& h)
|
||||
{
|
||||
const AnimationFrame project_frames[] {
|
||||
{ .duration_ms = 100 },
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
};
|
||||
const AnimationFrame short_layer_frames[] {
|
||||
{ .duration_ms = 100 },
|
||||
{ .duration_ms = 150 },
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
{ .duration_ms = 150, .face_pixels = {} },
|
||||
};
|
||||
const AnimationFrame long_layer_frames[] {
|
||||
{ .duration_ms = 500 },
|
||||
{ .duration_ms = 500, .face_pixels = {} },
|
||||
};
|
||||
const DocumentLayerConfig layers[] {
|
||||
{
|
||||
@@ -254,8 +255,8 @@ void preserves_per_layer_snapshot_timelines(pp::tests::Harness& h)
|
||||
void rejects_invalid_snapshot_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const DocumentLayerConfig layers[] { { .name = "Ink", .frames = {} } };
|
||||
const AnimationFrame frames[] { { .duration_ms = 100 } };
|
||||
const AnimationFrame bad_frames[] { { .duration_ms = 0 } };
|
||||
const AnimationFrame frames[] { { .duration_ms = 100, .face_pixels = {} } };
|
||||
const AnimationFrame bad_frames[] { { .duration_ms = 0, .face_pixels = {} } };
|
||||
const DocumentLayerConfig bad_layers[] { { .name = "", .frames = {} } };
|
||||
const DocumentLayerConfig bad_layer_frames[] { { .name = "Ink", .frames = bad_frames } };
|
||||
|
||||
@@ -393,6 +394,115 @@ void rejects_invalid_animation_frame_operations(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, max_frame_count > document.frames().size());
|
||||
}
|
||||
|
||||
void attaches_layer_frame_face_pixels(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto status = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels {
|
||||
.face_index = 2,
|
||||
.x = 3,
|
||||
.y = 4,
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
.rgba8 = { 10, 20, 30, 40 },
|
||||
});
|
||||
|
||||
PP_EXPECT(h, status.ok());
|
||||
PP_EXPECT(h, document.face_pixel_payload_count() == 1U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels.size() == 1U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].face_index == 2U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].x == 3U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].rgba8[3] == 40U);
|
||||
}
|
||||
|
||||
void replaces_existing_face_pixel_payload(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
PP_EXPECT(h, document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels {
|
||||
.face_index = 1,
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
.rgba8 = { 1, 2, 3, 4 },
|
||||
}).ok());
|
||||
PP_EXPECT(h, document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels {
|
||||
.face_index = 1,
|
||||
.x = 2,
|
||||
.y = 3,
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
.rgba8 = { 5, 6, 7, 8 },
|
||||
}).ok());
|
||||
|
||||
PP_EXPECT(h, document.face_pixel_payload_count() == 1U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].x == 2U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].rgba8[0] == 5U);
|
||||
}
|
||||
|
||||
void rejects_invalid_face_pixel_payloads(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
auto document = document_result.value();
|
||||
|
||||
const auto missing_layer = document.set_layer_frame_face_pixels(
|
||||
9,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
|
||||
const auto missing_frame = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
9,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
|
||||
const auto bad_face = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 6, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
|
||||
const auto zero_width = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 0, .height = 1, .rgba8 = {} });
|
||||
const auto outside_bounds = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 63, .y = 0, .width = 2, .height = 1, .rgba8 = { 1, 2, 3, 4, 5, 6, 7, 8 } });
|
||||
const auto bad_byte_count = document.set_layer_frame_face_pixels(
|
||||
0,
|
||||
0,
|
||||
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3 } });
|
||||
|
||||
PP_EXPECT(h, !missing_layer.ok());
|
||||
PP_EXPECT(h, missing_layer.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_frame.ok());
|
||||
PP_EXPECT(h, missing_frame.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_face.ok());
|
||||
PP_EXPECT(h, bad_face.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !zero_width.ok());
|
||||
PP_EXPECT(h, zero_width.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !outside_bounds.ok());
|
||||
PP_EXPECT(h, outside_bounds.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_byte_count.ok());
|
||||
PP_EXPECT(h, bad_byte_count.code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, document.face_pixel_payload_count() == 0U);
|
||||
}
|
||||
|
||||
void records_document_history_and_restores_snapshots(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
@@ -521,6 +631,9 @@ int main()
|
||||
harness.run("manages_animation_frames_and_duration", manages_animation_frames_and_duration);
|
||||
harness.run("moves_frames_and_preserves_active_frame_identity", moves_frames_and_preserves_active_frame_identity);
|
||||
harness.run("rejects_invalid_animation_frame_operations", rejects_invalid_animation_frame_operations);
|
||||
harness.run("attaches_layer_frame_face_pixels", attaches_layer_frame_face_pixels);
|
||||
harness.run("replaces_existing_face_pixel_payload", replaces_existing_face_pixel_payload);
|
||||
harness.run("rejects_invalid_face_pixel_payloads", rejects_invalid_face_pixel_payloads);
|
||||
harness.run("records_document_history_and_restores_snapshots", records_document_history_and_restores_snapshots);
|
||||
harness.run("applying_after_undo_discards_redo_branch", applying_after_undo_discards_redo_branch);
|
||||
harness.run("bounds_document_history_capacity", bounds_document_history_capacity);
|
||||
|
||||
148
tests/document/ppi_import_tests.cpp
Normal file
148
tests/document/ppi_import_tests.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "assets/ppi_header.h"
|
||||
#include "document/ppi_import.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using pp::assets::decode_ppi_project_images;
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::document::import_ppi_project_document;
|
||||
|
||||
namespace {
|
||||
|
||||
void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
|
||||
{
|
||||
bytes.push_back(static_cast<std::byte>(value & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
||||
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> 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> ppi_project_with_face_payload(std::vector<std::byte> payload)
|
||||
{
|
||||
std::vector<std::byte> bytes {
|
||||
std::byte { 'P' }, std::byte { 'P' }, std::byte { 'I' }, std::byte { 0 },
|
||||
};
|
||||
append_u32(bytes, 0);
|
||||
append_u32(bytes, 4);
|
||||
append_u32(bytes, 0);
|
||||
append_u32(bytes, 2);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 1024);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 4);
|
||||
bytes.resize(pp::assets::ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
||||
|
||||
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);
|
||||
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, 2);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 4);
|
||||
append_u32(bytes, static_cast<std::uint32_t>(payload.size()));
|
||||
bytes.insert(bytes.end(), payload.begin(), payload.end());
|
||||
for (std::uint32_t i = 1; i < 6U; ++i) {
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
append_u32(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void imports_decoded_ppi_pixels_into_document(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project_bytes = ppi_project_with_face_payload(transparent_png_1x1());
|
||||
const auto decoded = decode_ppi_project_images(project_bytes);
|
||||
PP_EXPECT(h, decoded.ok());
|
||||
|
||||
const auto document = import_ppi_project_document(decoded.value());
|
||||
|
||||
PP_EXPECT(h, document.ok());
|
||||
PP_EXPECT(h, document.value().width() == 64U);
|
||||
PP_EXPECT(h, document.value().height() == 32U);
|
||||
PP_EXPECT(h, document.value().face_pixel_payload_count() == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels.size() == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].face_index == 0U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].x == 2U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].y == 3U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].width == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].height == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].rgba8.size() == 4U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].rgba8[3] == 0U);
|
||||
}
|
||||
|
||||
void rejects_decoded_payloads_outside_document_layers(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project_bytes = ppi_project_with_face_payload(transparent_png_1x1());
|
||||
const auto decoded = decode_ppi_project_images(project_bytes);
|
||||
PP_EXPECT(h, decoded.ok());
|
||||
auto decoded_value = decoded.value();
|
||||
decoded_value.faces[0].layer_index = 99;
|
||||
|
||||
const auto document = import_ppi_project_document(decoded_value);
|
||||
|
||||
PP_EXPECT(h, !document.ok());
|
||||
PP_EXPECT(h, document.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("imports_decoded_ppi_pixels_into_document", imports_decoded_ppi_pixels_into_document);
|
||||
harness.run("rejects_decoded_payloads_outside_document_layers", rejects_decoded_payloads_outside_document_layers);
|
||||
return harness.finish();
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "assets/image_metadata.h"
|
||||
#include "assets/ppi_header.h"
|
||||
#include "document/document.h"
|
||||
#include "document/ppi_import.h"
|
||||
#include "foundation/parse.h"
|
||||
#include "foundation/result.h"
|
||||
#include "paint/stroke.h"
|
||||
@@ -405,79 +406,6 @@ int inspect_project(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Result<pp::paint::BlendMode> ppi_layer_blend_mode(std::uint32_t blend_mode) noexcept
|
||||
{
|
||||
switch (blend_mode) {
|
||||
case 0:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::normal);
|
||||
case 1:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::multiply);
|
||||
case 2:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::screen);
|
||||
case 3:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::color_dodge);
|
||||
case 4:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::overlay);
|
||||
default:
|
||||
return pp::foundation::Result<pp::paint::BlendMode>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document"));
|
||||
}
|
||||
}
|
||||
|
||||
pp::foundation::Result<pp::document::CanvasDocument> document_from_ppi_index(
|
||||
const pp::assets::PpiProjectIndex& project)
|
||||
{
|
||||
if (project.body.layers.empty()) {
|
||||
return pp::foundation::Result<pp::document::CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI project has no layers"));
|
||||
}
|
||||
|
||||
const auto& reference_frames = project.body.layers.front().frames;
|
||||
if (reference_frames.empty()) {
|
||||
return pp::foundation::Result<pp::document::CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI project has no frames"));
|
||||
}
|
||||
|
||||
std::vector<pp::document::AnimationFrame> frames;
|
||||
frames.reserve(reference_frames.size());
|
||||
for (const auto& frame : reference_frames) {
|
||||
frames.push_back(pp::document::AnimationFrame { .duration_ms = frame.duration_ms });
|
||||
}
|
||||
|
||||
std::vector<std::vector<pp::document::AnimationFrame>> layer_frames;
|
||||
layer_frames.reserve(project.body.layers.size());
|
||||
std::vector<pp::document::DocumentLayerConfig> layers;
|
||||
layers.reserve(project.body.layers.size());
|
||||
for (const auto& layer : project.body.layers) {
|
||||
const auto blend_mode = ppi_layer_blend_mode(layer.blend_mode);
|
||||
if (!blend_mode) {
|
||||
return pp::foundation::Result<pp::document::CanvasDocument>::failure(blend_mode.status());
|
||||
}
|
||||
|
||||
auto& frame_list = layer_frames.emplace_back();
|
||||
frame_list.reserve(layer.frames.size());
|
||||
for (const auto& frame : layer.frames) {
|
||||
frame_list.push_back(pp::document::AnimationFrame { .duration_ms = frame.duration_ms });
|
||||
}
|
||||
|
||||
layers.push_back(pp::document::DocumentLayerConfig {
|
||||
.name = layer.name,
|
||||
.visible = layer.visible,
|
||||
.alpha_locked = layer.alpha_locked,
|
||||
.opacity = layer.opacity,
|
||||
.blend_mode = blend_mode.value(),
|
||||
.frames = std::span<const pp::document::AnimationFrame>(frame_list.data(), frame_list.size()),
|
||||
});
|
||||
}
|
||||
|
||||
return pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig {
|
||||
.width = project.body.summary.width,
|
||||
.height = project.body.summary.height,
|
||||
.layers = layers,
|
||||
.frames = frames,
|
||||
});
|
||||
}
|
||||
|
||||
int load_project(int argc, char** argv)
|
||||
{
|
||||
InspectProjectArgs args;
|
||||
@@ -498,13 +426,13 @@ int load_project(int argc, char** argv)
|
||||
std::istreambuf_iterator<char>()
|
||||
};
|
||||
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
|
||||
const auto project = pp::assets::parse_ppi_project_index(std::span<const std::byte>(data, chars.size()));
|
||||
const auto project = pp::assets::decode_ppi_project_images(std::span<const std::byte>(data, chars.size()));
|
||||
if (!project) {
|
||||
print_error("load-project", project.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto document_result = document_from_ppi_index(project.value());
|
||||
const auto document_result = pp::document::import_ppi_project_document(project.value());
|
||||
if (!document_result) {
|
||||
print_error("load-project", document_result.status().message);
|
||||
return 2;
|
||||
@@ -513,7 +441,8 @@ int load_project(int argc, char** argv)
|
||||
const auto& document = document_result.value();
|
||||
std::cout << "{\"ok\":true,\"command\":\"load-project\""
|
||||
<< ",\"source\":\"ppi\""
|
||||
<< ",\"pixelDataLoaded\":false"
|
||||
<< ",\"pixelDataLoaded\":" << (document.face_pixel_payload_count() > 0U ? "true" : "false")
|
||||
<< ",\"facePayloads\":" << document.face_pixel_payload_count()
|
||||
<< ",\"document\":{\"width\":" << document.width()
|
||||
<< ",\"height\":" << document.height()
|
||||
<< ",\"layers\":" << document.layers().size()
|
||||
|
||||
Reference in New Issue
Block a user