#include "assets/ppi_header.h" #include "assets/image_metadata.h" #include "foundation/binary_stream.h" #include #include #include #include #include #include #include namespace pp::assets { namespace { [[nodiscard]] pp::foundation::Result read_u32(pp::foundation::ByteReader& reader) noexcept { return reader.read_u32_le(); } [[nodiscard]] pp::foundation::Result 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::numeric_limits::max())) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range(message)); } return value; } [[nodiscard]] pp::foundation::Result read_f32(pp::foundation::ByteReader& reader) noexcept { const auto bits = reader.read_u32_le(); if (!bits) { return pp::foundation::Result::failure(bits.status()); } return pp::foundation::Result::success(std::bit_cast(bits.value())); } void append_u32(std::vector& bytes, std::uint32_t value) { bytes.push_back(static_cast(value & 0xffU)); bytes.push_back(static_cast((value >> 8U) & 0xffU)); bytes.push_back(static_cast((value >> 16U) & 0xffU)); bytes.push_back(static_cast((value >> 24U) & 0xffU)); } void append_f32(std::vector& bytes, float value) { append_u32(bytes, std::bit_cast(value)); } void append_ascii(std::vector& bytes, std::string_view value) { for (const auto ch : value) { bytes.push_back(static_cast(ch)); } } [[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]] std::string generated_layer_name(std::string_view base_name, std::uint32_t layer_index, std::uint32_t layer_count) { if (layer_count == 1U) { return std::string(base_name); } std::string name(base_name); name.push_back(' '); name += std::to_string(layer_index + 1U); return name; } [[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept { const auto next = summary.compressed_face_bytes + static_cast(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 validate_face_png_payload( std::span payload, std::uint32_t width, std::uint32_t height) noexcept { const auto metadata = parse_png_metadata(payload); if (!metadata) { return pp::foundation::Result::failure(metadata.status()); } if (metadata.value().width != width || metadata.value().height != height) { return pp::foundation::Result::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::failure( pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA")); } return pp::foundation::Result::success(metadata.value()); } } pp::foundation::Result parse_ppi_header(std::span bytes) noexcept { if (bytes.size() < ppi_header_size) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("PPI header is truncated")); } pp::foundation::ByteReader reader(bytes.subspan(0, ppi_header_size)); const auto magic = reader.read_bytes(4); if (!magic || magic.value()[0] != std::byte { 'P' } || magic.value()[1] != std::byte { 'P' } || magic.value()[2] != std::byte { 'I' } || magic.value()[3] != std::byte { 0 }) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPI header magic is invalid")); } PpiHeaderInfo info; const auto doc_major = read_u32(reader); const auto doc_minor = read_u32(reader); const auto soft_major = read_u32(reader); const auto soft_minor = read_u32(reader); const auto soft_fix = read_u32(reader); const auto soft_build = read_u32(reader); const auto thumb_width = read_u32(reader); const auto thumb_height = read_u32(reader); const auto thumb_components = read_u32(reader); if (!doc_major || !doc_minor || !soft_major || !soft_minor || !soft_fix || !soft_build || !thumb_width || !thumb_height || !thumb_components) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("PPI header is truncated")); } info.document_version = { doc_major.value(), doc_minor.value() }; info.software_version = { soft_major.value(), soft_minor.value(), soft_fix.value(), soft_build.value(), }; info.thumbnail = { thumb_width.value(), thumb_height.value(), thumb_components.value(), }; if (info.document_version.major != 0 || info.document_version.minor < 1) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPI document version is unsupported")); } if (info.thumbnail.width != 128 || info.thumbnail.height != 128 || info.thumbnail.components != 4) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid")); } return pp::foundation::Result::success(info); } pp::foundation::Result ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept { if (thumbnail.width == 0 || thumbnail.height == 0 || thumbnail.components == 0) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid")); } const auto width = static_cast(thumbnail.width); const auto height = static_cast(thumbnail.height); const auto components = static_cast(thumbnail.components); if (width > std::numeric_limits::max() / height) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows")); } const auto pixels = width * height; if (pixels > std::numeric_limits::max() / components) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows")); } const auto bytes = pixels * components; if (bytes > static_cast(std::numeric_limits::max())) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("PPI thumbnail byte size exceeds addressable memory")); } return pp::foundation::Result::success(static_cast(bytes)); } pp::foundation::Result parse_ppi_project_layout(std::span bytes) noexcept { const auto header = parse_ppi_header(bytes); if (!header) { return pp::foundation::Result::failure(header.status()); } const auto thumbnail_bytes = ppi_thumbnail_byte_size(header.value().thumbnail); if (!thumbnail_bytes) { return pp::foundation::Result::failure(thumbnail_bytes.status()); } if (thumbnail_bytes.value() > std::numeric_limits::max() - ppi_header_size) { return pp::foundation::Result::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::failure( pp::foundation::Status::out_of_range("PPI thumbnail payload is truncated")); } return pp::foundation::Result::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 parse_ppi_body_impl( PpiHeaderInfo header, std::span 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::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::failure(canvas_status); } if (layer_count.value() == 0 || layer_count.value() > max_ppi_layer_count) { return pp::foundation::Result::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 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::failure(declared_frames.status()); } if (declared_frames.value() == 0 || declared_frames.value() > max_ppi_frame_count) { return pp::foundation::Result::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::failure( !order ? order.status() : (!opacity ? opacity.status() : name_length.status())); } if (order.value() >= summary.layer_count) { return pp::foundation::Result::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::failure( pp::foundation::Status::invalid_argument("PPI layer order is duplicated")); } seen_orders[order.value()] = true; } if (!std::isfinite(opacity.value())) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPI layer opacity must be finite")); } if (opacity.value() < 0.0F || opacity.value() > 1.0F) { return pp::foundation::Result::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::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::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(std::to_integer(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::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::failure( pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid")); } if (blend_mode.value() > 4U) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range")); } 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::failure(frame_count.status()); } if (frame_count.value() == 0 || frame_count.value() > max_ppi_frame_count) { return pp::foundation::Result::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::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::failure(duration.status()); } if (duration.value() == 0) { return pp::foundation::Result::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::failure(has_data.status()); } if (has_data.value() > 1U) { return pp::foundation::Result::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::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::failure( pp::foundation::Status::out_of_range("PPI dirty box is outside the canvas")); } if (data_size.value() == 0U) { return pp::foundation::Result::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::failure(byte_status); } const auto payload_offset = reader.position(); const auto payload = reader.read_bytes(data_size.value()); if (!payload) { return pp::foundation::Result::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::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(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::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::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::failure(info_status); } } if (!reader.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPI body has trailing bytes")); } if (index != nullptr) { index->summary = summary; } return pp::foundation::Result::success(summary); } } pp::foundation::Result parse_ppi_body_summary( PpiHeaderInfo header, std::span body) noexcept { return parse_ppi_body_impl(header, body, nullptr); } pp::foundation::Result parse_ppi_body_index( PpiHeaderInfo header, std::span body) { PpiBodyIndex index; const auto summary = parse_ppi_body_impl(header, body, &index); if (!summary) { return pp::foundation::Result::failure(summary.status()); } return pp::foundation::Result::success(std::move(index)); } pp::foundation::Result parse_ppi_project_summary(std::span bytes) noexcept { const auto layout = parse_ppi_project_layout(bytes); if (!layout) { return pp::foundation::Result::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::failure(body.status()); } return pp::foundation::Result::success(PpiProjectSummary { .layout = layout.value(), .body = body.value(), }); } pp::foundation::Result parse_ppi_project_index(std::span bytes) { const auto layout = parse_ppi_project_layout(bytes); if (!layout) { return pp::foundation::Result::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::failure(body.status()); } return pp::foundation::Result::success(PpiProjectIndex { .layout = layout.value(), .body = body.value(), }); } pp::foundation::Result decode_ppi_project_images(std::span bytes) { auto project = parse_ppi_project_index(bytes); if (!project) { return pp::foundation::Result::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::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::failure(image.status()); } if (image.value().width != face.png_width || image.value().height != face.png_height) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed")); } decoded.faces.push_back(PpiDecodedFacePayload { .layer_index = static_cast(layer_index), .frame_index = static_cast(frame_index), .face_index = static_cast(face_index), .descriptor = face, .image = image.value(), }); } } } return pp::foundation::Result::success(std::move(decoded)); } pp::foundation::Result> create_ppi_project(PpiProjectConfig config) { const auto canvas_status = validate_canvas_size(config.width, config.height); if (!canvas_status.ok()) { return pp::foundation::Result>::failure(canvas_status); } if (config.layers.empty() || config.layers.size() > max_ppi_layer_count) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer count is outside the configured range")); } std::uint32_t total_frame_count = 0; std::vector layer_frame_offsets; layer_frame_offsets.reserve(config.layers.size()); for (const auto& layer : config.layers) { if (layer.name.empty()) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("PPI layer name must not be empty")); } if (layer.name.size() > max_ppi_layer_name_length) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit")); } if (!std::isfinite(layer.metadata.opacity)) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("PPI layer opacity must be finite")); } if (layer.metadata.opacity < 0.0F || layer.metadata.opacity > 1.0F) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range")); } if (layer.metadata.blend_mode > 4U) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range")); } if (layer.frames.empty() || layer.frames.size() > max_ppi_frame_count) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range")); } if (layer.frames.size() > max_ppi_frame_count - total_frame_count) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI total layer frame count exceeds the configured range")); } layer_frame_offsets.push_back(total_frame_count); total_frame_count += static_cast(layer.frames.size()); for (const auto& frame : layer.frames) { if (frame.duration_ms == 0) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero")); } } } std::vector> seen_faces(total_frame_count); std::uint64_t total_payload_bytes = 0; for (const auto& face : config.dirty_faces) { if (face.layer_index >= config.layers.size()) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI dirty face layer index is outside the layer list")); } if (face.frame_index >= config.layers[face.layer_index].frames.size()) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI dirty face frame index is outside the frame list")); } if (face.face_index >= 6U) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI dirty face index is outside the cube face list")); } const auto slot_index = layer_frame_offsets[face.layer_index] + static_cast(face.frame_index); if (seen_faces[slot_index][face.face_index]) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("PPI dirty face slot is duplicated")); } seen_faces[slot_index][face.face_index] = true; if (face.width == 0 || face.height == 0) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("PPI dirty face dimensions must be greater than zero")); } if (face.x > config.width || face.width > config.width - face.x || face.y > config.height || face.height > config.height - face.y) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI dirty face box is outside the canvas")); } if (face.png_rgba8.empty()) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("PPI dirty face PNG payload must not be empty")); } if (face.png_rgba8.size() > static_cast(std::numeric_limits::max())) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI dirty face PNG payload is too large")); } const auto next_payload_bytes = total_payload_bytes + face.png_rgba8.size(); if (next_payload_bytes > max_ppi_face_payload_bytes) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI dirty face PNG payloads exceed the configured limit")); } total_payload_bytes = next_payload_bytes; const auto metadata = validate_face_png_payload(face.png_rgba8, face.width, face.height); if (!metadata) { return pp::foundation::Result>::failure(metadata.status()); } } std::vector 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, 0); append_u32(bytes, 0); append_u32(bytes, 0); append_u32(bytes, 128); append_u32(bytes, 128); append_u32(bytes, 4); constexpr std::size_t thumbnail_bytes = 128U * 128U * 4U; bytes.resize(ppi_header_size + thumbnail_bytes, std::byte { 0 }); append_u32(bytes, config.width); append_u32(bytes, config.height); append_u32(bytes, static_cast(config.layers.size())); append_u32(bytes, total_frame_count); for (std::uint32_t layer = 0; layer < config.layers.size(); ++layer) { const auto& layer_config = config.layers[layer]; append_u32(bytes, layer); append_f32(bytes, layer_config.metadata.opacity); append_u32(bytes, static_cast(layer_config.name.size())); append_ascii(bytes, layer_config.name); append_u32(bytes, layer_config.metadata.blend_mode); bytes.push_back(layer_config.metadata.alpha_locked ? std::byte { 1 } : std::byte { 0 }); bytes.push_back(layer_config.metadata.visible ? std::byte { 1 } : std::byte { 0 }); append_u32(bytes, static_cast(layer_config.frames.size())); for (std::uint32_t frame = 0; frame < layer_config.frames.size(); ++frame) { append_u32(bytes, layer_config.frames[frame].duration_ms); for (std::uint32_t face = 0; face < 6U; ++face) { const PpiDirtyFacePayloadConfig* dirty_face = nullptr; for (const auto& candidate : config.dirty_faces) { if (candidate.layer_index == layer && candidate.frame_index == frame && candidate.face_index == face) { dirty_face = &candidate; break; } } if (dirty_face == nullptr) { append_u32(bytes, 0); continue; } append_u32(bytes, 1); append_u32(bytes, dirty_face->x); append_u32(bytes, dirty_face->y); append_u32(bytes, dirty_face->x + dirty_face->width); append_u32(bytes, dirty_face->y + dirty_face->height); append_u32(bytes, static_cast(dirty_face->png_rgba8.size())); bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end()); } } } append_u32(bytes, 0); return pp::foundation::Result>::success(std::move(bytes)); } pp::foundation::Result> create_minimal_ppi_project(PpiMinimalProjectConfig config) { if (config.layer_count == 0 || config.layer_count > max_ppi_layer_count) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer count is outside the configured range")); } if (config.frame_count == 0 || config.frame_count > max_ppi_frame_count) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI frame count is outside the configured range")); } std::vector names; names.reserve(config.layer_count); std::vector> frame_lists; frame_lists.reserve(config.layer_count); std::vector layers; layers.reserve(config.layer_count); for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) { names.push_back(generated_layer_name(config.layer_name, layer, config.layer_count)); auto& frames = frame_lists.emplace_back(); frames.assign(config.frame_count, PpiFrameConfig { .duration_ms = config.frame_duration_ms }); layers.push_back(PpiLayerConfig { .name = names.back(), .metadata = config.layer_metadata, .frames = std::span(frames.data(), frames.size()), }); } return create_ppi_project(PpiProjectConfig { .width = config.width, .height = config.height, .layers = std::span(layers.data(), layers.size()), .dirty_faces = config.dirty_faces, }); } }