From 2b0cbe69a83848fb0b8012ed858fa3ab402889e3 Mon Sep 17 00:00:00 2001 From: Bradley Sepos Date: Sat, 27 May 2017 08:46:11 -0400 Subject: libhb: Add LapSharp sharpening filter. --- libhb/common.c | 4 + libhb/common.h | 1 + libhb/internal.h | 1 + libhb/lapsharp.c | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ libhb/param.c | 179 +++++++++++++++++++++++++++++++++++ libhb/preset.c | 6 +- test/test.c | 95 +++++++++++++++++++ 7 files changed, 567 insertions(+), 1 deletion(-) create mode 100644 libhb/lapsharp.c diff --git a/libhb/common.c b/libhb/common.c index c8a2f22f7..71a8d0e26 100644 --- a/libhb/common.c +++ b/libhb/common.c @@ -3959,6 +3959,10 @@ hb_filter_object_t * hb_filter_get( int filter_id ) filter = &hb_filter_crop_scale; break; + case HB_FILTER_LAPSHARP: + filter = &hb_filter_lapsharp; + break; + case HB_FILTER_UNSHARP: filter = &hb_filter_unsharp; break; diff --git a/libhb/common.h b/libhb/common.h index 562bbc362..a251365ac 100644 --- a/libhb/common.h +++ b/libhb/common.h @@ -1270,6 +1270,7 @@ enum HB_FILTER_NLMEANS, HB_FILTER_RENDER_SUB, HB_FILTER_CROP_SCALE, + HB_FILTER_LAPSHARP, HB_FILTER_UNSHARP, HB_FILTER_ROTATE, HB_FILTER_GRAYSCALE, diff --git a/libhb/internal.h b/libhb/internal.h index 0cffbdff4..4ffd6422a 100644 --- a/libhb/internal.h +++ b/libhb/internal.h @@ -465,6 +465,7 @@ extern hb_filter_object_t hb_filter_crop_scale; extern hb_filter_object_t hb_filter_rotate; extern hb_filter_object_t hb_filter_grayscale; extern hb_filter_object_t hb_filter_pad; +extern hb_filter_object_t hb_filter_lapsharp; extern hb_filter_object_t hb_filter_unsharp; extern hb_filter_object_t hb_filter_avfilter; diff --git a/libhb/lapsharp.c b/libhb/lapsharp.c new file mode 100644 index 000000000..2170247bb --- /dev/null +++ b/libhb/lapsharp.c @@ -0,0 +1,282 @@ +/* lapsharp.c + + Copyright (c) 2003-2017 HandBrake Team + This file is part of the HandBrake source code + Homepage: . + It may be used under the terms of the GNU General Public License v2. + For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html + */ + +#include "hb.h" + +#define LAPSHARP_STRENGTH_LUMA_DEFAULT 0.2 +#define LAPSHARP_STRENGTH_CHROMA_DEFAULT 0.2 + +#define LAPSHARP_KERNELS 3 +#define LAPSHARP_KERNEL_LUMA_DEFAULT 2 +#define LAPSHARP_KERNEL_CHROMA_DEFAULT 2 + +typedef struct +{ + double strength; // strength + int kernel; // which kernel to use; lapsharp_kernels[kernel] +} lapsharp_plane_context_t; + +typedef struct { + const int *mem; + const double coef; + const int size; +} lapsharp_kernel_t; + +// 4-neighbor laplacian kernel (lap) +// Sharpens vertical and horizontal edges, less effective on diagonals +static const int lapsharp_kernel_lap[] = +{ + 0, -1, 0, +-1, 5, -1, + 0, -1, 0 +}; + +// Isotropic laplacian kernel (isolap) +// Minimial directionality, sharpens all edges similarly +static const int lapsharp_kernel_isolap[] = +{ +-1, -4, -1, +-4, 25, -4, +-1, -4, -1 +}; + +// Laplacian of gaussian kernel (log) +// Slightly better at noise rejection +static const int lapsharp_kernel_log[] = +{ + 0, 0, -1, 0, 0, + 0, -1, -2, -1, 0, +-1, -2, 21, -2, -1, + 0, -1, -2, -1, 0, + 0, 0, -1, 0, 0 +}; + +static lapsharp_kernel_t lapsharp_kernels[] = +{ + { lapsharp_kernel_lap, (1.0 / 1), 3 }, + { lapsharp_kernel_isolap, (1.0 / 5), 3 }, + { lapsharp_kernel_log, (1.0 / 5), 5 } +}; + +struct hb_filter_private_s +{ + lapsharp_plane_context_t plane_ctx[3]; +}; + +static int hb_lapsharp_init(hb_filter_object_t *filter, + hb_filter_init_t *init); + +static int hb_lapsharp_work(hb_filter_object_t *filter, + hb_buffer_t ** buf_in, + hb_buffer_t ** buf_out); + +static void hb_lapsharp_close(hb_filter_object_t *filter); + +static const char hb_lapsharp_template[] = + "y-strength=^"HB_FLOAT_REG"$:y-kernel=^"HB_ALL_REG"$:" + "cb-strength=^"HB_FLOAT_REG"$:cb-kernel=^"HB_ALL_REG"$:" + "cr-strength=^"HB_FLOAT_REG"$:cr-kernel=^"HB_ALL_REG"$"; + +hb_filter_object_t hb_filter_lapsharp = +{ + .id = HB_FILTER_LAPSHARP, + .enforce_order = 1, + .name = "Sharpen (lapsharp)", + .settings = NULL, + .init = hb_lapsharp_init, + .work = hb_lapsharp_work, + .close = hb_lapsharp_close, + .settings_template = hb_lapsharp_template, +}; + +static void hb_lapsharp(const uint8_t *src, + uint8_t *dst, + const int width, + const int height, + const int stride, + lapsharp_plane_context_t * ctx) +{ + const lapsharp_kernel_t *kernel = &lapsharp_kernels[ctx->kernel]; + + // Sharpen using selected kernel + const int offset_min = -((kernel->size - 1) / 2); + const int offset_max = (kernel->size + 1) / 2; + const int stride_border = (stride - width) / 2; + int16_t pixel; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if ((y < offset_max) || + (y > height - offset_max) || + (x < stride_border + offset_max) || + (x > width + stride_border - offset_max)) + { + *(dst + stride*y + x) = *(src + stride*y + x); + continue; + } + pixel = 0; + for (int k = offset_min; k < offset_max; k++) + { + for (int j = offset_min; j < offset_max; j++) + { + pixel += kernel->mem[((j - offset_min) * kernel->size) + k - offset_min] * *(src + stride*(y + j) + (x + k)); + } + } + pixel = (int16_t)(((pixel * kernel->coef) - *(src + stride*y + x)) * ctx->strength) + *(src + stride*y + x); + pixel = pixel < 0 ? 0 : pixel; + pixel = pixel > 255 ? 255 : pixel; + *(dst + stride*y + x) = (uint8_t)(pixel); + } + } +} + +static int hb_lapsharp_init(hb_filter_object_t *filter, + hb_filter_init_t *init) +{ + filter->private_data = calloc(sizeof(struct hb_filter_private_s), 1); + hb_filter_private_t * pv = filter->private_data; + + char *kernel_string[3]; + + // Mark parameters unset + for (int c = 0; c < 3; c++) + { + pv->plane_ctx[c].strength = -1; + pv->plane_ctx[c].kernel = -1; + kernel_string[c] = NULL; + } + + // Read user parameters + if (filter->settings != NULL) + { + hb_dict_t * dict = filter->settings; + hb_dict_extract_double(&pv->plane_ctx[0].strength, dict, "y-strength"); + hb_dict_extract_string(&kernel_string[0], dict, "y-kernel"); + + hb_dict_extract_double(&pv->plane_ctx[1].strength, dict, "cb-strength"); + hb_dict_extract_string(&kernel_string[1], dict, "cb-kernel"); + + hb_dict_extract_double(&pv->plane_ctx[2].strength, dict, "cr-strength"); + hb_dict_extract_string(&kernel_string[2], dict, "cr-kernel"); + } + + // Convert kernel user string to internal id + for (int c = 0; c < 3; c++) + { + lapsharp_plane_context_t * ctx = &pv->plane_ctx[c]; + + ctx->kernel = -1; + + if (kernel_string[c] == NULL) + { + continue; + } + + if (!strcasecmp(kernel_string[c], "lap")) + { + ctx->kernel = 0; + } + else if (!strcasecmp(kernel_string[c], "isolap")) + { + ctx->kernel = 1; + } + else if (!strcasecmp(kernel_string[c], "log")) + { + ctx->kernel = 2; + } + + free(kernel_string[c]); + } + + // Cascade values + // Cr not set; inherit Cb. Cb not set; inherit Y. Y not set; defaults. + for (int c = 1; c < 3; c++) + { + lapsharp_plane_context_t * prev_ctx = &pv->plane_ctx[c - 1]; + lapsharp_plane_context_t * ctx = &pv->plane_ctx[c]; + + if (ctx->strength == -1) ctx->strength = prev_ctx->strength; + if (ctx->kernel == -1) ctx->kernel = prev_ctx->kernel; + } + + for (int c = 0; c < 3; c++) + { + lapsharp_plane_context_t * ctx = &pv->plane_ctx[c]; + + // Replace unset values with defaults + if (ctx->strength == -1) + { + ctx->strength = c ? LAPSHARP_STRENGTH_CHROMA_DEFAULT : + LAPSHARP_STRENGTH_LUMA_DEFAULT; + } + if (ctx->kernel == -1) + { + ctx->kernel = c ? LAPSHARP_KERNEL_CHROMA_DEFAULT : + LAPSHARP_KERNEL_LUMA_DEFAULT; + } + + // Sanitize + if (ctx->strength < 0) ctx->strength = 0; + if (ctx->strength > 1.5) ctx->strength = 1.5; + if ((ctx->kernel < 0) || (ctx->kernel >= LAPSHARP_KERNELS)) + { + ctx->kernel = c ? LAPSHARP_KERNEL_CHROMA_DEFAULT : LAPSHARP_KERNEL_LUMA_DEFAULT; + } + } + + return 0; +} + +static void hb_lapsharp_close(hb_filter_object_t * filter) +{ + hb_filter_private_t *pv = filter->private_data; + + if (pv == NULL) + { + return; + } + + free(pv); + filter->private_data = NULL; +} + +static int hb_lapsharp_work(hb_filter_object_t *filter, + hb_buffer_t ** buf_in, + hb_buffer_t ** buf_out) +{ + hb_filter_private_t *pv = filter->private_data; + hb_buffer_t *in = *buf_in, *out; + + if (in->s.flags & HB_BUF_FLAG_EOF) + { + *buf_out = in; + *buf_in = NULL; + return HB_FILTER_DONE; + } + + out = hb_frame_buffer_init(in->f.fmt, in->f.width, in->f.height); + + int c; + for (c = 0; c < 3; c++) + { + lapsharp_plane_context_t * ctx = &pv->plane_ctx[c]; + hb_lapsharp(in->plane[c].data, + out->plane[c].data, + in->plane[c].width, + in->plane[c].height, + in->plane[c].stride, + ctx); + } + + out->s = in->s; + *buf_out = out; + + return HB_FILTER_OK; +} diff --git a/libhb/param.c b/libhb/param.c index ae8d99618..92c972ed5 100644 --- a/libhb/param.c +++ b/libhb/param.c @@ -90,6 +90,27 @@ static hb_filter_param_t unsharp_tunes[] = { 0, NULL, NULL, NULL } }; +static hb_filter_param_t lapsharp_presets[] = +{ + { 1, "Custom", "custom", NULL }, + { 2, "Ultralight", "ultralight", NULL }, + { 3, "Light", "light", NULL }, + { 4, "Medium", "medium", NULL }, + { 5, "Strong", "strong", NULL }, + { 6, "Stronger", "stronger", NULL }, + { 0, NULL, NULL, NULL } +}; + +static hb_filter_param_t lapsharp_tunes[] = +{ + { 0, "None", "none", NULL }, + { 1, "Film", "film", NULL }, + { 2, "Grain", "grain", NULL }, + { 3, "Animation", "animation", NULL }, + { 4, "Sprite", "sprite", NULL }, + { 0, NULL, NULL, NULL } +}; + static hb_filter_param_t detelecine_presets[] = { { 0, "Off", "off", "disable=1" }, @@ -164,6 +185,9 @@ static filter_param_map_t param_map[] = { HB_FILTER_UNSHARP, unsharp_presets, unsharp_tunes, sizeof(unsharp_presets) / sizeof(hb_filter_param_t) }, + { HB_FILTER_LAPSHARP, lapsharp_presets, lapsharp_tunes, + sizeof(lapsharp_presets) / sizeof(hb_filter_param_t) }, + { HB_FILTER_DETELECINE, detelecine_presets, NULL, sizeof(detelecine_presets) / sizeof(hb_filter_param_t) }, @@ -562,6 +586,158 @@ static hb_dict_t * generate_unsharp_settings(const char *preset, return settings; } +static hb_dict_t * generate_lapsharp_settings(const char *preset, + const char *tune, + const char *custom) +{ + hb_dict_t * settings; + + if (preset == NULL) + return NULL; + + if (preset == NULL || !strcasecmp(preset, "custom")) + { + return hb_parse_filter_settings(custom); + } + if (!strcasecmp(preset, "ultralight") || + !strcasecmp(preset, "light") || + !strcasecmp(preset, "medium") || + !strcasecmp(preset, "strong") || + !strcasecmp(preset, "stronger")) + { + double strength[2]; + const char *kernel_string[2]; + + if (tune == NULL || !strcasecmp(tune, "none")) + { + strength[0] = strength[1] = 0.2; + kernel_string[0] = kernel_string[1] = "isolap"; + if (!strcasecmp(preset, "ultralight")) + { + strength[0] = strength[1] = 0.05; + } + else if (!strcasecmp(preset, "light")) + { + strength[0] = strength[1] = 0.1; + } + else if (!strcasecmp(preset, "strong")) + { + strength[0] = strength[1] = 0.3; + } + else if (!strcasecmp(preset, "stronger")) + { + strength[0] = strength[1] = 0.5; + } + } + else if (!strcasecmp(tune, "film")) + { + strength[0] = 0.2; strength[1] = 0.12; + kernel_string[0] = kernel_string[1] = "isolap"; + if (!strcasecmp(preset, "ultralight")) + { + strength[0] = 0.05; strength[1] = 0.03; + } + else if (!strcasecmp(preset, "light")) + { + strength[0] = 0.1; strength[1] = 0.06; + } + else if (!strcasecmp(preset, "strong")) + { + strength[0] = 0.3; strength[1] = 0.2; + } + else if (!strcasecmp(preset, "stronger")) + { + strength[0] = 0.5; strength[1] = 0.3; + } + } + else if (!strcasecmp(tune, "grain")) + { + strength[0] = 0.2; strength[1] = 0.1; + kernel_string[0] = kernel_string[1] = "log"; + if (!strcasecmp(preset, "ultralight")) + { + strength[0] = 0.05; strength[1] = 0.025; + } + else if (!strcasecmp(preset, "light")) + { + strength[0] = 0.1; strength[1] = 0.05; + } + else if (!strcasecmp(preset, "strong")) + { + strength[0] = 0.3; strength[1] = 0.15; + } + else if (!strcasecmp(preset, "stronger")) + { + strength[0] = 0.5; strength[1] = 0.25; + } + } + else if (!strcasecmp(tune, "animation")) + { + strength[0] = 0.15; strength[1] = 0.09; + kernel_string[0] = kernel_string[1] = "isolap"; + if (!strcasecmp(preset, "ultralight")) + { + strength[0] = 0.0375; strength[1] = 0.0225; + } + else if (!strcasecmp(preset, "light")) + { + strength[0] = 0.075; strength[1] = 0.05625; + } + else if (!strcasecmp(preset, "strong")) + { + strength[0] = 0.225; strength[1] = 0.15; + } + else if (!strcasecmp(preset, "stronger")) + { + strength[0] = 0.375; strength[1] = 0.225; + } + } + else if (!strcasecmp(tune, "sprite")) + { + strength[0] = strength[1] = 0.15; + kernel_string[0] = kernel_string[1] = "lap"; + if (!strcasecmp(preset, "ultralight")) + { + strength[0] = strength[1] = 0.0375; + } + else if (!strcasecmp(preset, "light")) + { + strength[0] = strength[1] = 0.075; + } + else if (!strcasecmp(preset, "strong")) + { + strength[0] = strength[1] = 0.225; + } + else if (!strcasecmp(preset, "stronger")) + { + strength[0] = strength[1] = 0.375; + } + } + else + { + fprintf(stderr, "Unrecognized lapsharp tune (%s).\n", tune); + return NULL; + } + + settings = hb_dict_init(); + hb_dict_set(settings, "y-strength", hb_value_double(strength[0])); + hb_dict_set(settings, "y-kernel", hb_value_string(kernel_string[0])); + + hb_dict_set(settings, "cb-strength", hb_value_double(strength[1])); + hb_dict_set(settings, "cb-kernel", hb_value_string(kernel_string[1])); + } + else + { + settings = hb_parse_filter_settings(preset); + if (tune != NULL) + { + fprintf(stderr, "Custom lapsharp parameters specified; ignoring lapsharp tune (%s).\n", tune); + } + } + + return settings; +} + int hb_validate_param_string(const char *regex_pattern, const char *param_string) { regex_t regex_temp; @@ -822,6 +998,9 @@ hb_generate_filter_settings(int filter_id, const char *preset, const char *tune, case HB_FILTER_NLMEANS: settings = generate_nlmeans_settings(preset, tune, custom); break; + case HB_FILTER_LAPSHARP: + settings = generate_lapsharp_settings(preset, tune, custom); + break; case HB_FILTER_UNSHARP: settings = generate_unsharp_settings(preset, tune, custom); break; diff --git a/libhb/preset.c b/libhb/preset.c index 08f7d186c..6c32eb9f6 100644 --- a/libhb/preset.c +++ b/libhb/preset.c @@ -1385,7 +1385,11 @@ int hb_preset_apply_filters(const hb_dict_t *preset, hb_dict_t *job_dict) strcasecmp(sharpen_filter, "off")) { int filter_id; - if (!strcasecmp(sharpen_filter, "unsharp")) + if (!strcasecmp(sharpen_filter, "lapsharp")) + { + filter_id = HB_FILTER_LAPSHARP; + } + else if (!strcasecmp(sharpen_filter, "unsharp")) { filter_id = HB_FILTER_UNSHARP; } diff --git a/test/test.c b/test/test.c index cc84d3e7b..7cf7d4f47 100644 --- a/test/test.c +++ b/test/test.c @@ -46,6 +46,7 @@ #include #endif +#define LAPSHARP_DEFAULT_PRESET "medium" #define UNSHARP_DEFAULT_PRESET "medium" #define NLMEANS_DEFAULT_PRESET "medium" #define DEINTERLACE_DEFAULT_PRESET "default" @@ -86,6 +87,10 @@ static int unsharp_disable = 0; static int unsharp_custom = 0; static char * unsharp = NULL; static char * unsharp_tune = NULL; +static int lapsharp_disable = 0; +static int lapsharp_custom = 0; +static char * lapsharp = NULL; +static char * lapsharp_tune = NULL; static int detelecine_disable = 0; static int detelecine_custom = 0; static char * detelecine = NULL; @@ -578,6 +583,8 @@ cleanup: free(nlmeans_tune); free(unsharp); free(unsharp_tune); + free(lapsharp); + free(lapsharp_tune); free(preset_export_name); free(preset_export_desc); free(preset_export_file); @@ -1095,6 +1102,9 @@ static void showFilterDefault(FILE* const out, int filter_id) case HB_FILTER_UNSHARP: preset = UNSHARP_DEFAULT_PRESET; break; + case HB_FILTER_LAPSHARP: + preset = LAPSHARP_DEFAULT_PRESET; + break; case HB_FILTER_NLMEANS: preset = NLMEANS_DEFAULT_PRESET; break; @@ -1121,6 +1131,7 @@ static void showFilterDefault(FILE* const out, int filter_id) case HB_FILTER_DEINTERLACE: case HB_FILTER_NLMEANS: case HB_FILTER_UNSHARP: + case HB_FILTER_LAPSHARP: case HB_FILTER_DECOMB: case HB_FILTER_DETELECINE: case HB_FILTER_HQDN3D: @@ -1624,6 +1635,19 @@ static void ShowHelp() fprintf( out, " Applies to unsharp presets only (does not affect\n" " custom settings)\n" +" --lapsharp[=string] Sharpen video with lapsharp filter\n"); + showFilterPresets(out, HB_FILTER_LAPSHARP); + showFilterKeys(out, HB_FILTER_LAPSHARP); + showFilterDefault(out, HB_FILTER_LAPSHARP); + fprintf( out, + +" --no-lapsharp Disable preset lapsharp filter\n" +" --lapsharp-tune \n" +" Tune lapsharp filter\n"); + showFilterTunes(out, HB_FILTER_LAPSHARP); + fprintf( out, +" Applies to lapsharp presets only (does not affect\n" +" custom settings)\n" " -7, --deblock[=string] Deblock video with pp7 filter\n"); showFilterKeys(out, HB_FILTER_DEBLOCK); showFilterDefault(out, HB_FILTER_DEBLOCK); @@ -1981,6 +2005,8 @@ static int ParseOptions( int argc, char ** argv ) #define QUEUE_IMPORT 311 #define FILTER_UNSHARP 312 #define FILTER_UNSHARP_TUNE 313 + #define FILTER_LAPSHARP 314 + #define FILTER_LAPSHARP_TUNE 315 for( ;; ) { @@ -2059,6 +2085,9 @@ static int ParseOptions( int argc, char ** argv ) { "unsharp", optional_argument, NULL, FILTER_UNSHARP }, { "no-unsharp", no_argument, &unsharp_disable, 1 }, { "unsharp-tune",required_argument, NULL, FILTER_UNSHARP_TUNE }, + { "lapsharp", optional_argument, NULL, FILTER_LAPSHARP }, + { "no-lapsharp", no_argument, &lapsharp_disable, 1 }, + { "lapsharp-tune", required_argument, NULL, FILTER_LAPSHARP_TUNE }, { "detelecine", optional_argument, NULL, '9' }, { "no-detelecine", no_argument, &detelecine_disable, 1 }, { "no-comb-detect", no_argument, &comb_detect_disable, 1 }, @@ -2501,6 +2530,21 @@ static int ParseOptions( int argc, char ** argv ) free(unsharp_tune); unsharp_tune = strdup(optarg); break; + case FILTER_LAPSHARP: + free(lapsharp); + if (optarg != NULL) + { + lapsharp = strdup(optarg); + } + else + { + lapsharp = strdup(LAPSHARP_DEFAULT_PRESET); + } + break; + case FILTER_LAPSHARP_TUNE: + free(lapsharp_tune); + lapsharp_tune = strdup(optarg); + break; case '9': free(detelecine); if (optarg != NULL) @@ -3018,6 +3062,32 @@ static int ParseOptions( int argc, char ** argv ) } } + if (lapsharp != NULL) + { + if (lapsharp_disable) + { + fprintf(stderr, + "Incompatible options --lapsharp and --no-lapsharp\n"); + return -1; + } + if (!hb_validate_filter_preset(HB_FILTER_LAPSHARP, lapsharp, + lapsharp_tune, NULL)) + { + // Nothing to do, but must validate preset before + // attempting to validate custom settings to prevent potential + // false positive + } + else if (!hb_validate_filter_string(HB_FILTER_LAPSHARP, lapsharp)) + { + lapsharp_custom = 1; + } + else + { + fprintf(stderr, "Invalid lapsharp option %s\n", lapsharp); + return -1; + } + } + return 0; } @@ -3958,6 +4028,31 @@ static hb_dict_t * PreparePreset(const char *preset_name) hb_value_string(unsharp)); } } + if (lapsharp_disable && !strcasecmp(s, "lapsharp")) + { + hb_dict_set(preset, "PictureSharpenFilter", hb_value_string("off")); + } + if (lapsharp != NULL) + { + hb_dict_set(preset, "PictureSharpenFilter", hb_value_string("lapsharp")); + if (!lapsharp_custom) + { + hb_dict_set(preset, "PictureSharpenPreset", + hb_value_string(lapsharp)); + if (lapsharp_tune != NULL) + { + hb_dict_set(preset, "PictureSharpenTune", + hb_value_string(lapsharp_tune)); + } + } + else + { + hb_dict_set(preset, "PictureSharpenPreset", + hb_value_string("custom")); + hb_dict_set(preset, "PictureSharpenCustom", + hb_value_string(lapsharp)); + } + } if (deblock_disable) { hb_dict_set(preset, "PictureDeblock", hb_value_string("0")); -- cgit v1.2.3