libheif/plugins/encoder_kvazaar.cc (562 lines of code) (raw):
/*
* HEIF codec.
* Copyright (c) 2023 Dirk Farin <dirk.farin@gmail.com>
*
* This file is part of libheif.
*
* libheif is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* libheif is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libheif. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libheif/heif.h"
#include "libheif/heif_plugin.h"
#include "encoder_kvazaar.h"
#include <memory>
#include <string> // apparently, this is a false positive of cpplint
#include <cstring>
#include <cassert>
#include <vector>
#include <algorithm>
extern "C" {
#include <kvazaar.h>
}
static const char* kError_unspecified_error = "Unspecified encoder error";
static const char* kError_unsupported_bit_depth = "Bit depth not supported by kvazaar";
static const char* kError_unsupported_chroma = "Unsupported chroma type";
//static const char* kError_unsupported_image_size = "Images smaller than 16 pixels are not supported";
struct encoder_struct_kvazaar
{
int quality = 75;
bool lossless = false;
std::vector<uint8_t> output_data;
size_t output_idx = 0;
};
static const int kvazaar_PLUGIN_PRIORITY = 100;
#define MAX_PLUGIN_NAME_LENGTH 80
static char plugin_name[MAX_PLUGIN_NAME_LENGTH];
static void kvazaar_set_default_parameters(void* encoder);
static const char* kvazaar_plugin_name()
{
strcpy(plugin_name, "kvazaar HEVC encoder");
return plugin_name;
}
#define MAX_NPARAMETERS 10
static struct heif_encoder_parameter kvazaar_encoder_params[MAX_NPARAMETERS];
static const struct heif_encoder_parameter* kvazaar_encoder_parameter_ptrs[MAX_NPARAMETERS + 1];
static void kvazaar_init_parameters()
{
struct heif_encoder_parameter* p = kvazaar_encoder_params;
const struct heif_encoder_parameter** d = kvazaar_encoder_parameter_ptrs;
int i = 0;
assert(i < MAX_NPARAMETERS);
p->version = 2;
p->name = heif_encoder_parameter_name_quality;
p->type = heif_encoder_parameter_type_integer;
p->integer.default_value = 50;
p->has_default = true;
p->integer.have_minimum_maximum = true;
p->integer.minimum = 0;
p->integer.maximum = 100;
p->integer.valid_values = NULL;
p->integer.num_valid_values = 0;
d[i++] = p++;
assert(i < MAX_NPARAMETERS);
p->version = 2;
p->name = heif_encoder_parameter_name_lossless;
p->type = heif_encoder_parameter_type_boolean;
p->boolean.default_value = false;
p->has_default = true;
d[i++] = p++;
d[i++] = nullptr;
}
const struct heif_encoder_parameter** kvazaar_list_parameters(void* encoder)
{
return kvazaar_encoder_parameter_ptrs;
}
static void kvazaar_init_plugin()
{
kvazaar_init_parameters();
}
static void kvazaar_cleanup_plugin()
{
}
static struct heif_error kvazaar_new_encoder(void** enc)
{
struct encoder_struct_kvazaar* encoder = new encoder_struct_kvazaar();
struct heif_error err = heif_error_ok;
*enc = encoder;
// set default parameters
kvazaar_set_default_parameters(encoder);
return err;
}
static void kvazaar_free_encoder(void* encoder_raw)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
delete encoder;
}
static struct heif_error kvazaar_set_parameter_quality(void* encoder_raw, int quality)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
if (quality < 0 || quality > 100) {
return heif_error_invalid_parameter_value;
}
encoder->quality = quality;
return heif_error_ok;
}
static struct heif_error kvazaar_get_parameter_quality(void* encoder_raw, int* quality)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
*quality = encoder->quality;
return heif_error_ok;
}
static struct heif_error kvazaar_set_parameter_lossless(void* encoder_raw, int enable)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
encoder->lossless = enable ? 1 : 0;
return heif_error_ok;
}
static struct heif_error kvazaar_get_parameter_lossless(void* encoder_raw, int* enable)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
*enable = encoder->lossless;
return heif_error_ok;
}
static struct heif_error kvazaar_set_parameter_logging_level(void* encoder_raw, int logging)
{
// struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
// return heif_error_invalid_parameter_value;
return heif_error_ok;
}
static struct heif_error kvazaar_get_parameter_logging_level(void* encoder_raw, int* loglevel)
{
// struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
*loglevel = 0;
return heif_error_ok;
}
static struct heif_error kvazaar_set_parameter_integer(void* encoder_raw, const char* name, int value)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
if (strcmp(name, heif_encoder_parameter_name_quality) == 0) {
return kvazaar_set_parameter_quality(encoder, value);
}
else if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) {
return kvazaar_set_parameter_lossless(encoder, value);
}
return heif_error_unsupported_parameter;
}
static struct heif_error kvazaar_get_parameter_integer(void* encoder_raw, const char* name, int* value)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
if (strcmp(name, heif_encoder_parameter_name_quality) == 0) {
return kvazaar_get_parameter_quality(encoder, value);
}
else if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) {
return kvazaar_get_parameter_lossless(encoder, value);
}
return heif_error_unsupported_parameter;
}
static struct heif_error kvazaar_set_parameter_boolean(void* encoder, const char* name, int value)
{
if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) {
return kvazaar_set_parameter_lossless(encoder, value);
}
return heif_error_unsupported_parameter;
}
// Unused, will use "kvazaar_get_parameter_integer" instead.
/*
static struct heif_error kvazaar_get_parameter_boolean(void* encoder, const char* name, int* value)
{
if (strcmp(name, heif_encoder_parameter_name_lossless)==0) {
return kvazaar_get_parameter_lossless(encoder,value);
}
return heif_error_unsupported_parameter;
}
*/
static struct heif_error kvazaar_set_parameter_string(void* encoder_raw, const char* name, const char* value)
{
return heif_error_unsupported_parameter;
}
static struct heif_error kvazaar_get_parameter_string(void* encoder_raw, const char* name,
char* value, int value_size)
{
return heif_error_unsupported_parameter;
}
static void kvazaar_set_default_parameters(void* encoder)
{
for (const struct heif_encoder_parameter** p = kvazaar_encoder_parameter_ptrs; *p; p++) {
const struct heif_encoder_parameter* param = *p;
if (param->has_default) {
switch (param->type) {
case heif_encoder_parameter_type_integer:
kvazaar_set_parameter_integer(encoder, param->name, param->integer.default_value);
break;
case heif_encoder_parameter_type_boolean:
kvazaar_set_parameter_boolean(encoder, param->name, param->boolean.default_value);
break;
case heif_encoder_parameter_type_string:
kvazaar_set_parameter_string(encoder, param->name, param->string.default_value);
break;
}
}
}
}
static void kvazaar_query_input_colorspace(heif_colorspace* colorspace, heif_chroma* chroma)
{
if (*colorspace == heif_colorspace_monochrome) {
*colorspace = heif_colorspace_monochrome;
*chroma = heif_chroma_monochrome;
}
else {
*colorspace = heif_colorspace_YCbCr;
*chroma = heif_chroma_420;
}
}
static void kvazaar_query_input_colorspace2(void* encoder_raw, heif_colorspace* colorspace, heif_chroma* chroma)
{
if (*colorspace == heif_colorspace_monochrome) {
*colorspace = heif_colorspace_monochrome;
*chroma = heif_chroma_monochrome;
}
else {
*colorspace = heif_colorspace_YCbCr;
if (*chroma != heif_chroma_420 &&
*chroma != heif_chroma_422 &&
*chroma != heif_chroma_444) {
*chroma = heif_chroma_420;
}
}
}
void kvazaar_query_encoded_size(void* encoder_raw, uint32_t input_width, uint32_t input_height,
uint32_t* encoded_width, uint32_t* encoded_height)
{
*encoded_width = (input_width + 7) & ~0x7;
*encoded_height = (input_height + 7) & ~0x7;
}
#if 0
static int rounded_size(int s)
{
s = (s + 1) & ~1;
if (s < 64) {
s = 64;
}
return s;
}
#endif
static void append_chunk_data(kvz_data_chunk* data, std::vector<uint8_t>& out)
{
if (!data || data->len == 0) {
return;
}
size_t old_size = out.size();
out.resize(old_size + data->len);
memcpy(out.data() + old_size, data->data, data->len);
if (data->next) {
append_chunk_data(data->next, out);
}
}
static void copy_plane(kvz_pixel* out_p, uint32_t out_stride, const uint8_t* in_p, uint32_t in_stride, int w, int h, int padded_width, int padded_height)
{
for (int y = 0; y < padded_height; y++) {
int sy = std::min(y, h - 1); // source y
memcpy(out_p + y * out_stride, in_p + sy * in_stride, w);
if (padded_width > w) {
memset(out_p + y * out_stride + w, *(in_p + sy * in_stride + w - 1), padded_width - w);
}
}
}
template<typename T, typename D>
std::unique_ptr<T, D> make_guard(T* ptr, D&& deleter) {
return std::unique_ptr<T, D>(ptr, deleter);
}
static struct heif_error kvazaar_encode_image(void* encoder_raw, const struct heif_image* image,
heif_image_input_class input_class)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y);
bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome);
heif_chroma chroma = heif_image_get_chroma_format(image);
const kvz_api* api = kvz_api_get(bit_depth);
if (api == nullptr) {
struct heif_error err = {
heif_error_Encoder_plugin_error,
heif_suberror_Unsupported_bit_depth,
kError_unsupported_bit_depth
};
return err;
}
auto uconfig = make_guard(api->config_alloc(), [api](kvz_config* cfg) { api->config_destroy(cfg); });
kvz_config* config = uconfig.get();
api->config_init(config); // param, encoder->preset.c_str(), encoder->tune.c_str());
#if HAVE_KVAZAAR_ENABLE_LOGGING
config->enable_logging_output = 0;
#endif
#if !ENABLE_MULTITHREADING_SUPPORT
// 0: Process everything with main thread
// -1 (default): Select automatically.
config->threads = 0;
#endif
#if 1
#if 0
while (ctuSize > 16 &&
(heif_image_get_width(image, heif_channel_Y) < ctuSize ||
heif_image_get_height(image, heif_channel_Y) < ctuSize)) {
ctuSize /= 2;
}
if (ctuSize < 16) {
api->config_destroy(config);
struct heif_error err = {
heif_error_Encoder_plugin_error,
heif_suberror_Invalid_parameter_value,
kError_unsupported_image_size
};
return err;
}
#endif
#else
// TODO: There seems to be a bug in kvazaar where increasing the CTU size between
// multiple encoding jobs causes a segmentation fault. E.g. encoding multiple
// times with a CTU of 16 works, the next encoding with a CTU of 32 crashes.
// Use hardcoded value of 64 and reject images that are too small.
if (heif_image_get_width(image, heif_channel_Y) < ctuSize ||
heif_image_get_height(image, heif_channel_Y) < ctuSize) {
api->param_free(param);
struct heif_error err = {
heif_error_Encoder_plugin_error,
heif_suberror_Invalid_parameter_value,
kError_unsupported_image_size
};
return err;
}
#endif
#if 0
// ctuSize should be a power of 2 in [16;64]
switch (ctuSize) {
case 64:
ctu = "64";
break;
case 32:
ctu = "32";
break;
case 16:
ctu = "16";
break;
default:
struct heif_error err = {
heif_error_Encoder_plugin_error,
heif_suberror_Invalid_parameter_value,
kError_unsupported_image_size
};
return err;
}
(void) ctu;
#endif
int input_width = heif_image_get_width(image, heif_channel_Y);
int input_height = heif_image_get_height(image, heif_channel_Y);
int input_chroma_width = 0;
int input_chroma_height = 0;
uint32_t encoded_width, encoded_height;
kvazaar_query_encoded_size(encoder_raw, input_width, input_height, &encoded_width, &encoded_height);
kvz_chroma_format kvzChroma;
int chroma_stride_shift = 0;
int chroma_height_shift = 0;
if (isGreyscale) {
config->input_format = KVZ_FORMAT_P400;
kvzChroma = KVZ_CSP_400;
}
else if (chroma == heif_chroma_420) {
config->input_format = KVZ_FORMAT_P420;
kvzChroma = KVZ_CSP_420;
chroma_stride_shift = 1;
chroma_height_shift = 1;
input_chroma_width = (input_width + 1) / 2;
input_chroma_height = (input_height + 1) / 2;
}
else if (chroma == heif_chroma_422) {
config->input_format = KVZ_FORMAT_P422;
kvzChroma = KVZ_CSP_422;
chroma_stride_shift = 1;
chroma_height_shift = 0;
input_chroma_width = (input_width + 1) / 2;
input_chroma_height = input_height;
}
else if (chroma == heif_chroma_444) {
config->input_format = KVZ_FORMAT_P444;
kvzChroma = KVZ_CSP_444;
chroma_stride_shift = 0;
chroma_height_shift = 0;
input_chroma_width = input_width;
input_chroma_height = input_height;
}
else {
return heif_error{
heif_error_Encoder_plugin_error,
heif_suberror_Unsupported_image_type,
kError_unsupported_chroma
};
}
if (chroma != heif_chroma_monochrome) {
int w = heif_image_get_width(image, heif_channel_Y);
int h = heif_image_get_height(image, heif_channel_Y);
if (chroma != heif_chroma_444) { w = (w + 1) / 2; }
if (chroma == heif_chroma_420) { h = (h + 1) / 2; }
assert(heif_image_get_width(image, heif_channel_Cb) == w);
assert(heif_image_get_width(image, heif_channel_Cr) == w);
assert(heif_image_get_height(image, heif_channel_Cb) == h);
assert(heif_image_get_height(image, heif_channel_Cr) == h);
(void) w;
(void) h;
}
struct heif_color_profile_nclx* nclx = nullptr;
heif_error err = heif_image_get_nclx_color_profile(image, &nclx);
if (err.code != heif_error_Ok) {
nclx = nullptr;
}
// make sure NCLX profile is deleted at end of function
auto nclx_deleter = std::unique_ptr<heif_color_profile_nclx, void (*)(heif_color_profile_nclx*)>(nclx, heif_nclx_color_profile_free);
if (nclx) {
config->vui.fullrange = nclx->full_range_flag;
}
else {
config->vui.fullrange = 1;
}
if (nclx &&
(input_class == heif_image_input_class_normal ||
input_class == heif_image_input_class_thumbnail)) {
config->vui.colorprim = nclx->color_primaries;
config->vui.transfer = nclx->transfer_characteristics;
config->vui.colormatrix = nclx->matrix_coefficients;
}
config->qp = ((100 - encoder->quality) * 51 + 50) / 100;
config->lossless = encoder->lossless ? 1 : 0;
config->width = encoded_width;
config->height = encoded_height;
// Note: it is ok to cast away the const, as the image content is not changed.
// However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function.
/*
err = heif_image_extend_padding_to_size(const_cast<struct heif_image*>(image),
param->sourceWidth,
param->sourceHeight);
if (err.code) {
return err;
}
*/
auto upic = make_guard(api->picture_alloc_csp(kvzChroma, encoded_width, encoded_height), [api](kvz_picture* pic) { api->picture_free(pic); });
kvz_picture* pic = upic.get();
if (!pic) {
return heif_error{
heif_error_Encoder_plugin_error,
heif_suberror_Encoder_encoding,
kError_unspecified_error
};
}
if (isGreyscale) {
int stride;
const uint8_t* data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride);
copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height);
}
else {
int stride;
const uint8_t* data;
data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride);
copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height);
data = heif_image_get_plane_readonly(image, heif_channel_Cb, &stride);
copy_plane(pic->u, pic->stride >> chroma_stride_shift, data, stride, input_chroma_width, input_chroma_height,
encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift);
data = heif_image_get_plane_readonly(image, heif_channel_Cr, &stride);
copy_plane(pic->v, pic->stride >> chroma_stride_shift, data, stride, input_chroma_width, input_chroma_height,
encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift);
}
auto uencoder = make_guard(api->encoder_open(config), [api](kvz_encoder* e) { api->encoder_close(e); });
kvz_encoder* kvzencoder = uencoder.get();
if (!kvzencoder) {
return heif_error{
heif_error_Encoder_plugin_error,
heif_suberror_Encoder_encoding,
kError_unspecified_error
};
}
kvz_data_chunk* data = nullptr;
auto free_data = [api](kvz_data_chunk** data){
if(*data) {
api->chunk_free(*data);
*data = nullptr;
}
};
auto data_deleter = std::unique_ptr<kvz_data_chunk*, decltype(free_data)>(&data, free_data);
uint32_t data_len;
int success;
success = api->encoder_headers(kvzencoder, &data, &data_len);
if (!success) {
return heif_error{
heif_error_Encoder_plugin_error,
heif_suberror_Encoder_encoding,
kError_unspecified_error
};
}
append_chunk_data(data, encoder->output_data);
free_data(&data);
success = api->encoder_encode(kvzencoder,
pic,
&data, &data_len,
nullptr, nullptr, nullptr);
if (!success) {
return heif_error{
heif_error_Encoder_plugin_error,
heif_suberror_Encoder_encoding,
kError_unspecified_error
};
}
append_chunk_data(data, encoder->output_data);
free_data(&data);
for (;;) {
success = api->encoder_encode(kvzencoder,
nullptr,
&data, &data_len,
nullptr, nullptr, nullptr);
if (!success) {
return heif_error{
heif_error_Encoder_plugin_error,
heif_suberror_Encoder_encoding,
kError_unspecified_error
};
}
if (data == nullptr || data->len == 0) {
break;
}
append_chunk_data(data, encoder->output_data);
free_data(&data);
}
(void) success;
return heif_error_ok;
}
static struct heif_error kvazaar_get_compressed_data(void* encoder_raw, uint8_t** data, int* size,
enum heif_encoded_data_type* type)
{
struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw;
if (encoder->output_idx == encoder->output_data.size()) {
*data = nullptr;
*size = 0;
return heif_error_ok;
}
size_t start_idx = encoder->output_idx;
while (start_idx < encoder->output_data.size() - 3 &&
(encoder->output_data[start_idx] != 0 ||
encoder->output_data[start_idx + 1] != 0 ||
encoder->output_data[start_idx + 2] != 1)) {
start_idx++;
}
size_t end_idx = start_idx + 1;
while (end_idx < encoder->output_data.size() - 3 &&
(encoder->output_data[end_idx] != 0 ||
encoder->output_data[end_idx + 1] != 0 ||
encoder->output_data[end_idx + 2] != 1)) {
end_idx++;
}
if (end_idx == encoder->output_data.size() - 3) {
end_idx = encoder->output_data.size();
}
*data = encoder->output_data.data() + start_idx + 3;
*size = (int) (end_idx - start_idx - 3);
encoder->output_idx = end_idx;
return heif_error_ok;
}
static const struct heif_encoder_plugin encoder_plugin_kvazaar
{
/* plugin_api_version */ 3,
/* compression_format */ heif_compression_HEVC,
/* id_name */ "kvazaar",
/* priority */ kvazaar_PLUGIN_PRIORITY,
/* supports_lossy_compression */ true,
/* supports_lossless_compression */ true,
/* get_plugin_name */ kvazaar_plugin_name,
/* init_plugin */ kvazaar_init_plugin,
/* cleanup_plugin */ kvazaar_cleanup_plugin,
/* new_encoder */ kvazaar_new_encoder,
/* free_encoder */ kvazaar_free_encoder,
/* set_parameter_quality */ kvazaar_set_parameter_quality,
/* get_parameter_quality */ kvazaar_get_parameter_quality,
/* set_parameter_lossless */ kvazaar_set_parameter_lossless,
/* get_parameter_lossless */ kvazaar_get_parameter_lossless,
/* set_parameter_logging_level */ kvazaar_set_parameter_logging_level,
/* get_parameter_logging_level */ kvazaar_get_parameter_logging_level,
/* list_parameters */ kvazaar_list_parameters,
/* set_parameter_integer */ kvazaar_set_parameter_integer,
/* get_parameter_integer */ kvazaar_get_parameter_integer,
/* set_parameter_boolean */ kvazaar_set_parameter_integer, // boolean also maps to integer function
/* get_parameter_boolean */ kvazaar_get_parameter_integer, // boolean also maps to integer function
/* set_parameter_string */ kvazaar_set_parameter_string,
/* get_parameter_string */ kvazaar_get_parameter_string,
/* query_input_colorspace */ kvazaar_query_input_colorspace,
/* encode_image */ kvazaar_encode_image,
/* get_compressed_data */ kvazaar_get_compressed_data,
/* query_input_colorspace (v2) */ kvazaar_query_input_colorspace2,
/* query_encoded_size (v3) */ kvazaar_query_encoded_size
};
const struct heif_encoder_plugin* get_encoder_plugin_kvazaar()
{
return &encoder_plugin_kvazaar;
}
#if PLUGIN_KVAZAAR
heif_plugin_info plugin_info {
1,
heif_plugin_type_encoder,
&encoder_plugin_kvazaar
};
#endif