Source code

Revision control

Copy as Markdown

Other Tools

// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <ostream>
#include <sstream>
#include <string>
#include <vector>
#include "lib/jpegli/decode.h"
#include "lib/jpegli/encode.h"
#include "lib/jpegli/test_params.h"
#include "lib/jpegli/test_utils.h"
#include "lib/jpegli/testing.h"
namespace jpegli {
namespace {
// A simple suspending source manager with an input buffer.
struct SourceManager {
jpeg_source_mgr pub;
std::vector<uint8_t> buffer;
SourceManager() {
pub.next_input_byte = nullptr;
pub.bytes_in_buffer = 0;
pub.init_source = init_source;
pub.fill_input_buffer = fill_input_buffer;
pub.skip_input_data = skip_input_data;
pub.resync_to_restart = jpegli_resync_to_restart;
pub.term_source = term_source;
}
static void init_source(j_decompress_ptr cinfo) {}
static boolean fill_input_buffer(j_decompress_ptr cinfo) { return FALSE; }
static void skip_input_data(j_decompress_ptr cinfo,
long num_bytes /* NOLINT */) {}
static void term_source(j_decompress_ptr cinfo) {}
};
// A destination manager that empties its output buffer into a SourceManager's
// input buffer. The buffer size is kept short because empty_output_buffer() is
// called only when the output buffer is full, and we want to update the decoder
// input frequently to demonstrate that streaming works.
constexpr size_t kOutputBufferSize = 1024;
struct DestinationManager {
jpeg_destination_mgr pub;
std::vector<uint8_t> buffer;
SourceManager* dest;
explicit DestinationManager(SourceManager* src)
: buffer(kOutputBufferSize), dest(src) {
pub.next_output_byte = buffer.data();
pub.free_in_buffer = buffer.size();
pub.init_destination = init_destination;
pub.empty_output_buffer = empty_output_buffer;
pub.term_destination = term_destination;
}
static void init_destination(j_compress_ptr cinfo) {}
static boolean empty_output_buffer(j_compress_ptr cinfo) {
auto* us = reinterpret_cast<DestinationManager*>(cinfo->dest);
jpeg_destination_mgr* src = &us->pub;
jpeg_source_mgr* dst = &us->dest->pub;
std::vector<uint8_t>& src_buf = us->buffer;
std::vector<uint8_t>& dst_buf = us->dest->buffer;
if (dst->bytes_in_buffer > 0 && dst->bytes_in_buffer < dst_buf.size()) {
memmove(dst_buf.data(), dst->next_input_byte, dst->bytes_in_buffer);
}
size_t src_len = src_buf.size() - src->free_in_buffer;
dst_buf.resize(dst->bytes_in_buffer + src_len);
memcpy(&dst_buf[dst->bytes_in_buffer], src_buf.data(), src_len);
dst->next_input_byte = dst_buf.data();
dst->bytes_in_buffer = dst_buf.size();
src->next_output_byte = src_buf.data();
src->free_in_buffer = src_buf.size();
return TRUE;
}
static void term_destination(j_compress_ptr cinfo) {
empty_output_buffer(cinfo);
}
};
struct TestConfig {
TestImage input;
CompressParams jparams;
};
class StreamingTestParam : public ::testing::TestWithParam<TestConfig> {};
TEST_P(StreamingTestParam, TestStreaming) {
jpeg_decompress_struct dinfo = {};
jpeg_compress_struct cinfo = {};
SourceManager src;
TestConfig config = GetParam();
TestImage& input = config.input;
TestImage output;
GeneratePixels(&input);
const auto try_catch_block = [&]() {
ERROR_HANDLER_SETUP(jpegli);
dinfo.err = cinfo.err;
dinfo.client_data = cinfo.client_data;
// Create a pair of compressor and decompressor objects, where the
// compressor's output is connected to the decompressor's input.
jpegli_create_decompress(&dinfo);
jpegli_create_compress(&cinfo);
dinfo.src = reinterpret_cast<jpeg_source_mgr*>(&src);
DestinationManager dest(&src);
cinfo.dest = reinterpret_cast<jpeg_destination_mgr*>(&dest);
cinfo.image_width = input.xsize;
cinfo.image_height = input.ysize;
cinfo.input_components = input.components;
cinfo.in_color_space = static_cast<J_COLOR_SPACE>(input.color_space);
jpegli_set_defaults(&cinfo);
cinfo.comp_info[0].v_samp_factor = config.jparams.v_sampling[0];
jpegli_set_progressive_level(&cinfo, 0);
cinfo.optimize_coding = FALSE;
jpegli_start_compress(&cinfo, TRUE);
size_t stride = cinfo.image_width * cinfo.input_components;
size_t iMCU_height = 8 * cinfo.max_v_samp_factor;
std::vector<uint8_t> row_bytes(iMCU_height * stride);
size_t y_in = 0;
size_t y_out = 0;
while (y_in < cinfo.image_height) {
// Feed one iMCU row at a time to the compressor.
size_t lines_in = std::min(iMCU_height, cinfo.image_height - y_in);
memcpy(row_bytes.data(), &input.pixels[y_in * stride], lines_in * stride);
std::vector<JSAMPROW> rows_in(lines_in);
for (size_t i = 0; i < lines_in; ++i) {
rows_in[i] = &row_bytes[i * stride];
}
EXPECT_EQ(lines_in,
jpegli_write_scanlines(&cinfo, rows_in.data(), lines_in));
y_in += lines_in;
if (y_in == cinfo.image_height) {
jpegli_finish_compress(&cinfo);
}
// Atfer the first iMCU row, we don't yet expect any output because the
// compressor delays processing to have context rows after the iMCU row.
if (y_in < std::min<size_t>(2 * iMCU_height, cinfo.image_height)) {
continue;
}
// After two iMCU rows, the compressor has started emitting compressed
// data. We check here that at least the scan header was output, because
// we expect that the compressor's output buffer was filled at least once
// while emitting the first compressed iMCU row.
if (y_in == std::min<size_t>(2 * iMCU_height, cinfo.image_height)) {
EXPECT_EQ(JPEG_REACHED_SOS,
jpegli_read_header(&dinfo, /*require_image=*/TRUE));
output.xsize = dinfo.image_width;
output.ysize = dinfo.image_height;
output.components = dinfo.num_components;
EXPECT_EQ(output.xsize, input.xsize);
EXPECT_EQ(output.ysize, input.ysize);
EXPECT_EQ(output.components, input.components);
EXPECT_TRUE(jpegli_start_decompress(&dinfo));
output.pixels.resize(output.ysize * stride);
if (y_in < cinfo.image_height) {
continue;
}
}
// After six iMCU rows, the compressor has emitted five iMCU rows of
// compressed data, of which we expect four full iMCU row of compressed
// data to be in the decoder's input buffer, but since the decoder also
// needs context rows for upsampling and smoothing, we don't expect any
// output to be ready yet.
if (y_in < 7 * iMCU_height && y_in < cinfo.image_height) {
continue;
}
// After five iMCU rows, we expect the decoder to have rendered the output
// with four iMCU rows of delay.
// TODO(szabadka) Reduce the processing delay in the decoder if possible.
size_t lines_out =
(y_in == cinfo.image_height ? cinfo.image_height - y_out
: iMCU_height);
std::vector<JSAMPROW> rows_out(lines_out);
for (size_t i = 0; i < lines_out; ++i) {
rows_out[i] =
reinterpret_cast<JSAMPLE*>(&output.pixels[(y_out + i) * stride]);
}
EXPECT_EQ(lines_out,
jpegli_read_scanlines(&dinfo, rows_out.data(), lines_out));
VerifyOutputImage(input, output, y_out, lines_out, 3.8f);
y_out += lines_out;
if (y_out == cinfo.image_height) {
EXPECT_TRUE(jpegli_finish_decompress(&dinfo));
}
}
return true;
};
EXPECT_TRUE(try_catch_block());
jpegli_destroy_decompress(&dinfo);
jpegli_destroy_compress(&cinfo);
}
std::vector<TestConfig> GenerateTests() {
std::vector<TestConfig> all_tests;
const size_t xsize0 = 1920;
const size_t ysize0 = 1080;
for (int dysize : {0, 1, 8, 9}) {
for (int v_sampling : {1, 2}) {
TestConfig config;
config.input.xsize = xsize0;
config.input.ysize = ysize0 + dysize;
config.jparams.h_sampling = {1, 1, 1};
config.jparams.v_sampling = {v_sampling, 1, 1};
all_tests.push_back(config);
}
}
return all_tests;
}
std::ostream& operator<<(std::ostream& os, const TestConfig& c) {
os << c.input;
os << c.jparams;
return os;
}
std::string TestDescription(
const testing::TestParamInfo<StreamingTestParam::ParamType>& info) {
std::stringstream name;
name << info.param;
return name.str();
}
JPEGLI_INSTANTIATE_TEST_SUITE_P(StreamingTest, StreamingTestParam,
testing::ValuesIn(GenerateTests()),
TestDescription);
} // namespace
} // namespace jpegli