Newer
Older
minerva / Userland / Libraries / LibGfx / ImageFormats / JPEGXLICC.cpp
@minerva minerva on 13 Jul 12 KB Initial commit
/*
 * Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/ConstrainedStream.h>
#include <AK/MemoryStream.h>
#include <LibGfx/ImageFormats/JPEGXLCommon.h>
#include <LibGfx/ImageFormats/JPEGXLEntropyDecoder.h>

namespace Gfx {

namespace {
/// E.4.1 - Data stream
u8 icc_context(u64 i, u8 b1, u8 b2)
{
    u8 p1 = 0;
    u8 p2 = 0;

    if (i <= 128)
        return 0;

    if (b1 >= 'a' && b1 <= 'z')
        p1 = 0;
    else if (b1 >= 'A' && b1 <= 'Z')
        p1 = 0;
    else if (b1 >= '0' && b1 <= '9')
        p1 = 1;
    else if (b1 == '.' || b1 == ',')
        p1 = 1;
    else if (b1 <= 1)
        p1 = 2 + b1;
    else if (b1 > 1 && b1 < 16)
        p1 = 4;
    else if (b1 > 240 && b1 < 255)
        p1 = 5;
    else if (b1 == 255)
        p1 = 6;
    else
        p1 = 7;

    if (b2 >= 'a' && b2 <= 'z')
        p2 = 0;
    else if (b2 >= 'A' && b2 <= 'Z')
        p2 = 0;
    else if (b2 >= '0' && b2 <= '9')
        p2 = 1;
    else if (b2 == '.' || b2 == ',')
        p2 = 1;
    else if (b2 < 16)
        p2 = 2;
    else if (b2 > 240)
        p2 = 3;
    else
        p2 = 4;

    return 1 + p1 + p2 * 8;
}

ErrorOr<ByteBuffer> read_encoded_icc_stream(LittleEndianInputBitStream& stream)
{
    auto const enc_size = TRY(U64(stream));

    auto decoder = TRY(EntropyDecoder::create(stream, 41));

    ByteBuffer uncompressed_icc_stream {};
    TRY(uncompressed_icc_stream.try_resize(enc_size));
    for (u64 index = 0; index < enc_size; ++index) {
        auto const prev_byte = index > 0 ? uncompressed_icc_stream[index - 1] : 0u;
        auto const prev_prev_byte = index > 1 ? uncompressed_icc_stream[index - 2] : 0u;
        auto const context = icc_context(index, prev_byte, prev_prev_byte);
        uncompressed_icc_stream[index] = TRY(decoder.decode_hybrid_uint(stream, context));
    }

    TRY(decoder.ensure_end_state());
    return uncompressed_icc_stream;
}
///

/// E.4.2 - Encoded ICC stream
ErrorOr<u64> read_varint(Stream& stream)
{
    u64 value = 0;
    u8 shift = 0;
    while (1) {
        if (shift >= 56)
            return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalint shift value in varint");
        auto const b = TRY(stream.read_value<u8>());
        value += (b & 127) << shift;
        if (b <= 127)
            break;
        shift += 7;
    }
    return value;
}
///

/// E.4.3 - ICC header
ErrorOr<void> read_icc_header(Stream& data_stream, u32 output_size, ByteBuffer& out)
{
    u8 const header_size = min(128u, output_size);

    for (u8 i = 0; i < header_size; ++i) {
        auto const e = TRY(data_stream.read_value<u8>());
        u8 p = 0;

        if (i == 0 || i == 1 || i == 2 || i == 3) {
            // 'output_size[i]' means byte i of output_size encoded as an
            // unsigned 32-bit integer in big endian order
            BigEndian const output_size_as_be { static_cast<u32>(output_size) };
            p = bit_cast<u8 const*>(&output_size_as_be)[i];
        } else if (i == 8) {
            p = 4;
        } else if (i >= 12 && i <= 23) {
            auto s = "mntrRGB XYZ "sv;
            p = s[i - 12];
        } else if (i >= 36 && i <= 39) {
            auto s = "acsp"sv;
            p = s[i - 36];
        } else if ((i == 41 || i == 42) && out[40] == 'A') {
            p = 'P';
        } else if (i == 43 && out[40] == 'A') {
            p = 'L';
        } else if (i == 41 && out[40] == 'M') {
            p = 'S';
        } else if (i == 42 && out[40] == 'M') {
            p = 'F';
        } else if (i == 43 && out[40] == 'M') {
            p = 'T';
        } else if (i == 42 && out[40] == 'S' && out[41] == 'G') {
            p = 'I';
        } else if (i == 43 && out[40] == 'S' && out[41] == 'G') {
            p = 32;
        } else if (i == 42 && out[40] == 'S' && out[41] == 'U') {
            p = 'N';
        } else if (i == 43 && out[40] == 'S' && out[41] == 'U') {
            p = 'W';
        } else if (i == 70) {
            p = 246;
        } else if (i == 71) {
            p = 214;
        } else if (i == 73) {
            p = 1;
        } else if (i == 78) {
            p = 211;
        } else if (i == 79) {
            p = 45;
        } else if (i >= 80 && i < 84) {
            p = out[4 + i - 80];
        }

        out.append((p + e) & 255);
    }

    return {};
}
///

/// E.4.4 - ICC tag list

ErrorOr<void> append_as_u32_be(ByteBuffer& buffer, u32 value)
{
    BigEndian be_value { value };
    return buffer.try_append({ &be_value, sizeof(be_value) });
}
ErrorOr<void> read_tag_list(ConstrainedStream& command_stream, Stream& data_stream, ByteBuffer& out)
{
    auto const v = TRY(read_varint(command_stream));
    if (v == 0)
        return {};
    auto const num_tags = v - 1;

    TRY(append_as_u32_be(out, num_tags));
    u32 previous_tagstart = num_tags * 12 + 128;
    u32 previous_tagsize = 0;

    // Then, the decoder repeatedly reads a tag as specified by the following code until a tag with tagcode
    // equal to 0 is read or until the end of the command stream is reached.
    while (command_stream.remaining() > 0) {
        auto const command = TRY(command_stream.read_value<u8>());
        auto const tagcode = command & 63;
        if (tagcode == 0)
            return {};

        Array<u8, 4> tag;
        if (tagcode == 1) {
            TRY(data_stream.read_until_filled(tag));
        } else if (tagcode == 2) {
            tag = Array<u8, 4>::from_span("rTRC"sv.bytes());
        } else if (tagcode == 3) {
            tag = Array<u8, 4>::from_span("rXYZ"sv.bytes());
        } else if (tagcode >= 4 && tagcode < 21) {
            static constexpr auto strings = to_array(
                { "cprt"sv, "wtpt"sv, "bkpt"sv, "rXYZ"sv, "gXYZ"sv, "bXYZ"sv, "kXYZ"sv, "rTRC"sv, "gTRC"sv,
                    "bTRC"sv, "kTRC"sv, "chad"sv, "desc"sv, "chrm"sv, "dmnd"sv, "dmdd"sv, "lumi"sv });
            tag = Array<u8, 4>::from_span(strings[tagcode - 4].bytes());
        } else {
            return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid tagcode in ICC profile");
        }

        auto tagstart = previous_tagstart + previous_tagsize;
        if ((command & 64) != 0)
            tagstart = TRY(read_varint(command_stream));

        u32 tagsize = previous_tagsize;
        if (tag == "rXYZ"sv.bytes() || tag == "gXYZ"sv.bytes() || tag == "bXYZ"sv.bytes()
            || tag == "kXYZ"sv.bytes() || tag == "wtpt"sv.bytes() || tag == "bkpt"sv.bytes()
            || tag == "lumi"sv.bytes())
            tagsize = 20;

        if ((command & 128) != 0)
            tagsize = TRY(read_varint(command_stream));
        previous_tagstart = tagstart;
        previous_tagsize = tagsize;

        // Write tag to output

        TRY(out.try_append(tag));
        TRY(append_as_u32_be(out, tagstart));
        TRY(append_as_u32_be(out, tagsize));
        if (tagcode == 2) {
            out.append("gTRC"sv.bytes());
            TRY(append_as_u32_be(out, tagstart));
            TRY(append_as_u32_be(out, tagsize));

            out.append("bTRC"sv.bytes());
            TRY(append_as_u32_be(out, tagstart));
            TRY(append_as_u32_be(out, tagsize));
        } else if (tagcode == 3) {
            out.append("gXYZ"sv.bytes());
            TRY(append_as_u32_be(out, tagstart + tagsize));
            TRY(append_as_u32_be(out, tagsize));

            out.append("bXYZ"sv.bytes());
            TRY(append_as_u32_be(out, tagstart + 2 * tagsize));
            TRY(append_as_u32_be(out, tagsize));
        }
    }

    return {};
}
///

/// E.4.5 - Main content
ErrorOr<void> shuffle(Bytes bytes, u8 width)
{
    auto temp = TRY(ByteBuffer::create_uninitialized(bytes.size()));

    u64 const height = (bytes.size() + width - 1) / width;

    u64 row_start = 0;
    u64 j = 0;
    for (size_t i = 0; i < bytes.size(); i++) {
        temp[i] = bytes[j];
        j += height;
        if (j >= bytes.size()) {
            ++row_start;
            j = row_start;
        }
    }

    temp.bytes().copy_to(bytes);
    return {};
}

ErrorOr<void> read_icc_main_content(ConstrainedStream& command_stream, Stream& data_stream, ByteBuffer& out)
{
    while (command_stream.remaining() > 0) {
        auto const command = TRY(command_stream.read_value<u8>());
        if (command == 1) {
            auto const num = TRY(read_varint(command_stream));
            auto bytes = TRY(out.get_bytes_for_writing(num));
            TRY(data_stream.read_until_filled(bytes));
        } else if (command == 2 or command == 3) {
            auto const num = TRY(read_varint(command_stream));
            auto bytes = TRY(out.get_bytes_for_writing(num));
            TRY(data_stream.read_until_filled(bytes));
            u8 width = (command == 2) ? 2 : 4;
            TRY(shuffle(bytes, width));
        } else if (command == 4) {
            u8 const flags = TRY(command_stream.read_value<u8>());
            u8 const width = (flags & 3) + 1;
            u8 const order = (flags & 12) >> 2;
            if (width == 3 || order == 3)
                return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid width or order value");

            u64 stride = width;
            if ((flags & 16) != 0)
                stride = TRY(read_varint(command_stream));

            if (stride * 4 >= out.size() || stride < width)
                return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid stride value");

            auto const num = TRY(read_varint(command_stream));
            ByteBuffer bytes;
            TRY(bytes.try_resize(num));
            TRY(data_stream.read_until_filled(bytes));
            if (width == 2 || width == 4)
                TRY(shuffle(bytes, width));

            for (u64 i = 0; i < num; i += width) {
                // NOTE: 0 <= order <= 2
                u8 const N = order + 1;
                Array<u32, 3> prev {};
                for (u8 j = 0; j < N; ++j) {
                    // "read u(width * 8) from the output ICC profile starting from
                    //  (stride * (j + 1)) bytes before the current output size,
                    //  interpreted as a big-endian unsigned integer of width bytes"
                    Array<u8, 4> bytes {};
                    for (u8 k = 0; k < width; ++k)
                        bytes[4 - width + k] = out[out.size() - stride * (j + 1) + k];
                    prev[j] = *bit_cast<BigEndian<u32> const*>(bytes.data());
                }

                u32 p;
                if (order == 0)
                    p = prev[0];
                else if (order == 1)
                    p = 2 * prev[0] - prev[1];
                else if (order == 2)
                    p = 3 * prev[0] - 3 * prev[1] + prev[2];

                for (u8 j = 0; j < width && i + j < num; ++j) {
                    u8 const val = (bytes[i + j] + (p >> (8 * (width - 1 - j)))) & 255;
                    TRY(out.try_append(val));
                }
            }
        } else if (command == 10) {
            TRY(out.try_append("XYZ "sv.bytes()));
            Array<u8, 4> constexpr zeros {};
            TRY(out.try_append(zeros));
            auto bytes = TRY(out.get_bytes_for_writing(12));
            TRY(data_stream.read_until_filled(bytes));
        } else if (command >= 16 and command < 24) {
            Array constexpr strings = { "XYZ "sv, "desc"sv, "text"sv, "mluc"sv, "para"sv, "curv"sv, "sf32"sv, "gbd "sv };
            TRY(out.try_append(strings[command - 16].bytes()));
            Array<u8, 4> constexpr zeros {};
            TRY(out.try_append(zeros));
        } else {
            return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid command in ICC main context");
        }
    }

    return {};
}
///
}

/// E.4 - ICC profile
ErrorOr<ByteBuffer> read_icc(LittleEndianInputBitStream& stream)
{
    auto const encoded_icc = TRY(read_encoded_icc_stream(stream));

    FixedMemoryStream buffer(encoded_icc);
    auto const output_size = TRY(read_varint(buffer));
    auto const commands_size = TRY(read_varint(buffer));

    ConstrainedStream command_stream { MaybeOwned<Stream>(buffer), commands_size };

    auto const data_offset = buffer.offset() + commands_size;
    FixedMemoryStream data_stream(encoded_icc);
    TRY(data_stream.discard(data_offset));

    ByteBuffer out;
    TRY(out.try_ensure_capacity(output_size));

    TRY(read_icc_header(data_stream, output_size, out));

    if (output_size <= 128)
        return out;

    TRY(read_tag_list(command_stream, data_stream, out));

    TRY(read_icc_main_content(command_stream, data_stream, out));

    return out;
}
///

}