BareGit
#include "image_processor.h"

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <limits>
#include <list>
#include <string>
#include <string_view>
#include <system_error>

#include <Magick++.h>
#include <spdlog/spdlog.h>

namespace
{

inline constexpr std::uint32_t MAX_IMAGE_SIDE = 2048;

struct ImageFormat
{
    std::string extension;
    std::string coder;
};

mw::E<ImageFormat> sniffFormat(const std::filesystem::path& input)
{
    std::array<unsigned char, 32> bytes{};
    std::ifstream stream(input, std::ios::binary);
    if(!stream)
    {
        return std::unexpected(
            mw::runtimeError("The uploaded image could not be opened"));
    }
    stream.read(
        reinterpret_cast<char*>(bytes.data()),
        static_cast<std::streamsize>(bytes.size()));
    const std::size_t size = static_cast<std::size_t>(stream.gcount());

    const std::array<unsigned char, 8> png = {
        0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
    if(size >= png.size() &&
       std::equal(png.begin(), png.end(), bytes.begin()))
    {
        return ImageFormat{"png", "PNG"};
    }
    if(size >= 3 && bytes[0] == 0xff && bytes[1] == 0xd8 &&
       bytes[2] == 0xff)
    {
        return ImageFormat{"jpg", "JPEG"};
    }
    if(size >= 12 &&
       std::string_view(
           reinterpret_cast<const char*>(bytes.data()), 4) == "RIFF" &&
       std::string_view(
           reinterpret_cast<const char*>(bytes.data() + 8), 4) == "WEBP")
    {
        return ImageFormat{"webp", "WEBP"};
    }
    if(size >= 16 &&
       std::string_view(
           reinterpret_cast<const char*>(bytes.data() + 4), 4) == "ftyp")
    {
        for(std::size_t offset = 8; offset + 4 <= size; offset += 4)
        {
            const std::string_view brand(
                reinterpret_cast<const char*>(bytes.data() + offset), 4);
            if(brand == "avif" || brand == "avis")
            {
                return ImageFormat{"avif", "AVIF"};
            }
        }
    }
    return std::unexpected(mw::httpError(
        415,
        "Card images must be JPEG, PNG, WebP, or AVIF files"));
}

std::string basename(CardAssetType type)
{
    switch(type)
    {
    case CardAssetType::FRONT_ART:
        return "front-art";
    case CardAssetType::FOIL_CONTROL:
        return "foil";
    case CardAssetType::THUMBNAIL:
        return "thumb";
    }
    return "image";
}

mw::E<Magick::Image> decodeSingleImage(
    const std::filesystem::path& input,
    const ImageFormat& format)
{
    try
    {
        std::list<Magick::Image> images;
        Magick::readImages(
            &images, format.coder + ":" + input.string());
        if(images.size() != 1)
        {
            return std::unexpected(mw::httpError(
                422,
                "Animated and multi-frame images are not supported"));
        }
        Magick::Image image = std::move(images.front());
        if(image.magick() != format.coder)
        {
            return std::unexpected(mw::httpError(
                415,
                "The uploaded image does not match its file signature"));
        }
        if(image.columns() == 0 || image.rows() == 0 ||
           image.columns() > MAX_IMAGE_SIDE ||
           image.rows() > MAX_IMAGE_SIDE)
        {
            return std::unexpected(mw::httpError(
                422,
                "Card images must be at most 2048 pixels on each side"));
        }
        return image;
    }
    catch(const Magick::Exception& error)
    {
        spdlog::warn("ImageMagick rejected an upload: {}", error.what());
        return std::unexpected(mw::httpError(
            422,
            "The uploaded image could not be decoded"));
    }
}

mw::E<void> renameImage(
    const std::filesystem::path& input,
    const std::filesystem::path& output)
{
    std::error_code filesystem_error;
    std::filesystem::rename(input, output, filesystem_error);
    if(filesystem_error)
    {
        return std::unexpected(mw::runtimeError(
            "Failed to normalize an uploaded image: " +
            filesystem_error.message()));
    }
    return {};
}

} // namespace

ImageProcessor::ImageProcessor(
    int avif_quality,
    std::uint32_t thumbnail_long_side)
        : avif_quality_(avif_quality),
          thumbnail_long_side_(thumbnail_long_side)
{}

mw::E<ProcessedImage> ImageProcessor::process(
    const std::filesystem::path& input,
    CardAssetType type) const
{
    auto format = sniffFormat(input);
    if(!format)
    {
        return std::unexpected(std::move(format.error()));
    }
    auto decoded = decodeSingleImage(input, *format);
    if(!decoded)
    {
        return std::unexpected(std::move(decoded.error()));
    }
    const bool convert_to_avif = format->extension == "png";
    const std::string extension = convert_to_avif
        ? "avif"
        : format->extension;
    const std::filesystem::path output =
        input.parent_path() / (basename(type) + "." + extension);

    if(convert_to_avif)
    {
        try
        {
            decoded->autoOrient();
            decoded->colorSpace(Magick::sRGBColorspace);
            decoded->strip();
            decoded->quality(static_cast<std::size_t>(avif_quality_));
            decoded->write("AVIF:" + output.string());
        }
        catch(const Magick::Exception& error)
        {
            std::error_code remove_error;
            std::filesystem::remove(output, remove_error);
            spdlog::error(
                "ImageMagick failed to convert an uploaded PNG: {}",
                error.what());
            return std::unexpected(mw::runtimeError(
                "Failed to process the uploaded image"));
        }
        std::error_code remove_error;
        if(!std::filesystem::remove(input, remove_error) || remove_error)
        {
            std::filesystem::remove(output, remove_error);
            return std::unexpected(mw::runtimeError(
                "Failed to replace the converted upload"));
        }
    }
    else if(input != output)
    {
        auto renamed = renameImage(input, output);
        if(!renamed)
        {
            return std::unexpected(std::move(renamed.error()));
        }
    }

    return ProcessedImage{
        output,
        extension,
        static_cast<std::uint32_t>(decoded->columns()),
        static_cast<std::uint32_t>(decoded->rows()),
        decoded->alpha(),
    };
}

mw::E<ProcessedImage> ImageProcessor::generatePlainThumbnail(
    const ProcessedImage& artwork,
    const std::filesystem::path& output) const
{
    auto format = sniffFormat(artwork.path);
    if(!format)
    {
        return std::unexpected(std::move(format.error()));
    }
    auto decoded = decodeSingleImage(artwork.path, *format);
    if(!decoded)
    {
        return std::unexpected(std::move(decoded.error()));
    }

    const std::uint64_t scaled_width =
        static_cast<std::uint64_t>(thumbnail_long_side_) * 5 + 3;
    const std::uint32_t width = static_cast<std::uint32_t>(scaled_width / 7);
    try
    {
        decoded->autoOrient();
        Magick::Geometry thumbnail_geometry(width, thumbnail_long_side_);
        thumbnail_geometry.aspect(true);
        decoded->resize(thumbnail_geometry);
        decoded->colorSpace(Magick::sRGBColorspace);
        decoded->strip();
        decoded->quality(static_cast<std::size_t>(avif_quality_));
        decoded->write("AVIF:" + output.string());
    }
    catch(const Magick::Exception& error)
    {
        std::error_code remove_error;
        std::filesystem::remove(output, remove_error);
        spdlog::error(
            "ImageMagick failed to generate a card thumbnail: {}",
            error.what());
        return std::unexpected(mw::runtimeError(
            "Failed to generate the card thumbnail"));
    }

    return ProcessedImage{
        output,
        "avif",
        width,
        thumbnail_long_side_,
        decoded->alpha(),
    };
}