LibGfx: Remove home-grown JPEG codec in favor of libjpeg-turbo

This commit is contained in:
Andreas Kling 2024-06-16 16:08:31 +02:00 committed by Andreas Kling
parent 808784092c
commit 2a888ca626
Notes: sideshowbarker 2024-07-17 06:09:44 +09:00
18 changed files with 215 additions and 3341 deletions

View file

@ -134,10 +134,6 @@
# cmakedefine01 JOB_DEBUG
#endif
#ifndef JPEG_DEBUG
# cmakedefine01 JPEG_DEBUG
#endif
#ifndef JPEG2000_DEBUG
# cmakedefine01 JPEG2000_DEBUG
#endif

View file

@ -29,7 +29,6 @@ set(IMAGE_DECODER_DEBUG ON)
set(IMAGE_LOADER_DEBUG ON)
set(JBIG2_DEBUG ON)
set(JOB_DEBUG ON)
set(JPEG_DEBUG ON)
set(JPEG2000_DEBUG ON)
set(JS_BYTECODE_DEBUG ON)
set(JS_MODULE_DEBUG ON)

View file

@ -573,11 +573,6 @@ if (BUILD_TESTING)
# It is therefore not reasonable to run it on Lagom, and we only run the Regex test
lagom_test(../../Tests/LibRegex/Regex.cpp LIBS LibRegex WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../Tests/LibRegex)
# test-jpeg-roundtrip
add_executable(test-jpeg-roundtrip
../../Userland/Utilities/test-jpeg-roundtrip.cpp)
target_link_libraries(test-jpeg-roundtrip AK LibGfx LibMain)
# JavaScriptTestRunner + LibTest tests
# test-js
add_executable(test-js

View file

@ -250,7 +250,6 @@ write_cmake_config("ak_debug_gen") {
"IMAGE_LOADER_DEBUG=",
"JBIG2_DEBUG=",
"JOB_DEBUG=",
"JPEG_DEBUG=",
"JPEG2000_DEBUG=",
"JS_BYTECODE_DEBUG=",
"JS_MODULE_DEBUG=",

View file

@ -508,24 +508,6 @@ TEST_CASE(test_jpeg_sof2_successive_aproximation)
TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 600, 800 }));
}
TEST_CASE(test_jpeg_sof1_12bits)
{
auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpg/12-bit.jpg"sv)));
EXPECT(Gfx::JPEGImageDecoderPlugin::sniff(file->bytes()));
auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEGImageDecoderPlugin::create(file->bytes()));
TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 320, 240 }));
}
TEST_CASE(test_jpeg_sof2_12bits)
{
auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpg/12-bit-progressive.jpg"sv)));
EXPECT(Gfx::JPEGImageDecoderPlugin::sniff(file->bytes()));
auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEGImageDecoderPlugin::create(file->bytes()));
TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 320, 240 }));
}
TEST_CASE(test_jpeg_empty_icc)
{
auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpg/gradient_empty_icc.jpg"sv)));
@ -561,8 +543,9 @@ TEST_CASE(test_jpeg_malformed_header)
for (auto test_input : test_inputs) {
auto file = TRY_OR_FAIL(Core::MappedFile::map(test_input));
auto plugin_decoder_or_error = Gfx::JPEGImageDecoderPlugin::create(file->bytes());
EXPECT(plugin_decoder_or_error.is_error());
auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEGImageDecoderPlugin::create(file->bytes()));
auto frame_or_error = plugin_decoder->frame(0);
EXPECT(frame_or_error.is_error());
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -114,3 +114,8 @@ add_dependencies(LibGfx generate_tiff_files_handler)
list(TRANSFORM generated_sources PREPEND "${CMAKE_CURRENT_BINARY_DIR}/")
install(FILES ${generated_sources} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/LibGfx/ImageFormats")
find_package(JPEG REQUIRED)
target_include_directories(LibGfx PRIVATE ${JPEG_INCLUDE_DIRS})
target_link_libraries(LibGfx PRIVATE ${JPEG_LIBRARIES})
target_link_directories(LibGfx PRIVATE ${JPEG_LIBRARY_DIRS})

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,21 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2022-2023, Lucas Chollet <lucas.chollet@serenityos.org>
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/MemoryStream.h>
#include <LibGfx/ImageFormats/ImageDecoder.h>
namespace Gfx {
struct JPEGLoadingContext;
// For the specification, see: https://www.w3.org/Graphics/JPEG/itu-t81.pdf
struct JPEGDecoderOptions {
enum class CMYK {
// For standalone jpeg files.
Normal,
// For jpeg data embedded in PDF files.
PDF,
};
CMYK cmyk { CMYK::Normal };
};
class JPEGImageDecoderPlugin : public ImageDecoderPlugin {
public:
static bool sniff(ReadonlyBytes);
static ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> create(ReadonlyBytes);
static ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> create_with_options(ReadonlyBytes, JPEGDecoderOptions = {});
virtual ~JPEGImageDecoderPlugin() override;
virtual IntSize size() override;
@ -46,7 +30,7 @@ public:
virtual ErrorOr<NonnullRefPtr<CMYKBitmap>> cmyk_frame() override;
private:
JPEGImageDecoderPlugin(NonnullOwnPtr<JPEGLoadingContext>);
explicit JPEGImageDecoderPlugin(NonnullOwnPtr<JPEGLoadingContext>);
NonnullOwnPtr<JPEGLoadingContext> m_context;
};

View file

@ -1,111 +0,0 @@
/*
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
// These names are defined in B.1.1.3 - Marker assignments
#define JPEG_APPN0 0XFFE0
#define JPEG_APPN1 0XFFE1
#define JPEG_APPN2 0XFFE2
#define JPEG_APPN3 0XFFE3
#define JPEG_APPN4 0XFFE4
#define JPEG_APPN5 0XFFE5
#define JPEG_APPN6 0XFFE6
#define JPEG_APPN7 0XFFE7
#define JPEG_APPN8 0XFFE8
#define JPEG_APPN9 0XFFE9
#define JPEG_APPN10 0XFFEA
#define JPEG_APPN11 0XFFEB
#define JPEG_APPN12 0XFFEC
#define JPEG_APPN13 0XFFED
#define JPEG_APPN14 0xFFEE
#define JPEG_APPN15 0xFFEF
#define JPEG_RESERVED1 0xFFF1
#define JPEG_RESERVED2 0xFFF2
#define JPEG_RESERVED3 0xFFF3
#define JPEG_RESERVED4 0xFFF4
#define JPEG_RESERVED5 0xFFF5
#define JPEG_RESERVED6 0xFFF6
#define JPEG_RESERVED7 0xFFF7
#define JPEG_RESERVED8 0xFFF8
#define JPEG_RESERVED9 0xFFF9
#define JPEG_RESERVEDA 0xFFFA
#define JPEG_RESERVEDB 0xFFFB
#define JPEG_RESERVEDC 0xFFFC
#define JPEG_RESERVEDD 0xFFFD
#define JPEG_RST0 0xFFD0
#define JPEG_RST1 0xFFD1
#define JPEG_RST2 0xFFD2
#define JPEG_RST3 0xFFD3
#define JPEG_RST4 0xFFD4
#define JPEG_RST5 0xFFD5
#define JPEG_RST6 0xFFD6
#define JPEG_RST7 0xFFD7
#define JPEG_ZRL 0xF0
#define JPEG_DHP 0xFFDE
#define JPEG_EXP 0xFFDF
#define JPEG_DAC 0XFFCC
#define JPEG_DHT 0XFFC4
#define JPEG_DQT 0XFFDB
#define JPEG_EOI 0xFFD9
#define JPEG_DRI 0XFFDD
#define JPEG_SOF0 0XFFC0
#define JPEG_SOF1 0XFFC1
#define JPEG_SOF2 0xFFC2
#define JPEG_SOF15 0xFFCF
#define JPEG_SOI 0XFFD8
#define JPEG_SOS 0XFFDA
#define JPEG_COM 0xFFFE
namespace Gfx {
using Marker = u16;
constexpr static u8 zigzag_map[64] {
0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34,
27, 20, 13, 6, 7, 14, 21, 28,
35, 42, 49, 56, 57, 50, 43, 36,
29, 22, 15, 23, 30, 37, 44, 51,
58, 59, 52, 45, 38, 31, 39, 46,
53, 60, 61, 54, 47, 55, 62, 63
};
/**
* MCU means group of data units that are coded together. A data unit is an 8x8
* block of component data. In interleaved scans, number of non-interleaved data
* units of a component C is Ch * Cv, where Ch and Cv represent the horizontal &
* vertical subsampling factors of the component, respectively. A MacroBlock is
* an 8x8 block of RGB values before encoding, and 8x8 block of YCbCr values when
* we're done decoding the huffman stream.
*/
struct Macroblock {
union {
i16 y[64] = { 0 };
i16 r[64];
};
union {
i16 cb[64] = { 0 };
i16 g[64];
};
union {
i16 cr[64] = { 0 };
i16 b[64];
};
i16 k[64] = { 0 };
};
}

View file

@ -1,656 +1,113 @@
/*
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "JPEGWriter.h"
#include "JPEGShared.h"
#include "JPEGWriterTables.h"
#include <AK/BitStream.h>
#include <AK/Endian.h>
#include <AK/Function.h>
#include <AK/Stream.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/CMYKBitmap.h>
#include <LibGfx/ImageFormats/JPEGWriter.h>
#include <jpeglib.h>
namespace Gfx {
namespace {
struct MemoryDestinationManager : public jpeg_destination_mgr {
Vector<u8>& buffer;
static constexpr size_t BUFFER_SIZE_INCREMENT = 65536;
enum Mode {
RGB,
CMYK,
MemoryDestinationManager(Vector<u8>& buffer)
: buffer(buffer)
{
init_destination = [](j_compress_ptr cinfo) {
auto* dest = static_cast<MemoryDestinationManager*>(cinfo->dest);
dest->buffer.resize(BUFFER_SIZE_INCREMENT);
dest->next_output_byte = dest->buffer.data();
dest->free_in_buffer = dest->buffer.capacity();
};
empty_output_buffer = [](j_compress_ptr cinfo) -> boolean {
auto* dest = static_cast<MemoryDestinationManager*>(cinfo->dest);
size_t old_size = dest->buffer.size();
dest->buffer.resize(old_size + BUFFER_SIZE_INCREMENT);
dest->next_output_byte = dest->buffer.data() + old_size;
dest->free_in_buffer = BUFFER_SIZE_INCREMENT;
return TRUE;
};
term_destination = [](j_compress_ptr cinfo) {
auto* dest = static_cast<MemoryDestinationManager*>(cinfo->dest);
dest->buffer.resize(dest->buffer.size() - dest->free_in_buffer);
};
next_output_byte = nullptr;
free_in_buffer = 0;
}
};
// This is basically a BigEndianOutputBitStream, the only difference
// is that it appends 0x00 after each 0xFF when it writes bits.
class JPEGBigEndianOutputBitStream : public Stream {
public:
explicit JPEGBigEndianOutputBitStream(Stream& stream)
: m_stream(stream)
{
}
virtual ErrorOr<Bytes> read_some(Bytes) override
{
return Error::from_errno(EBADF);
}
virtual ErrorOr<size_t> write_some(ReadonlyBytes bytes) override
{
VERIFY(m_bit_offset == 0);
return m_stream.write_some(bytes);
}
template<Unsigned T>
ErrorOr<void> write_bits(T value, size_t bit_count)
{
VERIFY(m_bit_offset <= 7);
while (bit_count > 0) {
u8 const next_bit = (value >> (bit_count - 1)) & 1;
bit_count--;
m_current_byte <<= 1;
m_current_byte |= next_bit;
m_bit_offset++;
if (m_bit_offset > 7) {
TRY(m_stream.write_value(m_current_byte));
if (m_current_byte == 0xFF)
TRY(m_stream.write_value<u8>(0));
m_bit_offset = 0;
m_current_byte = 0;
}
}
return {};
}
virtual bool is_eof() const override
{
return true;
}
virtual bool is_open() const override
{
return m_stream.is_open();
}
virtual void close() override
{
}
ErrorOr<void> align_to_byte_boundary(u8 filler = 0x0)
{
if (m_bit_offset == 0)
return {};
TRY(write_bits(filler, 8 - m_bit_offset));
VERIFY(m_bit_offset == 0);
return {};
}
private:
Stream& m_stream;
u8 m_current_byte { 0 };
size_t m_bit_offset { 0 };
};
class JPEGEncodingContext {
public:
JPEGEncodingContext(JPEGBigEndianOutputBitStream output_stream)
: m_bit_stream(move(output_stream))
{
}
ErrorOr<void> initialize_mcu(Bitmap const& bitmap)
{
u64 const horizontal_macroblocks = ceil_div(bitmap.width(), 8);
u64 const vertical_macroblocks = ceil_div(bitmap.height(), 8);
TRY(m_macroblocks.try_resize(horizontal_macroblocks * vertical_macroblocks));
for (u16 y {}; y < bitmap.height(); ++y) {
u16 const vertical_macroblock_index = y / 8;
u16 const vertical_pixel_offset = y - vertical_macroblock_index * 8;
for (u16 x {}; x < bitmap.width(); ++x) {
u16 const horizontal_macroblock_index = x / 8;
u16 const horizontal_pixel_offset = x - horizontal_macroblock_index * 8;
auto& macroblock = m_macroblocks[vertical_macroblock_index * horizontal_macroblocks + horizontal_macroblock_index];
auto const pixel_offset = vertical_pixel_offset * 8 + horizontal_pixel_offset;
auto const original_pixel = bitmap.get_pixel(x, y);
// Conversion from YCbCr to RGB isn't specified in the first JPEG specification but in the JFIF extension:
// See: https://www.itu.int/rec/dologin_pub.asp?lang=f&id=T-REC-T.871-201105-I!!PDF-E&type=items
// 7 - Conversion to and from RGB
auto const y_ = clamp(0.299 * original_pixel.red() + 0.587 * original_pixel.green() + 0.114 * original_pixel.blue(), 0, 255);
auto const cb = clamp(-0.1687 * original_pixel.red() - 0.3313 * original_pixel.green() + 0.5 * original_pixel.blue() + 128, 0, 255);
auto const cr = clamp(0.5 * original_pixel.red() - 0.4187 * original_pixel.green() - 0.0813 * original_pixel.blue() + 128, 0, 255);
// A.3.1 - Level shift
macroblock.r[pixel_offset] = y_ - 128;
macroblock.g[pixel_offset] = cb - 128;
macroblock.b[pixel_offset] = cr - 128;
}
}
return {};
}
ErrorOr<void> initialize_mcu(CMYKBitmap const& bitmap)
{
u64 const horizontal_macroblocks = ceil_div(bitmap.size().width(), 8);
u64 const vertical_macroblocks = ceil_div(bitmap.size().height(), 8);
TRY(m_macroblocks.try_resize(horizontal_macroblocks * vertical_macroblocks));
for (u16 y {}; y < bitmap.size().height(); ++y) {
u16 const vertical_macroblock_index = y / 8;
u16 const vertical_pixel_offset = y - vertical_macroblock_index * 8;
for (u16 x {}; x < bitmap.size().width(); ++x) {
u16 const horizontal_macroblock_index = x / 8;
u16 const horizontal_pixel_offset = x - horizontal_macroblock_index * 8;
auto& macroblock = m_macroblocks[vertical_macroblock_index * horizontal_macroblocks + horizontal_macroblock_index];
auto const pixel_offset = vertical_pixel_offset * 8 + horizontal_pixel_offset;
auto const original_pixel = bitmap.scanline(y)[x];
// To get YCCK, the CMY part is converted to RGB (ignoring the K component), and then the RGB is converted to YCbCr.
// r is `255 - c` (and similar for g/m b/y), but with the Adobe YCCK color transform marker, the CMY
// channels are stored inverted, which cancels out: 255 - (255 - x) == x.
// K is stored as-is (meaning it's inverted once for the color transform).
u8 r = original_pixel.c;
u8 g = original_pixel.m;
u8 b = original_pixel.y;
u8 k = 255 - original_pixel.k;
// See: https://www.itu.int/rec/dologin_pub.asp?lang=f&id=T-REC-T.871-201105-I!!PDF-E&type=items
// 7 - Conversion to and from RGB
auto const y_ = clamp(0.299 * r + 0.587 * g + 0.114 * b, 0, 255);
auto const cb = clamp(-0.1687 * r - 0.3313 * g + 0.5 * b + 128, 0, 255);
auto const cr = clamp(0.5 * r - 0.4187 * g - 0.0813 * b + 128, 0, 255);
// A.3.1 - Level shift
macroblock.r[pixel_offset] = y_ - 128;
macroblock.g[pixel_offset] = cb - 128;
macroblock.b[pixel_offset] = cr - 128;
macroblock.k[pixel_offset] = k - 128;
}
}
return {};
}
static Array<double, 64> create_cosine_lookup_table()
{
static constexpr double pi_over_16 = AK::Pi<double> / 16;
Array<double, 64> table;
for (u8 u = 0; u < 8; ++u) {
for (u8 x = 0; x < 8; ++x)
table[u * 8 + x] = cos((2 * x + 1) * u * pi_over_16);
}
return table;
}
void fdct_and_quantization(Mode mode)
{
static auto cosine_table = create_cosine_lookup_table();
for (auto& macroblock : m_macroblocks) {
constexpr double inverse_sqrt_2 = M_SQRT1_2;
auto const convert_one_component = [&](i16 component[], QuantizationTable const& table) {
Array<i16, 64> result {};
auto const sum_xy = [&](u8 u, u8 v) {
double sum {};
for (u8 y {}; y < 8; ++y) {
for (u8 x {}; x < 8; ++x)
sum += component[y * 8 + x] * cosine_table[u * 8 + x] * cosine_table[v * 8 + y];
}
return sum;
};
for (u8 v {}; v < 8; ++v) {
double const cv = v == 0 ? inverse_sqrt_2 : 1;
for (u8 u {}; u < 8; ++u) {
auto const table_index = v * 8 + u;
double const cu = u == 0 ? inverse_sqrt_2 : 1;
// A.3.3 - FDCT and IDCT
double const fdct = cu * cv * sum_xy(u, v) / 4;
// A.3.4 - DCT coefficient quantization
i16 const quantized = round(fdct / table.table[table_index]);
result[table_index] = quantized;
}
}
for (u8 i {}; i < result.size(); ++i)
component[i] = result[i];
};
convert_one_component(macroblock.y, m_luminance_quantization_table);
convert_one_component(macroblock.cb, m_chrominance_quantization_table);
convert_one_component(macroblock.cr, m_chrominance_quantization_table);
if (mode == Mode::CMYK)
convert_one_component(macroblock.k, m_luminance_quantization_table);
}
}
ErrorOr<void> write_huffman_stream(Mode mode)
{
for (auto& macroblock : m_macroblocks) {
TRY(encode_dc(dc_luminance_huffman_table, macroblock.y, 0));
TRY(encode_ac(ac_luminance_huffman_table, macroblock.y));
TRY(encode_dc(dc_chrominance_huffman_table, macroblock.cb, 1));
TRY(encode_ac(ac_chrominance_huffman_table, macroblock.cb));
TRY(encode_dc(dc_chrominance_huffman_table, macroblock.cr, 2));
TRY(encode_ac(ac_chrominance_huffman_table, macroblock.cr));
if (mode == Mode::CMYK) {
TRY(encode_dc(dc_luminance_huffman_table, macroblock.k, 3));
TRY(encode_ac(ac_luminance_huffman_table, macroblock.k));
}
}
TRY(m_bit_stream.align_to_byte_boundary(0xFF));
return {};
}
void set_luminance_quantization_table(QuantizationTable const& table, int quality)
{
set_quantization_table(m_luminance_quantization_table, table, quality);
}
void set_chrominance_quantization_table(QuantizationTable const& table, int quality)
{
set_quantization_table(m_chrominance_quantization_table, table, quality);
}
QuantizationTable const& luminance_quantization_table() const
{
return m_luminance_quantization_table;
}
QuantizationTable const& chrominance_quantization_table() const
{
return m_chrominance_quantization_table;
}
OutputHuffmanTable dc_luminance_huffman_table;
OutputHuffmanTable dc_chrominance_huffman_table;
OutputHuffmanTable ac_luminance_huffman_table;
OutputHuffmanTable ac_chrominance_huffman_table;
private:
static void set_quantization_table(QuantizationTable& destination, QuantizationTable const& source, int quality)
{
// In order to be compatible with libjpeg-turbo, we use the same coefficients as them.
quality = clamp(quality, 1, 100);
if (quality < 50)
quality = 5000 / quality;
else
quality = 200 - quality * 2;
destination = source;
for (u8 i {}; i < 64; ++i) {
auto const shifted_value = (destination.table[i] * quality + 50) / 100;
destination.table[i] = clamp(shifted_value, 1, 255);
}
}
ErrorOr<void> write_symbol(OutputHuffmanTable::Symbol symbol)
{
return m_bit_stream.write_bits(symbol.word, symbol.code_length);
}
ErrorOr<void> encode_dc(OutputHuffmanTable const& dc_table, i16 const component[], u8 component_id)
{
// F.1.2.1.3 - Huffman encoding procedures for DC coefficients
auto diff = component[0] - m_last_dc_values[component_id];
m_last_dc_values[component_id] = component[0];
auto const size = csize(diff);
TRY(write_symbol(dc_table.from_input_byte(size)));
if (diff < 0)
diff -= 1;
TRY(m_bit_stream.write_bits<u16>(diff, size));
return {};
}
ErrorOr<void> encode_ac(OutputHuffmanTable const& ac_table, i16 const component[])
{
{
// F.2 - Procedure for sequential encoding of AC coefficients with Huffman coding
u32 k {};
u32 r {};
while (k < 63) {
k++;
auto coefficient = component[zigzag_map[k]];
if (coefficient == 0) {
if (k == 63) {
TRY(write_symbol(ac_table.from_input_byte(0x00)));
break;
}
r += 1;
continue;
}
while (r > 15) {
TRY(write_symbol(ac_table.from_input_byte(0xF0)));
r -= 16;
}
{
// F.3 - Sequential encoding of a non-zero AC coefficient
auto const ssss = csize(coefficient);
auto const rs = (r << 4) + ssss;
TRY(write_symbol(ac_table.from_input_byte(rs)));
if (coefficient < 0)
coefficient -= 1;
TRY(m_bit_stream.write_bits<u16>(coefficient, ssss));
}
r = 0;
}
}
return {};
}
static u8 csize(i16 coefficient)
{
VERIFY(coefficient >= -2047 && coefficient <= 2047);
if (coefficient == 0)
return 0;
return floor(log2(abs(coefficient))) + 1;
}
QuantizationTable m_luminance_quantization_table {};
QuantizationTable m_chrominance_quantization_table {};
Vector<Macroblock> m_macroblocks {};
Array<i16, 4> m_last_dc_values {};
JPEGBigEndianOutputBitStream m_bit_stream;
};
ErrorOr<void> add_start_of_image(Stream& stream)
ErrorOr<void> JPEGWriter::encode_impl(Stream& stream, auto const& bitmap, Options const& options, ColorSpace color_space)
{
TRY(stream.write_value<BigEndian<Marker>>(JPEG_SOI));
struct jpeg_compress_struct cinfo { };
struct jpeg_error_mgr jerr { };
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
Vector<u8> buffer;
MemoryDestinationManager dest_manager(buffer);
cinfo.dest = &dest_manager;
cinfo.image_width = bitmap.size().width();
cinfo.image_height = bitmap.size().height();
cinfo.input_components = 4;
switch (color_space) {
case ColorSpace::RGB:
cinfo.in_color_space = JCS_EXT_BGRX;
break;
case ColorSpace::CMYK:
cinfo.in_color_space = JCS_CMYK;
break;
default:
VERIFY_NOT_REACHED();
}
jpeg_set_defaults(&cinfo);
jpeg_set_colorspace(&cinfo, JCS_YCbCr);
jpeg_set_quality(&cinfo, options.quality, TRUE);
if (options.icc_data.has_value()) {
jpeg_write_icc_profile(&cinfo, options.icc_data->data(), options.icc_data->size());
}
jpeg_start_compress(&cinfo, TRUE);
Vector<JSAMPLE> row_buffer;
row_buffer.resize(bitmap.size().width() * 4);
while (cinfo.next_scanline < cinfo.image_height) {
auto const* row_ptr = reinterpret_cast<u8 const*>(bitmap.scanline(cinfo.next_scanline));
JSAMPROW row_pointer = (JSAMPROW)row_ptr;
jpeg_write_scanlines(&cinfo, &row_pointer, 1);
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
TRY(stream.write_until_depleted(buffer));
return {};
}
ErrorOr<void> add_end_of_image(Stream& stream)
{
TRY(stream.write_value<BigEndian<Marker>>(JPEG_EOI));
return {};
}
ErrorOr<void> 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<BigEndian<Marker>>(JPEG_APPN2));
TRY(stream.write_value<BigEndian<u16>>(icc_chunk_header_size + chunk_size));
TRY(stream.write_until_depleted(icc_chunk_name.bytes()));
TRY(stream.write_value<u8>(chunk_id));
TRY(stream.write_value<u8>(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<void> add_frame_header(Stream& stream, JPEGEncodingContext const& context, IntSize size, Mode mode)
{
// B.2.2 - Frame header syntax
TRY(stream.write_value<BigEndian<Marker>>(JPEG_SOF0));
u16 const Nf = mode == Mode::CMYK ? 4 : 3;
// Lf = 8 + 3 × Nf
TRY(stream.write_value<BigEndian<u16>>(8 + 3 * Nf));
// P
TRY(stream.write_value<u8>(8));
// Y
TRY(stream.write_value<BigEndian<u16>>(size.height()));
// X
TRY(stream.write_value<BigEndian<u16>>(size.width()));
// Nf
TRY(stream.write_value<u8>(Nf));
// Encode Nf components
for (u8 i {}; i < Nf; ++i) {
// Ci
TRY(stream.write_value<u8>(i + 1));
// Hi and Vi
TRY(stream.write_value<u8>((1 << 4) | 1));
// Tqi
TRY(stream.write_value<u8>((i == 0 || i == 3 ? context.luminance_quantization_table() : context.chrominance_quantization_table()).id));
}
return {};
}
ErrorOr<void> add_ycck_color_transform_header(Stream& stream)
{
// T-REC-T.872-201206-I!!PDF-E.pdf, 6.5.3 APP14 marker segment for colour encoding
TRY(stream.write_value<BigEndian<Marker>>(JPEG_APPN14));
TRY(stream.write_value<BigEndian<u16>>(14));
TRY(stream.write_until_depleted("Adobe\0"sv.bytes()));
// These values are ignored.
TRY(stream.write_value<u8>(0x64));
TRY(stream.write_value<BigEndian<u16>>(0x0000));
TRY(stream.write_value<BigEndian<u16>>(0x0000));
// YCCK
TRY(stream.write_value<u8>(0x2));
return {};
}
ErrorOr<void> add_quantization_table(Stream& stream, QuantizationTable const& table)
{
// B.2.4.1 - Quantization table-specification syntax
TRY(stream.write_value<BigEndian<Marker>>(JPEG_DQT));
// Lq = 2 + 1 * 65
TRY(stream.write_value<BigEndian<u16>>(2 + 65));
// Pq and Tq
TRY(stream.write_value<u8>((0 << 4) | table.id));
for (u8 i = 0; i < 64; ++i)
TRY(stream.write_value<u8>(table.table[zigzag_map[i]]));
return {};
}
ErrorOr<Vector<Vector<u8>, 16>> sort_symbols_per_size(OutputHuffmanTable const& table)
{
// JPEG only allows symbol with a size less than or equal to 16.
Vector<Vector<u8>, 16> output {};
TRY(output.try_resize(16));
for (auto const& symbol : table.table)
TRY(output[symbol.code_length - 1].try_append(symbol.input_byte));
return output;
}
ErrorOr<void> add_huffman_table(Stream& stream, OutputHuffmanTable const& table)
{
// B.2.4.2 - Huffman table-specification syntax
TRY(stream.write_value<BigEndian<Marker>>(JPEG_DHT));
// Lh
TRY(stream.write_value<BigEndian<u16>>(2 + 17 + table.table.size()));
// Tc and Th
TRY(stream.write_value<u8>(table.id));
auto const vectorized_table = TRY(sort_symbols_per_size(table));
for (auto const& symbol_vector : vectorized_table)
TRY(stream.write_value<u8>(symbol_vector.size()));
for (auto const& symbol_vector : vectorized_table) {
for (auto symbol : symbol_vector)
TRY(stream.write_value<u8>(symbol));
}
return {};
}
ErrorOr<void> add_scan_header(Stream& stream, Mode mode)
{
// B.2.3 - Scan header syntax
TRY(stream.write_value<BigEndian<Marker>>(JPEG_SOS));
u16 const Ns = mode == Mode::CMYK ? 4 : 3;
// Ls - 6 + 2 × Ns
TRY(stream.write_value<BigEndian<u16>>(6 + 2 * Ns));
// Ns
TRY(stream.write_value<u8>(Ns));
// Encode Ns components
for (u8 i {}; i < Ns; ++i) {
// Csj
TRY(stream.write_value<u8>(i + 1));
// Tdj and Taj
// We're using 0 for luminance and 1 for chrominance
u8 const huffman_identifier = i == 0 || i == 3 ? 0 : 1;
TRY(stream.write_value<u8>((huffman_identifier << 4) | huffman_identifier));
}
// Ss
TRY(stream.write_value<u8>(0));
// Se
TRY(stream.write_value<u8>(63));
// Ah and Al
TRY(stream.write_value<u8>((0 << 4) | 0));
return {};
}
ErrorOr<void> add_headers(Stream& stream, JPEGEncodingContext& context, JPEGWriter::Options const& options, IntSize size, Mode mode)
{
context.set_luminance_quantization_table(s_default_luminance_quantization_table, options.quality);
context.set_chrominance_quantization_table(s_default_chrominance_quantization_table, options.quality);
context.dc_luminance_huffman_table = s_default_dc_luminance_huffman_table;
context.dc_chrominance_huffman_table = s_default_dc_chrominance_huffman_table;
context.ac_luminance_huffman_table = s_default_ac_luminance_huffman_table;
context.ac_chrominance_huffman_table = s_default_ac_chrominance_huffman_table;
TRY(add_start_of_image(stream));
if (options.icc_data.has_value())
TRY(add_icc_data(stream, options.icc_data.value()));
if (mode == Mode::CMYK)
TRY(add_ycck_color_transform_header(stream));
TRY(add_frame_header(stream, context, size, mode));
TRY(add_quantization_table(stream, context.luminance_quantization_table()));
TRY(add_quantization_table(stream, context.chrominance_quantization_table()));
TRY(add_huffman_table(stream, context.dc_luminance_huffman_table));
TRY(add_huffman_table(stream, context.dc_chrominance_huffman_table));
TRY(add_huffman_table(stream, context.ac_luminance_huffman_table));
TRY(add_huffman_table(stream, context.ac_chrominance_huffman_table));
TRY(add_scan_header(stream, mode));
return {};
}
ErrorOr<void> add_image(Stream& stream, JPEGEncodingContext& context, Mode mode)
{
context.fdct_and_quantization(mode);
TRY(context.write_huffman_stream(mode));
TRY(add_end_of_image(stream));
return {};
}
}
ErrorOr<void> JPEGWriter::encode(Stream& stream, Bitmap const& bitmap, Options const& options)
{
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));
return {};
return encode_impl(stream, bitmap, options, ColorSpace::RGB);
}
ErrorOr<void> JPEGWriter::encode(Stream& stream, CMYKBitmap const& bitmap, Options const& options)
{
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));
return {};
return encode_impl(stream, bitmap, options, ColorSpace::CMYK);
}
}

View file

@ -24,6 +24,12 @@ public:
static ErrorOr<void> encode(Stream&, CMYKBitmap const&, Options const& = {});
private:
enum class ColorSpace {
RGB,
CMYK,
};
static ErrorOr<void> encode_impl(Stream&, auto const&, Options const&, ColorSpace);
JPEGWriter() = delete;
};

View file

@ -1,457 +0,0 @@
/*
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Vector.h>
namespace Gfx {
struct QuantizationTable {
Array<u8, 64> table {};
u8 id {};
};
// K.1 - Quantization tables for luminance and chrominance components
// clang-format off
constexpr static QuantizationTable s_default_luminance_quantization_table {
.table = {
16, 11, 10, 16, 124, 140, 151, 161,
12, 12, 14, 19, 126, 158, 160, 155,
14, 13, 16, 24, 140, 157, 169, 156,
14, 17, 22, 29, 151, 187, 180, 162,
18, 22, 37, 56, 168, 109, 103, 177,
24, 35, 55, 64, 181, 104, 113, 192,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 199,
},
.id = 0,
};
constexpr static QuantizationTable s_default_chrominance_quantization_table {
.table = {
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
},
.id = 1,
};
constexpr static QuantizationTable s_dummy_quantization_table {
.table = {
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
},
.id = 1,
};
// clang-format on
struct OutputHuffmanTable {
struct Symbol {
u8 input_byte {};
u8 code_length {};
u16 word {};
};
Symbol from_input_byte(u8 input_byte) const
{
for (auto symbol : table) {
if (symbol.input_byte == input_byte)
return symbol;
}
VERIFY_NOT_REACHED();
}
Vector<Symbol, 16> table {};
u8 id {};
};
static OutputHuffmanTable s_default_dc_luminance_huffman_table {
.table = {
{ 0, 2, 0b00 },
{ 1, 3, 0b010 },
{ 2, 3, 0b011 },
{ 3, 3, 0b100 },
{ 4, 3, 0b101 },
{ 5, 3, 0b110 },
{ 6, 4, 0b1110 },
{ 7, 5, 0b11110 },
{ 8, 6, 0b111110 },
{ 9, 7, 0b1111110 },
{ 10, 8, 0b11111110 },
{ 11, 9, 0b111111110 },
},
.id = (0 << 4) | 0,
};
static OutputHuffmanTable s_default_dc_chrominance_huffman_table {
.table = {
{ 0, 2, 0b00 },
{ 1, 2, 0b01 },
{ 2, 2, 0b10 },
{ 3, 3, 0b110 },
{ 4, 4, 0b1110 },
{ 5, 5, 0b11110 },
{ 6, 6, 0b111110 },
{ 7, 7, 0b1111110 },
{ 8, 8, 0b11111110 },
{ 9, 9, 0b111111110 },
{ 10, 10, 0b1111111110 },
{ 11, 11, 0b11111111110 },
},
.id = (0 << 4) | 1,
};
static OutputHuffmanTable s_default_ac_luminance_huffman_table {
.table = {
{ 0x01, 2, 0b00 },
{ 0x02, 2, 0b01 },
{ 0x03, 3, 0b100 },
{ 0x00, 4, 0b1010 },
{ 0x04, 4, 0b1011 },
{ 0x11, 4, 0b1100 },
{ 0x05, 5, 0b11010 },
{ 0x12, 5, 0b11011 },
{ 0x21, 5, 0b11100 },
{ 0x31, 6, 0b111010 },
{ 0x41, 6, 0b111011 },
{ 0x06, 7, 0b1111000 },
{ 0x13, 7, 0b1111001 },
{ 0x51, 7, 0b1111010 },
{ 0x61, 7, 0b1111011 },
{ 0x07, 8, 0b11111000 },
{ 0x22, 8, 0b11111001 },
{ 0x71, 8, 0b11111010 },
{ 0x14, 9, 0b111110110 },
{ 0x32, 9, 0b111110111 },
{ 0x81, 9, 0b111111000 },
{ 0x91, 9, 0b111111001 },
{ 0xA1, 9, 0b111111010 },
{ 0x08, 10, 0b1111110110 },
{ 0x23, 10, 0b1111110111 },
{ 0x42, 10, 0b1111111000 },
{ 0xB1, 10, 0b1111111001 },
{ 0xC1, 10, 0b1111111010 },
{ 0x15, 11, 0b11111110110 },
{ 0x52, 11, 0b11111110111 },
{ 0xD1, 11, 0b11111111000 },
{ 0xF0, 11, 0b11111111001 },
{ 0x24, 12, 0b111111110100 },
{ 0x33, 12, 0b111111110101 },
{ 0x62, 12, 0b111111110110 },
{ 0x72, 12, 0b111111110111 },
{ 0x82, 15, 0b111111111000000 },
{ 0x09, 16, 0b1111111110000010 },
{ 0x0A, 16, 0b1111111110000011 },
{ 0x16, 16, 0b1111111110000100 },
{ 0x17, 16, 0b1111111110000101 },
{ 0x18, 16, 0b1111111110000110 },
{ 0x19, 16, 0b1111111110000111 },
{ 0x1A, 16, 0b1111111110001000 },
{ 0x25, 16, 0b1111111110001001 },
{ 0x26, 16, 0b1111111110001010 },
{ 0x27, 16, 0b1111111110001011 },
{ 0x28, 16, 0b1111111110001100 },
{ 0x29, 16, 0b1111111110001101 },
{ 0x2A, 16, 0b1111111110001110 },
{ 0x34, 16, 0b1111111110001111 },
{ 0x35, 16, 0b1111111110010000 },
{ 0x36, 16, 0b1111111110010001 },
{ 0x37, 16, 0b1111111110010010 },
{ 0x38, 16, 0b1111111110010011 },
{ 0x39, 16, 0b1111111110010100 },
{ 0x3A, 16, 0b1111111110010101 },
{ 0x43, 16, 0b1111111110010110 },
{ 0x44, 16, 0b1111111110010111 },
{ 0x45, 16, 0b1111111110011000 },
{ 0x46, 16, 0b1111111110011001 },
{ 0x47, 16, 0b1111111110011010 },
{ 0x48, 16, 0b1111111110011011 },
{ 0x49, 16, 0b1111111110011100 },
{ 0x4A, 16, 0b1111111110011101 },
{ 0x53, 16, 0b1111111110011110 },
{ 0x54, 16, 0b1111111110011111 },
{ 0x55, 16, 0b1111111110100000 },
{ 0x56, 16, 0b1111111110100001 },
{ 0x57, 16, 0b1111111110100010 },
{ 0x58, 16, 0b1111111110100011 },
{ 0x59, 16, 0b1111111110100100 },
{ 0x5A, 16, 0b1111111110100101 },
{ 0x63, 16, 0b1111111110100110 },
{ 0x64, 16, 0b1111111110100111 },
{ 0x65, 16, 0b1111111110101000 },
{ 0x66, 16, 0b1111111110101001 },
{ 0x67, 16, 0b1111111110101010 },
{ 0x68, 16, 0b1111111110101011 },
{ 0x69, 16, 0b1111111110101100 },
{ 0x6A, 16, 0b1111111110101101 },
{ 0x73, 16, 0b1111111110101110 },
{ 0x74, 16, 0b1111111110101111 },
{ 0x75, 16, 0b1111111110110000 },
{ 0x76, 16, 0b1111111110110001 },
{ 0x77, 16, 0b1111111110110010 },
{ 0x78, 16, 0b1111111110110011 },
{ 0x79, 16, 0b1111111110110100 },
{ 0x7A, 16, 0b1111111110110101 },
{ 0x83, 16, 0b1111111110110110 },
{ 0x84, 16, 0b1111111110110111 },
{ 0x85, 16, 0b1111111110111000 },
{ 0x86, 16, 0b1111111110111001 },
{ 0x87, 16, 0b1111111110111010 },
{ 0x88, 16, 0b1111111110111011 },
{ 0x89, 16, 0b1111111110111100 },
{ 0x8A, 16, 0b1111111110111101 },
{ 0x92, 16, 0b1111111110111110 },
{ 0x93, 16, 0b1111111110111111 },
{ 0x94, 16, 0b1111111111000000 },
{ 0x95, 16, 0b1111111111000001 },
{ 0x96, 16, 0b1111111111000010 },
{ 0x97, 16, 0b1111111111000011 },
{ 0x98, 16, 0b1111111111000100 },
{ 0x99, 16, 0b1111111111000101 },
{ 0x9A, 16, 0b1111111111000110 },
{ 0xA2, 16, 0b1111111111000111 },
{ 0xA3, 16, 0b1111111111001000 },
{ 0xA4, 16, 0b1111111111001001 },
{ 0xA5, 16, 0b1111111111001010 },
{ 0xA6, 16, 0b1111111111001011 },
{ 0xA7, 16, 0b1111111111001100 },
{ 0xA8, 16, 0b1111111111001101 },
{ 0xA9, 16, 0b1111111111001110 },
{ 0xAA, 16, 0b1111111111001111 },
{ 0xB2, 16, 0b1111111111010000 },
{ 0xB3, 16, 0b1111111111010001 },
{ 0xB4, 16, 0b1111111111010010 },
{ 0xB5, 16, 0b1111111111010011 },
{ 0xB6, 16, 0b1111111111010100 },
{ 0xB7, 16, 0b1111111111010101 },
{ 0xB8, 16, 0b1111111111010110 },
{ 0xB9, 16, 0b1111111111010111 },
{ 0xBA, 16, 0b1111111111011000 },
{ 0xC2, 16, 0b1111111111011001 },
{ 0xC3, 16, 0b1111111111011010 },
{ 0xC4, 16, 0b1111111111011011 },
{ 0xC5, 16, 0b1111111111011100 },
{ 0xC6, 16, 0b1111111111011101 },
{ 0xC7, 16, 0b1111111111011110 },
{ 0xC8, 16, 0b1111111111011111 },
{ 0xC9, 16, 0b1111111111100000 },
{ 0xCA, 16, 0b1111111111100001 },
{ 0xD2, 16, 0b1111111111100010 },
{ 0xD3, 16, 0b1111111111100011 },
{ 0xD4, 16, 0b1111111111100100 },
{ 0xD5, 16, 0b1111111111100101 },
{ 0xD6, 16, 0b1111111111100110 },
{ 0xD7, 16, 0b1111111111100111 },
{ 0xD8, 16, 0b1111111111101000 },
{ 0xD9, 16, 0b1111111111101001 },
{ 0xDA, 16, 0b1111111111101010 },
{ 0xE1, 16, 0b1111111111101011 },
{ 0xE2, 16, 0b1111111111101100 },
{ 0xE3, 16, 0b1111111111101101 },
{ 0xE4, 16, 0b1111111111101110 },
{ 0xE5, 16, 0b1111111111101111 },
{ 0xE6, 16, 0b1111111111110000 },
{ 0xE7, 16, 0b1111111111110001 },
{ 0xE8, 16, 0b1111111111110010 },
{ 0xE9, 16, 0b1111111111110011 },
{ 0xEA, 16, 0b1111111111110100 },
{ 0xF1, 16, 0b1111111111110101 },
{ 0xF2, 16, 0b1111111111110110 },
{ 0xF3, 16, 0b1111111111110111 },
{ 0xF4, 16, 0b1111111111111000 },
{ 0xF5, 16, 0b1111111111111001 },
{ 0xF6, 16, 0b1111111111111010 },
{ 0xF7, 16, 0b1111111111111011 },
{ 0xF8, 16, 0b1111111111111100 },
{ 0xF9, 16, 0b1111111111111101 },
{ 0xFA, 16, 0b1111111111111110 },
},
.id = (1 << 4) | 0,
};
static OutputHuffmanTable s_default_ac_chrominance_huffman_table {
.table = {
{ 0x00, 2, 0b00 },
{ 0x01, 2, 0b01 },
{ 0x02, 3, 0b100 },
{ 0x03, 4, 0b1010 },
{ 0x11, 4, 0b1011 },
{ 0x04, 5, 0b11000 },
{ 0x05, 5, 0b11001 },
{ 0x21, 5, 0b11010 },
{ 0x31, 5, 0b11011 },
{ 0x06, 6, 0b111000 },
{ 0x12, 6, 0b111001 },
{ 0x41, 6, 0b111010 },
{ 0x51, 6, 0b111011 },
{ 0x07, 7, 0b1111000 },
{ 0x61, 7, 0b1111001 },
{ 0x71, 7, 0b1111010 },
{ 0x13, 8, 0b11110110 },
{ 0x22, 8, 0b11110111 },
{ 0x32, 8, 0b11111000 },
{ 0x81, 8, 0b11111001 },
{ 0x08, 9, 0b111110100 },
{ 0x14, 9, 0b111110101 },
{ 0x42, 9, 0b111110110 },
{ 0x91, 9, 0b111110111 },
{ 0xA1, 9, 0b111111000 },
{ 0xB1, 9, 0b111111001 },
{ 0xC1, 9, 0b111111010 },
{ 0x09, 10, 0b1111110110 },
{ 0x23, 10, 0b1111110111 },
{ 0x33, 10, 0b1111111000 },
{ 0x52, 10, 0b1111111001 },
{ 0xF0, 10, 0b1111111010 },
{ 0x15, 11, 0b11111110110 },
{ 0x62, 11, 0b11111110111 },
{ 0x72, 11, 0b11111111000 },
{ 0xD1, 11, 0b11111111001 },
{ 0x0A, 12, 0b111111110100 },
{ 0x16, 12, 0b111111110101 },
{ 0x24, 12, 0b111111110110 },
{ 0x34, 12, 0b111111110111 },
{ 0xE1, 14, 0b11111111100000 },
{ 0x25, 15, 0b111111111000010 },
{ 0xF1, 15, 0b111111111000011 },
{ 0x17, 16, 0b1111111110001000 },
{ 0x18, 16, 0b1111111110001001 },
{ 0x19, 16, 0b1111111110001010 },
{ 0x1A, 16, 0b1111111110001011 },
{ 0x26, 16, 0b1111111110001100 },
{ 0x27, 16, 0b1111111110001101 },
{ 0x28, 16, 0b1111111110001110 },
{ 0x29, 16, 0b1111111110001111 },
{ 0x2A, 16, 0b1111111110010000 },
{ 0x35, 16, 0b1111111110010001 },
{ 0x36, 16, 0b1111111110010010 },
{ 0x37, 16, 0b1111111110010011 },
{ 0x38, 16, 0b1111111110010100 },
{ 0x39, 16, 0b1111111110010101 },
{ 0x3A, 16, 0b1111111110010110 },
{ 0x43, 16, 0b1111111110010111 },
{ 0x44, 16, 0b1111111110011000 },
{ 0x45, 16, 0b1111111110011001 },
{ 0x46, 16, 0b1111111110011010 },
{ 0x47, 16, 0b1111111110011011 },
{ 0x48, 16, 0b1111111110011100 },
{ 0x49, 16, 0b1111111110011101 },
{ 0x4A, 16, 0b1111111110011110 },
{ 0x53, 16, 0b1111111110011111 },
{ 0x54, 16, 0b1111111110100000 },
{ 0x55, 16, 0b1111111110100001 },
{ 0x56, 16, 0b1111111110100010 },
{ 0x57, 16, 0b1111111110100011 },
{ 0x58, 16, 0b1111111110100100 },
{ 0x59, 16, 0b1111111110100101 },
{ 0x5A, 16, 0b1111111110100110 },
{ 0x63, 16, 0b1111111110100111 },
{ 0x64, 16, 0b1111111110101000 },
{ 0x65, 16, 0b1111111110101001 },
{ 0x66, 16, 0b1111111110101010 },
{ 0x67, 16, 0b1111111110101011 },
{ 0x68, 16, 0b1111111110101100 },
{ 0x69, 16, 0b1111111110101101 },
{ 0x6A, 16, 0b1111111110101110 },
{ 0x73, 16, 0b1111111110101111 },
{ 0x74, 16, 0b1111111110110000 },
{ 0x75, 16, 0b1111111110110001 },
{ 0x76, 16, 0b1111111110110010 },
{ 0x77, 16, 0b1111111110110011 },
{ 0x78, 16, 0b1111111110110100 },
{ 0x79, 16, 0b1111111110110101 },
{ 0x7A, 16, 0b1111111110110110 },
{ 0x82, 16, 0b1111111110110111 },
{ 0x83, 16, 0b1111111110111000 },
{ 0x84, 16, 0b1111111110111001 },
{ 0x85, 16, 0b1111111110111010 },
{ 0x86, 16, 0b1111111110111011 },
{ 0x87, 16, 0b1111111110111100 },
{ 0x88, 16, 0b1111111110111101 },
{ 0x89, 16, 0b1111111110111110 },
{ 0x8A, 16, 0b1111111110111111 },
{ 0x92, 16, 0b1111111111000000 },
{ 0x93, 16, 0b1111111111000001 },
{ 0x94, 16, 0b1111111111000010 },
{ 0x95, 16, 0b1111111111000011 },
{ 0x96, 16, 0b1111111111000100 },
{ 0x97, 16, 0b1111111111000101 },
{ 0x98, 16, 0b1111111111000110 },
{ 0x99, 16, 0b1111111111000111 },
{ 0x9A, 16, 0b1111111111001000 },
{ 0xA2, 16, 0b1111111111001001 },
{ 0xA3, 16, 0b1111111111001010 },
{ 0xA4, 16, 0b1111111111001011 },
{ 0xA5, 16, 0b1111111111001100 },
{ 0xA6, 16, 0b1111111111001101 },
{ 0xA7, 16, 0b1111111111001110 },
{ 0xA8, 16, 0b1111111111001111 },
{ 0xA9, 16, 0b1111111111010000 },
{ 0xAA, 16, 0b1111111111010001 },
{ 0xB2, 16, 0b1111111111010010 },
{ 0xB3, 16, 0b1111111111010011 },
{ 0xB4, 16, 0b1111111111010100 },
{ 0xB5, 16, 0b1111111111010101 },
{ 0xB6, 16, 0b1111111111010110 },
{ 0xB7, 16, 0b1111111111010111 },
{ 0xB8, 16, 0b1111111111011000 },
{ 0xB9, 16, 0b1111111111011001 },
{ 0xBA, 16, 0b1111111111011010 },
{ 0xC2, 16, 0b1111111111011011 },
{ 0xC3, 16, 0b1111111111011100 },
{ 0xC4, 16, 0b1111111111011101 },
{ 0xC5, 16, 0b1111111111011110 },
{ 0xC6, 16, 0b1111111111011111 },
{ 0xC7, 16, 0b1111111111100000 },
{ 0xC8, 16, 0b1111111111100001 },
{ 0xC9, 16, 0b1111111111100010 },
{ 0xCA, 16, 0b1111111111100011 },
{ 0xD2, 16, 0b1111111111100100 },
{ 0xD3, 16, 0b1111111111100101 },
{ 0xD4, 16, 0b1111111111100110 },
{ 0xD5, 16, 0b1111111111100111 },
{ 0xD6, 16, 0b1111111111101000 },
{ 0xD7, 16, 0b1111111111101001 },
{ 0xD8, 16, 0b1111111111101010 },
{ 0xD9, 16, 0b1111111111101011 },
{ 0xDA, 16, 0b1111111111101100 },
{ 0xE2, 16, 0b1111111111101101 },
{ 0xE3, 16, 0b1111111111101110 },
{ 0xE4, 16, 0b1111111111101111 },
{ 0xE5, 16, 0b1111111111110000 },
{ 0xE6, 16, 0b1111111111110001 },
{ 0xE7, 16, 0b1111111111110010 },
{ 0xE8, 16, 0b1111111111110011 },
{ 0xE9, 16, 0b1111111111110100 },
{ 0xEA, 16, 0b1111111111110101 },
{ 0xF2, 16, 0b1111111111110110 },
{ 0xF3, 16, 0b1111111111110111 },
{ 0xF4, 16, 0b1111111111111000 },
{ 0xF5, 16, 0b1111111111111001 },
{ 0xF6, 16, 0b1111111111111010 },
{ 0xF7, 16, 0b1111111111111011 },
{ 0xF8, 16, 0b1111111111111100 },
{ 0xF9, 16, 0b1111111111111101 },
{ 0xFA, 16, 0b1111111111111110 },
},
.id = (1 << 4) | 1,
};
}

View file

@ -10,7 +10,6 @@ set(CMD_SOURCES
js.cpp
lzcat.cpp
tar.cpp
test-jpeg-roundtrip.cpp
ttfdisasm.cpp
unzip.cpp
wasm.cpp
@ -43,7 +42,6 @@ target_link_libraries(isobmff PRIVATE LibGfx)
target_link_libraries(js PRIVATE LibCrypto LibJS LibLine LibLocale LibTextCodec)
target_link_libraries(lzcat PRIVATE LibCompress)
target_link_libraries(tar PRIVATE LibArchive LibCompress LibFileSystem)
target_link_libraries(test-jpeg-roundtrip PRIVATE LibGfx)
target_link_libraries(ttfdisasm PRIVATE LibGfx)
target_link_libraries(unzip PRIVATE LibArchive LibCompress LibCrypto LibFileSystem)
target_link_libraries(wasm PRIVATE LibFileSystem LibJS LibLine LibWasm)

View file

@ -157,17 +157,6 @@ static ErrorOr<void> save_image(LoadedImage& image, StringView out_path, bool pp
return Core::OutputBufferedFile::create(move(output_stream));
};
if (image.bitmap.has<RefPtr<Gfx::CMYKBitmap>>()) {
auto& cmyk_frame = image.bitmap.get<RefPtr<Gfx::CMYKBitmap>>();
if (out_path.ends_with(".jpg"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".jpeg"sv, CaseSensitivity::CaseInsensitive)) {
TRY(Gfx::JPEGWriter::encode(*TRY(stream()), *cmyk_frame, { .icc_data = image.icc_data, .quality = jpeg_quality }));
return {};
}
return Error::from_string_view("Can save CMYK bitmaps only as .jpg, convert to RGB first with --convert-to-color-profile"sv);
}
auto& frame = image.bitmap.get<RefPtr<Gfx::Bitmap>>();
if (out_path.ends_with(".gif"sv, CaseSensitivity::CaseInsensitive)) {

View file

@ -1,105 +0,0 @@
/*
* Copyright (c) 2024, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/DeltaE.h>
#include <LibGfx/ICC/Profile.h>
#include <LibGfx/ICC/WellKnownProfiles.h>
#include <LibGfx/ImageFormats/JPEGLoader.h>
#include <LibGfx/ImageFormats/JPEGWriter.h>
#include <LibMain/Main.h>
struct Fixpoint {
Gfx::Color fixpoint;
int number_of_iterations {};
};
static ErrorOr<Fixpoint> compute_fixpoint(Gfx::Color start_color)
{
auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, { 8, 8 }));
bitmap->fill(start_color);
int number_of_iterations = 1;
Color last_color = start_color;
while (true) {
AllocatingMemoryStream stream;
TRY(Gfx::JPEGWriter::encode(stream, *bitmap));
auto data = TRY(stream.read_until_eof());
auto plugin_decoder = TRY(Gfx::JPEGImageDecoderPlugin::create(data));
auto frame = TRY(plugin_decoder->frame(0));
Color current_color = frame.image->get_pixel(4, 4);
if (current_color == last_color)
break;
++number_of_iterations;
last_color = current_color;
bitmap = *frame.image;
}
return Fixpoint { last_color, number_of_iterations };
}
static ErrorOr<float> perceived_distance_in_sRGB(Gfx::Color a, Gfx::Color b)
{
auto sRGB = TRY(Gfx::ICC::sRGB());
Array<u8, 3> array_a { a.red(), a.green(), a.blue() };
Array<u8, 3> array_b { b.red(), b.green(), b.blue() };
return DeltaE(TRY(sRGB->to_lab(array_a)), TRY(sRGB->to_lab(array_b)));
}
struct Stats {
float max_delta {};
int max_number_of_iterations {};
};
static ErrorOr<void> test(Gfx::Color color, Stats& stats)
{
auto fixpoint = TRY(compute_fixpoint(color));
float perceived_distance = TRY(perceived_distance_in_sRGB(color, fixpoint.fixpoint));
outln("color {} converges to {} after saving {} times, delta {}", color, fixpoint.fixpoint, fixpoint.number_of_iterations, perceived_distance);
stats.max_delta = max(stats.max_delta, perceived_distance);
stats.max_number_of_iterations = max(stats.max_number_of_iterations, fixpoint.number_of_iterations);
return {};
}
ErrorOr<int> serenity_main(Main::Arguments)
{
Stats stats;
TRY(test(Gfx::Color::Red, stats));
TRY(test(Gfx::Color::Green, stats));
TRY(test(Gfx::Color::Blue, stats));
TRY(test(Gfx::Color::LightBlue, stats));
TRY(test(Gfx::Color::MidRed, stats));
TRY(test(Gfx::Color::MidGreen, stats));
TRY(test(Gfx::Color::MidBlue, stats));
TRY(test(Gfx::Color::DarkRed, stats));
TRY(test(Gfx::Color::DarkGreen, stats));
TRY(test(Gfx::Color::DarkBlue, stats));
TRY(test(Gfx::Color::Cyan, stats));
TRY(test(Gfx::Color::Magenta, stats));
TRY(test(Gfx::Color::Yellow, stats));
TRY(test(Gfx::Color::Black, stats));
TRY(test(Gfx::Color::DarkGray, stats));
TRY(test(Gfx::Color::MidGray, stats));
TRY(test(Gfx::Color::LightGray, stats));
TRY(test(Gfx::Color::White, stats));
outln();
outln("max delta {}, max number of iterations {}", stats.max_delta, stats.max_number_of_iterations);
return 0;
}

View file

@ -6,6 +6,7 @@
"platform": "linux | freebsd | openbsd"
},
"icu",
"libjpeg-turbo",
"sqlite3",
"woff2"
],
@ -18,6 +19,10 @@
"name": "icu",
"version": "74.2#1"
},
{
"name": "libjpeg-turbo",
"version": "3.0.2"
},
{
"name": "sqlite3",
"version": "3.45.3"