From f1aba025261270b69f0dbd97ac23c5da51993e93 Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Thu, 23 Oct 2025 12:15:45 +0200 Subject: [PATCH] LibGfx/JPEG: Add a deringing pass to the encoder The idea is described here https://kornel.ski/deringing/. And allows reducing the noise around sharp white edges. This is visible when encoding an image like `buggie.png`. One nice aspect of the optimization is that it only affects macroblocks where it can help. --- .../LibGfx/ImageFormats/JPEGWriter.cpp | 71 ++++++++++++++++++- .../LibGfx/ImageFormats/JPEGWriter.h | 6 ++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp index aa3147fe5e8335..4ab90cb841ada0 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp @@ -98,6 +98,40 @@ class JPEGBigEndianOutputBitStream : public Stream { size_t m_bit_offset { 0 }; }; +void interpolate(f32* component, f32 max_value, i8 start, i8 stop) +{ + // We're creating a uniform (ɑ = 1) Catmull–Rom curve for the missing points. + // That means that tᵢ₊₁ = tᵢ + 1. + // Note that component[start] should be interpolated but component[stop] should not. + + // p1 and p2 are set to the ceil value. + // p0 is set to the last non-max value if possible, otherwise the value of p3 for symmetry. + // The same logic is applied to p3. + f32 const p0 = start == 0 ? component[zigzag_map[stop]] : component[zigzag_map[start - 1]]; + f32 const p1 = max_value; + f32 const p2 = max_value; + f32 const p3 = stop > 63 ? p0 : component[zigzag_map[stop]]; + + f32 const t0 = 0.0f; + f32 const t1 = 1; + f32 const t2 = 2; + f32 const t3 = 3; + + f32 const step = 1. / (stop - start + 1); + f32 t = t1; + for (i8 i = start; i < stop; ++i) { + t += step; + f32 const A1 = p0 * (t1 - t) / (t1 - t0) + p1 * (t - t0) / (t1 - t0); + f32 const A2 = p1 * (t2 - t) / (t2 - t1) + p2 * (t - t1) / (t2 - t1); + f32 const A3 = p2 * (t3 - t) / (t3 - t2) + p3 * (t - t2) / (t3 - t2); + f32 const B1 = A1 * (t2 - t) / (t2 - t0) + A2 * (t - t0) / (t2 - t0); + f32 const B2 = A2 * (t3 - t) / (t3 - t1) + A3 * (t - t1) / (t3 - t1); + f32 const C = B1 * (t2 - t) / (t2 - t1) + B2 * (t - t1) / (t2 - t1); + + component[zigzag_map[i]] = C; + } +} + class JPEGEncodingContext { public: JPEGEncodingContext(JPEGBigEndianOutputBitStream output_stream) @@ -256,6 +290,35 @@ class JPEGEncodingContext { } } + void apply_deringing() + { + // The method used here is described at: https://kornel.ski/deringing/. + + for (auto& macroblock : m_macroblocks) { + for (auto component : { macroblock.r, macroblock.g, macroblock.b }) { + static constexpr auto maximum_value = NumericLimits::max(); + Optional start; + u8 i = 0; + for (; i < 64; ++i) { + if (component[zigzag_map[i]] == maximum_value) { + if (!start.has_value()) + start = i; + else + continue; + } else { + if (start.has_value() && i - *start > 2) { + interpolate(component, maximum_value, *start, i); + } + start.clear(); + } + } + + if (start != 0 && component[zigzag_map[63]] == maximum_value) + interpolate(component, maximum_value, *start, 64); + } + } + } + ErrorOr write_huffman_stream(Mode mode) { for (auto& float_macroblock : m_macroblocks) { @@ -633,8 +696,10 @@ ErrorOr add_headers(Stream& stream, JPEGEncodingContext& context, JPEGWrit return {}; } -ErrorOr add_image(Stream& stream, JPEGEncodingContext& context, Mode mode) +ErrorOr add_image(Stream& stream, JPEGEncodingContext& context, JPEGWriter::Options const& options, Mode mode) { + if (options.use_deringing == JPEGEncoderOptions::UseDeringing::Yes) + context.apply_deringing(); context.convert_to_ycbcr(mode); context.fdct_and_quantization(mode); TRY(context.write_huffman_stream(mode)); @@ -649,7 +714,7 @@ ErrorOr JPEGWriter::encode(Stream& stream, Bitmap const& bitmap, Options c JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } }; TRY(add_headers(stream, context, options, bitmap.size(), Mode::RGB)); TRY(context.initialize_mcu(bitmap)); - TRY(add_image(stream, context, Mode::RGB)); + TRY(add_image(stream, context, options, Mode::RGB)); return {}; } @@ -658,7 +723,7 @@ ErrorOr JPEGWriter::encode(Stream& stream, CMYKBitmap const& bitmap, Optio JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } }; TRY(add_headers(stream, context, options, bitmap.size(), Mode::CMYK)); TRY(context.initialize_mcu(bitmap)); - TRY(add_image(stream, context, Mode::CMYK)); + TRY(add_image(stream, context, options, Mode::CMYK)); return {}; } diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h index 4608623dda8074..5cad56aaa89108 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h @@ -12,8 +12,14 @@ namespace Gfx { struct JPEGEncoderOptions { + enum class UseDeringing : u8 { + Yes, + No, + }; + Optional icc_data; u8 quality { 75 }; + UseDeringing use_deringing { UseDeringing::Yes }; }; class JPEGWriter {