Compare commits
10 Commits
d0ef88be89
...
ad255a6ddf
| Author | SHA1 | Date | |
|---|---|---|---|
| ad255a6ddf | |||
| 88507df90e | |||
| 10e5d5b5ae | |||
| c16cab87bd | |||
| 7319cb9aa9 | |||
| 677d0b33a8 | |||
| f1ee1b28a1 | |||
| 2da247f0fb | |||
| 37854ea8b9 | |||
| dc252b2f24 |
@@ -98,11 +98,20 @@ target_link_libraries(pp_foundation
|
||||
add_library(pp_assets STATIC
|
||||
src/assets/image_format.cpp
|
||||
src/assets/image_metadata.cpp
|
||||
src/assets/image_pixels.cpp
|
||||
src/assets/ppi_header.cpp
|
||||
src/assets/settings_document.cpp)
|
||||
target_include_directories(pp_assets
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_include_directories(pp_assets
|
||||
SYSTEM PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/libs/stb")
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/assets/image_pixels.cpp
|
||||
PROPERTIES
|
||||
COMPILE_OPTIONS "/analyze-")
|
||||
endif()
|
||||
target_link_libraries(pp_assets
|
||||
PUBLIC
|
||||
pp_foundation
|
||||
@@ -113,7 +122,8 @@ target_link_libraries(pp_assets
|
||||
add_library(pp_paint STATIC
|
||||
src/paint/brush.cpp
|
||||
src/paint/blend.cpp
|
||||
src/paint/stroke.cpp)
|
||||
src/paint/stroke.cpp
|
||||
src/paint/stroke_script.cpp)
|
||||
target_include_directories(pp_paint
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
@@ -125,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
|
||||
|
||||
@@ -90,14 +90,32 @@ Known local toolchain state:
|
||||
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
|
||||
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
|
||||
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
|
||||
including foundation event/logging/task queue coverage, PNG metadata, PPI
|
||||
header, settings document, document frame move/duration coverage, paint
|
||||
brush/stroke coverage, renderer shader descriptor coverage, UI color
|
||||
parsing, and layout XML parse coverage.
|
||||
including foundation event/logging/task queue coverage, PNG metadata and
|
||||
decode, PPI header/layout, settings document, document
|
||||
snapshot/per-layer-frame/move/duration/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
|
||||
`pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture.
|
||||
- `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout,
|
||||
body summary fields, layer/frame descriptors, and dirty-face PNG payload
|
||||
metadata, and is covered by `pano_cli_inspect_project_layout_smoke` with a
|
||||
minimal PPI fixture.
|
||||
- `pp_assets_image_pixels_tests` decodes PNG payloads to RGBA8 and rejects
|
||||
corrupt image payloads.
|
||||
- `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`.
|
||||
- `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted
|
||||
automation and is covered by `pano_cli_simulate_stroke_smoke`.
|
||||
- `pano_cli simulate-stroke-script` loads a text stroke script fixture and is
|
||||
covered by `pano_cli_simulate_stroke_script_smoke`.
|
||||
- `panopainter_validate_shaders` validates the current combined GLSL shader
|
||||
files for one vertex stage marker, one fragment stage marker, valid marker
|
||||
order, and existing relative includes.
|
||||
|
||||
@@ -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 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-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` and `pano_cli inspect-project` recognize only the fixed PPI header, not thumbnail bytes or the project body | 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` | Full PPI load/save fixtures cover thumbnail, layers, 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
|
||||
|
||||
@@ -249,10 +249,10 @@ Goal: make each component reachable by automated tools and future agents.
|
||||
Status: in progress. `tests/` exists, `desktop-fast` runs headlessly, and
|
||||
PowerShell/bash wrappers exist for
|
||||
configure/build/test/analyze/platform-build/package-smoke. `pano_cli` exists
|
||||
with JSON automation commands for creating a `pp_document` model and
|
||||
inspecting image signatures, PPI headers, and layout XML; full document/app
|
||||
integration is debt-tracked as DEBT-0010 and full PPI body parsing is
|
||||
debt-tracked as DEBT-0013.
|
||||
with JSON automation commands for creating a `pp_document` model, metadata-only
|
||||
PPI project loading, and inspecting image signatures, PPI headers, and layout
|
||||
XML; full document/app integration is debt-tracked as DEBT-0010 and full PPI
|
||||
body parsing is debt-tracked as DEBT-0013.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
@@ -312,25 +312,39 @@ 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 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, layer/frame indexing, dirty-face PNG payload metadata validation,
|
||||
asset-level RGBA PNG payload decoding, and a pure typed settings document
|
||||
model, with
|
||||
corrupt/truncated/unsupported, extreme-dimension, and key/value limit tests.
|
||||
`pp_paint` has started with pure brush parameter validation/stamp evaluation,
|
||||
CPU reference math for the five current shader blend modes, and deterministic
|
||||
stroke spacing/interpolation. `pp_document` has
|
||||
started with a pure canvas/layer/frame model, layer metadata operations, frame
|
||||
move/duration queries, and layer/frame/undo-redo history invariant tests.
|
||||
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, 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
|
||||
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, and
|
||||
`pano_cli inspect-image` exposes PNG IHDR metadata as JSON,
|
||||
`pano_cli inspect-project` reports validated PPI thumbnail/body byte layout,
|
||||
body summary, layer/frame descriptors, dirty-face PNG payload metadata, and
|
||||
asset-level decode coverage, and
|
||||
`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. `pano_cli parse-layout` exercises the XML layout path.
|
||||
Continue expanding document behavior toward legacy Canvas parity and then port
|
||||
OpenGL classes behind the renderer boundary.
|
||||
frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke
|
||||
sampler for scripted-stroke automation. `pano_cli simulate-stroke-script`
|
||||
loads stroke script fixtures, parses them through `pp_paint`, and samples every
|
||||
stroke. `pano_cli parse-layout` exercises the XML layout path. Continue
|
||||
expanding document behavior toward legacy Canvas parity and then port OpenGL
|
||||
classes behind the renderer boundary.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
@@ -536,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_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
|
||||
@@ -562,13 +576,22 @@ Results:
|
||||
- `pp_foundation_trace_tests` passed.
|
||||
- `pp_assets_image_format_tests` passed.
|
||||
- `pp_assets_image_metadata_tests` passed.
|
||||
- `pp_assets_ppi_header_tests` passed.
|
||||
- `pp_assets_image_pixels_tests` passed, including RGBA8 PNG decode and corrupt
|
||||
payload rejection.
|
||||
- `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout,
|
||||
body summary validation, layer/frame indexing, dirty-face PNG payload
|
||||
metadata validation, and decoded dirty-face payload coverage.
|
||||
- `pp_assets_settings_document_tests` passed.
|
||||
- `pp_paint_brush_tests` passed.
|
||||
- `pp_paint_blend_tests` passed.
|
||||
- `pp_paint_stroke_tests` passed.
|
||||
- `pp_document_tests` passed, including frame move, duration, and history
|
||||
invariants.
|
||||
- `pp_paint_stroke_script_tests` passed.
|
||||
- `pp_document_tests` passed, including snapshot construction, alpha-lock
|
||||
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.
|
||||
@@ -581,7 +604,17 @@ Results:
|
||||
test.
|
||||
- `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, layer/frame descriptors, and
|
||||
dirty-face PNG payload metadata JSON.
|
||||
- `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.
|
||||
- `pano_cli_simulate_stroke_script_smoke` passed and reports deterministic
|
||||
aggregate stroke-script counts/distances.
|
||||
- `panopainter_validate_shaders` passed, validating 25 shader programs and 7
|
||||
shader includes for stage markers and include graph integrity.
|
||||
- PowerShell analyze automation returns JSON summaries and includes the shader
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Presets = @("android-arm64"),
|
||||
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_image_metadata_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_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_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}"
|
||||
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"
|
||||
|
||||
94
src/assets/image_pixels.cpp
Normal file
94
src/assets/image_pixels.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "assets/image_pixels.h"
|
||||
|
||||
#include "assets/image_metadata.h"
|
||||
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#define STB_IMAGE_STATIC
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include <stb/stb_image.h>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> rgba_byte_size(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height) noexcept
|
||||
{
|
||||
const auto pixels = static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
|
||||
constexpr auto channels = 4ULL;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / channels) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("RGBA byte size overflows"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * channels;
|
||||
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("RGBA byte size exceeds addressable memory"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> bytes)
|
||||
{
|
||||
const auto metadata = parse_png_metadata(bytes);
|
||||
if (!metadata) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(metadata.status());
|
||||
}
|
||||
|
||||
if (bytes.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::out_of_range("PNG payload is too large for the decoder"));
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int source_components = 0;
|
||||
auto* decoded = stbi_load_from_memory(
|
||||
reinterpret_cast<const stbi_uc*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
&width,
|
||||
&height,
|
||||
&source_components,
|
||||
4);
|
||||
if (decoded == nullptr) {
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::invalid_argument("PNG payload could not be decoded"));
|
||||
}
|
||||
|
||||
const auto cleanup = [decoded]() noexcept {
|
||||
stbi_image_free(decoded);
|
||||
};
|
||||
|
||||
if (width <= 0 || height <= 0
|
||||
|| static_cast<std::uint32_t>(width) != metadata.value().width
|
||||
|| static_cast<std::uint32_t>(height) != metadata.value().height) {
|
||||
cleanup();
|
||||
return pp::foundation::Result<Rgba8Image>::failure(
|
||||
pp::foundation::Status::invalid_argument("decoded PNG dimensions are inconsistent"));
|
||||
}
|
||||
|
||||
const auto byte_count = rgba_byte_size(metadata.value().width, metadata.value().height);
|
||||
if (!byte_count) {
|
||||
cleanup();
|
||||
return pp::foundation::Result<Rgba8Image>::failure(byte_count.status());
|
||||
}
|
||||
|
||||
Rgba8Image image {
|
||||
.width = metadata.value().width,
|
||||
.height = metadata.value().height,
|
||||
.pixels = {},
|
||||
};
|
||||
image.pixels.assign(decoded, decoded + byte_count.value());
|
||||
cleanup();
|
||||
|
||||
return pp::foundation::Result<Rgba8Image>::success(std::move(image));
|
||||
}
|
||||
|
||||
}
|
||||
21
src/assets/image_pixels.h
Normal file
21
src/assets/image_pixels.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
struct Rgba8Image {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<Rgba8Image> decode_png_rgba8(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
#include "assets/ppi_header.h"
|
||||
|
||||
#include "assets/image_metadata.h"
|
||||
#include "foundation/binary_stream.h"
|
||||
|
||||
#include <bit>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
@@ -11,6 +16,93 @@ 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();
|
||||
}
|
||||
|
||||
[[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 pp::foundation::Result<ImageMetadata>::failure(metadata.status());
|
||||
}
|
||||
|
||||
if (metadata.value().width != width || metadata.value().height != height) {
|
||||
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::Result<ImageMetadata>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<ImageMetadata>::success(metadata.value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte> bytes) noexcept
|
||||
@@ -70,4 +162,458 @@ pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte
|
||||
return pp::foundation::Result<PpiHeaderInfo>::success(info);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept
|
||||
{
|
||||
if (thumbnail.width == 0 || thumbnail.height == 0 || thumbnail.components == 0) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
|
||||
}
|
||||
|
||||
const auto width = static_cast<std::uint64_t>(thumbnail.width);
|
||||
const auto height = static_cast<std::uint64_t>(thumbnail.height);
|
||||
const auto components = static_cast<std::uint64_t>(thumbnail.components);
|
||||
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||
}
|
||||
|
||||
const auto pixels = width * height;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||
}
|
||||
|
||||
const auto bytes = pixels * components;
|
||||
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("PPI thumbnail byte size exceeds addressable memory"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
const auto header = parse_ppi_header(bytes);
|
||||
if (!header) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(header.status());
|
||||
}
|
||||
|
||||
const auto thumbnail_bytes = ppi_thumbnail_byte_size(header.value().thumbnail);
|
||||
if (!thumbnail_bytes) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(thumbnail_bytes.status());
|
||||
}
|
||||
|
||||
if (thumbnail_bytes.value() > std::numeric_limits<std::size_t>::max() - ppi_header_size) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
|
||||
}
|
||||
|
||||
const auto body_offset = ppi_header_size + thumbnail_bytes.value();
|
||||
if (bytes.size() < body_offset) {
|
||||
return pp::foundation::Result<PpiProjectLayout>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI thumbnail payload is truncated"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiProjectLayout>::success(PpiProjectLayout {
|
||||
.header = header.value(),
|
||||
.thumbnail_offset = ppi_header_size,
|
||||
.thumbnail_bytes = thumbnail_bytes.value(),
|
||||
.body_offset = body_offset,
|
||||
.body_bytes = bytes.size() - body_offset,
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
|
||||
PpiHeaderInfo header,
|
||||
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");
|
||||
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,
|
||||
};
|
||||
|
||||
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) {
|
||||
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 (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"));
|
||||
}
|
||||
|
||||
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_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) {
|
||||
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"));
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
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");
|
||||
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"));
|
||||
}
|
||||
|
||||
if (index != nullptr) {
|
||||
layer_summary.frames[frame_index].duration_ms = duration.value();
|
||||
}
|
||||
}
|
||||
|
||||
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 byte_status = add_payload_bytes(summary, data_size.value());
|
||||
if (!byte_status.ok()) {
|
||||
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_metadata = validate_face_png_payload(
|
||||
payload.value(),
|
||||
x1.value() - x0.value(),
|
||||
y1.value() - y0.value());
|
||||
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) {
|
||||
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"));
|
||||
}
|
||||
|
||||
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);
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::span<const std::byte> bytes)
|
||||
{
|
||||
auto project = parse_ppi_project_index(bytes);
|
||||
if (!project) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(project.status());
|
||||
}
|
||||
|
||||
PpiDecodedProjectImages decoded {
|
||||
.project = project.value(),
|
||||
.faces = {},
|
||||
};
|
||||
decoded.faces.reserve(decoded.project.body.summary.rgba_face_payload_count);
|
||||
|
||||
const auto body = bytes.subspan(decoded.project.layout.body_offset, decoded.project.layout.body_bytes);
|
||||
for (std::size_t layer_index = 0; layer_index < decoded.project.body.layers.size(); ++layer_index) {
|
||||
const auto& layer = decoded.project.body.layers[layer_index];
|
||||
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
|
||||
const auto& frame = layer.frames[frame_index];
|
||||
for (std::size_t face_index = 0; face_index < frame.faces.size(); ++face_index) {
|
||||
const auto& face = frame.faces[face_index];
|
||||
if (!face.has_data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (face.body_payload_offset > body.size()
|
||||
|| face.payload_bytes > body.size() - face.body_payload_offset) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI face payload range is outside the body"));
|
||||
}
|
||||
|
||||
const auto image = decode_png_rgba8(
|
||||
body.subspan(face.body_payload_offset, face.payload_bytes));
|
||||
if (!image) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(image.status());
|
||||
}
|
||||
|
||||
if (image.value().width != face.png_width || image.value().height != face.png_height) {
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
|
||||
pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed"));
|
||||
}
|
||||
|
||||
decoded.faces.push_back(PpiDecodedFacePayload {
|
||||
.layer_index = static_cast<std::uint32_t>(layer_index),
|
||||
.frame_index = static_cast<std::uint32_t>(frame_index),
|
||||
.face_index = static_cast<std::uint32_t>(face_index),
|
||||
.descriptor = face,
|
||||
.image = image.value(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiDecodedProjectImages>::success(std::move(decoded));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "assets/image_pixels.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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;
|
||||
@@ -34,7 +43,104 @@ struct PpiHeaderInfo {
|
||||
PpiThumbnailInfo thumbnail;
|
||||
};
|
||||
|
||||
struct PpiProjectLayout {
|
||||
PpiHeaderInfo header;
|
||||
std::size_t thumbnail_offset = 0;
|
||||
std::size_t thumbnail_bytes = 0;
|
||||
std::size_t body_offset = 0;
|
||||
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::uint32_t rgba_face_payload_count = 0;
|
||||
std::uint64_t compressed_face_bytes = 0;
|
||||
std::uint32_t info_bytes = 0;
|
||||
};
|
||||
|
||||
struct PpiProjectSummary {
|
||||
PpiProjectLayout layout;
|
||||
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;
|
||||
};
|
||||
|
||||
struct PpiDecodedFacePayload {
|
||||
std::uint32_t layer_index = 0;
|
||||
std::uint32_t frame_index = 0;
|
||||
std::uint32_t face_index = 0;
|
||||
PpiFacePayloadSummary descriptor;
|
||||
Rgba8Image image;
|
||||
};
|
||||
|
||||
struct PpiDecodedProjectImages {
|
||||
PpiProjectIndex project;
|
||||
std::vector<PpiDecodedFacePayload> faces;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept;
|
||||
|
||||
[[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<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);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(
|
||||
std::span<const std::byte> bytes);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::document {
|
||||
|
||||
@@ -33,6 +35,15 @@ namespace {
|
||||
return "Layer " + std::to_string(index + 1U);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint64_t frame_duration_sum(std::span<const AnimationFrame> frames) noexcept
|
||||
{
|
||||
std::uint64_t duration = 0;
|
||||
for (const auto& frame : frames) {
|
||||
duration += frame.duration_ms;
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept
|
||||
{
|
||||
if (name.empty()) {
|
||||
@@ -46,6 +57,38 @@ namespace {
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_layer_opacity(float opacity) noexcept
|
||||
{
|
||||
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||
return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_blend_mode(pp::paint::BlendMode blend_mode) noexcept
|
||||
{
|
||||
switch (blend_mode) {
|
||||
case pp::paint::BlendMode::normal:
|
||||
case pp::paint::BlendMode::multiply:
|
||||
case pp::paint::BlendMode::screen:
|
||||
case pp::paint::BlendMode::color_dodge:
|
||||
case pp::paint::BlendMode::overlay:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_frame_duration(std::uint32_t duration_ms) noexcept
|
||||
{
|
||||
if (duration_ms < min_frame_duration_ms) {
|
||||
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept
|
||||
{
|
||||
if (index >= layer_count) {
|
||||
@@ -64,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)
|
||||
@@ -76,11 +180,97 @@ pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig con
|
||||
CanvasDocument document;
|
||||
document.width_ = config.width;
|
||||
document.height_ = config.height;
|
||||
document.frames_.push_back(AnimationFrame {});
|
||||
document.layers_.reserve(config.layer_count);
|
||||
for (std::uint32_t i = 0; i < config.layer_count; ++i) {
|
||||
document.layers_.push_back(Layer { .name = default_layer_name(i) });
|
||||
document.layers_.push_back(Layer {
|
||||
.name = default_layer_name(i),
|
||||
.frames = document.frames_,
|
||||
});
|
||||
}
|
||||
|
||||
return pp::foundation::Result<CanvasDocument>::success(document);
|
||||
}
|
||||
|
||||
pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(DocumentSnapshotConfig config)
|
||||
{
|
||||
const auto status = validate_config(DocumentConfig {
|
||||
.width = config.width,
|
||||
.height = config.height,
|
||||
.layer_count = static_cast<std::uint32_t>(config.layers.size()),
|
||||
});
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(status);
|
||||
}
|
||||
|
||||
if (config.frames.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("document must contain at least one frame"));
|
||||
}
|
||||
|
||||
if (config.frames.size() > max_frame_count) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
CanvasDocument document;
|
||||
document.width_ = config.width;
|
||||
document.height_ = config.height;
|
||||
document.layers_.reserve(config.layers.size());
|
||||
for (const auto& layer_config : config.layers) {
|
||||
const auto name_status = validate_layer_name(layer_config.name);
|
||||
if (!name_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(name_status);
|
||||
}
|
||||
|
||||
const auto opacity_status = validate_layer_opacity(layer_config.opacity);
|
||||
if (!opacity_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(opacity_status);
|
||||
}
|
||||
|
||||
const auto blend_status = validate_blend_mode(layer_config.blend_mode);
|
||||
if (!blend_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(blend_status);
|
||||
}
|
||||
|
||||
const auto layer_frames = layer_config.frames.empty() ? config.frames : layer_config.frames;
|
||||
if (layer_frames.empty()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::invalid_argument("document layer must contain at least one frame"));
|
||||
}
|
||||
|
||||
if (layer_frames.size() > max_frame_count) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(
|
||||
pp::foundation::Status::out_of_range("document layer frame count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
for (const auto& frame_config : layer_frames) {
|
||||
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
|
||||
}
|
||||
}
|
||||
|
||||
document.layers_.push_back(Layer {
|
||||
.name = std::string(layer_config.name),
|
||||
.visible = layer_config.visible,
|
||||
.alpha_locked = layer_config.alpha_locked,
|
||||
.opacity = layer_config.opacity,
|
||||
.blend_mode = layer_config.blend_mode,
|
||||
.frames = {},
|
||||
});
|
||||
document.layers_.back().frames.assign(layer_frames.begin(), layer_frames.end());
|
||||
}
|
||||
|
||||
document.frames_.reserve(config.frames.size());
|
||||
for (const auto& frame_config : config.frames) {
|
||||
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
|
||||
}
|
||||
|
||||
document.frames_.push_back(frame_config);
|
||||
}
|
||||
document.frames_.push_back(AnimationFrame {});
|
||||
|
||||
return pp::foundation::Result<CanvasDocument>::success(document);
|
||||
}
|
||||
@@ -107,13 +297,34 @@ std::size_t CanvasDocument::active_frame_index() const noexcept
|
||||
|
||||
std::uint64_t CanvasDocument::animation_duration_ms() const noexcept
|
||||
{
|
||||
std::uint64_t duration = 0;
|
||||
for (const auto& frame : frames_) {
|
||||
duration += frame.duration_ms;
|
||||
std::uint64_t duration = frame_duration_sum(frames_);
|
||||
for (const auto& layer : layers_) {
|
||||
duration = std::max(duration, frame_duration_sum(layer.frames));
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> CanvasDocument::layer_animation_duration_ms(std::size_t index) const noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(index_status);
|
||||
}
|
||||
|
||||
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_;
|
||||
@@ -141,6 +352,7 @@ pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view n
|
||||
}
|
||||
layer.name = std::string(name);
|
||||
}
|
||||
layer.frames = frames_;
|
||||
layers_.push_back(layer);
|
||||
active_layer_index_ = layers_.size() - 1U;
|
||||
return pp::foundation::Result<std::size_t>::success(active_layer_index_);
|
||||
@@ -229,6 +441,17 @@ pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
if (!index_status.ok()) {
|
||||
return index_status;
|
||||
}
|
||||
|
||||
layers_[index].alpha_locked = alpha_locked;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept
|
||||
{
|
||||
const auto index_status = validate_layer_index(index, layers_.size());
|
||||
@@ -236,8 +459,9 @@ pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, floa
|
||||
return index_status;
|
||||
}
|
||||
|
||||
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
|
||||
return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
|
||||
const auto opacity_status = validate_layer_opacity(opacity);
|
||||
if (!opacity_status.ok()) {
|
||||
return opacity_status;
|
||||
}
|
||||
|
||||
layers_[index].opacity = opacity;
|
||||
@@ -251,17 +475,13 @@ pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, p
|
||||
return index_status;
|
||||
}
|
||||
|
||||
switch (blend_mode) {
|
||||
case pp::paint::BlendMode::normal:
|
||||
case pp::paint::BlendMode::multiply:
|
||||
case pp::paint::BlendMode::screen:
|
||||
case pp::paint::BlendMode::color_dodge:
|
||||
case pp::paint::BlendMode::overlay:
|
||||
layers_[index].blend_mode = blend_mode;
|
||||
return pp::foundation::Status::success();
|
||||
const auto blend_status = validate_blend_mode(blend_mode);
|
||||
if (!blend_status.ok()) {
|
||||
return blend_status;
|
||||
}
|
||||
|
||||
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
|
||||
layers_[index].blend_mode = blend_mode;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
|
||||
@@ -271,12 +491,16 @@ pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t dura
|
||||
pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
if (duration_ms < min_frame_duration_ms) {
|
||||
const auto duration_status = validate_frame_duration(duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("frame duration must be greater than zero"));
|
||||
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, .face_pixels = {} });
|
||||
}
|
||||
active_frame_index_ = frames_.size() - 1U;
|
||||
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
||||
}
|
||||
@@ -296,6 +520,13 @@ pp::foundation::Result<std::size_t> CanvasDocument::duplicate_frame(std::size_t
|
||||
|
||||
const auto insert_at = index + 1U;
|
||||
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(insert_at), frames_[index]);
|
||||
for (auto& layer : layers_) {
|
||||
if (index < layer.frames.size()) {
|
||||
layer.frames.insert(
|
||||
layer.frames.begin() + static_cast<std::ptrdiff_t>(insert_at),
|
||||
layer.frames[index]);
|
||||
}
|
||||
}
|
||||
active_frame_index_ = insert_at;
|
||||
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
|
||||
}
|
||||
@@ -312,6 +543,11 @@ pp::foundation::Status CanvasDocument::remove_frame(std::size_t index)
|
||||
}
|
||||
|
||||
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
for (auto& layer : layers_) {
|
||||
if (index < layer.frames.size() && layer.frames.size() > 1U) {
|
||||
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
}
|
||||
}
|
||||
if (active_frame_index_ >= frames_.size()) {
|
||||
active_frame_index_ = frames_.size() - 1U;
|
||||
} else if (active_frame_index_ > index) {
|
||||
@@ -334,6 +570,13 @@ pp::foundation::Status CanvasDocument::move_frame(std::size_t from, std::size_t
|
||||
const auto frame = frames_[from];
|
||||
frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(from));
|
||||
frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(to), frame);
|
||||
for (auto& layer : layers_) {
|
||||
if (from < layer.frames.size() && to < layer.frames.size()) {
|
||||
const auto layer_frame = layer.frames[from];
|
||||
layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(from));
|
||||
layer.frames.insert(layer.frames.begin() + static_cast<std::ptrdiff_t>(to), layer_frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (active_frame_index_ == from) {
|
||||
active_frame_index_ = to;
|
||||
@@ -353,11 +596,17 @@ pp::foundation::Status CanvasDocument::set_frame_duration(std::size_t index, std
|
||||
return index_status;
|
||||
}
|
||||
|
||||
if (duration_ms < min_frame_duration_ms) {
|
||||
return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
|
||||
const auto duration_status = validate_frame_duration(duration_ms);
|
||||
if (!duration_status.ok()) {
|
||||
return duration_status;
|
||||
}
|
||||
|
||||
frames_[index].duration_ms = duration_ms;
|
||||
for (auto& layer : layers_) {
|
||||
if (index < layer.frames.size()) {
|
||||
layer.frames[index].duration_ms = duration_ms;
|
||||
}
|
||||
}
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
@@ -372,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,26 +28,57 @@ struct DocumentConfig {
|
||||
std::uint32_t layer_count = 1;
|
||||
};
|
||||
|
||||
struct Layer {
|
||||
std::string name;
|
||||
bool visible = true;
|
||||
float opacity = 1.0F;
|
||||
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||
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 {
|
||||
std::string name;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
float opacity = 1.0F;
|
||||
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||
std::vector<AnimationFrame> frames;
|
||||
};
|
||||
|
||||
struct DocumentLayerConfig {
|
||||
std::string_view name;
|
||||
bool visible = true;
|
||||
bool alpha_locked = false;
|
||||
float opacity = 1.0F;
|
||||
pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
|
||||
std::span<const AnimationFrame> frames;
|
||||
};
|
||||
|
||||
struct DocumentSnapshotConfig {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::span<const DocumentLayerConfig> layers;
|
||||
std::span<const AnimationFrame> frames;
|
||||
};
|
||||
|
||||
class CanvasDocument {
|
||||
public:
|
||||
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create(DocumentConfig config);
|
||||
[[nodiscard]] static pp::foundation::Result<CanvasDocument> create_from_snapshot(DocumentSnapshotConfig config);
|
||||
|
||||
[[nodiscard]] std::uint32_t width() const noexcept;
|
||||
[[nodiscard]] std::uint32_t height() const noexcept;
|
||||
[[nodiscard]] std::size_t active_layer_index() const noexcept;
|
||||
[[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;
|
||||
|
||||
@@ -54,6 +88,7 @@ public:
|
||||
[[nodiscard]] pp::foundation::Status set_active_layer(std::size_t index) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status rename_layer(std::size_t index, std::string_view name);
|
||||
[[nodiscard]] pp::foundation::Status set_layer_visible(std::size_t index, bool visible) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_opacity(std::size_t index, float opacity) noexcept;
|
||||
[[nodiscard]] pp::foundation::Status set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept;
|
||||
|
||||
@@ -63,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);
|
||||
|
||||
}
|
||||
210
src/paint/stroke_script.cpp
Normal file
210
src/paint/stroke_script.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "paint/stroke_script.h"
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::string_view trim(std::string_view text) noexcept
|
||||
{
|
||||
while (!text.empty() && (text.front() == ' ' || text.front() == '\t' || text.front() == '\r')) {
|
||||
text.remove_prefix(1);
|
||||
}
|
||||
|
||||
while (!text.empty() && (text.back() == ' ' || text.back() == '\t' || text.back() == '\r')) {
|
||||
text.remove_suffix(1);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view strip_comment(std::string_view line) noexcept
|
||||
{
|
||||
const auto comment = line.find('#');
|
||||
if (comment == std::string_view::npos) {
|
||||
return line;
|
||||
}
|
||||
|
||||
return line.substr(0, comment);
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<float> parse_float_token(std::string_view token) noexcept
|
||||
{
|
||||
token = trim(token);
|
||||
if (token.empty() || token.size() >= 64U) {
|
||||
return pp::foundation::Result<float>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
|
||||
}
|
||||
|
||||
std::array<char, 64> buffer {};
|
||||
for (std::size_t i = 0; i < token.size(); ++i) {
|
||||
buffer[i] = token[i];
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
errno = 0;
|
||||
const auto value = std::strtof(buffer.data(), &end);
|
||||
if (errno != 0 || end != buffer.data() + static_cast<std::ptrdiff_t>(token.size()) || !std::isfinite(value)) {
|
||||
return pp::foundation::Result<float>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<float>::success(value);
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::size_t> split_tokens(
|
||||
std::string_view line,
|
||||
std::array<std::string_view, 8>& tokens) noexcept
|
||||
{
|
||||
std::size_t count = 0;
|
||||
std::size_t offset = 0;
|
||||
while (offset < line.size()) {
|
||||
while (offset < line.size() && (line[offset] == ' ' || line[offset] == '\t')) {
|
||||
++offset;
|
||||
}
|
||||
|
||||
if (offset >= line.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto token_start = offset;
|
||||
while (offset < line.size() && line[offset] != ' ' && line[offset] != '\t') {
|
||||
++offset;
|
||||
}
|
||||
|
||||
if (count >= tokens.size()) {
|
||||
return pp::foundation::Result<std::size_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script line has too many tokens"));
|
||||
}
|
||||
|
||||
tokens[count] = line.substr(token_start, offset - token_start);
|
||||
++count;
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::size_t>::success(count);
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<StrokeScriptStroke> parse_stroke_line(std::string_view line) noexcept
|
||||
{
|
||||
std::array<std::string_view, 8> tokens {};
|
||||
const auto token_count = split_tokens(line, tokens);
|
||||
if (!token_count) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(token_count.status());
|
||||
}
|
||||
|
||||
if (token_count.value() != tokens.size() || tokens[0] != "stroke") {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script line must be 'stroke x1 y1 p1 x2 y2 p2 spacing'"));
|
||||
}
|
||||
|
||||
const auto x1 = parse_float_token(tokens[1]);
|
||||
const auto y1 = parse_float_token(tokens[2]);
|
||||
const auto p1 = parse_float_token(tokens[3]);
|
||||
const auto x2 = parse_float_token(tokens[4]);
|
||||
const auto y2 = parse_float_token(tokens[5]);
|
||||
const auto p2 = parse_float_token(tokens[6]);
|
||||
const auto spacing = parse_float_token(tokens[7]);
|
||||
if (!x1) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(x1.status());
|
||||
}
|
||||
if (!y1) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(y1.status());
|
||||
}
|
||||
if (!p1) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(p1.status());
|
||||
}
|
||||
if (!x2) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(x2.status());
|
||||
}
|
||||
if (!y2) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(y2.status());
|
||||
}
|
||||
if (!p2) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(p2.status());
|
||||
}
|
||||
if (!spacing) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(spacing.status());
|
||||
}
|
||||
|
||||
if (spacing.value() <= 0.0F) {
|
||||
return pp::foundation::Result<StrokeScriptStroke>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script spacing must be greater than zero"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<StrokeScriptStroke>::success(StrokeScriptStroke {
|
||||
.start = StrokePoint {
|
||||
.x = x1.value(),
|
||||
.y = y1.value(),
|
||||
.pressure = p1.value(),
|
||||
},
|
||||
.end = StrokePoint {
|
||||
.x = x2.value(),
|
||||
.y = y2.value(),
|
||||
.pressure = p2.value(),
|
||||
},
|
||||
.spacing = spacing.value(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<StrokeScript> parse_stroke_script(std::string_view text)
|
||||
{
|
||||
if (text.empty()) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script must not be empty"));
|
||||
}
|
||||
|
||||
if (text.size() > max_stroke_script_bytes) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::out_of_range("stroke script exceeds the configured size limit"));
|
||||
}
|
||||
|
||||
StrokeScript script;
|
||||
std::size_t offset = 0;
|
||||
while (offset <= text.size()) {
|
||||
const auto line_start = offset;
|
||||
const auto line_end = text.find('\n', line_start);
|
||||
if (line_end == std::string_view::npos) {
|
||||
offset = text.size() + 1U;
|
||||
} else {
|
||||
offset = line_end + 1U;
|
||||
}
|
||||
|
||||
auto line = text.substr(line_start, (line_end == std::string_view::npos) ? std::string_view::npos : line_end - line_start);
|
||||
if (line.size() > max_stroke_script_line_length) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::out_of_range("stroke script line exceeds the configured length limit"));
|
||||
}
|
||||
|
||||
line = trim(strip_comment(line));
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (script.strokes.size() >= max_stroke_script_strokes) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::out_of_range("stroke script stroke count exceeds the configured limit"));
|
||||
}
|
||||
|
||||
const auto stroke = parse_stroke_line(line);
|
||||
if (!stroke) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(stroke.status());
|
||||
}
|
||||
|
||||
script.strokes.push_back(stroke.value());
|
||||
}
|
||||
|
||||
if (script.strokes.empty()) {
|
||||
return pp::foundation::Result<StrokeScript>::failure(
|
||||
pp::foundation::Status::invalid_argument("stroke script must contain at least one stroke"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<StrokeScript>::success(script);
|
||||
}
|
||||
|
||||
}
|
||||
28
src/paint/stroke_script.h
Normal file
28
src/paint/stroke_script.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
#include "paint/stroke.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace pp::paint {
|
||||
|
||||
constexpr std::size_t max_stroke_script_bytes = 1024 * 1024;
|
||||
constexpr std::size_t max_stroke_script_line_length = 512;
|
||||
constexpr std::size_t max_stroke_script_strokes = 10000;
|
||||
|
||||
struct StrokeScriptStroke {
|
||||
StrokePoint start;
|
||||
StrokePoint end;
|
||||
float spacing = 1.0F;
|
||||
};
|
||||
|
||||
struct StrokeScript {
|
||||
std::vector<StrokeScriptStroke> strokes;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<StrokeScript> parse_stroke_script(std::string_view text);
|
||||
|
||||
}
|
||||
@@ -86,6 +86,16 @@ add_test(NAME pp_assets_image_metadata_tests COMMAND pp_assets_image_metadata_te
|
||||
set_tests_properties(pp_assets_image_metadata_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_image_pixels_tests
|
||||
assets/image_pixels_tests.cpp)
|
||||
target_link_libraries(pp_assets_image_pixels_tests PRIVATE
|
||||
pp_assets
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_assets_image_pixels_tests COMMAND pp_assets_image_pixels_tests)
|
||||
set_tests_properties(pp_assets_image_pixels_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_ppi_header_tests
|
||||
assets/ppi_header_tests.cpp)
|
||||
target_link_libraries(pp_assets_ppi_header_tests PRIVATE
|
||||
@@ -136,6 +146,16 @@ add_test(NAME pp_paint_stroke_tests COMMAND pp_paint_stroke_tests)
|
||||
set_tests_properties(pp_paint_stroke_tests PROPERTIES
|
||||
LABELS "paint;desktop-fast")
|
||||
|
||||
add_executable(pp_paint_stroke_script_tests
|
||||
paint/stroke_script_tests.cpp)
|
||||
target_link_libraries(pp_paint_stroke_script_tests PRIVATE
|
||||
pp_paint
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_paint_stroke_script_tests COMMAND pp_paint_stroke_script_tests)
|
||||
set_tests_properties(pp_paint_stroke_script_tests PROPERTIES
|
||||
LABELS "paint;desktop-fast")
|
||||
|
||||
add_executable(pp_document_tests
|
||||
document/document_tests.cpp)
|
||||
target_link_libraries(pp_document_tests PRIVATE
|
||||
@@ -146,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
|
||||
@@ -220,8 +250,32 @@ if(TARGET pano_cli)
|
||||
LABELS "assets;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"format\":\"png\".*\"width\":320.*\"height\":240.*\"components\":4.*\"colorType\":\"rgba\"")
|
||||
|
||||
add_test(NAME pano_cli_inspect_project_layout_smoke
|
||||
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\\}.*\"layers\":\\[\\{\"index\":0,\"storedOrder\":0,\"name\":\"Ink\"")
|
||||
|
||||
add_test(NAME pano_cli_load_project_metadata_smoke
|
||||
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.*\"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")
|
||||
set_tests_properties(pano_cli_parse_layout_smoke PROPERTIES
|
||||
LABELS "ui;integration;desktop-fast")
|
||||
|
||||
add_test(NAME pano_cli_simulate_stroke_smoke
|
||||
COMMAND pano_cli simulate-stroke --x1 0 --y1 0 --x2 10 --y2 0 --spacing 2)
|
||||
set_tests_properties(pano_cli_simulate_stroke_smoke PROPERTIES
|
||||
LABELS "paint;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"samples\":6.*\"distance\":10")
|
||||
|
||||
add_test(NAME pano_cli_simulate_stroke_script_smoke
|
||||
COMMAND pano_cli simulate-stroke-script --path "${CMAKE_CURRENT_SOURCE_DIR}/data/strokes/two-strokes.ppstroke")
|
||||
set_tests_properties(pano_cli_simulate_stroke_script_smoke PROPERTIES
|
||||
LABELS "paint;integration;desktop-fast"
|
||||
PASS_REGULAR_EXPRESSION "\"strokes\":2.*\"samples\":9.*\"distance\":20")
|
||||
endif()
|
||||
|
||||
67
tests/assets/image_pixels_tests.cpp
Normal file
67
tests/assets/image_pixels_tests.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "assets/image_pixels.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
using pp::assets::decode_png_rgba8;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::byte, 68> transparent_png_1x1 {
|
||||
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
||||
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
|
||||
std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 },
|
||||
std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 },
|
||||
std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 },
|
||||
std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 },
|
||||
std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 },
|
||||
};
|
||||
|
||||
void decodes_png_to_rgba8_pixels(pp::tests::Harness& h)
|
||||
{
|
||||
const auto image = decode_png_rgba8(transparent_png_1x1);
|
||||
|
||||
PP_EXPECT(h, image.ok());
|
||||
PP_EXPECT(h, image.value().width == 1U);
|
||||
PP_EXPECT(h, image.value().height == 1U);
|
||||
PP_EXPECT(h, image.value().pixels.size() == 4U);
|
||||
PP_EXPECT(h, image.value().pixels[0] == 0U);
|
||||
PP_EXPECT(h, image.value().pixels[1] == 0U);
|
||||
PP_EXPECT(h, image.value().pixels[2] == 0U);
|
||||
PP_EXPECT(h, image.value().pixels[3] == 0U);
|
||||
}
|
||||
|
||||
void rejects_corrupt_png_payload(pp::tests::Harness& h)
|
||||
{
|
||||
auto corrupt = transparent_png_1x1;
|
||||
corrupt[0] = std::byte { 0x00 };
|
||||
|
||||
const auto image = decode_png_rgba8(corrupt);
|
||||
|
||||
PP_EXPECT(h, !image.ok());
|
||||
PP_EXPECT(h, image.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("decodes_png_to_rgba8_pixels", decodes_png_to_rgba8_pixels);
|
||||
harness.run("rejects_corrupt_png_payload", rejects_corrupt_png_payload);
|
||||
return harness.finish();
|
||||
}
|
||||
@@ -2,12 +2,19 @@
|
||||
#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::decode_ppi_project_images;
|
||||
using pp::assets::parse_ppi_project_index;
|
||||
using pp::assets::parse_ppi_project_summary;
|
||||
using pp::assets::parse_ppi_project_layout;
|
||||
using pp::assets::ppi_header_size;
|
||||
using pp::assets::ppi_thumbnail_byte_size;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
@@ -20,6 +27,26 @@ void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
|
||||
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||
}
|
||||
|
||||
void append_u32_be(std::vector<std::byte>& bytes, std::uint32_t value)
|
||||
{
|
||||
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>(value & 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 {
|
||||
@@ -40,6 +67,127 @@ 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> png_ihdr_payload(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint8_t bit_depth = 8U,
|
||||
std::uint8_t color_type = 6U)
|
||||
{
|
||||
std::vector<std::byte> bytes {
|
||||
std::byte { 0x89 },
|
||||
std::byte { 0x50 },
|
||||
std::byte { 0x4e },
|
||||
std::byte { 0x47 },
|
||||
std::byte { 0x0d },
|
||||
std::byte { 0x0a },
|
||||
std::byte { 0x1a },
|
||||
std::byte { 0x0a },
|
||||
};
|
||||
append_u32_be(bytes, 13);
|
||||
bytes.push_back(std::byte { 'I' });
|
||||
bytes.push_back(std::byte { 'H' });
|
||||
bytes.push_back(std::byte { 'D' });
|
||||
bytes.push_back(std::byte { 'R' });
|
||||
append_u32_be(bytes, width);
|
||||
append_u32_be(bytes, height);
|
||||
bytes.push_back(static_cast<std::byte>(bit_depth));
|
||||
bytes.push_back(static_cast<std::byte>(color_type));
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 0 });
|
||||
append_u32_be(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::byte> transparent_png_1x1()
|
||||
{
|
||||
return {
|
||||
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
|
||||
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
|
||||
std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 },
|
||||
std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 },
|
||||
std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 },
|
||||
std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 },
|
||||
std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 },
|
||||
std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f },
|
||||
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
|
||||
std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 },
|
||||
std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 },
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<std::byte> minimal_project()
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
||||
append_minimal_body(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::byte> project_with_single_face_payload(
|
||||
std::vector<std::byte> payload,
|
||||
std::uint32_t dirty_width = 8,
|
||||
std::uint32_t dirty_height = 4)
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
|
||||
|
||||
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, 2 + dirty_width);
|
||||
append_u32(bytes, 3 + dirty_height);
|
||||
append_u32(bytes, static_cast<std::uint32_t>(payload.size()));
|
||||
bytes.insert(bytes.end(), payload.begin(), payload.end());
|
||||
|
||||
for (std::uint32_t i = 1; i < 6U; ++i) {
|
||||
append_u32(bytes, 0);
|
||||
}
|
||||
append_u32(bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void parses_legacy_ppi_header(pp::tests::Harness& h)
|
||||
{
|
||||
const auto bytes = valid_header();
|
||||
@@ -96,6 +244,146 @@ void rejects_unsupported_document_versions(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, minor_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h)
|
||||
{
|
||||
const auto bytes = minimal_project();
|
||||
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
|
||||
PP_EXPECT(h, layout.ok());
|
||||
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 == 73U);
|
||||
}
|
||||
|
||||
void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h)
|
||||
{
|
||||
auto bytes = valid_header();
|
||||
bytes.resize(ppi_header_size + (128U * 128U * 4U) - 1U, std::byte { 0 });
|
||||
|
||||
const auto layout = parse_ppi_project_layout(bytes);
|
||||
|
||||
PP_EXPECT(h, !layout.ok());
|
||||
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.rgba_face_payload_count == 0U);
|
||||
PP_EXPECT(h, summary.value().body.compressed_face_bytes == 0U);
|
||||
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));
|
||||
const auto summary = parse_ppi_project_summary(project);
|
||||
|
||||
PP_EXPECT(h, summary.ok());
|
||||
PP_EXPECT(h, summary.value().body.dirty_face_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.rgba_face_payload_count == 1U);
|
||||
PP_EXPECT(h, summary.value().body.compressed_face_bytes == 33U);
|
||||
}
|
||||
|
||||
void decodes_dirty_face_png_payloads(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(transparent_png_1x1(), 1, 1);
|
||||
const auto decoded = decode_ppi_project_images(project);
|
||||
|
||||
PP_EXPECT(h, decoded.ok());
|
||||
PP_EXPECT(h, decoded.value().faces.size() == 1U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].layer_index == 0U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].frame_index == 0U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].face_index == 0U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.width == 1U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.height == 1U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U);
|
||||
PP_EXPECT(h, decoded.value().faces[0].image.pixels[3] == 0U);
|
||||
}
|
||||
|
||||
void rejects_metadata_only_payload_when_decoding_pixels(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4));
|
||||
const auto decoded = decode_ppi_project_images(project);
|
||||
|
||||
PP_EXPECT(h, !decoded.ok());
|
||||
PP_EXPECT(h, decoded.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_invalid_dirty_face_png_payloads(pp::tests::Harness& h)
|
||||
{
|
||||
auto mismatched_dimensions = project_with_single_face_payload(png_ihdr_payload(7, 4));
|
||||
auto non_rgba = project_with_single_face_payload(png_ihdr_payload(8, 4, 8, 2));
|
||||
auto bad_signature_payload = png_ihdr_payload(8, 4);
|
||||
bad_signature_payload[0] = std::byte { 0 };
|
||||
auto bad_signature = project_with_single_face_payload(bad_signature_payload);
|
||||
|
||||
const auto mismatched_result = parse_ppi_project_summary(mismatched_dimensions);
|
||||
const auto non_rgba_result = parse_ppi_project_summary(non_rgba);
|
||||
const auto bad_signature_result = parse_ppi_project_summary(bad_signature);
|
||||
|
||||
PP_EXPECT(h, !mismatched_result.ok());
|
||||
PP_EXPECT(h, mismatched_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !non_rgba_result.ok());
|
||||
PP_EXPECT(h, non_rgba_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_signature_result.ok());
|
||||
PP_EXPECT(h, bad_signature_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -104,5 +392,14 @@ int main()
|
||||
harness.run("parses_legacy_ppi_header", parses_legacy_ppi_header);
|
||||
harness.run("rejects_truncated_invalid_magic_and_bad_thumbnail", rejects_truncated_invalid_magic_and_bad_thumbnail);
|
||||
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("indexes_project_layers_frames_and_faces", indexes_project_layers_frames_and_faces);
|
||||
harness.run("validates_dirty_face_png_payload_metadata", validates_dirty_face_png_payload_metadata);
|
||||
harness.run("decodes_dirty_face_png_payloads", decodes_dirty_face_png_payloads);
|
||||
harness.run("rejects_metadata_only_payload_when_decoding_pixels", rejects_metadata_only_payload_when_decoding_pixels);
|
||||
harness.run("rejects_invalid_dirty_face_png_payloads", rejects_invalid_dirty_face_png_payloads);
|
||||
harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries);
|
||||
return harness.finish();
|
||||
}
|
||||
|
||||
BIN
tests/data/projects/minimal-project.ppi
Normal file
BIN
tests/data/projects/minimal-project.ppi
Normal file
Binary file not shown.
3
tests/data/strokes/two-strokes.ppstroke
Normal file
3
tests/data/strokes/two-strokes.ppstroke
Normal file
@@ -0,0 +1,3 @@
|
||||
# PanoPainter automation stroke script
|
||||
stroke 0 0 0.25 10 0 0.75 2
|
||||
stroke 10 0 1 10 10 0.5 5
|
||||
@@ -8,6 +8,10 @@ using pp::paint::BlendMode;
|
||||
using pp::document::CanvasDocument;
|
||||
using pp::document::DocumentHistory;
|
||||
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;
|
||||
@@ -31,6 +35,8 @@ void creates_document_with_default_layers(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, document.value().active_layer_index() == 0U);
|
||||
PP_EXPECT(h, document.value().frames().size() == 1U);
|
||||
PP_EXPECT(h, document.value().frames()[0].duration_ms == 100U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames.size() == 1U);
|
||||
PP_EXPECT(h, document.value().layers()[0].frames[0].duration_ms == 100U);
|
||||
PP_EXPECT(h, document.value().animation_duration_ms() == 100U);
|
||||
PP_EXPECT(h, document.value().active_frame_index() == 0U);
|
||||
}
|
||||
@@ -106,11 +112,13 @@ void updates_layer_metadata(pp::tests::Harness& h)
|
||||
|
||||
PP_EXPECT(h, document.rename_layer(1, "Ink").ok());
|
||||
PP_EXPECT(h, document.set_layer_visible(1, false).ok());
|
||||
PP_EXPECT(h, document.set_layer_alpha_locked(1, true).ok());
|
||||
PP_EXPECT(h, document.set_layer_opacity(1, 0.25F).ok());
|
||||
PP_EXPECT(h, document.set_layer_blend_mode(1, BlendMode::multiply).ok());
|
||||
|
||||
PP_EXPECT(h, document.layers()[1].name == std::string_view("Ink"));
|
||||
PP_EXPECT(h, !document.layers()[1].visible);
|
||||
PP_EXPECT(h, document.layers()[1].alpha_locked);
|
||||
PP_EXPECT(h, std::fabs(document.layers()[1].opacity - 0.25F) < 0.0001F);
|
||||
PP_EXPECT(h, document.layers()[1].blend_mode == BlendMode::multiply);
|
||||
}
|
||||
@@ -129,6 +137,7 @@ void rejects_invalid_layer_metadata(pp::tests::Harness& h)
|
||||
const auto bad_opacity_high = document.set_layer_opacity(0, 1.1F);
|
||||
const auto bad_opacity_nan = document.set_layer_opacity(0, std::nanf(""));
|
||||
const auto missing_visible = document.set_layer_visible(2, true);
|
||||
const auto missing_alpha_lock = document.set_layer_alpha_locked(2, true);
|
||||
const auto missing_blend = document.set_layer_blend_mode(2, BlendMode::normal);
|
||||
const auto bad_blend = document.set_layer_blend_mode(0, static_cast<BlendMode>(255));
|
||||
const auto bad_add_layer = document.add_layer(std::string(max_layer_name_length + 1U, 'x'));
|
||||
@@ -147,6 +156,8 @@ void rejects_invalid_layer_metadata(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, bad_opacity_nan.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_visible.ok());
|
||||
PP_EXPECT(h, missing_visible.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_alpha_lock.ok());
|
||||
PP_EXPECT(h, missing_alpha_lock.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !missing_blend.ok());
|
||||
PP_EXPECT(h, missing_blend.code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !bad_blend.ok());
|
||||
@@ -155,6 +166,143 @@ void rejects_invalid_layer_metadata(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, bad_add_layer.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void creates_document_from_snapshot_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const DocumentLayerConfig layers[] {
|
||||
{
|
||||
.name = "Ink",
|
||||
.visible = false,
|
||||
.alpha_locked = true,
|
||||
.opacity = 0.5F,
|
||||
.blend_mode = BlendMode::screen,
|
||||
.frames = {},
|
||||
},
|
||||
{
|
||||
.name = "Glaze",
|
||||
.visible = true,
|
||||
.alpha_locked = false,
|
||||
.opacity = 0.75F,
|
||||
.blend_mode = BlendMode::overlay,
|
||||
.frames = {},
|
||||
},
|
||||
};
|
||||
const AnimationFrame frames[] {
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
{ .duration_ms = 250, .face_pixels = {} },
|
||||
};
|
||||
|
||||
const auto document_result = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 128,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = frames,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
PP_EXPECT(h, document_result.value().layers().size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].name == std::string_view("Ink"));
|
||||
PP_EXPECT(h, !document_result.value().layers()[0].visible);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].alpha_locked);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].blend_mode == BlendMode::screen);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames.size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames[1].duration_ms == 250U);
|
||||
PP_EXPECT(h, document_result.value().frames().size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().animation_duration_ms() == 350U);
|
||||
}
|
||||
|
||||
void preserves_per_layer_snapshot_timelines(pp::tests::Harness& h)
|
||||
{
|
||||
const AnimationFrame project_frames[] {
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
};
|
||||
const AnimationFrame short_layer_frames[] {
|
||||
{ .duration_ms = 100, .face_pixels = {} },
|
||||
{ .duration_ms = 150, .face_pixels = {} },
|
||||
};
|
||||
const AnimationFrame long_layer_frames[] {
|
||||
{ .duration_ms = 500, .face_pixels = {} },
|
||||
};
|
||||
const DocumentLayerConfig layers[] {
|
||||
{
|
||||
.name = "Short",
|
||||
.frames = short_layer_frames,
|
||||
},
|
||||
{
|
||||
.name = "Long",
|
||||
.frames = long_layer_frames,
|
||||
},
|
||||
};
|
||||
|
||||
const auto document_result = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 128,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = project_frames,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
PP_EXPECT(h, document_result.value().frames().size() == 1U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames.size() == 2U);
|
||||
PP_EXPECT(h, document_result.value().layers()[1].frames.size() == 1U);
|
||||
PP_EXPECT(h, document_result.value().layers()[0].frames[1].duration_ms == 150U);
|
||||
PP_EXPECT(h, document_result.value().layers()[1].frames[0].duration_ms == 500U);
|
||||
PP_EXPECT(h, document_result.value().animation_duration_ms() == 500U);
|
||||
const auto layer_duration = document_result.value().layer_animation_duration_ms(0);
|
||||
PP_EXPECT(h, layer_duration.ok());
|
||||
PP_EXPECT(h, layer_duration.value() == 250U);
|
||||
}
|
||||
|
||||
void rejects_invalid_snapshot_metadata(pp::tests::Harness& h)
|
||||
{
|
||||
const DocumentLayerConfig layers[] { { .name = "Ink", .frames = {} } };
|
||||
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 } };
|
||||
|
||||
const auto no_layers = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = {},
|
||||
.frames = frames,
|
||||
});
|
||||
const auto no_frames = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = {},
|
||||
});
|
||||
const auto bad_frame = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = layers,
|
||||
.frames = bad_frames,
|
||||
});
|
||||
const auto bad_layer = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = bad_layers,
|
||||
.frames = frames,
|
||||
});
|
||||
const auto bad_layer_frame = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
||||
.width = 64,
|
||||
.height = 64,
|
||||
.layers = bad_layer_frames,
|
||||
.frames = frames,
|
||||
});
|
||||
|
||||
PP_EXPECT(h, !no_layers.ok());
|
||||
PP_EXPECT(h, no_layers.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !no_frames.ok());
|
||||
PP_EXPECT(h, no_frames.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_frame.ok());
|
||||
PP_EXPECT(h, bad_frame.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_layer.ok());
|
||||
PP_EXPECT(h, bad_layer.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_layer_frame.ok());
|
||||
PP_EXPECT(h, bad_layer_frame.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void manages_animation_frames_and_duration(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
@@ -167,6 +315,7 @@ void manages_animation_frames_and_duration(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, added.value() == 1U);
|
||||
PP_EXPECT(h, document.active_frame_index() == 1U);
|
||||
PP_EXPECT(h, document.frames()[1].duration_ms == 250U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[1].duration_ms == 250U);
|
||||
|
||||
const auto duplicated = document.duplicate_frame(1);
|
||||
PP_EXPECT(h, duplicated.ok());
|
||||
@@ -174,6 +323,7 @@ void manages_animation_frames_and_duration(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, document.frames()[2].duration_ms == 250U);
|
||||
PP_EXPECT(h, document.set_frame_duration(2, 333).ok());
|
||||
PP_EXPECT(h, document.frames()[2].duration_ms == 333U);
|
||||
PP_EXPECT(h, document.layers()[0].frames[2].duration_ms == 333U);
|
||||
PP_EXPECT(h, document.animation_duration_ms() == 683U);
|
||||
|
||||
PP_EXPECT(h, document.remove_frame(1).ok());
|
||||
@@ -244,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(
|
||||
@@ -366,9 +625,15 @@ int main()
|
||||
harness.run("moves_layers_and_preserves_active_layer_identity", moves_layers_and_preserves_active_layer_identity);
|
||||
harness.run("updates_layer_metadata", updates_layer_metadata);
|
||||
harness.run("rejects_invalid_layer_metadata", rejects_invalid_layer_metadata);
|
||||
harness.run("creates_document_from_snapshot_metadata", creates_document_from_snapshot_metadata);
|
||||
harness.run("preserves_per_layer_snapshot_timelines", preserves_per_layer_snapshot_timelines);
|
||||
harness.run("rejects_invalid_snapshot_metadata", rejects_invalid_snapshot_metadata);
|
||||
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();
|
||||
}
|
||||
86
tests/paint/stroke_script_tests.cpp
Normal file
86
tests/paint/stroke_script_tests.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "paint/stroke_script.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::paint::max_stroke_script_bytes;
|
||||
using pp::paint::max_stroke_script_line_length;
|
||||
using pp::paint::parse_stroke_script;
|
||||
|
||||
namespace {
|
||||
|
||||
void parses_comments_and_multiple_strokes(pp::tests::Harness& h)
|
||||
{
|
||||
constexpr std::string_view script_text =
|
||||
"# scripted automation fixture\n"
|
||||
"stroke 0 0 0.25 10 0 0.75 2\n"
|
||||
"\n"
|
||||
"stroke 10 0 1 10 10 0.5 5 # trailing comment\n";
|
||||
|
||||
const auto script = parse_stroke_script(script_text);
|
||||
|
||||
PP_EXPECT(h, script.ok());
|
||||
PP_EXPECT(h, script.value().strokes.size() == 2U);
|
||||
PP_EXPECT(h, script.value().strokes[0].start.x == 0.0F);
|
||||
PP_EXPECT(h, script.value().strokes[0].start.pressure == 0.25F);
|
||||
PP_EXPECT(h, script.value().strokes[0].end.x == 10.0F);
|
||||
PP_EXPECT(h, script.value().strokes[0].spacing == 2.0F);
|
||||
PP_EXPECT(h, script.value().strokes[1].end.y == 10.0F);
|
||||
PP_EXPECT(h, script.value().strokes[1].end.pressure == 0.5F);
|
||||
}
|
||||
|
||||
void rejects_malformed_stroke_scripts(pp::tests::Harness& h)
|
||||
{
|
||||
const auto empty = parse_stroke_script("");
|
||||
const auto comments_only = parse_stroke_script("# nope\n\n");
|
||||
const auto unknown = parse_stroke_script("move 0 0 1 10 0 1 2\n");
|
||||
const auto missing_tokens = parse_stroke_script("stroke 0 0 1 10 0 1\n");
|
||||
const auto too_many_tokens = parse_stroke_script("stroke 0 0 1 10 0 1 2 extra\n");
|
||||
const auto bad_number = parse_stroke_script("stroke 0 0 1 10 nope 1 2\n");
|
||||
const auto nan_number = parse_stroke_script("stroke 0 0 1 10 nan 1 2\n");
|
||||
const auto zero_spacing = parse_stroke_script("stroke 0 0 1 10 0 1 0\n");
|
||||
|
||||
PP_EXPECT(h, !empty.ok());
|
||||
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !comments_only.ok());
|
||||
PP_EXPECT(h, comments_only.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !unknown.ok());
|
||||
PP_EXPECT(h, unknown.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !missing_tokens.ok());
|
||||
PP_EXPECT(h, missing_tokens.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !too_many_tokens.ok());
|
||||
PP_EXPECT(h, too_many_tokens.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !bad_number.ok());
|
||||
PP_EXPECT(h, bad_number.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !nan_number.ok());
|
||||
PP_EXPECT(h, nan_number.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !zero_spacing.ok());
|
||||
PP_EXPECT(h, zero_spacing.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_oversized_stroke_scripts(pp::tests::Harness& h)
|
||||
{
|
||||
const std::string oversized_script(max_stroke_script_bytes + 1U, 'x');
|
||||
const std::string oversized_line(max_stroke_script_line_length + 1U, 'x');
|
||||
|
||||
const auto too_large_script = parse_stroke_script(oversized_script);
|
||||
const auto too_large_line = parse_stroke_script(oversized_line);
|
||||
|
||||
PP_EXPECT(h, !too_large_script.ok());
|
||||
PP_EXPECT(h, too_large_script.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !too_large_line.ok());
|
||||
PP_EXPECT(h, too_large_line.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("parses_comments_and_multiple_strokes", parses_comments_and_multiple_strokes);
|
||||
harness.run("rejects_malformed_stroke_scripts", rejects_malformed_stroke_scripts);
|
||||
harness.run("rejects_oversized_stroke_scripts", rejects_oversized_stroke_scripts);
|
||||
return harness.finish();
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
#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"
|
||||
#include "paint/stroke_script.h"
|
||||
#include "ui_core/layout_xml.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -37,12 +40,66 @@ struct InspectProjectArgs {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct SimulateStrokeArgs {
|
||||
std::uint32_t x1 = 0;
|
||||
std::uint32_t y1 = 0;
|
||||
std::uint32_t x2 = 0;
|
||||
std::uint32_t y2 = 0;
|
||||
std::uint32_t spacing = 1;
|
||||
};
|
||||
|
||||
struct SimulateStrokeScriptArgs {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
void print_error(std::string_view command, std::string_view message)
|
||||
{
|
||||
std::cout << "{\"ok\":false,\"command\":\"" << command
|
||||
<< "\",\"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
|
||||
@@ -50,7 +107,10 @@ void print_help()
|
||||
<< " create-document --width N --height N [--layers N] [--frames N] [--frame-duration-ms N]\n"
|
||||
<< " inspect-image --path FILE\n"
|
||||
<< " inspect-project --path FILE\n"
|
||||
<< " load-project --path FILE\n"
|
||||
<< " parse-layout --path FILE\n"
|
||||
<< " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n"
|
||||
<< " simulate-stroke-script --path FILE\n"
|
||||
<< " --help\n";
|
||||
}
|
||||
|
||||
@@ -271,23 +331,296 @@ int inspect_project(int argc, char** argv)
|
||||
std::istreambuf_iterator<char>()
|
||||
};
|
||||
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
|
||||
const auto header = pp::assets::parse_ppi_header(std::span<const std::byte>(data, chars.size()));
|
||||
if (!header) {
|
||||
print_error("inspect-project", header.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\":\"" << header.value().document_version.major
|
||||
<< "." << header.value().document_version.minor << "\""
|
||||
<< ",\"softwareVersion\":\"" << header.value().software_version.major
|
||||
<< "." << header.value().software_version.minor
|
||||
<< "." << header.value().software_version.fix
|
||||
<< "." << header.value().software_version.build << "\""
|
||||
<< ",\"thumbnail\":{\"width\":" << header.value().thumbnail.width
|
||||
<< ",\"height\":" << header.value().thumbnail.height
|
||||
<< ",\"components\":" << header.value().thumbnail.components
|
||||
<< "}}\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;
|
||||
}
|
||||
|
||||
int load_project(int argc, char** argv)
|
||||
{
|
||||
InspectProjectArgs args;
|
||||
const auto status = parse_inspect_project_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("load-project", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::ifstream stream(args.path, std::ios::binary);
|
||||
if (!stream) {
|
||||
print_error("load-project", "project file could not be opened");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const std::string chars {
|
||||
std::istreambuf_iterator<char>(stream),
|
||||
std::istreambuf_iterator<char>()
|
||||
};
|
||||
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
|
||||
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 = pp::document::import_ppi_project_document(project.value());
|
||||
if (!document_result) {
|
||||
print_error("load-project", document_result.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto& document = document_result.value();
|
||||
std::cout << "{\"ok\":true,\"command\":\"load-project\""
|
||||
<< ",\"source\":\"ppi\""
|
||||
<< ",\"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()
|
||||
<< ",\"frames\":" << document.frames().size()
|
||||
<< ",\"animationDurationMs\":" << document.animation_duration_ms()
|
||||
<< ",\"layerNames\":[";
|
||||
for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) {
|
||||
if (layer_index != 0U) {
|
||||
std::cout << ",";
|
||||
}
|
||||
std::cout << "\"" << json_escape(document.layers()[layer_index].name) << "\"";
|
||||
}
|
||||
std::cout << "],\"layerFrameCounts\":[";
|
||||
for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) {
|
||||
if (layer_index != 0U) {
|
||||
std::cout << ",";
|
||||
}
|
||||
std::cout << document.layers()[layer_index].frames.size();
|
||||
}
|
||||
std::cout << "],\"layerDurationsMs\":[";
|
||||
for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) {
|
||||
if (layer_index != 0U) {
|
||||
std::cout << ",";
|
||||
}
|
||||
const auto duration = document.layer_animation_duration_ms(layer_index);
|
||||
std::cout << (duration ? duration.value() : 0U);
|
||||
}
|
||||
std::cout << "]}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_simulate_stroke_args(int argc, char** argv, SimulateStrokeArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string_view key(argv[i]);
|
||||
if (key == "--x1" || key == "--y1" || key == "--x2" || key == "--y2" || key == "--spacing") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
|
||||
const auto value = pp::foundation::parse_u32(argv[++i]);
|
||||
if (!value) {
|
||||
return value.status();
|
||||
}
|
||||
|
||||
if (key == "--x1") {
|
||||
args.x1 = value.value();
|
||||
} else if (key == "--y1") {
|
||||
args.y1 = value.value();
|
||||
} else if (key == "--x2") {
|
||||
args.x2 = value.value();
|
||||
} else if (key == "--y2") {
|
||||
args.y2 = value.value();
|
||||
} else {
|
||||
args.spacing = value.value();
|
||||
}
|
||||
} else {
|
||||
return pp::foundation::Status::invalid_argument("unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
if (args.spacing == 0) {
|
||||
return pp::foundation::Status::invalid_argument("stroke spacing must be greater than zero");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
int simulate_stroke(int argc, char** argv)
|
||||
{
|
||||
SimulateStrokeArgs args;
|
||||
const auto status = parse_simulate_stroke_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("simulate-stroke", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const pp::paint::StrokePoint points[] {
|
||||
pp::paint::StrokePoint {
|
||||
.x = static_cast<float>(args.x1),
|
||||
.y = static_cast<float>(args.y1),
|
||||
.pressure = 1.0F,
|
||||
},
|
||||
pp::paint::StrokePoint {
|
||||
.x = static_cast<float>(args.x2),
|
||||
.y = static_cast<float>(args.y2),
|
||||
.pressure = 1.0F,
|
||||
},
|
||||
};
|
||||
const auto samples = pp::paint::sample_stroke(
|
||||
points,
|
||||
pp::paint::StrokeSamplingConfig {
|
||||
.spacing = static_cast<float>(args.spacing),
|
||||
});
|
||||
if (!samples) {
|
||||
print_error("simulate-stroke", samples.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto& first = samples.value().front();
|
||||
const auto& last = samples.value().back();
|
||||
std::cout << "{\"ok\":true,\"command\":\"simulate-stroke\""
|
||||
<< ",\"samples\":" << samples.value().size()
|
||||
<< ",\"first\":{\"x\":" << first.x << ",\"y\":" << first.y << "}"
|
||||
<< ",\"last\":{\"x\":" << last.x << ",\"y\":" << last.y
|
||||
<< ",\"distance\":" << last.distance << "}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_simulate_stroke_script_args(int argc, char** argv, SimulateStrokeScriptArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string_view key(argv[i]);
|
||||
if (key == "--path") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
|
||||
args.path = argv[++i];
|
||||
} else {
|
||||
return pp::foundation::Status::invalid_argument("unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
if (args.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("path must not be empty");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
int simulate_stroke_script(int argc, char** argv)
|
||||
{
|
||||
SimulateStrokeScriptArgs args;
|
||||
const auto status = parse_simulate_stroke_script_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("simulate-stroke-script", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::ifstream stream(args.path, std::ios::binary);
|
||||
if (!stream) {
|
||||
print_error("simulate-stroke-script", "stroke script file could not be opened");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const std::string text {
|
||||
std::istreambuf_iterator<char>(stream),
|
||||
std::istreambuf_iterator<char>()
|
||||
};
|
||||
const auto script = pp::paint::parse_stroke_script(text);
|
||||
if (!script) {
|
||||
print_error("simulate-stroke-script", script.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::size_t total_samples = 0;
|
||||
float total_distance = 0.0F;
|
||||
for (const auto& stroke : script.value().strokes) {
|
||||
const pp::paint::StrokePoint points[] { stroke.start, stroke.end };
|
||||
const auto samples = pp::paint::sample_stroke(
|
||||
points,
|
||||
pp::paint::StrokeSamplingConfig {
|
||||
.spacing = stroke.spacing,
|
||||
});
|
||||
if (!samples) {
|
||||
print_error("simulate-stroke-script", samples.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
total_samples += samples.value().size();
|
||||
total_distance += samples.value().back().distance;
|
||||
}
|
||||
|
||||
std::cout << "{\"ok\":true,\"command\":\"simulate-stroke-script\""
|
||||
<< ",\"strokes\":" << script.value().strokes.size()
|
||||
<< ",\"samples\":" << total_samples
|
||||
<< ",\"distance\":" << total_distance << "}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -372,6 +705,18 @@ int main(int argc, char** argv)
|
||||
return inspect_project(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "load-project") {
|
||||
return load_project(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "simulate-stroke") {
|
||||
return simulate_stroke(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "simulate-stroke-script") {
|
||||
return simulate_stroke_script(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "parse-layout") {
|
||||
return parse_layout(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user