Program Listing for File WebpWriter.hpp

Return to documentation for file (include\util\function\image\WebpWriter.hpp)

#pragma once

#include "util/common/Result.hpp"
#include "util/function/FunctionContext.hpp"

#include <webp/encode.h>

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <span>
#include <string>
#include <string_view>
#include <vector>

namespace PDJE_UTIL::function::image {

enum class RasterPixelFormat { gray8, gray_alpha8, rgb8, rgba8 };

struct RasterImageView {
    std::span<const std::uint8_t> pixels       = {};
    std::size_t                   width        = 0;
    std::size_t                   height       = 0;
    std::size_t                   stride       = 0;
    RasterPixelFormat            pixel_format = RasterPixelFormat::rgba8;
};

struct EncodeWebpArgs {
    RasterImageView image;
    int             compression_level = -1;
};

struct WriteWebpArgs {
    RasterImageView       image;
    std::filesystem::path output_path;
    int                   compression_level = -1;
};

namespace detail {

struct ImageLayout {
    std::size_t row_bytes        = 0;
    std::size_t effective_stride = 0;
};

inline bool
checked_multiply(std::size_t lhs, std::size_t rhs, std::size_t &result) noexcept
{
    if (lhs != 0 && rhs > (std::numeric_limits<std::size_t>::max() / lhs)) {
        return false;
    }

    result = lhs * rhs;
    return true;
}

inline bool
checked_add(std::size_t lhs, std::size_t rhs, std::size_t &result) noexcept
{
    if (rhs > (std::numeric_limits<std::size_t>::max() - lhs)) {
        return false;
    }

    result = lhs + rhs;
    return true;
}

inline constexpr std::size_t
bytes_per_pixel(RasterPixelFormat pixel_format) noexcept
{
    switch (pixel_format) {
    case RasterPixelFormat::gray8:
        return 1;
    case RasterPixelFormat::gray_alpha8:
        return 2;
    case RasterPixelFormat::rgb8:
        return 3;
    case RasterPixelFormat::rgba8:
        return 4;
    }

    return 0;
}

inline common::Result<ImageLayout>
validate_image(const RasterImageView &image)
{
    if (image.pixels.data() == nullptr) {
        return common::Result<ImageLayout>::failure(
            { common::StatusCode::invalid_argument,
              "RasterImageView.pixels must reference valid image data." });
    }

    if (image.width == 0 || image.height == 0) {
        return common::Result<ImageLayout>::failure(
            { common::StatusCode::invalid_argument,
              "RasterImageView width and height must be greater than zero." });
    }

    if (image.width > 16383 || image.height > 16383) {
        return common::Result<ImageLayout>::failure(
            { common::StatusCode::invalid_argument,
              "RasterImageView dimensions must fit within WebP limits." });
    }

    ImageLayout layout;
    if (!checked_multiply(image.width,
                          bytes_per_pixel(image.pixel_format),
                          layout.row_bytes)) {
        return common::Result<ImageLayout>::failure(
            { common::StatusCode::invalid_argument,
              "RasterImageView row size overflows size_t." });
    }

    layout.effective_stride =
        image.stride == 0 ? layout.row_bytes : image.stride;
    if (layout.effective_stride < layout.row_bytes) {
        return common::Result<ImageLayout>::failure(
            { common::StatusCode::invalid_argument,
              "RasterImageView.stride must be zero or greater than or equal to "
              "the packed row size." });
    }

    std::size_t required_bytes = layout.row_bytes;
    if (image.height > 1) {
        std::size_t tail_bytes = 0;
        if (!checked_multiply(
                layout.effective_stride, image.height - 1, tail_bytes) ||
            !checked_add(tail_bytes, layout.row_bytes, required_bytes)) {
            return common::Result<ImageLayout>::failure(
                { common::StatusCode::invalid_argument,
                  "RasterImageView buffer size calculation overflows "
                  "size_t." });
        }
    }

    if (image.pixels.size() < required_bytes) {
        return common::Result<ImageLayout>::failure(
            { common::StatusCode::invalid_argument,
              "RasterImageView.pixels is smaller than the specified width, "
              "height, and stride require." });
    }

    return common::Result<ImageLayout>::success(layout);
}

inline common::Result<std::vector<std::uint8_t>>
pack_rgba(const RasterImageView &image, const ImageLayout &layout)
{
    std::size_t packed_size = 0;
    if (!checked_multiply(
            image.width * image.height, std::size_t{ 4 }, packed_size)) {
        return common::Result<std::vector<std::uint8_t>>::failure(
            { common::StatusCode::invalid_argument,
              "RasterImageView packed RGBA buffer size overflows size_t." });
    }

    std::vector<std::uint8_t> rgba(packed_size, 0);
    for (std::size_t y = 0; y < image.height; ++y) {
        const auto *src_row =
            image.pixels.data() + (y * layout.effective_stride);
        auto *dst_row = rgba.data() + ((y * image.width) * 4);

        for (std::size_t x = 0; x < image.width; ++x) {
            const auto *src =
                src_row + (x * bytes_per_pixel(image.pixel_format));
            auto *dst = dst_row + (x * 4);

            switch (image.pixel_format) {
            case RasterPixelFormat::gray8:
                dst[0] = src[0];
                dst[1] = src[0];
                dst[2] = src[0];
                dst[3] = 255;
                break;
            case RasterPixelFormat::gray_alpha8:
                dst[0] = src[0];
                dst[1] = src[0];
                dst[2] = src[0];
                dst[3] = src[1];
                break;
            case RasterPixelFormat::rgb8:
                dst[0] = src[0];
                dst[1] = src[1];
                dst[2] = src[2];
                dst[3] = 255;
                break;
            case RasterPixelFormat::rgba8:
                dst[0] = src[0];
                dst[1] = src[1];
                dst[2] = src[2];
                dst[3] = src[3];
                break;
            }
        }
    }

    return common::Result<std::vector<std::uint8_t>>::success(std::move(rgba));
}

} // namespace detail

inline common::Result<std::vector<std::uint8_t>>
encode_webp(const EncodeWebpArgs &args, function::EvalOptions options = {})
{
    (void)options;

    if (args.compression_level < -1 || args.compression_level > 9) {
        return common::Result<std::vector<std::uint8_t>>::failure(
            { common::StatusCode::invalid_argument,
              "EncodeWebpArgs.compression_level must be between -1 and 9." });
    }

    auto layout = detail::validate_image(args.image);
    if (!layout.ok()) {
        return common::Result<std::vector<std::uint8_t>>::failure(
            layout.status());
    }

    auto packed_rgba = detail::pack_rgba(args.image, layout.value());
    if (!packed_rgba.ok()) {
        return common::Result<std::vector<std::uint8_t>>::failure(
            packed_rgba.status());
    }

    std::uint8_t *encoded_bytes = nullptr;
    const auto    encoded_size =
        WebPEncodeLosslessRGBA(packed_rgba.value().data(),
                               static_cast<int>(args.image.width),
                               static_cast<int>(args.image.height),
                               static_cast<int>(args.image.width * 4),
                               &encoded_bytes);
    if (encoded_size == 0 || encoded_bytes == nullptr) {
        return common::Result<std::vector<std::uint8_t>>::failure(
            { common::StatusCode::internal_error,
              "WebPEncodeLosslessRGBA() failed while encoding the image." });
    }

    std::vector<std::uint8_t> bytes(encoded_size);
    std::memcpy(bytes.data(), encoded_bytes, encoded_size);
    WebPFree(encoded_bytes);

    return common::Result<std::vector<std::uint8_t>>::success(std::move(bytes));
}

inline common::Result<void>
write_webp(const WriteWebpArgs &args, function::EvalOptions options = {})
{
    (void)options;

    if (args.output_path.empty()) {
        return common::Result<void>::failure(
            { common::StatusCode::invalid_argument,
              "WriteWebpArgs.output_path must not be empty." });
    }

    auto encoded = encode_webp(
        { .image = args.image, .compression_level = args.compression_level });
    if (!encoded.ok()) {
        return common::Result<void>::failure(encoded.status());
    }

    std::ofstream output(args.output_path, std::ios::binary | std::ios::trunc);
    if (!output.is_open()) {
        return common::Result<void>::failure(
            { common::StatusCode::io_error,
              "Failed to open the WebP output path for writing." });
    }

    const auto &bytes = encoded.value();
    output.write(reinterpret_cast<const char *>(bytes.data()),
                 static_cast<std::streamsize>(bytes.size()));
    if (!output.good()) {
        return common::Result<void>::failure(
            { common::StatusCode::io_error,
              "Failed while writing WebP bytes to the output path." });
    }

    return common::Result<void>::success();
}

} // namespace PDJE_UTIL::function::image