diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp index 90c119b733..854324ec01 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp @@ -356,6 +356,45 @@ ErrorOr add_end_of_image(Stream& stream) return {}; } +ErrorOr add_icc_data(Stream& stream, ReadonlyBytes icc_data) +{ + // https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf, JFIF section + constexpr StringView icc_chunk_name = "ICC_PROFILE\0"sv; + + // One JPEG chunk is at most 65535 bytes long, which includes the size of the 2-byte + // "length" field. This leaves 65533 bytes for the actual data. One ICC chunk needs + // 12 bytes for the "ICC_PROFILE\0" app id and then one byte each for the current + // sequence number and the number of ICC chunks. This leaves 65519 bytes for the + // ICC data. + constexpr size_t icc_chunk_header_size = 2 + icc_chunk_name.length() + 1 + 1; + constexpr size_t max_chunk_size = 65535 - icc_chunk_header_size; + static_assert(max_chunk_size == 65519); + + constexpr size_t max_number_of_icc_chunks = 255; // Chunk IDs are stored in an u8 and start at 1. + constexpr size_t max_icc_data_size = max_chunk_size * max_number_of_icc_chunks; + + // "The 1-byte chunk count limits the size of embeddable profiles to 16 707 345 bytes."" + static_assert(max_icc_data_size == 16'707'345); + + if (icc_data.size() > max_icc_data_size) + return Error::from_string_view("JPEGWriter: icc data too large for jpeg format"sv); + + size_t const number_of_icc_chunks = AK::ceil_div(icc_data.size(), max_chunk_size); + for (size_t chunk_id = 1; chunk_id <= number_of_icc_chunks; ++chunk_id) { + size_t const chunk_size = min(icc_data.size(), max_chunk_size); + + TRY(stream.write_value>(JPEG_APPN2)); + TRY(stream.write_value>(icc_chunk_header_size + chunk_size)); + TRY(stream.write_until_depleted(icc_chunk_name.bytes())); + TRY(stream.write_value(chunk_id)); + TRY(stream.write_value(number_of_icc_chunks)); + TRY(stream.write_until_depleted(icc_data.slice(0, chunk_size))); + icc_data = icc_data.slice(chunk_size); + } + VERIFY(icc_data.is_empty()); + return {}; +} + ErrorOr add_frame_header(Stream& stream, JPEGEncodingContext const& context, Bitmap const& bitmap) { // B.2.2 - Frame header syntax @@ -494,6 +533,9 @@ ErrorOr JPEGWriter::encode(Stream& stream, Bitmap const& bitmap, Options c TRY(add_start_of_image(stream)); + if (options.icc_data.has_value()) + TRY(add_icc_data(stream, options.icc_data.value())); + TRY(add_frame_header(stream, context, bitmap)); TRY(add_quantization_table(stream, context.luminance_quantization_table())); diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h index 4ab8cc1a4f..acab0504e9 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h @@ -12,6 +12,7 @@ namespace Gfx { struct JPEGEncoderOptions { + Optional icc_data; u8 quality { 75 }; }; diff --git a/Userland/Utilities/image.cpp b/Userland/Utilities/image.cpp index 5e7556dc44..572223509d 100644 --- a/Userland/Utilities/image.cpp +++ b/Userland/Utilities/image.cpp @@ -133,7 +133,7 @@ static ErrorOr save_image(LoadedImage& image, StringView out_path, bool pp auto buffered_stream = TRY(Core::OutputBufferedFile::create(move(output_stream))); if (out_path.ends_with(".jpg"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".jpeg"sv, CaseSensitivity::CaseInsensitive)) { - TRY(Gfx::JPEGWriter::encode(*buffered_stream, *frame, { .quality = jpeg_quality })); + TRY(Gfx::JPEGWriter::encode(*buffered_stream, *frame, { .icc_data = image.icc_data, .quality = jpeg_quality })); return {}; } if (out_path.ends_with(".ppm"sv, CaseSensitivity::CaseInsensitive)) {