From 07d6b7f56d68a6465f34fa85accc1db0238f4440 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Thu, 19 Oct 2017 16:01:46 +0200 Subject: [PATCH] lcms: Add LCMS ICC color correction element https://bugzilla.gnome.org/show_bug.cgi?id=765927 --- configure.ac | 11 + ext/Makefile.am | 8 + ext/colormanagement/Makefile.am | 11 + ext/colormanagement/gstcolormanagement.c | 37 + ext/colormanagement/gstlcms.c | 889 +++++++++++++++++++++++ ext/colormanagement/gstlcms.h | 102 +++ 6 files changed, 1058 insertions(+) create mode 100644 ext/colormanagement/Makefile.am create mode 100644 ext/colormanagement/gstcolormanagement.c create mode 100644 ext/colormanagement/gstlcms.c create mode 100644 ext/colormanagement/gstlcms.h diff --git a/configure.ac b/configure.ac index df58fc594e..4aa55d7789 100644 --- a/configure.ac +++ b/configure.ac @@ -2480,6 +2480,15 @@ AG_GST_CHECK_FEATURE(LADSPA, [ladspa], ladspa, [ AC_SUBST(LRDF_CFLAGS) ]) +dnl *** LCM2 *** +#translit(dnm, m, l) AM_CONDITIONAL(USE_LCMS2, true) +AG_GST_CHECK_FEATURE(LCMS2, [LCMS colormanagement plugin], lcms2, [ + PKG_CHECK_MODULES(LCMS2, lcms2 >= 2.7, HAVE_LCMS2="yes", [ + HAVE_LCMS2="no" + ]) + AC_SUBST(LCMS2_LIBS) +]) + dnl *** LV2 *** translit(dnm, m, l) AM_CONDITIONAL(USE_LV2, true) AG_GST_CHECK_FEATURE(LV2, [lv2], lv2, [ @@ -3454,6 +3463,7 @@ AM_CONDITIONAL(USE_KATE, false) AM_CONDITIONAL(USE_KMS, false) AM_CONDITIONAL(USE_TIGER, false) AM_CONDITIONAL(USE_LADSPA, false) +AM_CONDITIONAL(USE_LCMS2, false) AM_CONDITIONAL(USE_LV2, false) AM_CONDITIONAL(USE_LIBDE265, false) AM_CONDITIONAL(USE_LIBMMS, false) @@ -3739,6 +3749,7 @@ ext/assrender/Makefile ext/bs2b/Makefile ext/bz2/Makefile ext/chromaprint/Makefile +ext/colormanagement/Makefile ext/curl/Makefile ext/dash/Makefile ext/dc1394/Makefile diff --git a/ext/Makefile.am b/ext/Makefile.am index bc2abed317..aeaa9f1f69 100644 --- a/ext/Makefile.am +++ b/ext/Makefile.am @@ -22,6 +22,12 @@ else BZ2_DIR= endif +if USE_LCMS2 +COLORMANAGEMENT_DIR=colormanagement +else +COLORMANAGEMENT_DIR= +endif + if USE_CHROMAPRINT CHROMAPRINT_DIR=chromaprint else @@ -401,6 +407,7 @@ SUBDIRS=\ $(AUDIOFILE_DIR) \ $(BS2B_DIR) \ $(BZ2_DIR) \ + $(COLORMANAGEMENT_DIR) \ $(CHROMAPRINT_DIR) \ $(CURL_DIR) \ $(DASH_DIR) \ @@ -467,6 +474,7 @@ DIST_SUBDIRS = \ assrender \ bs2b \ bz2 \ + colormanagement \ chromaprint \ curl \ dash \ diff --git a/ext/colormanagement/Makefile.am b/ext/colormanagement/Makefile.am new file mode 100644 index 0000000000..ff4a7c2ef6 --- /dev/null +++ b/ext/colormanagement/Makefile.am @@ -0,0 +1,11 @@ +plugin_LTLIBRARIES = libgstcolormanagement.la + +libgstcolormanagement_la_SOURCES = gstcolormanagement.c \ + gstlcms.c + +libgstcolormanagement_la_CFLAGS = $(GST_PLUGINS_BASE_CFLAGS) $(GST_CFLAGS) $(LCMS2_CFLAGS) +libgstcolormanagement_la_LIBADD = $(GST_PLUGINS_BASE_LIBS) -lgstvideo-1.0 $(GST_LIBS) $(LCMS2_LIBS) +libgstcolormanagement_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS) +libgstcolormanagement_la_LIBTOOLFLAGS = $(GST_PLUGIN_LIBTOOLFLAGS) + +noinst_HEADERS = gstlcms.h diff --git a/ext/colormanagement/gstcolormanagement.c b/ext/colormanagement/gstcolormanagement.c new file mode 100644 index 0000000000..a54f3f7deb --- /dev/null +++ b/ext/colormanagement/gstcolormanagement.c @@ -0,0 +1,37 @@ +/* + * GStreamer gstreamer-colormanagement + * Copyright (C) 2016 Andreas Frisch + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gstlcms.h" + +static gboolean +plugin_init (GstPlugin * plugin) +{ + return gst_element_register (plugin, "lcms", GST_RANK_NONE, GST_TYPE_LCMS); +} + +GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + colormanagement, + "Color management correction plugins", + plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN); diff --git a/ext/colormanagement/gstlcms.c b/ext/colormanagement/gstlcms.c new file mode 100644 index 0000000000..831af2e603 --- /dev/null +++ b/ext/colormanagement/gstlcms.c @@ -0,0 +1,889 @@ +/* + * GStreamer gstreamer-lcms + * Copyright (C) 2016 Andreas Frisch + * + * gstlcms.c + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * SECTION:element-lcms + * @short_description: Uses LittleCMS 2 to perform ICC profile correction + * + * This is a color management plugin that uses LittleCMS 2 to correct + * frames using the given ICC (International Color Consortium) profiles. + * Falls back to internal sRGB profile if no ICC file is specified in property. + * + * + * Example launch line + * (write everything in one line, without the backslash characters) + * |[ + * gst-launch-1.0 filesrc location=photo_camera.png ! pngdec ! \ + * videoconvert ! lcms input-profile=sRGB.icc dest-profile=printer.icc \ + * pngenc ! filesink location=photo_print.png + * ]| + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gstlcms.h" + +#include +#include +#include +#include + +GST_DEBUG_CATEGORY_STATIC (lcms_debug); +#define GST_CAT_DEFAULT lcms_debug + +/* GstLcms properties */ +enum +{ + PROP_0, + PROP_INTENT, + PROP_LOOKUP_METHOD, + PROP_SRC_FILE, + PROP_DST_FILE, + PROP_PRESERVE_BLACK, + PROP_EMBEDDED_PROFILE +}; + +GType +gst_lcms_intent_get_type (void) +{ + static volatile gsize intent_type = 0; + static const GEnumValue intent[] = { + {GST_LCMS_INTENT_PERCEPTUAL, "Perceptual", + "perceptual"}, + {GST_LCMS_INTENT_RELATIVE_COLORIMETRIC, "Relative Colorimetric", + "relative"}, + {GST_LCMS_INTENT_SATURATION, "Saturation", + "saturation"}, + {GST_LCMS_INTENT_ABSOLUTE_COLORIMETRIC, "Absolute Colorimetric", + "absolute"}, + {0, NULL, NULL}, + }; + + if (g_once_init_enter (&intent_type)) { + GType tmp = g_enum_register_static ("GstLcmsIntent", intent); + g_once_init_leave (&intent_type, tmp); + } + return (GType) intent_type; +} + +static GType +gst_lcms_lookup_method_get_type (void) +{ + static volatile gsize lookup_method_type = 0; + static const GEnumValue lookup_method[] = { + {GST_LCMS_LOOKUP_METHOD_UNCACHED, + "Uncached, calculate every pixel on the fly (very slow playback)", + "uncached"}, + {GST_LCMS_LOOKUP_METHOD_PRECALCULATED, + "Precalculate lookup table (takes a long time getting READY)", + "precalculated"}, + {GST_LCMS_LOOKUP_METHOD_CACHED, + "Calculate and cache color replacement values on first occurence", + "cached"}, + {0, NULL, NULL}, + }; + + if (g_once_init_enter (&lookup_method_type)) { + GType tmp = g_enum_register_static ("GstLcmsLookupMethod", lookup_method); + g_once_init_leave (&lookup_method_type, tmp); + } + return (GType) lookup_method_type; +} + +#define DEFAULT_INTENT GST_LCMS_INTENT_PERCEPTUAL +#define DEFAULT_LOOKUP_METHOD GST_LCMS_LOOKUP_METHOD_CACHED +#define DEFAULT_PRESERVE_BLACK FALSE +#define DEFAULT_EMBEDDED_PROFILE TRUE + +static GstStaticPadTemplate gst_lcms_src_template = +GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE ("{ " + "ARGB, BGRA, ABGR, RGBA, xRGB," "RGBx, xBGR, BGRx, RGB, BGR }")) + ); + +static GstStaticPadTemplate gst_lcms_sink_template = +GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE ("{ " + "ARGB, BGRA, ABGR, RGBA, xRGB," "RGBx, xBGR, BGRx, RGB, BGR }")) + ); + +static void gst_lcms_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_lcms_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); +static void gst_lcms_finalize (GObject * object); +static GstStateChangeReturn gst_lcms_change_state (GstElement * element, + GstStateChange transition); +static gboolean gst_lcms_set_info (GstVideoFilter * vfilter, GstCaps * incaps, + GstVideoInfo * in_info, GstCaps * outcaps, GstVideoInfo * out_info); +static gboolean gst_lcms_sink_event (GstBaseTransform * trans, + GstEvent * event); +static void gst_lcms_handle_tags (GstLcms * lcms, GstTagList * taglist); +static void gst_lcms_handle_tag_sample (GstLcms * lcms, GstSample * sample); +static GstFlowReturn gst_lcms_transform_frame (GstVideoFilter * vfilter, + GstVideoFrame * inframe, GstVideoFrame * outframe); +static GstFlowReturn gst_lcms_transform_frame_ip (GstVideoFilter * vfilter, + GstVideoFrame * frame); + +static void gst_lcms_get_ready (GstLcms * lcms); +static void gst_lcms_create_transform (GstLcms * lcms); +static void gst_lcms_cleanup_cms (GstLcms * lcms); +static void gst_lcms_init_lookup_table (GstLcms * lcms); +static void gst_lcms_process_rgb (GstLcms * lcms, GstVideoFrame * inframe, + GstVideoFrame * outframe); + +G_DEFINE_TYPE (GstLcms, gst_lcms, GST_TYPE_VIDEO_FILTER); + +static void +gst_lcms_class_init (GstLcmsClass * klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + GstElementClass *element_class = (GstElementClass *) klass; + GstBaseTransformClass *trans_class = (GstBaseTransformClass *) klass; + GstVideoFilterClass *vfilter_class = (GstVideoFilterClass *) klass; + + GST_DEBUG_CATEGORY_INIT (lcms_debug, "lcms", 0, "lcms"); + + gobject_class->set_property = gst_lcms_set_property; + gobject_class->get_property = gst_lcms_get_property; + gobject_class->finalize = gst_lcms_finalize; + + g_object_class_install_property (gobject_class, PROP_INTENT, + g_param_spec_enum ("intent", "Rendering intent", + "Select the rendering intent of the color correction", + GST_TYPE_LCMS_INTENT, DEFAULT_INTENT, + (G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_SRC_FILE, + g_param_spec_string ("input-profile", "Input ICC profile file", + "Specify the input ICC profile file to apply", NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_DST_FILE, + g_param_spec_string ("dest-profile", "Destination ICC profile file", + "Specify the destination ICC profile file to apply", NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property (gobject_class, PROP_LOOKUP_METHOD, + g_param_spec_enum ("lookup", "Lookup method", + "Select the caching method for the color compensation calculations", + GST_TYPE_LCMS_LOOKUP_METHOD, DEFAULT_LOOKUP_METHOD, + (G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property (gobject_class, PROP_PRESERVE_BLACK, + g_param_spec_boolean ("preserve-black", "Preserve black", + "Select whether purely black pixels should be preserved", + DEFAULT_PRESERVE_BLACK, + (G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property (gobject_class, PROP_EMBEDDED_PROFILE, + g_param_spec_boolean ("embedded-profile", "Embedded Profile", + "Extract and use source profiles embedded in images", + DEFAULT_EMBEDDED_PROFILE, + (G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + gst_element_class_set_static_metadata (element_class, + "LCMS2 ICC correction", "Filter/Effect/Video", + "Uses LittleCMS 2 to perform ICC profile correction", + "Andreas Frisch "); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&gst_lcms_sink_template)); + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&gst_lcms_src_template)); + + element_class->change_state = GST_DEBUG_FUNCPTR (gst_lcms_change_state); + + trans_class->sink_event = GST_DEBUG_FUNCPTR (gst_lcms_sink_event); + + vfilter_class->set_info = GST_DEBUG_FUNCPTR (gst_lcms_set_info); + vfilter_class->transform_frame_ip = + GST_DEBUG_FUNCPTR (gst_lcms_transform_frame_ip); + vfilter_class->transform_frame = GST_DEBUG_FUNCPTR (gst_lcms_transform_frame); +} + +static void +gst_lcms_init (GstLcms * lcms) +{ + lcms->color_lut = NULL; + lcms->cms_inp_profile = NULL; + lcms->cms_dst_profile = NULL; + lcms->cms_transform = NULL; +} + +static void +gst_lcms_finalize (GObject * object) +{ + GstLcms *lcms = GST_LCMS (object); + if (lcms->color_lut) + g_free (lcms->color_lut); + g_free (lcms->inp_profile_filename); + g_free (lcms->dst_profile_filename); + G_OBJECT_CLASS (gst_lcms_parent_class)->finalize (object); +} + +static void +gst_lcms_set_intent (GstLcms * lcms, GstLcmsIntent intent) +{ + GEnumValue *val = + g_enum_get_value (G_ENUM_CLASS (g_type_class_ref + (GST_TYPE_LCMS_INTENT)), intent); + const gchar *value_nick; + + g_return_if_fail (GST_IS_LCMS (lcms)); + if (!val) { + GST_ERROR_OBJECT (lcms, "no such rendering intent %i!", intent); + return; + } + value_nick = val->value_nick; + + GST_OBJECT_LOCK (lcms); + lcms->intent = intent; + GST_OBJECT_UNLOCK (lcms); + + GST_DEBUG_OBJECT (lcms, "successfully set rendering intent to %s (%i)", + value_nick, intent); + return; +} + +static GstLcmsIntent +gst_lcms_get_intent (GstLcms * lcms) +{ + g_return_val_if_fail (GST_IS_LCMS (lcms), (GstLcmsIntent) - 1); + return lcms->intent; +} + +static void +gst_lcms_set_lookup_method (GstLcms * lcms, GstLcmsLookupMethod method) +{ + GEnumValue *val = + g_enum_get_value (G_ENUM_CLASS (g_type_class_ref + (GST_TYPE_LCMS_LOOKUP_METHOD)), method); + const gchar *value_nick; + + g_return_if_fail (GST_IS_LCMS (lcms)); + if (!val) { + GST_ERROR_OBJECT (lcms, "no such lookup method %i!", method); + return; + } + value_nick = val->value_nick; + + GST_OBJECT_LOCK (lcms); + lcms->lookup_method = method; + GST_OBJECT_UNLOCK (lcms); + + GST_DEBUG_OBJECT (lcms, "successfully set lookup method to %s (%i)", + value_nick, method); + return; +} + +static GstLcmsLookupMethod +gst_lcms_get_lookup_method (GstLcms * lcms) +{ + g_return_val_if_fail (GST_IS_LCMS (lcms), (GstLcmsLookupMethod) - 1); + return lcms->lookup_method; +} + +static void +gst_lcms_set_property (GObject * object, guint prop_id, const GValue * value, + GParamSpec * pspec) +{ + const gchar *filename; + GstLcms *lcms = GST_LCMS (object); + + switch (prop_id) { + case PROP_SRC_FILE: + { + GST_OBJECT_LOCK (lcms); + filename = g_value_get_string (value); + if (filename + && g_file_test (filename, + (GFileTest) (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))) { + if (lcms->inp_profile_filename) + g_free (lcms->inp_profile_filename); + lcms->inp_profile_filename = g_strdup (filename); + } else { + GST_WARNING_OBJECT (lcms, "Input profile file '%s' not found!", + filename); + } + GST_OBJECT_UNLOCK (lcms); + break; + } + case PROP_DST_FILE: + { + GST_OBJECT_LOCK (lcms); + filename = g_value_get_string (value); + if (g_file_test (filename, + (GFileTest) (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))) { + if (lcms->dst_profile_filename) + g_free (lcms->dst_profile_filename); + lcms->dst_profile_filename = g_strdup (filename); + } else { + GST_WARNING_OBJECT (lcms, "Destination profile file '%s' not found!", + filename); + } + GST_OBJECT_UNLOCK (lcms); + break; + } + case PROP_INTENT: + gst_lcms_set_intent (lcms, (GstLcmsIntent) g_value_get_enum (value)); + break; + case PROP_LOOKUP_METHOD: + gst_lcms_set_lookup_method (lcms, + (GstLcmsLookupMethod) g_value_get_enum (value)); + break; + case PROP_PRESERVE_BLACK: + lcms->preserve_black = g_value_get_boolean (value); + break; + case PROP_EMBEDDED_PROFILE: + lcms->embeddedprofiles = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_lcms_get_property (GObject * object, guint prop_id, GValue * value, + GParamSpec * pspec) +{ + GstLcms *lcms = GST_LCMS (object); + + switch (prop_id) { + case PROP_SRC_FILE: + g_value_set_string (value, lcms->inp_profile_filename); + break; + case PROP_DST_FILE: + g_value_set_string (value, lcms->dst_profile_filename); + break; + case PROP_INTENT: + g_value_set_enum (value, gst_lcms_get_intent (lcms)); + break; + case PROP_LOOKUP_METHOD: + g_value_set_enum (value, gst_lcms_get_lookup_method (lcms)); + break; + case PROP_PRESERVE_BLACK: + g_value_set_boolean (value, lcms->preserve_black); + break; + case PROP_EMBEDDED_PROFILE: + g_value_set_boolean (value, lcms->embeddedprofiles); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GstStateChangeReturn +gst_lcms_change_state (GstElement * element, GstStateChange transition) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + GstLcms *lcms = GST_LCMS (element); + + switch (transition) { + case GST_STATE_CHANGE_NULL_TO_READY: + GST_DEBUG_OBJECT (lcms, "GST_STATE_CHANGE_NULL_TO_READY"); + gst_lcms_get_ready (lcms); + break; + case GST_STATE_CHANGE_PAUSED_TO_PLAYING: + { + if (!lcms->cms_inp_profile) { + if (!lcms->cms_dst_profile) { + GST_WARNING_OBJECT (lcms, + "No input or output ICC profile specified, falling back to passthrough!"); + gst_base_transform_set_passthrough (GST_BASE_TRANSFORM (lcms), TRUE); + GST_BASE_TRANSFORM_CLASS (GST_LCMS_GET_CLASS + (lcms))->transform_ip_on_passthrough = lcms->embeddedprofiles; + return GST_STATE_CHANGE_SUCCESS; + } + lcms->cms_inp_profile = cmsCreate_sRGBProfile (); + GST_INFO_OBJECT (lcms, + "No input profile specified, falling back to sRGB"); + } + } + + default: + break; + } + + if (ret == GST_STATE_CHANGE_SUCCESS) + ret = + GST_ELEMENT_CLASS (gst_lcms_parent_class)->change_state (element, + transition); + + switch (transition) { + case GST_STATE_CHANGE_PLAYING_TO_PAUSED: + case GST_STATE_CHANGE_PAUSED_TO_READY: + break; + case GST_STATE_CHANGE_READY_TO_NULL: + gst_lcms_cleanup_cms (lcms); + default: + break; + } + return ret; +} + +static void +gst_lcms_get_ready (GstLcms * lcms) +{ + if (lcms->inp_profile_filename) { + lcms->cms_inp_profile = + cmsOpenProfileFromFile (lcms->inp_profile_filename, "r"); + if (!lcms->cms_inp_profile) + GST_ERROR_OBJECT (lcms, "Couldn't parse input ICC profile '%s'", + lcms->inp_profile_filename); + else + GST_DEBUG_OBJECT (lcms, "Successfully opened input ICC profile '%s'", + lcms->inp_profile_filename); + } + + if (lcms->dst_profile_filename) { + lcms->cms_dst_profile = + cmsOpenProfileFromFile (lcms->dst_profile_filename, "r"); + if (!lcms->cms_dst_profile) + GST_ERROR_OBJECT (lcms, + "Couldn't parse destination ICC profile '%s'", + lcms->dst_profile_filename); + else + GST_DEBUG_OBJECT (lcms, "Successfully opened output ICC profile '%s'", + lcms->dst_profile_filename); + } + + if (lcms->lookup_method != GST_LCMS_LOOKUP_METHOD_UNCACHED) { + gst_lcms_init_lookup_table (lcms); + } +} + +static void +gst_lcms_cleanup_cms (GstLcms * lcms) +{ + if (lcms->cms_inp_profile) { + cmsCloseProfile (lcms->cms_inp_profile); + lcms->cms_inp_profile = NULL; + } + if (lcms->cms_dst_profile) { + cmsCloseProfile (lcms->cms_dst_profile); + lcms->cms_dst_profile = NULL; + } + if (lcms->cms_transform) { + cmsDeleteTransform (lcms->cms_transform); + lcms->cms_transform = NULL; + } +} + +static void +gst_lcms_init_lookup_table (GstLcms * lcms) +{ + guint32 p; + const guint32 color_max = 0x01000000; + + if (lcms->color_lut) + g_free (lcms->color_lut); + + lcms->color_lut = g_new (guint32, color_max); + + if (lcms->color_lut == NULL) { + GST_ELEMENT_ERROR (lcms, RESOURCE, FAILED, ("LUT alloc failed"), + ("Unable to open allocate memory for lookup table!")); + return; + } + + if (lcms->lookup_method == GST_LCMS_LOOKUP_METHOD_PRECALCULATED) { + cmsHTRANSFORM hTransform; + hTransform = + cmsCreateTransform (lcms->cms_inp_profile, TYPE_RGB_8, + lcms->cms_dst_profile, TYPE_RGB_8, lcms->intent, 0); + /*FIXME use cmsFLAGS_COPY_ALPHA when new lcms2 2.8 release is available */ + for (p = 0; p < color_max; p++) + cmsDoTransform (hTransform, (const cmsUInt32Number *) &p, + &lcms->color_lut[p], 1); + cmsDeleteTransform (hTransform); + GST_DEBUG_OBJECT (lcms, "writing lookup table finished"); + } else if (lcms->lookup_method == GST_LCMS_LOOKUP_METHOD_CACHED) { + memset (lcms->color_lut, 0xAA, color_max * sizeof (guint32)); + GST_DEBUG_OBJECT (lcms, "initialized empty lookup table for caching"); + } + if (lcms->preserve_black) + lcms->color_lut[0] = 0x000000; +} + +static cmsUInt32Number +gst_lcms_cms_format_from_gst (GstVideoFormat gst_format) +{ + cmsUInt32Number cms_format = 0; + switch (gst_format) { + case GST_VIDEO_FORMAT_ARGB: + case GST_VIDEO_FORMAT_xRGB: + cms_format = TYPE_ARGB_8; + break; + case GST_VIDEO_FORMAT_xBGR: + case GST_VIDEO_FORMAT_ABGR: + cms_format = TYPE_ABGR_8; + break; + case GST_VIDEO_FORMAT_BGRA: + case GST_VIDEO_FORMAT_BGRx: + cms_format = TYPE_BGRA_8; + break; + case GST_VIDEO_FORMAT_BGR: + cms_format = TYPE_BGR_8; + break; + case GST_VIDEO_FORMAT_RGBA: + case GST_VIDEO_FORMAT_RGBx: + cms_format = TYPE_RGBA_8; + break; + case GST_VIDEO_FORMAT_RGB: + cms_format = TYPE_RGB_8; + break; + default: + break; + } + return cms_format; +} + +static gboolean +gst_lcms_set_info (GstVideoFilter * vfilter, GstCaps * incaps, + GstVideoInfo * in_info, GstCaps * outcaps, GstVideoInfo * out_info) +{ + GstLcms *lcms = GST_LCMS (vfilter); + + GST_DEBUG_OBJECT (lcms, + "setting caps: in %" GST_PTR_FORMAT " out %" GST_PTR_FORMAT, incaps, + outcaps); + + lcms->cms_inp_format = + gst_lcms_cms_format_from_gst (GST_VIDEO_INFO_FORMAT (in_info)); + lcms->cms_dst_format = + gst_lcms_cms_format_from_gst (GST_VIDEO_INFO_FORMAT (out_info)); + + if (gst_base_transform_is_passthrough (GST_BASE_TRANSFORM (lcms))) + return TRUE; + + if (!lcms->cms_inp_format || !lcms->cms_dst_format) + goto invalid_caps; + + if (lcms->cms_inp_format == lcms->cms_dst_format + && lcms->lookup_method != GST_LCMS_LOOKUP_METHOD_UNCACHED) { + gst_base_transform_set_in_place (GST_BASE_TRANSFORM (lcms), TRUE); + } else + gst_base_transform_set_in_place (GST_BASE_TRANSFORM (lcms), FALSE); + + gst_lcms_create_transform (lcms); + lcms->process = gst_lcms_process_rgb; + + return TRUE; + +invalid_caps: + { + GST_ERROR_OBJECT (lcms, "Invalid caps: %" GST_PTR_FORMAT, incaps); + return FALSE; + } +} + +static void +gst_lcms_create_transform (GstLcms * lcms) +{ + if (!lcms->cms_dst_profile) { + lcms->cms_dst_profile = cmsCreate_sRGBProfile (); + GST_INFO_OBJECT (lcms, "No output profile specified, falling back to sRGB"); + } + lcms->cms_transform = + cmsCreateTransform (lcms->cms_inp_profile, lcms->cms_inp_format, + lcms->cms_dst_profile, lcms->cms_dst_format, lcms->intent, 0); + if (lcms->cms_transform) { + GST_DEBUG_OBJECT (lcms, "created transformation format=%i->%i", + lcms->cms_inp_format, lcms->cms_dst_format); + } else { + GST_WARNING_OBJECT (lcms, + "couldn't create transformation format=%i->%i, fallback to passthrough!", + lcms->cms_inp_format, lcms->cms_dst_format); + gst_base_transform_set_passthrough (GST_BASE_TRANSFORM (lcms), TRUE); + } +} + +static gboolean +gst_lcms_sink_event (GstBaseTransform * trans, GstEvent * event) +{ + gboolean ret = FALSE; + GstLcms *lcms = GST_LCMS (trans); + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_TAG: + { + if (lcms->embeddedprofiles) { + GstTagList *taglist = NULL; + /* icc profiles might be embedded in attachments */ + gst_event_parse_tag (event, &taglist); + gst_lcms_handle_tags (lcms, taglist); + } + break; + } + default: + break; + } + ret = + GST_BASE_TRANSFORM_CLASS (gst_lcms_parent_class)->sink_event (trans, + event); + return ret; +} + +static void +gst_lcms_handle_tag_sample (GstLcms * lcms, GstSample * sample) +{ + GstBuffer *buf; + const GstStructure *structure; + + buf = gst_sample_get_buffer (sample); + structure = gst_sample_get_info (sample); + + if (!buf || !structure) + return; + + if (gst_structure_has_name (structure, "application/vnd.iccprofile")) { + if (!lcms->inp_profile_filename + && lcms->lookup_method != GST_LCMS_LOOKUP_METHOD_UNCACHED) { + GstMapInfo map; + const gchar *icc_name; + icc_name = gst_structure_get_string (structure, "icc-name"); + gst_buffer_map (buf, &map, GST_MAP_READ); + lcms->cms_inp_profile = cmsOpenProfileFromMem (map.data, map.size); + gst_buffer_unmap (buf, &map); + if (!lcms->cms_inp_profile) + GST_WARNING_OBJECT (lcms, + "Couldn't parse embedded input ICC profile '%s'", icc_name); + else { + GST_DEBUG_OBJECT (lcms, + "Successfully opened embedded input ICC profile '%s'", icc_name); + if (lcms->cms_inp_format) { + gst_lcms_create_transform (lcms); + gst_lcms_init_lookup_table (lcms); + } + } + } else { + GST_DEBUG_OBJECT (lcms, + "disregarding embedded ICC profile because input profile file was explicitly specified"); + } + } else + GST_DEBUG_OBJECT (lcms, "attachment is not an ICC profile"); +} + +static void +gst_lcms_handle_tags (GstLcms * lcms, GstTagList * taglist) +{ + guint tag_size; + + if (!taglist) + return; + + tag_size = gst_tag_list_get_tag_size (taglist, GST_TAG_ATTACHMENT); + if (tag_size > 0) { + guint index; + GstSample *sample; + for (index = 0; index < tag_size; index++) { + if (gst_tag_list_get_sample_index (taglist, GST_TAG_ATTACHMENT, index, + &sample)) { + gst_lcms_handle_tag_sample (lcms, sample); + gst_sample_unref (sample); + } + } + } +} + +static GstFlowReturn +gst_lcms_transform_frame_ip (GstVideoFilter * vfilter, GstVideoFrame * inframe) +{ + GstLcms *lcms = GST_LCMS (vfilter); + if (!gst_base_transform_is_passthrough (GST_BASE_TRANSFORM (lcms))) + lcms->process (lcms, inframe, NULL); + return GST_FLOW_OK; +} + +static GstFlowReturn +gst_lcms_transform_frame (GstVideoFilter * vfilter, GstVideoFrame * inframe, + GstVideoFrame * outframe) +{ + GstLcms *lcms = GST_LCMS (vfilter); + if (!gst_base_transform_is_passthrough (GST_BASE_TRANSFORM (lcms))) + lcms->process (lcms, inframe, outframe); + return GST_FLOW_OK; +} + +static void +gst_lcms_process_rgb (GstLcms * lcms, GstVideoFrame * inframe, + GstVideoFrame * outframe) +{ + gint height; + gint width, in_stride, out_stride; + gint in_pixel_stride, out_pixel_stride; + gint in_offsets[4], out_offsets[4]; + guint8 *in_data, *out_data; + gint i, j; + gint in_row_wrap, out_row_wrap; + guint8 alpha = 0; + + in_data = (guint8 *) GST_VIDEO_FRAME_PLANE_DATA (inframe, 0); + in_stride = GST_VIDEO_FRAME_PLANE_STRIDE (inframe, 0); + width = GST_VIDEO_FRAME_COMP_WIDTH (inframe, 0); + height = GST_VIDEO_FRAME_COMP_HEIGHT (inframe, 0); + in_pixel_stride = GST_VIDEO_FRAME_COMP_PSTRIDE (inframe, 0); + + in_offsets[0] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 0); + in_offsets[1] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 1); + in_offsets[2] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 2); + in_offsets[3] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 3); + + if (outframe) { + if (width != GST_VIDEO_FRAME_COMP_WIDTH (outframe, 0) + || height != GST_VIDEO_FRAME_COMP_HEIGHT (outframe, 0)) { + GST_WARNING_OBJECT (lcms, + "can't transform, input dimensions != output dimensions!"); + return; + } + out_data = (guint8 *) GST_VIDEO_FRAME_PLANE_DATA (outframe, 0); + out_stride = GST_VIDEO_FRAME_PLANE_STRIDE (outframe, 0); + out_pixel_stride = GST_VIDEO_FRAME_COMP_PSTRIDE (outframe, 0); + out_offsets[0] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 0); + out_offsets[1] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 1); + out_offsets[2] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 2); + out_offsets[3] = GST_VIDEO_FRAME_COMP_OFFSET (inframe, 3); + GST_LOG_OBJECT (lcms, + "transforming frame (%ix%i) stride=%i->%i pixel_stride=%i->%i format=%s->%s", + width, height, in_stride, out_stride, in_pixel_stride, out_pixel_stride, + gst_video_format_to_string (inframe->info.finfo->format), + gst_video_format_to_string (outframe->info.finfo->format)); + } else { /* in-place transformation */ + GST_LOG_OBJECT (lcms, + "transforming frame IN-PLACE (%ix%i) pixel_stride=%i format=%s", width, + height, in_pixel_stride, + gst_video_format_to_string (inframe->info.finfo->format)); + out_data = in_data; + out_stride = in_stride; + out_pixel_stride = in_pixel_stride; + out_offsets[0] = in_offsets[0]; + out_offsets[1] = in_offsets[1]; + out_offsets[2] = in_offsets[2]; + out_offsets[3] = in_offsets[3]; + } + + in_row_wrap = in_stride - in_pixel_stride * width; + out_row_wrap = out_stride - out_pixel_stride * width; + + if (lcms->lookup_method == GST_LCMS_LOOKUP_METHOD_UNCACHED) { + if (!GST_VIDEO_FORMAT_INFO_HAS_ALPHA (inframe->info.finfo) + && !lcms->preserve_black) { + GST_DEBUG_OBJECT (lcms, + "GST_LCMS_LOOKUP_METHOD_UNCACHED WITHOUT alpha AND WITHOUT preserve-black -> picture-at-once transformation!"); + cmsDoTransformStride (lcms->cms_transform, in_data, out_data, + height * width, out_pixel_stride); + } else { + GST_DEBUG_OBJECT (lcms, + "GST_LCMS_LOOKUP_METHOD_UNCACHED WITH alpha or preserve-black -> pixel-by-pixel transformation!"); + for (i = 0; i < height; i++) { + for (j = 0; j < width; j++) { + if (GST_VIDEO_FORMAT_INFO_HAS_ALPHA (inframe->info.finfo)) + alpha = in_data[in_offsets[3]]; + if (lcms->preserve_black && (in_data[in_offsets[0]] == 0x00) + && (in_data[in_offsets[1]] == 0x00) + && (in_data[in_offsets[2]] == 0x0)) + out_data[out_offsets[0]] = out_data[out_offsets[1]] = + out_data[out_offsets[2]] = 0x00; + else + cmsDoTransformStride (lcms->cms_transform, in_data, out_data, 1, + in_pixel_stride); + if (alpha) + out_data[in_offsets[3]] = alpha; + in_data += in_pixel_stride; + out_data += out_pixel_stride; + } + in_data += in_row_wrap; + out_data += out_row_wrap; + } + } + } else if (lcms->lookup_method == GST_LCMS_LOOKUP_METHOD_PRECALCULATED) { + guint32 color, new_color; + GST_LOG_OBJECT (lcms, "GST_LCMS_LOOKUP_METHOD_PRECALCULATED"); + for (i = 0; i < height; i++) { + for (j = 0; j < width; j++) { + color = + in_data[in_offsets[0]] | + in_data[in_offsets[1]] << 0x08 | in_data[in_offsets[2]] << 0x10; + new_color = lcms->color_lut[color]; + out_data[out_offsets[0]] = (new_color & 0x0000FF) >> 0x00; + out_data[out_offsets[1]] = (new_color & 0x00FF00) >> 0x08; + out_data[out_offsets[2]] = (new_color & 0xFF0000) >> 0x10; + GST_TRACE_OBJECT (lcms, + "(%i:%i)@%p original color 0x%08X (dest was 0x%08X)", i, j, in_data, + color, new_color); + if (GST_VIDEO_FORMAT_INFO_HAS_ALPHA (inframe->info.finfo)) { + out_data[in_offsets[3]] = in_data[out_offsets[3]]; + } + in_data += in_pixel_stride; + out_data += out_pixel_stride; + } + in_data += in_row_wrap; + out_data += out_row_wrap; + } + } else if (lcms->lookup_method == GST_LCMS_LOOKUP_METHOD_CACHED) { + guint32 color, new_color; + GST_LOG_OBJECT (lcms, "GST_LCMS_LOOKUP_METHOD_CACHED"); + for (i = 0; i < height; i++) { + for (j = 0; j < width; j++) { + if (GST_VIDEO_FORMAT_INFO_HAS_ALPHA (inframe->info.finfo)) + alpha = in_data[in_offsets[3]]; + color = + in_data[in_offsets[0]] | + in_data[in_offsets[1]] << 0x08 | in_data[in_offsets[2]] << 0x10; + new_color = lcms->color_lut[color]; + if (new_color == 0xAAAAAAAA) { + cmsDoTransform (lcms->cms_transform, in_data, out_data, 1); + new_color = + out_data[out_offsets[0]] | + out_data[out_offsets[1]] << 0x08 | + out_data[out_offsets[2]] << 0x10; + GST_OBJECT_LOCK (lcms); + lcms->color_lut[color] = new_color; + GST_OBJECT_UNLOCK (lcms); + GST_TRACE_OBJECT (lcms, "cached color 0x%08X -> 0x%08X", color, + new_color); + } else { + out_data[out_offsets[0]] = (new_color & 0x0000FF) >> 0x00; + out_data[out_offsets[1]] = (new_color & 0x00FF00) >> 0x08; + out_data[out_offsets[2]] = (new_color & 0xFF0000) >> 0x10; + } + if (alpha) { + out_data[in_offsets[3]] = alpha; + } + in_data += in_pixel_stride; + out_data += out_pixel_stride; + } + in_data += in_row_wrap; + out_data += out_row_wrap; + } + } +} diff --git a/ext/colormanagement/gstlcms.h b/ext/colormanagement/gstlcms.h new file mode 100644 index 0000000000..70710ec9cb --- /dev/null +++ b/ext/colormanagement/gstlcms.h @@ -0,0 +1,102 @@ +/* + * GStreamer gstreamer-lcms + * Copyright (C) 2016 Andreas Frisch + * + * gstlcms.h + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __GST_LCMS_H__ +#define __GST_LCMS_H__ + +#include +#include +#include + +#include + +G_BEGIN_DECLS + +typedef enum +{ + GST_LCMS_INTENT_PERCEPTUAL = 0, + GST_LCMS_INTENT_RELATIVE_COLORIMETRIC, + GST_LCMS_INTENT_SATURATION, + GST_LCMS_INTENT_ABSOLUTE_COLORIMETRIC, +} GstLcmsIntent; + +#define GST_TYPE_LCMS_INTENT (gst_lcms_intent_get_type ()) + +typedef enum +{ + GST_LCMS_LOOKUP_METHOD_UNCACHED = 0, + GST_LCMS_LOOKUP_METHOD_PRECALCULATED, + GST_LCMS_LOOKUP_METHOD_CACHED, + GST_LCMS_LOOKUP_METHOD_FILE, +} GstLcmsLookupMethod; + +#define GST_TYPE_LCMS_LOOKUP_METHOD (gst_lcms_lookup_method_get_type ()) + +#define GST_TYPE_LCMS (gst_lcms_get_type()) +#define GST_LCMS(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_LCMS,GstLcms)) +#define GST_LCMS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_LCMS,GstLcmsClass)) +#define GST_LCMS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GST_TYPE_LCMS,GstLcmsClass)) +#define GST_IS_LCMS(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_LCMS)) +#define GST_IS_LCMS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_LCMS)) + +typedef struct _GstLcms GstLcms; +typedef struct _GstLcmsClass GstLcmsClass; + +/** + * GstLcms: + * + * Opaque data structure. + */ +struct _GstLcms +{ + GstVideoFilter videofilter; + + /* < private > */ + gboolean embeddedprofiles; + GstLcmsIntent intent; + GstLcmsLookupMethod lookup_method; + + cmsHPROFILE cms_inp_profile, cms_dst_profile; + cmsHTRANSFORM cms_transform; + cmsUInt32Number cms_inp_format, cms_dst_format; + + gchar *inp_profile_filename; + gchar *dst_profile_filename; + + guint32 *color_lut; + + gboolean preserve_black; + + void (*process) (GstLcms * lcms, GstVideoFrame * inframe, + GstVideoFrame * outframe); +}; + +struct _GstLcmsClass +{ + GstVideoFilterClass parent_class; +}; + +G_GNUC_INTERNAL GType gst_lcms_get_type (void); +G_GNUC_INTERNAL GType gst_lcms_intent_get_type (void); + +G_END_DECLS +#endif /* __GST_LCMS_H__ */