mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 16:57:35 +00:00
LibGfx/TIFF: Add support for images with CCITT3 1D compression
This compression (tag Compression=2) is not very popular on its own, but a base to implement CCITT3 2D and CCITT4 compressions. As the format has no real benefits, it is quite hard to find an app that accepts tho encode that for you. So I used the following program that calls `libtiff` directly: ```cpp #include <vector> #include <cstdlib> #include <iostream> #include <tiffio.h> // An array containing 0 and 1 of length width * height. extern std::vector<uint8_t> array; int main() { // From: https://stackoverflow.com/a/34257789 TIFF *image = TIFFOpen("input.tif", "w"); int const width = 400; int const height = 300; TIFFSetField(image, TIFFTAG_IMAGEWIDTH, width); TIFFSetField(image, TIFFTAG_IMAGELENGTH, height); TIFFSetField(image, TIFFTAG_PHOTOMETRIC, 0); TIFFSetField(image, TIFFTAG_COMPRESSION, COMPRESSION_CCITTRLE); TIFFSetField(image, TIFFTAG_BITSPERSAMPLE, 1); TIFFSetField(image, TIFFTAG_SAMPLESPERPIXEL, 1); TIFFSetField(image, TIFFTAG_ROWSPERSTRIP, 1); std::vector<uint8_t> scan_line(width / 8 + 8, 0); int count = 0; for (int i = 0; i < height; i++) { std::fill(scan_line.begin(), scan_line.end(), 0); for (int x = 0; x < width; ++x) { uint8_t eight_pixels = scan_line.at(x / 8); eight_pixels = eight_pixels << 1; eight_pixels |= !array.at(i * width + x); scan_line.at(x / 8) = eight_pixels; } int bytes = int(width / 8.0 + 0.5); if (TIFFWriteScanline(image, scan_line.data(), i, bytes) != 1) std::cerr << "Something went wrong\n"; } TIFFClose(image); } ```
This commit is contained in:
parent
7266d8c35d
commit
64912d4d02
6 changed files with 388 additions and 1 deletions
|
@ -406,6 +406,18 @@ TEST_CASE(test_tiff_uncompressed)
|
|||
EXPECT_EQ(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::Red);
|
||||
}
|
||||
|
||||
TEST_CASE(test_tiff_ccitt3_1d)
|
||||
{
|
||||
auto file = MUST(Core::MappedFile::map(TEST_INPUT("tiff/ccitt3_1d.tiff"sv)));
|
||||
EXPECT(Gfx::TIFFImageDecoderPlugin::sniff(file->bytes()));
|
||||
auto plugin_decoder = TRY_OR_FAIL(Gfx::TIFFImageDecoderPlugin::create(file->bytes()));
|
||||
|
||||
auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 400, 300 }));
|
||||
|
||||
EXPECT_EQ(frame.image->get_pixel(0, 0), Gfx::Color::NamedColor::White);
|
||||
EXPECT_EQ(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::Black);
|
||||
}
|
||||
|
||||
TEST_CASE(test_tiff_lzw)
|
||||
{
|
||||
auto file = MUST(Core::MappedFile::map(TEST_INPUT("tiff/lzw.tiff"sv)));
|
||||
|
|
BIN
Tests/LibGfx/test-inputs/tiff/ccitt3_1d.tiff
Normal file
BIN
Tests/LibGfx/test-inputs/tiff/ccitt3_1d.tiff
Normal file
Binary file not shown.
|
@ -37,7 +37,7 @@ set(SOURCES
|
|||
ImageFormats/BMPLoader.cpp
|
||||
ImageFormats/BMPWriter.cpp
|
||||
ImageFormats/BooleanDecoder.cpp
|
||||
ImageFormats/TIFFLoader.cpp
|
||||
ImageFormats/CCITTDecoder.cpp
|
||||
ImageFormats/DDSLoader.cpp
|
||||
ImageFormats/GIFLoader.cpp
|
||||
ImageFormats/ICOLoader.cpp
|
||||
|
@ -57,6 +57,7 @@ set(SOURCES
|
|||
ImageFormats/QOILoader.cpp
|
||||
ImageFormats/QOIWriter.cpp
|
||||
ImageFormats/TGALoader.cpp
|
||||
ImageFormats/TIFFLoader.cpp
|
||||
ImageFormats/TinyVGLoader.cpp
|
||||
ImageFormats/WebPLoader.cpp
|
||||
ImageFormats/WebPLoaderLossless.cpp
|
||||
|
|
333
Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.cpp
Normal file
333
Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.cpp
Normal file
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Array.h>
|
||||
#include <AK/BitStream.h>
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibGfx/ImageFormats/CCITTDecoder.h>
|
||||
|
||||
namespace Gfx::CCITT {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Code {
|
||||
u16 run_length {};
|
||||
u8 code_length {};
|
||||
u16 code {};
|
||||
};
|
||||
|
||||
// Table 2/T.4 – Terminating codes
|
||||
constexpr Array white_terminating_codes = {
|
||||
Code { 0, 8, 0b00110101 },
|
||||
Code { 1, 6, 0b000111 },
|
||||
Code { 2, 4, 0b0111 },
|
||||
Code { 3, 4, 0b1000 },
|
||||
Code { 4, 4, 0b1011 },
|
||||
Code { 5, 4, 0b1100 },
|
||||
Code { 6, 4, 0b1110 },
|
||||
Code { 7, 4, 0b1111 },
|
||||
Code { 8, 5, 0b10011 },
|
||||
Code { 9, 5, 0b10100 },
|
||||
Code { 10, 5, 0b00111 },
|
||||
Code { 11, 5, 0b01000 },
|
||||
Code { 12, 6, 0b001000 },
|
||||
Code { 13, 6, 0b000011 },
|
||||
Code { 14, 6, 0b110100 },
|
||||
Code { 15, 6, 0b110101 },
|
||||
Code { 16, 6, 0b101010 },
|
||||
Code { 17, 6, 0b101011 },
|
||||
Code { 18, 7, 0b0100111 },
|
||||
Code { 19, 7, 0b0001100 },
|
||||
Code { 20, 7, 0b0001000 },
|
||||
Code { 21, 7, 0b0010111 },
|
||||
Code { 22, 7, 0b0000011 },
|
||||
Code { 23, 7, 0b0000100 },
|
||||
Code { 24, 7, 0b0101000 },
|
||||
Code { 25, 7, 0b0101011 },
|
||||
Code { 26, 7, 0b0010011 },
|
||||
Code { 27, 7, 0b0100100 },
|
||||
Code { 28, 7, 0b0011000 },
|
||||
Code { 29, 8, 0b00000010 },
|
||||
Code { 30, 8, 0b00000011 },
|
||||
Code { 31, 8, 0b00011010 },
|
||||
Code { 32, 8, 0b00011011 },
|
||||
Code { 33, 8, 0b00010010 },
|
||||
Code { 34, 8, 0b00010011 },
|
||||
Code { 35, 8, 0b00010100 },
|
||||
Code { 36, 8, 0b00010101 },
|
||||
Code { 37, 8, 0b00010110 },
|
||||
Code { 38, 8, 0b00010111 },
|
||||
Code { 39, 8, 0b00101000 },
|
||||
Code { 40, 8, 0b00101001 },
|
||||
Code { 41, 8, 0b00101010 },
|
||||
Code { 42, 8, 0b00101011 },
|
||||
Code { 43, 8, 0b00101100 },
|
||||
Code { 44, 8, 0b00101101 },
|
||||
Code { 45, 8, 0b00000100 },
|
||||
Code { 46, 8, 0b00000101 },
|
||||
Code { 47, 8, 0b00001010 },
|
||||
Code { 48, 8, 0b00001011 },
|
||||
Code { 49, 8, 0b01010010 },
|
||||
Code { 50, 8, 0b01010011 },
|
||||
Code { 51, 8, 0b01010100 },
|
||||
Code { 52, 8, 0b01010101 },
|
||||
Code { 53, 8, 0b00100100 },
|
||||
Code { 54, 8, 0b00100101 },
|
||||
Code { 55, 8, 0b01011000 },
|
||||
Code { 56, 8, 0b01011001 },
|
||||
Code { 57, 8, 0b01011010 },
|
||||
Code { 58, 8, 0b01011011 },
|
||||
Code { 59, 8, 0b01001010 },
|
||||
Code { 60, 8, 0b01001011 },
|
||||
Code { 61, 8, 0b00110010 },
|
||||
Code { 62, 8, 0b00110011 },
|
||||
Code { 63, 8, 0b00110100 },
|
||||
};
|
||||
|
||||
// Table 2/T.4 – Terminating codes
|
||||
constexpr Array black_terminating_codes = {
|
||||
Code { 0, 10, 0b0000110111 },
|
||||
Code { 1, 3, 0b010 },
|
||||
Code { 2, 2, 0b11 },
|
||||
Code { 3, 2, 0b10 },
|
||||
Code { 4, 3, 0b011 },
|
||||
Code { 5, 4, 0b0011 },
|
||||
Code { 6, 4, 0b0010 },
|
||||
Code { 7, 5, 0b00011 },
|
||||
Code { 8, 6, 0b000101 },
|
||||
Code { 9, 6, 0b000100 },
|
||||
Code { 10, 7, 0b0000100 },
|
||||
Code { 11, 7, 0b0000101 },
|
||||
Code { 12, 7, 0b0000111 },
|
||||
Code { 13, 8, 0b00000100 },
|
||||
Code { 14, 8, 0b00000111 },
|
||||
Code { 15, 9, 0b000011000 },
|
||||
Code { 16, 10, 0b0000010111 },
|
||||
Code { 17, 10, 0b0000011000 },
|
||||
Code { 18, 10, 0b0000001000 },
|
||||
Code { 19, 11, 0b00001100111 },
|
||||
Code { 20, 11, 0b00001101000 },
|
||||
Code { 21, 11, 0b00001101100 },
|
||||
Code { 22, 11, 0b00000110111 },
|
||||
Code { 23, 11, 0b00000101000 },
|
||||
Code { 24, 11, 0b00000010111 },
|
||||
Code { 25, 11, 0b00000011000 },
|
||||
Code { 26, 12, 0b000011001010 },
|
||||
Code { 27, 12, 0b000011001011 },
|
||||
Code { 28, 12, 0b000011001100 },
|
||||
Code { 29, 12, 0b000011001101 },
|
||||
Code { 30, 12, 0b000001101000 },
|
||||
Code { 31, 12, 0b000001101001 },
|
||||
Code { 32, 12, 0b000001101010 },
|
||||
Code { 33, 12, 0b000001101011 },
|
||||
Code { 34, 12, 0b000011010010 },
|
||||
Code { 35, 12, 0b000011010011 },
|
||||
Code { 36, 12, 0b000011010100 },
|
||||
Code { 37, 12, 0b000011010101 },
|
||||
Code { 38, 12, 0b000011010110 },
|
||||
Code { 39, 12, 0b000011010111 },
|
||||
Code { 40, 12, 0b000001101100 },
|
||||
Code { 41, 12, 0b000001101101 },
|
||||
Code { 42, 12, 0b000011011010 },
|
||||
Code { 43, 12, 0b000011011011 },
|
||||
Code { 44, 12, 0b000001010100 },
|
||||
Code { 45, 12, 0b000001010101 },
|
||||
Code { 46, 12, 0b000001010110 },
|
||||
Code { 47, 12, 0b000001010111 },
|
||||
Code { 48, 12, 0b000001100100 },
|
||||
Code { 49, 12, 0b000001100101 },
|
||||
Code { 50, 12, 0b000001010010 },
|
||||
Code { 51, 12, 0b000001010011 },
|
||||
Code { 52, 12, 0b000000100100 },
|
||||
Code { 53, 12, 0b000000110111 },
|
||||
Code { 54, 12, 0b000000111000 },
|
||||
Code { 55, 12, 0b000000100111 },
|
||||
Code { 56, 12, 0b000000101000 },
|
||||
Code { 57, 12, 0b000001011000 },
|
||||
Code { 58, 12, 0b000001011001 },
|
||||
Code { 59, 12, 0b000000101011 },
|
||||
Code { 60, 12, 0b000000101100 },
|
||||
Code { 61, 12, 0b000001011010 },
|
||||
Code { 62, 12, 0b000001100110 },
|
||||
Code { 63, 12, 0b000001100111 },
|
||||
};
|
||||
|
||||
// Table 3a/T.4 – Make-up codes
|
||||
constexpr Array white_make_up_codes = {
|
||||
Code { 64, 5, 0b11011 },
|
||||
Code { 128, 5, 0b10010 },
|
||||
Code { 192, 6, 0b010111 },
|
||||
Code { 256, 7, 0b0110111 },
|
||||
Code { 320, 8, 0b00110110 },
|
||||
Code { 384, 8, 0b00110111 },
|
||||
Code { 448, 8, 0b01100100 },
|
||||
Code { 512, 8, 0b01100101 },
|
||||
Code { 576, 8, 0b01101000 },
|
||||
Code { 640, 8, 0b01100111 },
|
||||
Code { 704, 9, 0b011001100 },
|
||||
Code { 768, 9, 0b011001101 },
|
||||
Code { 832, 9, 0b011010010 },
|
||||
Code { 896, 9, 0b011010011 },
|
||||
Code { 960, 9, 0b011010100 },
|
||||
Code { 1024, 9, 0b011010101 },
|
||||
Code { 1088, 9, 0b011010110 },
|
||||
Code { 1152, 9, 0b011010111 },
|
||||
Code { 1216, 9, 0b011011000 },
|
||||
Code { 1280, 9, 0b011011001 },
|
||||
Code { 1344, 9, 0b011011010 },
|
||||
Code { 1408, 9, 0b011011011 },
|
||||
Code { 1472, 9, 0b010011000 },
|
||||
Code { 1536, 9, 0b010011001 },
|
||||
Code { 1600, 9, 0b010011010 },
|
||||
Code { 1664, 6, 0b011000 },
|
||||
Code { 1728, 9, 0b010011011 },
|
||||
Code { 0xE0L, 12, 0b000000000001 },
|
||||
};
|
||||
|
||||
// Table 3a/T.4 – Make-up codes
|
||||
constexpr Array black_make_up_codes = {
|
||||
Code { 64, 10, 0b0000001111 },
|
||||
Code { 128, 12, 0b000011001000 },
|
||||
Code { 192, 12, 0b000011001001 },
|
||||
Code { 256, 12, 0b000001011011 },
|
||||
Code { 320, 12, 0b000000110011 },
|
||||
Code { 384, 12, 0b000000110100 },
|
||||
Code { 448, 12, 0b000000110101 },
|
||||
Code { 512, 13, 0b0000001101100 },
|
||||
Code { 576, 13, 0b0000001101101 },
|
||||
Code { 640, 13, 0b0000001001010 },
|
||||
Code { 704, 13, 0b0000001001011 },
|
||||
Code { 768, 13, 0b0000001001100 },
|
||||
Code { 832, 13, 0b0000001001101 },
|
||||
Code { 896, 13, 0b0000001110010 },
|
||||
Code { 960, 13, 0b0000001110011 },
|
||||
Code { 1024, 13, 0b0000001110100 },
|
||||
Code { 1088, 13, 0b0000001110101 },
|
||||
Code { 1152, 13, 0b0000001110110 },
|
||||
Code { 1216, 13, 0b0000001110111 },
|
||||
Code { 1280, 13, 0b0000001010010 },
|
||||
Code { 1344, 13, 0b0000001010011 },
|
||||
Code { 1408, 13, 0b0000001010100 },
|
||||
Code { 1472, 13, 0b0000001010101 },
|
||||
Code { 1536, 13, 0b0000001011010 },
|
||||
Code { 1600, 13, 0b0000001011011 },
|
||||
Code { 1664, 13, 0b0000001100100 },
|
||||
Code { 1728, 13, 0b0000001100101 },
|
||||
Code { 0xE0L, 11, 0b00000000000 },
|
||||
};
|
||||
|
||||
// Table 3b/T.4 – Make-up codes
|
||||
constexpr Array common_make_up_codes = {
|
||||
Code { 1792, 11, 0b00000001000 },
|
||||
Code { 1856, 11, 0b00000001100 },
|
||||
Code { 1920, 11, 0b00000001101 },
|
||||
Code { 1984, 12, 0b000000010010 },
|
||||
Code { 2048, 12, 0b000000010011 },
|
||||
Code { 2112, 12, 0b000000010100 },
|
||||
Code { 2176, 12, 0b000000010101 },
|
||||
Code { 2240, 12, 0b000000010110 },
|
||||
Code { 2304, 12, 0b000000010111 },
|
||||
Code { 2368, 12, 0b000000011100 },
|
||||
Code { 2432, 12, 0b000000011101 },
|
||||
Code { 2496, 12, 0b000000011110 },
|
||||
Code { 2560, 12, 0b000000011111 },
|
||||
};
|
||||
|
||||
template<size_t Size>
|
||||
Optional<Code> get_code_from_table(Array<Code, Size> const& array, u16 code_word, u8 code_size)
|
||||
{
|
||||
for (auto const& code : array) {
|
||||
if (code.code_length == code_size && code.code == code_word)
|
||||
return code;
|
||||
}
|
||||
return OptionalNone {};
|
||||
}
|
||||
|
||||
Optional<Code> get_markup_code(Color color, u16 code_word, u8 code_size)
|
||||
{
|
||||
if (auto maybe_value = get_code_from_table(common_make_up_codes, code_word, code_size); maybe_value.has_value())
|
||||
return maybe_value.value();
|
||||
|
||||
if (color == Color::NamedColor::White)
|
||||
return get_code_from_table(white_make_up_codes, code_word, code_size);
|
||||
return get_code_from_table(black_make_up_codes, code_word, code_size);
|
||||
}
|
||||
|
||||
Optional<Code> get_terminal_code(Color color, u16 code_word, u8 code_size)
|
||||
{
|
||||
if (color == Color::NamedColor::White)
|
||||
return get_code_from_table(white_terminating_codes, code_word, code_size);
|
||||
return get_code_from_table(black_terminating_codes, code_word, code_size);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ErrorOr<ByteBuffer> decode_ccitt3_1d(ReadonlyBytes bytes, u32 image_width, u32 image_height)
|
||||
{
|
||||
auto strip_stream = make<FixedMemoryStream>(bytes);
|
||||
auto bit_stream = make<BigEndianInputBitStream>(MaybeOwned<Stream>(*strip_stream));
|
||||
|
||||
ByteBuffer decoded_bytes = TRY(ByteBuffer::create_zeroed(ceil_div(image_width * image_height, 8)));
|
||||
auto output_stream = make<FixedMemoryStream>(decoded_bytes.bytes());
|
||||
auto decoded_bits = make<BigEndianOutputBitStream>(MaybeOwned<Stream>(*output_stream));
|
||||
auto const ccitt_white = Color::NamedColor::White;
|
||||
auto const ccitt_black = Color::NamedColor::Black;
|
||||
|
||||
Color current_color { ccitt_black };
|
||||
u32 run_length {};
|
||||
|
||||
u32 column = 0;
|
||||
|
||||
while (!bit_stream->is_eof() || run_length > 0) {
|
||||
if (run_length > 0) {
|
||||
run_length--;
|
||||
TRY(decoded_bits->write_bits(current_color == ccitt_white ? 0u : 1u, 1));
|
||||
|
||||
++column;
|
||||
if (column == image_width) {
|
||||
column = 0;
|
||||
bit_stream->align_to_byte_boundary();
|
||||
TRY(decoded_bits->align_to_byte_boundary());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
current_color = current_color == ccitt_white ? ccitt_black : ccitt_white;
|
||||
|
||||
if (column == 0) {
|
||||
current_color = ccitt_white;
|
||||
}
|
||||
|
||||
u8 size {};
|
||||
u16 potential_code {};
|
||||
while (size < 14) {
|
||||
potential_code <<= 1;
|
||||
potential_code |= TRY(bit_stream->read_bit());
|
||||
size++;
|
||||
|
||||
if (auto const maybe_markup = get_markup_code(current_color, potential_code, size); maybe_markup.has_value()) {
|
||||
run_length += maybe_markup->run_length;
|
||||
// OK, let's reset the loop to read a terminal code now
|
||||
size = 0;
|
||||
potential_code = 0;
|
||||
} else if (auto const maybe_terminal = get_terminal_code(current_color, potential_code, size); maybe_terminal.has_value()) {
|
||||
run_length += maybe_terminal->run_length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (size == 14)
|
||||
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid CCITT code");
|
||||
|
||||
if (column + run_length > image_width)
|
||||
return Error::from_string_literal("TIFFImageDecoderPlugin: CCITT codes encode for more that a line");
|
||||
}
|
||||
|
||||
return decoded_bytes;
|
||||
}
|
||||
|
||||
}
|
26
Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.h
Normal file
26
Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteBuffer.h>
|
||||
|
||||
namespace Gfx::CCITT {
|
||||
|
||||
// You can find a great overview of CCITT compression schemes here:
|
||||
// https://www.fileformat.info/mirror/egff/ch09_05.htm
|
||||
|
||||
// The CCITT3 specification is accessible at this page:
|
||||
// https://www.itu.int/rec/T-REC-T.4/en
|
||||
|
||||
// The unidimensional scheme is originally described in:
|
||||
// 4.1 One-dimensional coding scheme
|
||||
// However, this function implements the TIFF variant (see TIFFLoader.h for a spec link),
|
||||
// differences are detailed in section:
|
||||
// Section 10: Modified Huffman Compression
|
||||
ErrorOr<ByteBuffer> decode_ccitt3_1d(ReadonlyBytes bytes, u32 image_width, u32 image_height);
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
#include <AK/Endian.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibCompress/LZWDecoder.h>
|
||||
#include <LibGfx/ImageFormats/CCITTDecoder.h>
|
||||
#include <LibGfx/ImageFormats/TIFFMetadata.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
@ -161,6 +162,20 @@ private:
|
|||
TRY(loop_over_pixels(move(identity)));
|
||||
break;
|
||||
}
|
||||
case Compression::CCITT: {
|
||||
if (m_metadata.bits_per_sample()->size() > 1)
|
||||
return Error::from_string_literal("TIFFImageDecoderPlugin: CCITT image with BitsPerSample greater than one, aborting...");
|
||||
|
||||
ByteBuffer decoded_bytes {};
|
||||
auto decode_ccitt_1D_strip = [&](u32 num_bytes) -> ErrorOr<ReadonlyBytes> {
|
||||
auto const encoded_bytes = TRY(m_stream->read_in_place<u8 const>(num_bytes));
|
||||
decoded_bytes = TRY(CCITT::decode_ccitt3_1d(encoded_bytes, *m_metadata.image_width(), *m_metadata.rows_per_strip()));
|
||||
return decoded_bytes;
|
||||
};
|
||||
|
||||
TRY(loop_over_pixels(move(decode_ccitt_1D_strip)));
|
||||
break;
|
||||
}
|
||||
case Compression::LZW: {
|
||||
ByteBuffer decoded_bytes {};
|
||||
auto decode_lzw_strip = [&](u32 num_bytes) -> ErrorOr<ReadonlyBytes> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue