streaming_test.cc (8826B)
1 // Copyright (c) the JPEG XL Project Authors. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 #include <algorithm> 7 #include <cstddef> 8 #include <cstdint> 9 #include <cstring> 10 #include <ostream> 11 #include <sstream> 12 #include <string> 13 #include <vector> 14 15 #include "lib/jpegli/decode.h" 16 #include "lib/jpegli/encode.h" 17 #include "lib/jpegli/test_params.h" 18 #include "lib/jpegli/test_utils.h" 19 #include "lib/jpegli/testing.h" 20 21 namespace jpegli { 22 namespace { 23 24 // A simple suspending source manager with an input buffer. 25 struct SourceManager { 26 jpeg_source_mgr pub; 27 std::vector<uint8_t> buffer; 28 29 SourceManager() { 30 pub.next_input_byte = nullptr; 31 pub.bytes_in_buffer = 0; 32 pub.init_source = init_source; 33 pub.fill_input_buffer = fill_input_buffer; 34 pub.skip_input_data = skip_input_data; 35 pub.resync_to_restart = jpegli_resync_to_restart; 36 pub.term_source = term_source; 37 } 38 39 static void init_source(j_decompress_ptr cinfo) {} 40 static boolean fill_input_buffer(j_decompress_ptr cinfo) { return FALSE; } 41 static void skip_input_data(j_decompress_ptr cinfo, 42 long num_bytes /* NOLINT */) {} 43 static void term_source(j_decompress_ptr cinfo) {} 44 }; 45 46 // A destination manager that empties its output buffer into a SourceManager's 47 // input buffer. The buffer size is kept short because empty_output_buffer() is 48 // called only when the output buffer is full, and we want to update the decoder 49 // input frequently to demonstrate that streaming works. 50 constexpr size_t kOutputBufferSize = 1024; 51 struct DestinationManager { 52 jpeg_destination_mgr pub; 53 std::vector<uint8_t> buffer; 54 SourceManager* dest; 55 56 explicit DestinationManager(SourceManager* src) 57 : buffer(kOutputBufferSize), dest(src) { 58 pub.next_output_byte = buffer.data(); 59 pub.free_in_buffer = buffer.size(); 60 pub.init_destination = init_destination; 61 pub.empty_output_buffer = empty_output_buffer; 62 pub.term_destination = term_destination; 63 } 64 65 static void init_destination(j_compress_ptr cinfo) {} 66 67 static boolean empty_output_buffer(j_compress_ptr cinfo) { 68 auto* us = reinterpret_cast<DestinationManager*>(cinfo->dest); 69 jpeg_destination_mgr* src = &us->pub; 70 jpeg_source_mgr* dst = &us->dest->pub; 71 std::vector<uint8_t>& src_buf = us->buffer; 72 std::vector<uint8_t>& dst_buf = us->dest->buffer; 73 if (dst->bytes_in_buffer > 0 && dst->bytes_in_buffer < dst_buf.size()) { 74 memmove(dst_buf.data(), dst->next_input_byte, dst->bytes_in_buffer); 75 } 76 size_t src_len = src_buf.size() - src->free_in_buffer; 77 dst_buf.resize(dst->bytes_in_buffer + src_len); 78 memcpy(&dst_buf[dst->bytes_in_buffer], src_buf.data(), src_len); 79 dst->next_input_byte = dst_buf.data(); 80 dst->bytes_in_buffer = dst_buf.size(); 81 src->next_output_byte = src_buf.data(); 82 src->free_in_buffer = src_buf.size(); 83 return TRUE; 84 } 85 86 static void term_destination(j_compress_ptr cinfo) { 87 empty_output_buffer(cinfo); 88 } 89 }; 90 91 struct TestConfig { 92 TestImage input; 93 CompressParams jparams; 94 }; 95 96 class StreamingTestParam : public ::testing::TestWithParam<TestConfig> {}; 97 98 TEST_P(StreamingTestParam, TestStreaming) { 99 jpeg_decompress_struct dinfo = {}; 100 jpeg_compress_struct cinfo = {}; 101 SourceManager src; 102 TestConfig config = GetParam(); 103 TestImage& input = config.input; 104 TestImage output; 105 GeneratePixels(&input); 106 const auto try_catch_block = [&]() { 107 ERROR_HANDLER_SETUP(jpegli); 108 dinfo.err = cinfo.err; 109 dinfo.client_data = cinfo.client_data; 110 // Create a pair of compressor and decompressor objects, where the 111 // compressor's output is connected to the decompressor's input. 112 jpegli_create_decompress(&dinfo); 113 jpegli_create_compress(&cinfo); 114 dinfo.src = reinterpret_cast<jpeg_source_mgr*>(&src); 115 DestinationManager dest(&src); 116 cinfo.dest = reinterpret_cast<jpeg_destination_mgr*>(&dest); 117 118 cinfo.image_width = input.xsize; 119 cinfo.image_height = input.ysize; 120 cinfo.input_components = input.components; 121 cinfo.in_color_space = static_cast<J_COLOR_SPACE>(input.color_space); 122 jpegli_set_defaults(&cinfo); 123 cinfo.comp_info[0].v_samp_factor = config.jparams.v_sampling[0]; 124 jpegli_set_progressive_level(&cinfo, 0); 125 cinfo.optimize_coding = FALSE; 126 jpegli_start_compress(&cinfo, TRUE); 127 128 size_t stride = cinfo.image_width * cinfo.input_components; 129 size_t iMCU_height = 8 * cinfo.max_v_samp_factor; 130 std::vector<uint8_t> row_bytes(iMCU_height * stride); 131 size_t y_in = 0; 132 size_t y_out = 0; 133 while (y_in < cinfo.image_height) { 134 // Feed one iMCU row at a time to the compressor. 135 size_t lines_in = std::min(iMCU_height, cinfo.image_height - y_in); 136 memcpy(row_bytes.data(), &input.pixels[y_in * stride], lines_in * stride); 137 std::vector<JSAMPROW> rows_in(lines_in); 138 for (size_t i = 0; i < lines_in; ++i) { 139 rows_in[i] = &row_bytes[i * stride]; 140 } 141 EXPECT_EQ(lines_in, 142 jpegli_write_scanlines(&cinfo, rows_in.data(), lines_in)); 143 y_in += lines_in; 144 if (y_in == cinfo.image_height) { 145 jpegli_finish_compress(&cinfo); 146 } 147 148 // Atfer the first iMCU row, we don't yet expect any output because the 149 // compressor delays processing to have context rows after the iMCU row. 150 if (y_in < std::min<size_t>(2 * iMCU_height, cinfo.image_height)) { 151 continue; 152 } 153 154 // After two iMCU rows, the compressor has started emitting compressed 155 // data. We check here that at least the scan header was output, because 156 // we expect that the compressor's output buffer was filled at least once 157 // while emitting the first compressed iMCU row. 158 if (y_in == std::min<size_t>(2 * iMCU_height, cinfo.image_height)) { 159 EXPECT_EQ(JPEG_REACHED_SOS, 160 jpegli_read_header(&dinfo, /*require_image=*/TRUE)); 161 output.xsize = dinfo.image_width; 162 output.ysize = dinfo.image_height; 163 output.components = dinfo.num_components; 164 EXPECT_EQ(output.xsize, input.xsize); 165 EXPECT_EQ(output.ysize, input.ysize); 166 EXPECT_EQ(output.components, input.components); 167 EXPECT_TRUE(jpegli_start_decompress(&dinfo)); 168 output.pixels.resize(output.ysize * stride); 169 if (y_in < cinfo.image_height) { 170 continue; 171 } 172 } 173 174 // After six iMCU rows, the compressor has emitted five iMCU rows of 175 // compressed data, of which we expect four full iMCU row of compressed 176 // data to be in the decoder's input buffer, but since the decoder also 177 // needs context rows for upsampling and smoothing, we don't expect any 178 // output to be ready yet. 179 if (y_in < 7 * iMCU_height && y_in < cinfo.image_height) { 180 continue; 181 } 182 183 // After five iMCU rows, we expect the decoder to have rendered the output 184 // with four iMCU rows of delay. 185 // TODO(szabadka) Reduce the processing delay in the decoder if possible. 186 size_t lines_out = 187 (y_in == cinfo.image_height ? cinfo.image_height - y_out 188 : iMCU_height); 189 std::vector<JSAMPROW> rows_out(lines_out); 190 for (size_t i = 0; i < lines_out; ++i) { 191 rows_out[i] = 192 reinterpret_cast<JSAMPLE*>(&output.pixels[(y_out + i) * stride]); 193 } 194 EXPECT_EQ(lines_out, 195 jpegli_read_scanlines(&dinfo, rows_out.data(), lines_out)); 196 VerifyOutputImage(input, output, y_out, lines_out, 3.8f); 197 y_out += lines_out; 198 199 if (y_out == cinfo.image_height) { 200 EXPECT_TRUE(jpegli_finish_decompress(&dinfo)); 201 } 202 } 203 return true; 204 }; 205 EXPECT_TRUE(try_catch_block()); 206 jpegli_destroy_decompress(&dinfo); 207 jpegli_destroy_compress(&cinfo); 208 } 209 210 std::vector<TestConfig> GenerateTests() { 211 std::vector<TestConfig> all_tests; 212 const size_t xsize0 = 1920; 213 const size_t ysize0 = 1080; 214 for (int dysize : {0, 1, 8, 9}) { 215 for (int v_sampling : {1, 2}) { 216 TestConfig config; 217 config.input.xsize = xsize0; 218 config.input.ysize = ysize0 + dysize; 219 config.jparams.h_sampling = {1, 1, 1}; 220 config.jparams.v_sampling = {v_sampling, 1, 1}; 221 all_tests.push_back(config); 222 } 223 } 224 return all_tests; 225 } 226 227 std::ostream& operator<<(std::ostream& os, const TestConfig& c) { 228 os << c.input; 229 os << c.jparams; 230 return os; 231 } 232 233 std::string TestDescription( 234 const testing::TestParamInfo<StreamingTestParam::ParamType>& info) { 235 std::stringstream name; 236 name << info.param; 237 return name.str(); 238 } 239 240 JPEGLI_INSTANTIATE_TEST_SUITE_P(StreamingTest, StreamingTestParam, 241 testing::ValuesIn(GenerateTests()), 242 TestDescription); 243 244 } // namespace 245 } // namespace jpegli