/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2013 Richard Hughes * * Licensed under the GNU Lesser General Public License Version 2.1 * * This library 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 2.1 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * SECTION:cd-icc * @short_description: An object to read and write a binary ICC profile */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "cd-context-lcms.h" #include "cd-icc.h" static void cd_icc_class_init (CdIccClass *klass); static void cd_icc_init (CdIcc *icc); static gboolean cd_icc_load_named_colors (CdIcc *icc, GError **error); static void cd_icc_finalize (GObject *object); #define GET_PRIVATE(o) (cd_icc_get_instance_private (o)) typedef enum { CD_MLUC_DESCRIPTION, CD_MLUC_COPYRIGHT, CD_MLUC_MANUFACTURER, CD_MLUC_MODEL, CD_MLUC_LAST } CdIccMluc; /** * CdIccPrivate: * * Private #CdIcc data **/ typedef struct { CdColorspace colorspace; CdProfileKind kind; cmsContext context_lcms; cmsHPROFILE lcms_profile; gboolean can_delete; gchar *checksum; gchar *filename; gchar *characterization_data; gdouble version; GHashTable *mluc_data[CD_MLUC_LAST]; /* key is 'en_GB' or '' for default */ GHashTable *metadata; gint64 creation_time; guint32 size; GPtrArray *named_colors; guint temperature; CdColorXYZ white; CdColorXYZ red; CdColorXYZ green; CdColorXYZ blue; } CdIccPrivate; G_DEFINE_TYPE_WITH_PRIVATE (CdIcc, cd_icc, G_TYPE_OBJECT) enum { PROP_0, PROP_SIZE, PROP_FILENAME, PROP_VERSION, PROP_KIND, PROP_COLORSPACE, PROP_CAN_DELETE, PROP_CHECKSUM, PROP_RED, PROP_GREEN, PROP_BLUE, PROP_WHITE, PROP_TEMPERATURE, PROP_LAST }; /** * cd_icc_error_quark: * * Return value: An error quark. * * Since: 0.1.32 **/ GQuark cd_icc_error_quark (void) { static GQuark quark = 0; if (!quark) quark = g_quark_from_static_string ("cd_icc_error"); return quark; } /** * cd_icc_fix_utf8_string: * * NC entries are supposed to be 7-bit ASCII, although some profile vendors * try to be clever which breaks handling them as UTF-8. **/ static gboolean cd_icc_fix_utf8_string (GString *string) { guint i; guchar tmp; /* replace clever characters */ for (i = 0; i < string->len; i++) { tmp = (guchar) string->str[i]; /* (R) */ if (tmp == 0xae) { string->str[i] = 0xc2; g_string_insert_c (string, i + 1, tmp); i += 1; } /* unknown */ if (tmp == 0x86) g_string_erase (string, i, 1); } /* check if we repaired it okay */ return g_utf8_validate (string->str, string->len, NULL); } static void cd_icc_uint32_to_str (guint32 id, gchar *str) { /* this is a hack */ memcpy (str, &id, 4); str[4] = '\0'; } static gpointer cd_icc_read_tag (CdIcc *icc, cmsTagSignature sig, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); gchar sig_string[5]; gpointer tmp; /* ensure context error is not present to aid debugging */ cd_context_lcms_error_clear (priv->context_lcms); /* read raw value */ tmp = cmsReadTag (priv->lcms_profile, sig); if (tmp != NULL) return tmp; /* any context error? */ if (!cd_context_lcms_error_check (priv->context_lcms, error)) return NULL; /* missing value */ cd_icc_uint32_to_str (GINT32_FROM_BE (sig), sig_string); g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA, "No data for tag %s [0x%04x]", sig_string, sig); return NULL; } static gboolean cd_icc_write_tag (CdIcc *icc, cmsTagSignature sig, gpointer data, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); gchar sig_string[5]; /* ensure context error is not present to aid debugging */ cd_context_lcms_error_clear (priv->context_lcms); /* write raw value */ if (cmsWriteTag (priv->lcms_profile, sig, data)) return TRUE; /* due to a bug in lcms2, writing with data==NULL returns FALSE * with no conext error set */ if (data == NULL) return TRUE; /* any context error? */ if (!cd_context_lcms_error_check (priv->context_lcms, error)) return FALSE; /* missing value */ cd_icc_uint32_to_str (GINT32_FROM_BE (sig), sig_string); g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA, "Failed to write tag %s [0x%04x]", sig_string, sig); return FALSE; } static gchar * cd_icc_get_precooked_md5 (cmsHPROFILE lcms_profile) { cmsUInt8Number icc_id[16]; gboolean md5_precooked = FALSE; gchar *md5 = NULL; guint i; /* check to see if we have a pre-cooked MD5 */ cmsGetHeaderProfileID (lcms_profile, icc_id); for (i = 0; i < 16; i++) { if (icc_id[i] != 0) { md5_precooked = TRUE; break; } } if (md5_precooked == FALSE) return NULL; /* convert to a hex string */ md5 = g_new0 (gchar, 32 + 1); for (i = 0; i < 16; i++) g_snprintf (md5 + i * 2, 3, "%02x", icc_id[i]); return md5; } /** * cd_icc_to_string: * @icc: a #CdIcc instance. * * Returns a string representation of the ICC profile. * * Return value: an allocated string * * Since: 0.1.32 **/ gchar * cd_icc_to_string (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsInt32Number tag_size; cmsTagSignature sig; cmsTagSignature sig_link; cmsTagTypeSignature tag_type; gboolean ret; gchar tag_str[5] = " "; GDateTime *created; GError *error_local = NULL; GString *str; guint32 i; guint32 number_tags; guint32 tmp; guint64 header_flags; g_autofree gchar *profile_id = NULL; g_return_val_if_fail (CD_IS_ICC (icc), NULL); /* setup error handler */ /* print header */ str = g_string_new ("icc:\nHeader:\n"); /* print size */ tmp = cd_icc_get_size (icc); if (tmp > 0) g_string_append_printf (str, " Size\t\t= %" G_GUINT32_FORMAT " bytes\n", tmp); /* version */ g_string_append_printf (str, " Version\t= %.1f\n", cd_icc_get_version (icc)); /* device class */ g_string_append_printf (str, " Profile Kind\t= %s\n", cd_profile_kind_to_string (cd_icc_get_kind (icc))); /* colorspace */ g_string_append_printf (str, " Colorspace\t= %s\n", cd_colorspace_to_string (cd_icc_get_colorspace (icc))); /* PCS */ g_string_append (str, " Conn. Space\t= "); switch (cmsGetPCS (priv->lcms_profile)) { case cmsSigXYZData: g_string_append (str, "xyz\n"); break; case cmsSigLabData: g_string_append (str, "lab\n"); break; default: g_string_append (str, "unknown\n"); break; } /* date and time */ created = cd_icc_get_created (icc); if (created != NULL) { g_autofree gchar *created_str = NULL; created_str = g_date_time_format (created, "%F, %T"); g_string_append_printf (str, " Date, Time\t= %s\n", created_str); g_date_time_unref (created); } /* profile use flags */ g_string_append (str, " Flags\t\t= "); tmp = cmsGetHeaderFlags (priv->lcms_profile); g_string_append (str, (tmp & cmsEmbeddedProfileTrue) > 0 ? "Embedded profile" : "Not embedded profile"); g_string_append (str, ", "); g_string_append (str, (tmp & cmsUseWithEmbeddedDataOnly) > 0 ? "Use with embedded data only" : "Use anywhere"); g_string_append (str, "\n"); /* header attributes */ cmsGetHeaderAttributes (priv->lcms_profile, &header_flags); g_string_append (str, " Dev. Attrbts\t= "); g_string_append (str, (header_flags & cmsTransparency) > 0 ? "transparency" : "reflective"); g_string_append (str, ", "); g_string_append (str, (header_flags & cmsMatte) > 0 ? "matte" : "glossy"); g_string_append (str, "\n"); /* rendering intent */ g_string_append (str, " Rndrng Intnt\t= "); switch (cmsGetHeaderRenderingIntent (priv->lcms_profile)) { case INTENT_PERCEPTUAL: g_string_append (str, "perceptual\n"); break; case INTENT_RELATIVE_COLORIMETRIC: g_string_append (str, "relative-colorimetric\n"); break; case INTENT_SATURATION: g_string_append (str, "saturation\n"); break; case INTENT_ABSOLUTE_COLORIMETRIC: g_string_append (str, "absolute-colorimetric\n"); break; default: g_string_append (str, "unknown\n"); break; } /* creator */ tmp = cmsGetHeaderCreator (priv->lcms_profile); cd_icc_uint32_to_str (GUINT32_FROM_BE (tmp), tag_str); g_string_append_printf (str, " Creator\t= %s\n", tag_str); /* profile ID */ profile_id = cd_icc_get_precooked_md5 (priv->lcms_profile); g_string_append_printf (str, " Profile ID\t= %s", profile_id); /* print tags */ g_string_append (str, "\n"); number_tags = cmsGetTagCount (priv->lcms_profile); for (i = 0; i < number_tags; i++) { sig = cmsGetTagSignature (priv->lcms_profile, i); /* convert to text */ cd_icc_uint32_to_str (GUINT32_FROM_BE (sig), tag_str); /* print header */ g_string_append_printf (str, "tag %02u:\n", (guint) i); g_string_append_printf (str, " sig\t'%s' [0x%x]\n", tag_str, (guint) sig); /* is this linked to another data area? */ sig_link = cmsTagLinkedTo (priv->lcms_profile, sig); if (sig_link != 0) { cd_icc_uint32_to_str (GUINT32_FROM_BE (sig_link), tag_str); g_string_append_printf (str, " link\t'%s' [0x%x]\n", tag_str, sig_link); continue; } /* get the tag type */ tag_size = cmsReadRawTag (priv->lcms_profile, sig, NULL, 0); if (tag_size == 0 || tag_size > 16 * 1024 * 1024) { g_string_append_printf (str, "WARNING: Tag size impossible %i", tag_size); continue; } g_string_append_printf (str, " size\t%i\n", tag_size); cmsReadRawTag (priv->lcms_profile, sig, &tmp, 4); cd_icc_uint32_to_str (tmp, tag_str); tag_type = GUINT32_FROM_BE (tmp); g_string_append_printf (str, " type\t'%s' [0x%x]\n", tag_str, tag_type); /* print tag details */ switch (tag_type) { case cmsSigTextType: case cmsSigTextDescriptionType: case cmsSigMultiLocalizedUnicodeType: { cmsMLU *mlu; guint32 j; guint32 mlu_size; g_string_append_printf (str, "Text:\n"); mlu = cd_icc_read_tag (icc, sig, &error_local); if (mlu == NULL) { g_string_append_printf (str, "WARNING: %s", error_local->message); g_clear_error (&error_local); break; } mlu_size = cmsMLUtranslationsCount (mlu); if (mlu_size == 0) g_string_append_printf (str, " Info:\t\tMLU empty!\n"); for (j = 0; j < mlu_size; j++) { gchar country_code[3] = "\0\0\0"; gchar language_code[3] = "\0\0\0"; guint32 text_size; g_autofree gchar *text_buffer = NULL; g_autofree gunichar *wtext = NULL; g_autoptr(GError) error = NULL; ret = cmsMLUtranslationsCodes (mlu, j, language_code, country_code); if (!ret) continue; text_size = cmsMLUgetWide (mlu, language_code, country_code, NULL, 0); if (text_size == 0) continue; wtext = g_malloc (text_size); cmsMLUgetWide (mlu, language_code, country_code, (wchar_t *) wtext, text_size); text_buffer = g_ucs4_to_utf8 ((gunichar *) wtext, -1, NULL, NULL, &error); if (text_buffer == NULL) { g_string_append_printf (str, " %s_%s:\tInvalid: '%s'\n", language_code[0] != '\0' ? language_code : "en", country_code[0] != '\0' ? country_code : "US", error->message); continue; } g_string_append_printf (str, " %s_%s:\t%s [%" G_GUINT32_FORMAT " bytes]\n", language_code[0] != '\0' ? language_code : "**", country_code[0] != '\0' ? country_code : "**", text_buffer, text_size); } break; } case cmsSigXYZType: { cmsCIEXYZ *xyz; xyz = cd_icc_read_tag (icc, sig, &error_local); if (xyz == NULL) { g_string_append_printf (str, "WARNING: %s", error_local->message); g_clear_error (&error_local); break; } g_string_append_printf (str, "XYZ:\n"); g_string_append_printf (str, " X:%f Y:%f Z:%f\n", xyz->X, xyz->Y, xyz->Z); break; } case cmsSigCurveType: { cmsToneCurve *curve; gdouble estimated_gamma; g_string_append_printf (str, "Curve:\n"); curve = cd_icc_read_tag (icc, sig, &error_local); if (curve == NULL) { g_string_append_printf (str, "WARNING: %s", error_local->message); g_clear_error (&error_local); break; } estimated_gamma = cmsEstimateGamma (curve, 0.01); if (estimated_gamma > 0) { g_string_append_printf (str, " Curve is gamma of %f\n", estimated_gamma); } break; } case cmsSigViewingConditionsType: { cmsICCViewingConditions *v; v = cmsReadTag(priv->lcms_profile, sig); if (v == NULL) { g_warning ("cannot read view tag"); continue; } g_string_append_printf (str, "ViewingCondition:\n"); g_string_append (str, " Illuminant Type: "); switch (v->IlluminantType) { case cmsILLUMINANT_TYPE_D50: g_string_append (str, "D50\n"); break; case cmsILLUMINANT_TYPE_D65: g_string_append (str, "D65\n"); break; case cmsILLUMINANT_TYPE_D93: g_string_append (str, "D93\n"); break; case cmsILLUMINANT_TYPE_F2: g_string_append (str, "F2\n"); break; case cmsILLUMINANT_TYPE_D55: g_string_append (str, "D55\n"); break; case cmsILLUMINANT_TYPE_A: g_string_append (str, "A\n"); break; case cmsILLUMINANT_TYPE_E: g_string_append (str, "E\n"); break; case cmsILLUMINANT_TYPE_F8: g_string_append (str, "F8\n"); break; default: g_string_append (str, "Unknown\n"); } g_string_append_printf (str, " Illuminant: X:%f Y:%f Z:%f\n", v->IlluminantXYZ.X, v->IlluminantXYZ.Y, v->IlluminantXYZ.Z); g_string_append_printf (str, " Surround: X:%f Y:%f Z:%f\n", v->SurroundXYZ.X, v->SurroundXYZ.Y, v->SurroundXYZ.Z); break; } case cmsSigDictType: { cmsHANDLE dict; const cmsDICTentry *entry; g_string_append_printf (str, "Dictionary:\n"); dict = cd_icc_read_tag (icc, sig, &error_local); if (dict == NULL) { g_string_append_printf (str, "WARNING: %s", error_local->message); g_clear_error (&error_local); break; } for (entry = cmsDictGetEntryList (dict); entry != NULL; entry = cmsDictNextEntry (entry)) { g_autofree gchar *ascii_name = NULL; g_autofree gchar *ascii_value = NULL; /* convert from wchar_t to UTF-8 */ ascii_name = g_ucs4_to_utf8 ((gunichar *) entry->Name, -1, NULL, NULL, NULL); ascii_value = g_ucs4_to_utf8 ((gunichar *) entry->Value, -1, NULL, NULL, NULL); g_string_append_printf (str, " %s\t->\t%s\n", ascii_name != NULL ? ascii_name : "Invalid UCS4", ascii_value != NULL ? ascii_value : "Invalid UCS4"); } break; } case cmsSigVcgtType: { const cmsToneCurve **vcgt; g_string_append (str, "VideoCardGammaTable:\n"); vcgt = cd_icc_read_tag (icc, sig, &error_local); if (vcgt == NULL) { g_string_append_printf (str, "WARNING: %s", error_local->message); g_clear_error (&error_local); break; } g_string_append_printf (str, " channels\t = %i\n", 3); g_string_append_printf (str, " entries\t = %" G_GUINT32_FORMAT "\n", cmsGetToneCurveEstimatedTableEntries (vcgt[0])); break; } case cmsSigNamedColor2Type: { CdColorLab lab; cmsNAMEDCOLORLIST *nc2; cmsUInt16Number pcs[3]; gchar name[cmsMAX_PATH]; gchar prefix[33]; gchar suffix[33]; guint j; g_string_append_printf (str, "Named colors:\n"); nc2 = cd_icc_read_tag (icc, sig, &error_local); if (nc2 == NULL) { g_string_append_printf (str, "WARNING: %s", error_local->message); g_clear_error (&error_local); break; } /* get the number of NCs */ tmp = cmsNamedColorCount (nc2); if (tmp == 0) { g_string_append_printf (str, " Info:\t\tNo NC's!\n"); continue; } for (j = 0; j < tmp; j++) { g_autoptr(GString) string = NULL; /* parse title */ string = g_string_new (""); ret = cmsNamedColorInfo (nc2, j, name, prefix, suffix, (cmsUInt16Number *)&pcs, NULL); if (!ret) { g_string_append_printf (str, " Info:\t\tFailed to get NC #%" G_GUINT32_FORMAT, j); continue; } if (prefix[0] != '\0') g_string_append_printf (string, "%s ", prefix); g_string_append (string, name); if (suffix[0] != '\0') g_string_append_printf (string, " %s", suffix); /* check is valid */ ret = g_utf8_validate (string->str, string->len, NULL); if (!ret) { g_string_append (str, " Info:\t\tInvalid 7 bit ASCII / UTF8\n"); ret = cd_icc_fix_utf8_string (string); if (!ret) { g_string_append (str, " Info:\t\tIFailed to fix: skipping entry\n"); continue; } } /* get color */ cmsLabEncoded2Float ((cmsCIELab *) &lab, pcs); g_string_append_printf (str, " %03u:\t %s\tL:%.2f a:%.3f b:%.3f\n", (guint) j, string->str, lab.L, lab.a, lab.b); } break; } default: break; } /* done! */ g_string_append_printf (str, "\n"); } /* remove trailing newline */ if (str->len > 0) g_string_truncate (str, str->len - 1); return g_string_free (str, FALSE); } /** * cd_icc_get_tags: * @icc: a #CdIcc instance. * @error: A #GError or %NULL * * Returns the internal tag table. Most users do not need to do this. * * Return value: (transfer full): the tag tables as an array of strings * * Since: 1.1.6 **/ gchar ** cd_icc_get_tags (CdIcc *icc, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); GPtrArray *tags; cmsTagSignature sig; gchar *tmp; guint32 i; guint32 number_tags; tags = g_ptr_array_new (); number_tags = cmsGetTagCount (priv->lcms_profile); for (i = 0; i < number_tags; i++) { sig = cmsGetTagSignature (priv->lcms_profile, i); tmp = g_new0 (gchar, 5); cd_icc_uint32_to_str (GUINT32_FROM_BE (sig), tmp); g_ptr_array_add (tags, tmp); } g_ptr_array_add (tags, NULL); return (gchar **) g_ptr_array_free (tags, FALSE); } static cmsTagSignature cd_icc_str_to_tag (const gchar *tag) { guint32 id; if (strlen (tag) != 4) return 0; memcpy (&id, tag, 4); return GUINT32_TO_BE (id); } /** * cd_icc_get_tag_data: * @icc: a #CdIcc instance. * @tag: a 4 bytes tag description, e.g. "cprt" or "vcgt" * @error: A #GError or %NULL * * Returns the raw data for the specific tag. * Most users do not need to do this. * * Return value: (transfer full): the data for the tag * * Since: 1.1.6 **/ GBytes * cd_icc_get_tag_data (CdIcc *icc, const gchar *tag, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsInt32Number tag_size; cmsTagSignature sig; gchar *tmp; /* read tag */ sig = cd_icc_str_to_tag (tag); if (sig == 0) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_PARSE, "Tag '%s' was not valid", tag); return NULL; } tag_size = cmsReadRawTag (priv->lcms_profile, sig, NULL, 0); if (tag_size == 0 || tag_size > 16 * 1024 * 1024) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA, "Tag size %i was not valid", tag_size); return NULL; } /* return data */ tmp = g_new0 (gchar, tag_size); cmsReadRawTag (priv->lcms_profile, sig, tmp, tag_size); return g_bytes_new_with_free_func (tmp, tag_size, g_free, tmp); } /** * cd_icc_set_tag_data: * @icc: a #CdIcc instance. * @tag: a 4 bytes tag description, e.g. "cprt" or "vcgt" * @data: a variable sized data entry * @error: A #GError or %NULL * * Sets the raw data for the specific tag. * Most users do not need to do this. * * Since: 1.1.6 **/ gboolean cd_icc_set_tag_data (CdIcc *icc, const gchar *tag, GBytes *data, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsTagSignature sig; gboolean ret; /* work around an LCMS API quirk in that you can't do cmsWriteRawTag() * if the tag already exists. Use the undocumented usage of * cmsWriteTag() to delete the tag first */ sig = cd_icc_str_to_tag (tag); if (sig == 0) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_PARSE, "Tag '%s' was not valid", tag); return FALSE; } cmsWriteTag (priv->lcms_profile, sig, NULL); ret = cmsWriteRawTag (priv->lcms_profile, sig, g_bytes_get_data (data, NULL), g_bytes_get_size (data)); if (!ret) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "Failed to write %" G_GSIZE_FORMAT " bytes", g_bytes_get_size (data)); } return ret; } /* map lcms profile class to colord type */ const struct { cmsProfileClassSignature lcms; CdProfileKind colord; } map_profile_kind[] = { { cmsSigInputClass, CD_PROFILE_KIND_INPUT_DEVICE }, { cmsSigDisplayClass, CD_PROFILE_KIND_DISPLAY_DEVICE }, { cmsSigOutputClass, CD_PROFILE_KIND_OUTPUT_DEVICE }, { cmsSigLinkClass, CD_PROFILE_KIND_DEVICELINK }, { cmsSigColorSpaceClass, CD_PROFILE_KIND_COLORSPACE_CONVERSION }, { cmsSigAbstractClass, CD_PROFILE_KIND_ABSTRACT }, { cmsSigNamedColorClass, CD_PROFILE_KIND_NAMED_COLOR }, { 0, CD_PROFILE_KIND_LAST } }; /* map lcms colorspace to colord type */ const struct { cmsColorSpaceSignature lcms; CdColorspace colord; } map_colorspace[] = { { cmsSigXYZData, CD_COLORSPACE_XYZ }, { cmsSigLabData, CD_COLORSPACE_LAB }, { cmsSigLuvData, CD_COLORSPACE_LUV }, { cmsSigYCbCrData, CD_COLORSPACE_YCBCR }, { cmsSigYxyData, CD_COLORSPACE_YXY }, { cmsSigRgbData, CD_COLORSPACE_RGB }, { cmsSigGrayData, CD_COLORSPACE_GRAY }, { cmsSigHsvData, CD_COLORSPACE_HSV }, { cmsSigCmykData, CD_COLORSPACE_CMYK }, { cmsSigCmyData, CD_COLORSPACE_CMY }, { 0, CD_COLORSPACE_LAST } }; static gboolean cd_icc_calc_whitepoint (CdIcc *icc, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsBool bpc[2] = { FALSE, FALSE }; cmsCIEXYZ whitepoint; cmsFloat64Number adaption[2] = { 0, 0 }; cmsHPROFILE profiles[2]; cmsHTRANSFORM transform; cmsUInt32Number intents[2] = { INTENT_ABSOLUTE_COLORIMETRIC, INTENT_ABSOLUTE_COLORIMETRIC }; gboolean ret = TRUE; gdouble temp_float; guint8 data[3] = { 255, 255, 255 }; /* do Lab to RGB transform to get primaries */ profiles[0] = priv->lcms_profile; profiles[1] = cmsCreateXYZProfileTHR (priv->context_lcms); transform = cmsCreateExtendedTransform (priv->context_lcms, 2, profiles, bpc, intents, adaption, NULL, 0, /* gamut ICC */ TYPE_RGB_8, TYPE_XYZ_DBL, cmsFLAGS_NOOPTIMIZE); if (transform == NULL) { ret = FALSE; g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_PARSE, "failed to setup RGB -> XYZ transform"); goto out; } /* run white through the transform */ cmsDoTransform (transform, data, &whitepoint, 1); cd_color_xyz_set (&priv->white, whitepoint.X, whitepoint.Y, whitepoint.Z); /* get temperature rounded to nearest 100K */ temp_float = cd_color_xyz_to_cct (&priv->white); if (temp_float > 0) priv->temperature = (((guint) temp_float) / 100) * 100; out: if (profiles[1] != NULL) cmsCloseProfile (profiles[1]); if (transform != NULL) cmsDeleteTransform (transform); return ret; } static gboolean cd_icc_load_characterization_data (CdIcc *icc, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsMLU *mlu; gboolean ret = TRUE; GError *error_local = NULL; guint32 text_size; /* this can only be non-localized text */ mlu = cd_icc_read_tag (icc, cmsSigCharTargetTag, &error_local); if (mlu == NULL) { /* no data is okay */ if (g_error_matches (error_local, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA)) { g_error_free (error_local); priv->characterization_data = NULL; goto out; } ret = FALSE; g_propagate_error (error, error_local); goto out; } text_size = cmsMLUgetASCII (mlu, cmsNoLanguage, cmsNoCountry, NULL, 0); priv->characterization_data = g_new0 (gchar, text_size + 1); cmsMLUgetASCII (mlu, cmsNoLanguage, cmsNoCountry, priv->characterization_data, text_size); out: return ret; } static gboolean cd_icc_load_primaries (CdIcc *icc, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsCIEXYZ *cie_xyz; cmsHPROFILE xyz_profile = NULL; cmsHTRANSFORM transform = NULL; gboolean ret = TRUE; gdouble rgb_values[3]; /* get white point */ ret = cd_icc_calc_whitepoint (icc, error); if (!ret) goto out; /* get the illuminants from the primaries */ cie_xyz = cd_icc_read_tag (icc, cmsSigRedMatrixColumnTag, NULL); if (cie_xyz != NULL) { cd_color_xyz_copy ((CdColorXYZ *) cie_xyz, &priv->red); cie_xyz = cd_icc_read_tag (icc, cmsSigGreenMatrixColumnTag, error); if (cie_xyz == NULL) { ret = FALSE; goto out; } cd_color_xyz_copy ((CdColorXYZ *) cie_xyz, &priv->green); cie_xyz = cd_icc_read_tag (icc, cmsSigBlueMatrixColumnTag, error); if (cie_xyz == NULL) { ret = FALSE; goto out; } cd_color_xyz_copy ((CdColorXYZ *) cie_xyz, &priv->blue); goto out; } /* get the illuminants by running it through the profile */ xyz_profile = cmsCreateXYZProfileTHR (priv->context_lcms); transform = cmsCreateTransformTHR (priv->context_lcms, priv->lcms_profile, TYPE_RGB_DBL, xyz_profile, TYPE_XYZ_DBL, INTENT_PERCEPTUAL, 0); if (transform == NULL) { ret = FALSE; g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_PARSE, "failed to setup RGB -> XYZ transform"); goto out; } /* red */ rgb_values[0] = 1.0; rgb_values[1] = 0.0; rgb_values[2] = 0.0; cmsDoTransform (transform, rgb_values, &priv->red, 1); /* green */ rgb_values[0] = 0.0; rgb_values[1] = 1.0; rgb_values[2] = 0.0; cmsDoTransform (transform, rgb_values, &priv->green, 1); /* blue */ rgb_values[0] = 0.0; rgb_values[1] = 0.0; rgb_values[2] = 1.0; cmsDoTransform (transform, rgb_values, &priv->blue, 1); out: if (transform != NULL) cmsDeleteTransform (transform); if (xyz_profile != NULL) cmsCloseProfile (xyz_profile); return ret; } static gboolean cd_icc_load_metadata_item (CdIcc *icc, const gunichar *name, const gunichar *value, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); g_autoptr(GError) error_local = NULL; g_autofree gchar *ascii_name = NULL; g_autofree gchar *ascii_value = NULL; /* parse name */ ascii_name = g_ucs4_to_utf8 (name, -1, NULL, NULL, &error_local); if (ascii_name == NULL) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_CORRUPTION_DETECTED, "Could not convert name in dict: %s", error_local->message); return FALSE; } /* parse value */ ascii_value = g_ucs4_to_utf8 (value, -1, NULL, NULL, &error_local); if (ascii_value == NULL) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_CORRUPTION_DETECTED, "Could not convert value in dict: %s", error_local->message); return FALSE; } /* all okay */ g_hash_table_insert (priv->metadata, g_strdup (ascii_name), g_strdup (ascii_value)); return TRUE; } static gboolean cd_icc_load_metadata (CdIcc *icc, GError **error) { cmsHANDLE dict; const cmsDICTentry *entry; GError *error_local = NULL; /* get dictionary metadata */ dict = cd_icc_read_tag (icc, cmsSigMetaTag, &error_local); if (dict == NULL) { /* no data is okay */ if (g_error_matches (error_local, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA)) { g_error_free (error_local); return TRUE; } g_propagate_error (error, error_local); return FALSE; } for (entry = cmsDictGetEntryList (dict); entry != NULL; entry = cmsDictNextEntry (entry)) { if (!cd_icc_load_metadata_item (icc, (const gunichar *) entry->Name, (const gunichar *) entry->Value, error)) return FALSE; } return TRUE; } static gboolean cd_icc_load (CdIcc *icc, CdIccLoadFlags flags, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsColorSpaceSignature colorspace; cmsProfileClassSignature profile_class; guint i; /* get version */ priv->version = cmsGetProfileVersion (priv->lcms_profile); /* convert profile kind */ profile_class = cmsGetDeviceClass (priv->lcms_profile); for (i = 0; map_profile_kind[i].colord != CD_PROFILE_KIND_LAST; i++) { if (map_profile_kind[i].lcms == profile_class) { priv->kind = map_profile_kind[i].colord; break; } } /* convert colorspace */ colorspace = cmsGetColorSpace (priv->lcms_profile); for (i = 0; map_colorspace[i].colord != CD_COLORSPACE_LAST; i++) { if (map_colorspace[i].lcms == colorspace) { priv->colorspace = map_colorspace[i].colord; break; } } /* read optional metadata? */ if ((flags & CD_ICC_LOAD_FLAGS_METADATA) > 0) { if (!cd_icc_load_metadata (icc, error)) return FALSE; } /* get precooked profile ID if one exists */ priv->checksum = cd_icc_get_precooked_md5 (priv->lcms_profile); /* read default translations */ cd_icc_get_description (icc, NULL, NULL); cd_icc_get_copyright (icc, NULL, NULL); cd_icc_get_manufacturer (icc, NULL, NULL); cd_icc_get_model (icc, NULL, NULL); if ((flags & CD_ICC_LOAD_FLAGS_TRANSLATIONS) > 0) { /* FIXME: get the locale list from LCMS */ } /* read named colors if the client cares */ if ((flags & CD_ICC_LOAD_FLAGS_NAMED_COLORS) > 0) { if (!cd_icc_load_named_colors (icc, error)) return FALSE; } /* read primaries if the client cares */ if ((flags & CD_ICC_LOAD_FLAGS_PRIMARIES) > 0 && priv->colorspace == CD_COLORSPACE_RGB) { if (!cd_icc_load_primaries (icc, error)) return FALSE; } /* read characterization data if the client cares */ if ((flags & CD_ICC_LOAD_FLAGS_CHARACTERIZATION) > 0) { if (!cd_icc_load_characterization_data (icc, error)) return FALSE; } return TRUE; } /** * cd_icc_load_data: * @icc: a #CdIcc instance. * @data: binary data * @data_len: Length of @data * @flags: a set of #CdIccLoadFlags * @error: A #GError or %NULL * * Loads an ICC profile from raw byte data. * * Since: 0.1.32 **/ gboolean cd_icc_load_data (CdIcc *icc, const guint8 *data, gsize data_len, CdIccLoadFlags flags, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), FALSE); g_return_val_if_fail (data != NULL, FALSE); g_return_val_if_fail (priv->lcms_profile == NULL, FALSE); /* ensure we have the header */ if (data_len < 0x84) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_PARSE, "icc was not valid (file size too small)"); return FALSE; } /* load icc into lcms */ priv->lcms_profile = cmsOpenProfileFromMemTHR (priv->context_lcms, data, data_len); if (priv->lcms_profile == NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_PARSE, "failed to load: not an ICC icc"); return FALSE; } /* save length to avoid trusting the profile */ priv->size = data_len; /* load cached data */ if (!cd_icc_load (icc, flags, error)) return FALSE; /* calculate the data MD5 if there was no embedded profile */ if (priv->checksum == NULL && (flags & CD_ICC_LOAD_FLAGS_FALLBACK_MD5) > 0) { priv->checksum = g_compute_checksum_for_data (G_CHECKSUM_MD5, (const guchar *) data, data_len); } return TRUE; } static gboolean cd_util_write_dict_entry (cmsHANDLE dict, const gchar *key, const gchar *value, GError **error) { gboolean ret = FALSE; g_autofree gunichar *mb_key = NULL; g_autofree gunichar *mb_value = NULL; mb_key = g_utf8_to_ucs4 (key, -1, NULL, NULL, error); if (mb_key == NULL) return FALSE; mb_value = g_utf8_to_ucs4 (value, -1, NULL, NULL, error); if (mb_value == NULL) return FALSE; ret = cmsDictAddEntry (dict, (const wchar_t *) mb_key, (const wchar_t *) mb_value, NULL, NULL); if (!ret) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "Failed to write dict entry"); return FALSE; } return TRUE; } typedef struct { gchar *language_code; /* will always be xx\0 */ gchar *country_code; /* will always be xx\0 */ gunichar *wtext; } CdMluObject; static void cd_util_mlu_object_free (gpointer data) { CdMluObject *obj = (CdMluObject *) data; g_free (obj->language_code); g_free (obj->country_code); g_free (obj->wtext); g_free (obj); } static CdMluObject * cd_util_mlu_object_parse (const gchar *locale, const gchar *utf8_text, GError **error) { CdMluObject *obj = NULL; guint type; gunichar *wtext; g_autofree gchar *key = NULL; g_auto(GStrv) split = NULL; /* untranslated version */ if (locale == NULL || locale[0] == '\0') { wtext = g_utf8_to_ucs4 (utf8_text, -1, NULL, NULL, error); if (wtext == NULL) return NULL; obj = g_new0 (CdMluObject, 1); obj->wtext = wtext; return obj; } /* ignore ##@latin */ if (g_strstr_len (locale, -1, "@") != NULL) { g_set_error (error, CD_ICC_ERROR, CD_PROFILE_ERROR_FAILED_TO_PARSE, "ignoring @latin"); return NULL; } key = g_strdup (locale); g_strdelimit (key, ".", '\0'); split = g_strsplit (key, "_", -1); if (strlen (split[0]) != 2) { g_set_error (error, CD_ICC_ERROR, CD_PROFILE_ERROR_FAILED_TO_PARSE, "ignoring invalid country code: %s", split[0]); return NULL; } type = g_strv_length (split); if (type > 2) { g_set_error (error, CD_ICC_ERROR, CD_PROFILE_ERROR_FAILED_TO_PARSE, "ignoring invalid locale: %s", key); return NULL; } /* convert to wchars */ wtext = g_utf8_to_ucs4 (utf8_text, -1, NULL, NULL, error); if (wtext == NULL) { g_set_error (error, CD_ICC_ERROR, CD_PROFILE_ERROR_FAILED_TO_PARSE, "ignoring invalid UTF-8 text: %s", utf8_text); return NULL; } /* lv */ if (type == 1) { obj = g_new0 (CdMluObject, 1); obj->language_code = g_strdup (split[0]); obj->wtext = wtext; return obj; } /* en_GB */ if (strlen (split[1]) != 2) { g_set_error (error, CD_ICC_ERROR, CD_PROFILE_ERROR_FAILED_TO_PARSE, "ignoring invalid locale: %s", split[1]); return NULL; } obj = g_new0 (CdMluObject, 1); obj->language_code = g_strdup (split[0]); obj->country_code = g_strdup (split[1]); obj->wtext = wtext; return obj; } static gboolean cd_util_write_tag_ascii (CdIcc *icc, cmsTagSignature sig, const gchar *value, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsMLU *mlu = NULL; gboolean ret = TRUE; /* nothing set */ if (value == NULL) { ret = cd_icc_write_tag (icc, sig, NULL, error); goto out; } /* set value */ mlu = cmsMLUalloc (priv->context_lcms, 1); ret = cmsMLUsetASCII (mlu, "en", "US", value); if (!ret) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "cannot write MLU text"); goto out; } /* write tag */ ret = cd_icc_write_tag (icc, sig, mlu, error); if (!ret) goto out; out: if (mlu != NULL) cmsMLUfree (mlu); return ret; } static gboolean cd_util_write_tag_ascii_default (CdIcc *icc, cmsTagSignature sig, GHashTable *hash, GError **error) { const gchar *value; /* get default value */ value = g_hash_table_lookup (hash, ""); return cd_util_write_tag_ascii (icc, sig, value, error); } static gint cd_util_sort_mlu_array_cb (gconstpointer a, gconstpointer b) { CdMluObject *sa = *((CdMluObject **) a); CdMluObject *sb = *((CdMluObject **) b); return g_strcmp0 (sa->language_code, sb->language_code); } static gboolean cd_util_write_tag_localized (CdIcc *icc, cmsTagSignature sig, GHashTable *hash, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); CdMluObject *obj; GError *error_local = NULL; GList *l; cmsMLU *mlu = NULL; const gchar *locale; const gchar *value; gboolean ret = TRUE; guint i; g_autoptr(GList) keys = NULL; g_autoptr(GPtrArray) array = NULL; /* convert all the hash entries into CdMluObject's */ keys = g_hash_table_get_keys (hash); array = g_ptr_array_new_with_free_func (cd_util_mlu_object_free); for (l = keys; l != NULL; l = l->next) { locale = l->data; value = g_hash_table_lookup (hash, locale); if (value == NULL) continue; obj = cd_util_mlu_object_parse (locale, value, &error_local); if (obj == NULL) { g_warning ("failed to parse localized text %s[%s]: %s", value, locale, error_local->message); g_clear_error (&error_local); continue; } g_ptr_array_add (array, obj); } /* delete tag if there is no data */ if (array->len == 0) { ret = cd_icc_write_tag (icc, sig, NULL, error); goto out; } /* sort the data so we always write the default first */ g_ptr_array_sort (array, cd_util_sort_mlu_array_cb); /* create MLU object to hold all the translations */ mlu = cmsMLUalloc (priv->context_lcms, array->len); for (i = 0; i < array->len; i++) { obj = g_ptr_array_index (array, i); if (obj->language_code == NULL && obj->country_code == NULL) { /* the default translation is encoded as en_US rather * than NoLanguage_NoCountry as the latter means * 'the first entry' when reading */ ret = cmsMLUsetWide (mlu, "en", "US", (const wchar_t *) obj->wtext); } else { /* casting to wchar_t is okay as gunichar is 4 bytes * on Linux and OS-X, and colord is never going to * be compiled for Windows */ ret = cmsMLUsetWide (mlu, obj->language_code != NULL ? obj->language_code : cmsNoLanguage, obj->country_code != NULL ? obj->country_code : cmsNoCountry, (const wchar_t *) obj->wtext); } if (!ret) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "cannot write MLU text"); goto out; } } /* write tag */ ret = cd_icc_write_tag (icc, sig, mlu, error); if (!ret) goto out; out: if (mlu != NULL) cmsMLUfree (mlu); return ret; } static gboolean cd_icc_save_file_mkdir_parents (GFile *file, GError **error) { g_autoptr(GFile) parent_dir = NULL; /* get parent directory */ parent_dir = g_file_get_parent (file); if (parent_dir == NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_CREATE, "could not get parent dir"); return FALSE; } /* ensure destination does not already exist */ if (g_file_query_exists (parent_dir, NULL)) return TRUE; if (!g_file_make_directory_with_parents (parent_dir, NULL, error)) return FALSE; return TRUE; } static GBytes * cd_icc_serialize_profile (CdIcc *icc, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsUInt32Number length = 0; gboolean ret; g_autofree gchar *data_tmp = NULL; /* get size of profile */ ret = cmsSaveProfileToMem (priv->lcms_profile, NULL, &length); if (!ret) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "failed to dump ICC file"); return NULL; } /* sanity check to 16Mb */ if (length == 0 || length > 16 * 1024 * 1024) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "failed to save ICC file, requested %u " "bytes and limit is 16Mb", length); return NULL; } /* allocate and get profile data */ data_tmp = g_new0 (gchar, length); ret = cmsSaveProfileToMem (priv->lcms_profile, data_tmp, &length); if (!ret) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "failed to dump ICC file to memory"); return NULL; } /* success */ return g_bytes_new (data_tmp, length); } /** * cd_icc_save_data: * @icc: a #CdIcc instance. * @flags: a set of #CdIccSaveFlags * @error: A #GError or %NULL * * Saves an ICC profile to an allocated memory location. * * Return vale: A #GBytes structure, or %NULL for error * * Since: 1.0.2 **/ GBytes * cd_icc_save_data (CdIcc *icc, CdIccSaveFlags flags, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsHANDLE dict = NULL; const gchar *key; const gchar *value; gboolean ret = FALSE; GBytes *data = NULL; GList *l; guint i; g_autoptr(GList) md_keys = NULL; g_return_val_if_fail (CD_IS_ICC (icc), NULL); /* convert profile kind */ for (i = 0; map_profile_kind[i].colord != CD_PROFILE_KIND_LAST; i++) { if (map_profile_kind[i].colord == priv->kind) { cmsSetDeviceClass (priv->lcms_profile, map_profile_kind[i].lcms); break; } } /* convert colorspace */ for (i = 0; map_colorspace[i].colord != CD_COLORSPACE_LAST; i++) { if (map_colorspace[i].colord == priv->colorspace) { cmsSetColorSpace (priv->lcms_profile, map_colorspace[i].lcms); break; } } /* set version */ if (priv->version > 0.0) cmsSetProfileVersion (priv->lcms_profile, priv->version); /* save metadata */ if (g_hash_table_size (priv->metadata) != 0) { dict = cmsDictAlloc (priv->context_lcms); md_keys = g_hash_table_get_keys (priv->metadata); if (md_keys != NULL) { for (l = md_keys; l != NULL; l = l->next) { key = l->data; value = g_hash_table_lookup (priv->metadata, key); ret = cd_util_write_dict_entry (dict, key, value, error); if (!ret) goto out; } } ret = cd_icc_write_tag (icc, cmsSigMetaTag, dict, error); if (!ret) goto out; } else { ret = cd_icc_write_tag (icc, cmsSigMetaTag, NULL, error); if (!ret) goto out; } /* save characterization data */ if (priv->characterization_data != NULL) { ret = cd_util_write_tag_ascii (icc, cmsSigCharTargetTag, priv->characterization_data, error); if (!ret) goto out; } else { ret = cd_icc_write_tag (icc, cmsSigCharTargetTag, NULL, error); if (!ret) goto out; } /* save translations */ if (priv->version < 4.0) { /* v2 profiles cannot have a mluc type for cmsSigProfileDescriptionTag * so use the non-standard Apple extension cmsSigProfileDescriptionTagML * and only write a en_US version for the description */ ret = cd_util_write_tag_ascii_default (icc, cmsSigProfileDescriptionTag, priv->mluc_data[CD_MLUC_DESCRIPTION], error); if (!ret) goto out; ret = cd_util_write_tag_localized (icc, cmsSigProfileDescriptionMLTag, priv->mluc_data[CD_MLUC_DESCRIPTION], error); if (!ret) goto out; ret = cd_util_write_tag_ascii_default (icc, cmsSigCopyrightTag, priv->mluc_data[CD_MLUC_COPYRIGHT], error); if (!ret) goto out; ret = cd_util_write_tag_ascii_default (icc, cmsSigDeviceMfgDescTag, priv->mluc_data[CD_MLUC_MANUFACTURER], error); if (!ret) goto out; ret = cd_util_write_tag_ascii_default (icc, cmsSigDeviceModelDescTag, priv->mluc_data[CD_MLUC_MODEL], error); if (!ret) goto out; } else { /* v4 profiles can use mluc types for all fields */ ret = cd_util_write_tag_localized (icc, cmsSigProfileDescriptionTag, priv->mluc_data[CD_MLUC_DESCRIPTION], error); if (!ret) goto out; ret = cd_util_write_tag_localized (icc, cmsSigCopyrightTag, priv->mluc_data[CD_MLUC_COPYRIGHT], error); if (!ret) goto out; ret = cd_util_write_tag_localized (icc, cmsSigDeviceMfgDescTag, priv->mluc_data[CD_MLUC_MANUFACTURER], error); if (!ret) goto out; ret = cd_util_write_tag_localized (icc, cmsSigDeviceModelDescTag, priv->mluc_data[CD_MLUC_MODEL], error); if (!ret) goto out; } /* finally, optionally override the created date */ if (priv->creation_time != -1) { /* * LCMS2 maintainer rejects adding API to do this, * instead suggesting you munge the header directly: * https://github.com/mm2/Little-CMS/issues/71 */ struct tm creation_time; cmsICCHeader *header; g_autoptr(GByteArray) mutable_data = NULL; data = cd_icc_serialize_profile (icc, error); if (data == NULL) goto out; mutable_data = g_bytes_unref_to_array (data); data = NULL; if (!gmtime_r (&priv->creation_time, &creation_time)) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "failed to translate creation time: %s (%i)", g_strerror (errno), errno); goto out; } header = (cmsICCHeader*)mutable_data->data; _cmsEncodeDateTimeNumber (&header->date, &creation_time); /* we've changed the creation date, recalculate MD5 */ cmsCloseProfile (priv->lcms_profile); priv->lcms_profile = cmsOpenProfileFromMemTHR (priv->context_lcms, mutable_data->data, mutable_data->len); } /* write profile id */ ret = cmsMD5computeID (priv->lcms_profile); if (!ret) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "failed to compute profile id"); goto out; } data = cd_icc_serialize_profile (icc, error); out: if (dict != NULL) cmsDictFree (dict); return data; } /** * cd_icc_get_characterization_data: * @icc: a #CdIcc instance. * * Gets any characterization data used to build the profile. * This function will only return results if the profile was loaded with the * %CD_ICC_LOAD_FLAGS_CHARACTERIZATION flag. * * Return value: TI3 string data * * Since: 1.1.1 **/ const gchar * cd_icc_get_characterization_data (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return priv->characterization_data; } /** * cd_icc_set_characterization_data: * @icc: a #CdIcc instance. * @data: TI3 string data, or %NULL * * Sets the characterization data used to build the profile. * * Since: 1.1.1 **/ void cd_icc_set_characterization_data (CdIcc *icc, const gchar *data) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (CD_IS_ICC (icc)); g_free (priv->characterization_data); priv->characterization_data = g_strdup (data); } /** * cd_icc_save_file: * @icc: a #CdIcc instance. * @file: a #GFile * @flags: a set of #CdIccSaveFlags * @cancellable: A #GCancellable or %NULL * @error: A #GError or %NULL * * Saves an ICC profile to a local or remote file. * * Return vale: %TRUE for success. * * Since: 0.1.32 **/ gboolean cd_icc_save_file (CdIcc *icc, GFile *file, CdIccSaveFlags flags, GCancellable *cancellable, GError **error) { gboolean ret; g_autoptr(GBytes) data = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail (CD_IS_ICC (icc), FALSE); g_return_val_if_fail (G_IS_FILE (file), FALSE); /* get data */ data = cd_icc_save_data (icc, flags, error); if (data == NULL) return FALSE; /* ensure parent directories exist */ if (!cd_icc_save_file_mkdir_parents (file, error)) return FALSE; /* actually write file */ ret = g_file_replace_contents (file, g_bytes_get_data (data, NULL), g_bytes_get_size (data), NULL, FALSE, G_FILE_CREATE_NONE, NULL, cancellable, &error_local); if (!ret) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_SAVE, "failed to save ICC file: %s", error_local->message); return FALSE; } return TRUE; } /** * cd_icc_save_default: * @icc: a #CdIcc instance. * @flags: a set of #CdIccSaveFlags * @cancellable: A #GCancellable or %NULL * @error: A #GError or %NULL * * Saves an ICC profile to the default per-user location. * * Return vale: %TRUE for success. * * Since: 1.1.1 **/ gboolean cd_icc_save_default (CdIcc *icc, CdIccSaveFlags flags, GCancellable *cancellable, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); const gchar *root = "edid"; /* TODO: only for cd_icc_create_from_edid() */ g_autofree gchar *basename = NULL; g_autofree gchar *filename = NULL; g_autoptr(GFile) file = NULL; g_return_val_if_fail (CD_IS_ICC (icc), FALSE); /* build a per-user filename */ basename = g_strdup_printf ("%s-%s.icc", root, priv->checksum); filename = g_build_filename (g_get_user_data_dir (), "icc", basename, NULL); file = g_file_new_for_path (filename); return cd_icc_save_file (icc, file, flags, cancellable, error); } /** * cd_icc_set_filename: * @icc: a #CdIcc instance. * @filename: a filename, or %NULL * * Sets the filename, which may be required if the ICC profile has been loaded * using cd_icc_load_fd() from a disk cache. * * Since: 1.1.1 **/ void cd_icc_set_filename (CdIcc *icc, const gchar *filename) { CdIccPrivate *priv = GET_PRIVATE (icc); g_free (priv->filename); priv->filename = g_strdup (filename); } /** * cd_icc_load_file: * @icc: a #CdIcc instance. * @file: a #GFile * @flags: a set of #CdIccLoadFlags * @cancellable: A #GCancellable or %NULL * @error: A #GError or %NULL * * Loads an ICC profile from a local or remote file. * * Since: 0.1.32 **/ gboolean cd_icc_load_file (CdIcc *icc, GFile *file, CdIccLoadFlags flags, GCancellable *cancellable, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); gboolean ret = FALSE; gsize length; g_autoptr(GError) error_local = NULL; g_autofree gchar *data = NULL; g_autoptr(GFileInfo) info = NULL; g_return_val_if_fail (CD_IS_ICC (icc), FALSE); g_return_val_if_fail (G_IS_FILE (file), FALSE); /* load files */ ret = g_file_load_contents (file, cancellable, &data, &length, NULL, &error_local); if (!ret) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_OPEN, "failed to load file: %s", error_local->message); return FALSE; } /* parse the data */ ret = cd_icc_load_data (icc, (const guint8 *) data, length, flags, error); if (!ret) return FALSE; /* find out if the user could delete this profile */ info = g_file_query_info (file, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE, G_FILE_QUERY_INFO_NONE, cancellable, &error_local); if (info == NULL) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_OPEN, "failed to query file: %s", error_local->message); return FALSE; } priv->can_delete = g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE); /* save filename for later */ priv->filename = g_file_get_path (file); return TRUE; } /** * cd_icc_load_fd: * @icc: a #CdIcc instance. * @fd: a file descriptor * @flags: a set of #CdIccLoadFlags * @error: A #GError or %NULL * * Loads an ICC profile from an open file descriptor. * * Since: 0.1.32 **/ gboolean cd_icc_load_fd (CdIcc *icc, gint fd, CdIccLoadFlags flags, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); FILE *stream = NULL; g_return_val_if_fail (CD_IS_ICC (icc), FALSE); g_return_val_if_fail (fd > 0, FALSE); /* convert the file descriptor to a stream */ stream = fdopen (fd, "r"); if (stream == NULL) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_OPEN, "failed to open stream from fd %i", fd); return FALSE; } /* parse the ICC file */ priv->lcms_profile = cmsOpenProfileFromStreamTHR (priv->context_lcms, stream, "r"); if (priv->lcms_profile == NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_OPEN, "failed to open stream"); return FALSE; } /* load cached data */ return cd_icc_load (icc, flags, error); } /** * cd_icc_get_handle: * @icc: a #CdIcc instance. * * Return the cmsHPROFILE instance used locally. This may be required if you * are using the profile in a transform. * * Return value: (transfer none): Do not call cmsCloseProfile() on this value! **/ gpointer cd_icc_get_handle (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return priv->lcms_profile; } /** * cd_icc_get_context: * @icc: a #CdIcc instance. * * Return the cmsContext instance used locally. This may be required if you * are using native LCMS calls and then cd_icc_load_handle(). * * Return value: (transfer none): Do not call cmsDeleteContext() on this value! * * Since: 1.1.7 **/ gpointer cd_icc_get_context (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return priv->context_lcms; } /** * cd_icc_load_handle: * @icc: a #CdIcc instance. * @handle: a cmsHPROFILE instance * @flags: a set of #CdIccLoadFlags * @error: A #GError or %NULL * * Set the internal cmsHPROFILE instance. This may be required if you create * the profile using cmsCreateRGBProfileTHR() and then want to use the * functionality in #CdIcc. * * Do not call cmsCloseProfile() on @handle in the caller, this will be done * when the @icc object is finalized. Treat the profile like it's been adopted * by this module. * * To handle the internal error callback, you should use the thread-safe * creation function, e.g. cmsCreateNULLProfileTHR(). The @context_id should be * set as the value of cd_icc_get_context() for this object. * * Additionally, this function cannot be called more than once, and also can't * be called if cd_icc_load_file() has previously been used on the @icc object. * * Since: 0.1.33 **/ gboolean cd_icc_load_handle (CdIcc *icc, gpointer handle, CdIccLoadFlags flags, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsContext context; g_return_val_if_fail (CD_IS_ICC (icc), FALSE); g_return_val_if_fail (handle != NULL, FALSE); g_return_val_if_fail (priv->lcms_profile == NULL, FALSE); /* check the THR version has been correctly set up */ context = cmsGetProfileContextID (handle); if (context == NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_CREATE, "lcms2 threadsafe version (THR) not used, " "or context not set"); return FALSE; } /* load profile */ priv->lcms_profile = handle; return cd_icc_load (icc, flags, error); } /** * cd_icc_get_size: * * Gets the ICC profile file size * * Return value: The size in bytes, or 0 for unknown. * * Since: 0.1.32 **/ guint32 cd_icc_get_size (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), 0); return priv->size; } /** * cd_icc_get_filename: * @icc: A valid #CdIcc * * Gets the filename of the ICC data, if one exists. * * Return value: A filename, or %NULL * * Since: 0.1.32 **/ const gchar * cd_icc_get_filename (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return priv->filename; } /** * cd_icc_get_version: * @icc: a #CdIcc instance. * * Gets the ICC profile version, typically 2.1 or 4.2 * * Return value: A floating point version number, or 0.0 for unknown * * Since: 0.1.32 **/ gdouble cd_icc_get_version (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), 0.0f); return priv->version; } /** * cd_icc_set_version: * @icc: a #CdIcc instance. * @version: the profile version, e.g. 2.1 or 4.0 * * Sets the profile version. * * Since: 0.1.32 **/ void cd_icc_set_version (CdIcc *icc, gdouble version) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (CD_IS_ICC (icc)); priv->version = version; g_object_notify (G_OBJECT (icc), "version"); } /** * cd_icc_get_kind: * @icc: a #CdIcc instance. * * Gets the profile kind. * * Return value: The kind, e.g. %CD_PROFILE_KIND_INPUT * * Since: 0.1.32 **/ CdProfileKind cd_icc_get_kind (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), CD_PROFILE_KIND_UNKNOWN); return priv->kind; } /** * cd_icc_set_kind: * @icc: a #CdIcc instance. * @kind: the profile kind, e.g. %CD_PROFILE_KIND_DISPLAY_DEVICE * * Sets the profile kind. * * Since: 0.1.32 **/ void cd_icc_set_kind (CdIcc *icc, CdProfileKind kind) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (CD_IS_ICC (icc)); priv->kind = kind; g_object_notify (G_OBJECT (icc), "kind"); } /** * cd_icc_get_colorspace: * @icc: a #CdIcc instance. * * Gets the profile colorspace * * Return value: The profile colorspace, e.g. %CD_COLORSPACE_RGB * * Since: 0.1.32 **/ CdColorspace cd_icc_get_colorspace (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), CD_COLORSPACE_UNKNOWN); return priv->colorspace; } /** * cd_icc_set_colorspace: * @icc: a #CdIcc instance. * @colorspace: the profile colorspace, e.g. %CD_COLORSPACE_RGB * * Sets the colorspace kind. * * Since: 0.1.32 **/ void cd_icc_set_colorspace (CdIcc *icc, CdColorspace colorspace) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (CD_IS_ICC (icc)); priv->colorspace = colorspace; g_object_notify (G_OBJECT (icc), "colorspace"); } /** * cd_icc_get_metadata: * @icc: A valid #CdIcc * * Gets all the metadata from the ICC profile. * * Return value: (transfer container): The profile metadata * * Since: 0.1.32 **/ GHashTable * cd_icc_get_metadata (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return g_hash_table_ref (priv->metadata); } /** * cd_icc_get_metadata_item: * @icc: A valid #CdIcc * @key: the dictionary key * * Gets an item of data from the ICC metadata store. * * Return value: The dictionary data, or %NULL if the key does not exist. * * Since: 0.1.32 **/ const gchar * cd_icc_get_metadata_item (CdIcc *icc, const gchar *key) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); g_return_val_if_fail (key != NULL, NULL); return (const gchar *) g_hash_table_lookup (priv->metadata, key); } /** * cd_icc_add_metadata: * @icc: A valid #CdIcc * @key: the metadata key * @value: the UTF-8 metadata value * * Sets an item of data to the profile metadata, overwriting it if * it already exists. * * Since: 0.1.32 **/ void cd_icc_add_metadata (CdIcc *icc, const gchar *key, const gchar *value) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (CD_IS_ICC (icc)); g_return_if_fail (key != NULL); g_return_if_fail (g_utf8_validate (key, -1, NULL)); g_return_if_fail (value != NULL); g_return_if_fail (g_utf8_validate (value, -1, NULL)); g_hash_table_insert (priv->metadata, g_strdup (key), g_strdup (value)); } /** * cd_icc_remove_metadata: * @icc: A valid #CdIcc * @key: the metadata key * * Removes an item of metadata. * * Since: 0.1.32 **/ void cd_icc_remove_metadata (CdIcc *icc, const gchar *key) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (CD_IS_ICC (icc)); g_return_if_fail (key != NULL); g_hash_table_remove (priv->metadata, key); } static gboolean cd_icc_load_named_colors (CdIcc *icc, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); CdColorLab lab; CdColorSwatch *swatch; cmsNAMEDCOLORLIST *nc2; cmsUInt16Number pcs[3]; gboolean ret = TRUE; gchar name[cmsMAX_PATH]; gchar prefix[33]; gchar suffix[33]; GError *error_local = NULL; GString *string; guint j; guint size; /* do any named colors exist? */ nc2 = cd_icc_read_tag (icc, cmsSigNamedColor2Type, &error_local); if (nc2 == NULL) { /* no data is okay */ if (g_error_matches (error_local, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA)) { g_error_free (error_local); return TRUE; } g_propagate_error (error, error_local); return FALSE; } /* get each NC */ size = cmsNamedColorCount (nc2); for (j = 0; j < size; j++) { /* parse title */ ret = cmsNamedColorInfo (nc2, j, name, prefix, suffix, (cmsUInt16Number *) &pcs, NULL); if (!ret) continue; string = g_string_new (""); if (prefix[0] != '\0') g_string_append_printf (string, "%s ", prefix); g_string_append (string, name); if (suffix[0] != '\0') g_string_append_printf (string, " %s", suffix); /* check is valid */ ret = g_utf8_validate (string->str, string->len, NULL); if (!ret) ret = cd_icc_fix_utf8_string (string); /* save color if valid */ if (ret) { cmsLabEncoded2Float ((cmsCIELab *) &lab, pcs); swatch = cd_color_swatch_new (); cd_color_swatch_set_name (swatch, string->str); cd_color_swatch_set_value (swatch, (const CdColorLab *) &lab); g_ptr_array_add (priv->named_colors, swatch); } g_string_free (string, TRUE); } return TRUE; } /** * cd_icc_get_named_colors: * @icc: a #CdIcc instance. * * Gets any named colors in the profile. * This function will only return results if the profile was loaded with the * %CD_ICC_LOAD_FLAGS_NAMED_COLORS flag. * * Return value: (transfer container) (element-type CdColorSwatch): An array of color swatches * * Since: 0.1.32 **/ GPtrArray * cd_icc_get_named_colors (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return g_ptr_array_ref (priv->named_colors); } /** * cd_icc_get_can_delete: * @icc: a #CdIcc instance. * * Finds out if the profile could be deleted. * This is only applicable for profiles loaded with cd_icc_load_file() as * obviously data and fd's cannot be sanely unlinked. * * Return value: %TRUE if g_file_delete() would likely work * * Since: 0.1.32 **/ gboolean cd_icc_get_can_delete (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), FALSE); return priv->can_delete; } /** * cd_icc_get_created: * @icc: A valid #CdIcc * * Gets the ICC creation date and time. * * Return value: A #GDateTime object, or %NULL for not set * * Since: 0.1.32 **/ GDateTime * cd_icc_get_created (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); struct tm created_tm; time_t created_t; g_return_val_if_fail (CD_IS_ICC (icc), NULL); /* If the creation time has been overridden, return that */ if (priv->creation_time != (gint64)-1) return g_date_time_new_from_unix_local (priv->creation_time); /* get the profile creation time and date */ if (!cmsGetHeaderCreationDateTime (priv->lcms_profile, &created_tm)) return NULL; created_tm.tm_isdst = -1; /* convert to UNIX time */ created_t = mktime (&created_tm); if (created_t == (time_t) -1) return NULL; /* instantiate object */ return g_date_time_new_from_unix_local (created_t); } /** * cd_icc_set_created * @icc: A valid #CdIcc * * Sets the ICC creation date and time. * * Since: 1.4.2 **/ void cd_icc_set_created (CdIcc *icc, GDateTime* creation_time) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (CD_IS_ICC (icc)); priv->creation_time = g_date_time_to_unix (creation_time); } /** * cd_icc_get_checksum: * @icc: A valid #CdIcc * * Gets the profile checksum if one exists. * This will either be the embedded profile ID, or the file checksum if * the #CdIcc object was loaded using cd_icc_load_data() or cd_icc_load_file() * and the %CD_ICC_LOAD_FLAGS_FALLBACK_MD5 flag is used. * * Return value: An embedded MD5 checksum, or %NULL for not set * * Since: 0.1.32 **/ const gchar * cd_icc_get_checksum (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return priv->checksum; } static gchar * cd_icc_get_locale_key (const gchar *locale) { gchar *locale_key; /* en_US is the default locale in an ICC profile */ if (locale == NULL || g_str_has_prefix (locale, "en_US")) return g_strdup (""); locale_key = g_strdup (locale); g_strdelimit (locale_key, ".(", '\0'); return locale_key; } static const gchar * cd_icc_get_mluc_data (CdIcc *icc, const gchar *locale, CdIccMluc mluc, cmsTagSignature *sigs, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsMLU *mlu = NULL; const gchar *country_code = "\0\0\0"; const gchar *language_code = "\0\0\0"; const gchar *value; gchar *tmp; guint32 text_size; guint i; g_autofree gchar *locale_key = NULL; g_autofree gchar *text_buffer = NULL; g_autofree gunichar *wtext = NULL; g_return_val_if_fail (CD_IS_ICC (icc), NULL); /* does cache entry exist already? */ locale_key = cd_icc_get_locale_key (locale); value = g_hash_table_lookup (priv->mluc_data[mluc], locale_key); if (value != NULL) goto out; /* convert the locale into something we can use as a key, in this case * 'en_GB.UTF-8' -> 'en_GB' * 'fr' -> 'fr' */ if (locale_key[0] != '\0') { /* decompose it into language and country codes */ tmp = g_strstr_len (locale_key, -1, "_"); language_code = locale_key; if (tmp != NULL) { country_code = tmp + 1; *tmp = '\0'; } /* check the format is correct */ if (strlen (language_code) != 2) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_INVALID_LOCALE, "invalid locale: %s", locale); goto out; } if (country_code != NULL && country_code[0] != '\0' && strlen (country_code) != 2) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_INVALID_LOCALE, "invalid locale: %s", locale); goto out; } } else { /* lcms maps this to 'default' */ language_code = "en"; country_code = "US"; } /* read each MLU entry in order of preference */ for (i = 0; sigs[i] != 0; i++) { mlu = cd_icc_read_tag (icc, sigs[i], NULL); if (mlu != NULL) break; } if (mlu == NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA, "cmsSigProfile*Tag missing"); goto out; } /* get required size for wide chars */ text_size = cmsMLUgetWide (mlu, language_code, country_code, NULL, 0); if (text_size == 0) goto out; /* load wide chars */ wtext = g_new (gunichar, text_size); text_size = cmsMLUgetWide (mlu, language_code, country_code, (wchar_t *) wtext, text_size); if (text_size == 0) goto out; /* insert UTF-8 value into locale cache */ text_buffer = g_ucs4_to_utf8 (wtext, -1, NULL, NULL, error); if (text_buffer == NULL) goto out; tmp = g_strdup (text_buffer); g_hash_table_insert (priv->mluc_data[mluc], g_strdup (locale_key), tmp); value = tmp; out: return value; } /** * cd_icc_get_description: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @error: A #GError or %NULL * * Gets the profile description. * If the translated text is not available in the selected locale then the * default untranslated (en_US) text is returned. * * Return value: The text as a UTF-8 string, or %NULL of the locale is invalid * or the tag does not exist. * * Since: 0.1.32 **/ const gchar * cd_icc_get_description (CdIcc *icc, const gchar *locale, GError **error) { cmsTagSignature sigs[] = { cmsSigProfileDescriptionMLTag, cmsSigProfileDescriptionTag, 0 }; return cd_icc_get_mluc_data (icc, locale, CD_MLUC_DESCRIPTION, sigs, error); } /** * cd_icc_get_copyright: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @error: A #GError or %NULL * * Gets the profile copyright. * If the translated text is not available in the selected locale then the * default untranslated (en_US) text is returned. * * Return value: The text as a UTF-8 string, or %NULL of the locale is invalid * or the tag does not exist. * * Since: 0.1.32 **/ const gchar * cd_icc_get_copyright (CdIcc *icc, const gchar *locale, GError **error) { cmsTagSignature sigs[] = { cmsSigCopyrightTag, 0 }; return cd_icc_get_mluc_data (icc, locale, CD_MLUC_COPYRIGHT, sigs, error); } /** * cd_icc_get_manufacturer: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @error: A #GError or %NULL * * Gets the profile manufacturer. * If the translated text is not available in the selected locale then the * default untranslated (en_US) text is returned. * * Return value: The text as a UTF-8 string, or %NULL of the locale is invalid * or the tag does not exist. * * Since: 0.1.32 **/ const gchar * cd_icc_get_manufacturer (CdIcc *icc, const gchar *locale, GError **error) { cmsTagSignature sigs[] = { cmsSigDeviceMfgDescTag, 0 }; return cd_icc_get_mluc_data (icc, locale, CD_MLUC_MANUFACTURER, sigs, error); } /** * cd_icc_get_model: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @error: A #GError or %NULL * * Gets the profile model. * If the translated text is not available in the selected locale then the * default untranslated (en_US) text is returned. * * Return value: The text as a UTF-8 string, or %NULL of the locale is invalid * or the tag does not exist. * * Since: 0.1.32 **/ const gchar * cd_icc_get_model (CdIcc *icc, const gchar *locale, GError **error) { cmsTagSignature sigs[] = { cmsSigDeviceModelDescTag, 0 }; return cd_icc_get_mluc_data (icc, locale, CD_MLUC_MODEL, sigs, error); } /** * cd_icc_set_description: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @value: (allow-none): New UTF-8 string value * * Sets the profile description for a specific locale. * * Since: 0.1.32 **/ void cd_icc_set_description (CdIcc *icc, const gchar *locale, const gchar *value) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (value == NULL || g_utf8_validate (value, -1, NULL)); g_hash_table_insert (priv->mluc_data[CD_MLUC_DESCRIPTION], cd_icc_get_locale_key (locale), g_strdup (value)); } /** * cd_icc_set_description_items: * @icc: A valid #CdIcc * @values: New translated values, with the key being the locale. * * Sets the profile descriptions for specific locales. * * Since: 0.1.32 **/ void cd_icc_set_description_items (CdIcc *icc, GHashTable *values) { GList *l; const gchar *key; const gchar *value; g_autoptr(GList) keys = NULL; g_return_if_fail (CD_IS_ICC (icc)); /* add each translation */ keys = g_hash_table_get_keys (values); for (l = keys; l != NULL; l = l->next) { key = l->data; value = g_hash_table_lookup (values, key); cd_icc_set_description (icc, key, value); } } /** * cd_icc_set_copyright: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @value: (allow-none): New UTF-8 string value * * Sets the profile _copyright for a specific locale. * * Since: 0.1.32 **/ void cd_icc_set_copyright (CdIcc *icc, const gchar *locale, const gchar *value) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (value == NULL || g_utf8_validate (value, -1, NULL)); g_hash_table_insert (priv->mluc_data[CD_MLUC_COPYRIGHT], cd_icc_get_locale_key (locale), g_strdup (value)); } /** * cd_icc_set_copyright_items: * @icc: A valid #CdIcc * @values: New translated values, with the key being the locale. * * Sets the profile copyrights for specific locales. * * Since: 0.1.32 **/ void cd_icc_set_copyright_items (CdIcc *icc, GHashTable *values) { const gchar *key; const gchar *value; GList *l; g_autoptr(GList) keys = NULL; g_return_if_fail (CD_IS_ICC (icc)); /* add each translation */ keys = g_hash_table_get_keys (values); for (l = keys; l != NULL; l = l->next) { key = l->data; value = g_hash_table_lookup (values, key); cd_icc_set_copyright (icc, key, value); } } /** * cd_icc_set_manufacturer: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @value: (allow-none): New UTF-8 string value * * Sets the profile manufacturer for a specific locale. * * Since: 0.1.32 **/ void cd_icc_set_manufacturer (CdIcc *icc, const gchar *locale, const gchar *value) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (value == NULL || g_utf8_validate (value, -1, NULL)); g_hash_table_insert (priv->mluc_data[CD_MLUC_MANUFACTURER], cd_icc_get_locale_key (locale), g_strdup (value)); } /** * cd_icc_set_manufacturer_items: * @icc: A valid #CdIcc * @values: New translated values, with the key being the locale. * * Sets the profile manufacturers for specific locales. * * Since: 0.1.32 **/ void cd_icc_set_manufacturer_items (CdIcc *icc, GHashTable *values) { const gchar *key; const gchar *value; GList *l; g_autoptr(GList) keys = NULL; g_return_if_fail (CD_IS_ICC (icc)); /* add each translation */ keys = g_hash_table_get_keys (values); for (l = keys; l != NULL; l = l->next) { key = l->data; value = g_hash_table_lookup (values, key); cd_icc_set_manufacturer (icc, key, value); } } /** * cd_icc_set_model: * @icc: A valid #CdIcc * @locale: A locale, e.g. "en_GB.UTF-8" or %NULL for the profile default * @value: (allow-none): New UTF-8 string value * * Sets the profile model for a specific locale. * * Since: 0.1.32 **/ void cd_icc_set_model (CdIcc *icc, const gchar *locale, const gchar *value) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_if_fail (value == NULL || g_utf8_validate (value, -1, NULL)); g_hash_table_insert (priv->mluc_data[CD_MLUC_MODEL], cd_icc_get_locale_key (locale), g_strdup (value)); } /** * cd_icc_set_model_items: * @icc: A valid #CdIcc * @values: New translated values, with the key being the locale. * * Sets the profile models for specific locales. * * Since: 0.1.32 **/ void cd_icc_set_model_items (CdIcc *icc, GHashTable *values) { const gchar *key; const gchar *value; GList *l; g_autoptr(GList) keys = NULL; g_return_if_fail (CD_IS_ICC (icc)); /* add each translation */ keys = g_hash_table_get_keys (values); for (l = keys; l != NULL; l = l->next) { key = l->data; value = g_hash_table_lookup (values, key); cd_icc_set_model (icc, key, value); } } /** * cd_icc_get_temperature: * @icc: A valid #CdIcc * * Gets the ICC color temperature, rounded to the nearest 100K. * This function will only return results if the profile was loaded with the * %CD_ICC_LOAD_FLAGS_PRIMARIES flag. * * Return value: The color temperature in Kelvin, or 0 for error. * * Since: 0.1.32 **/ guint cd_icc_get_temperature (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), 0); return priv->temperature; } /** * cd_icc_get_red: * @icc: a valid #CdIcc instance * * Gets the profile red chromaticity value. * This function will only return results if the profile was loaded with the * %CD_ICC_LOAD_FLAGS_PRIMARIES flag. * * Return value: the #CdColorXYZ value * * Since: 0.1.32 **/ const CdColorXYZ * cd_icc_get_red (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return &priv->red; } /** * cd_icc_get_green: * @icc: a valid #CdIcc instance * * Gets the profile green chromaticity value. * This function will only return results if the profile was loaded with the * %CD_ICC_LOAD_FLAGS_PRIMARIES flag. * * Return value: the #CdColorXYZ value * * Since: 0.1.32 **/ const CdColorXYZ * cd_icc_get_green (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return &priv->green; } /** * cd_icc_get_blue: * @icc: a valid #CdIcc instance * * Gets the profile red chromaticity value. * This function will only return results if the profile was loaded with the * %CD_ICC_LOAD_FLAGS_PRIMARIES flag. * * Return value: the #CdColorXYZ value * * Since: 0.1.32 **/ const CdColorXYZ * cd_icc_get_blue (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return &priv->blue; } /** * cd_icc_get_white: * @icc: a valid #CdIcc instance * * Gets the profile white point. * This function will only return results if the profile was loaded with the * %CD_ICC_LOAD_FLAGS_PRIMARIES flag. * * Return value: the #CdColorXYZ value * * Since: 0.1.32 **/ const CdColorXYZ * cd_icc_get_white (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); g_return_val_if_fail (CD_IS_ICC (icc), NULL); return &priv->white; } /** * cd_icc_create_default_full: * @icc: A valid #CdIcc * @flags: a set of #CdIccLoadFlags * @error: (out): A #GError, or %NULL * * Creates a default sRGB ICC profile. * * Return value: %TRUE for success * * Since: 1.4.5 **/ gboolean cd_icc_create_default_full (CdIcc *icc, CdIccLoadFlags flags, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); gboolean ret = TRUE; /* not loaded */ if (priv->lcms_profile != NULL) { ret = FALSE; g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_CREATE, "already loaded or generated"); goto out; } /* create our generated ICC */ priv->lcms_profile = cmsCreate_sRGBProfileTHR (priv->context_lcms); if (priv->lcms_profile == NULL) { ret = FALSE; g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_CREATE, "failed to create sRGB profile"); goto out; } /* get defaults from profile */ ret = cd_icc_load (icc, flags, error); if (!ret) goto out; /* set any extra profile metadata */ cd_icc_add_metadata (icc, CD_PROFILE_METADATA_DATA_SOURCE, CD_PROFILE_METADATA_DATA_SOURCE_STANDARD); cd_icc_add_metadata (icc, CD_PROFILE_METADATA_STANDARD_SPACE, cd_standard_space_to_string (CD_STANDARD_SPACE_SRGB)); out: return ret; } /** * cd_icc_create_default: * @icc: A valid #CdIcc * @error: (out): A #GError, or %NULL * * Creates a default sRGB ICC profile. * * Return value: %TRUE for success * * Since: 1.1.2 **/ gboolean cd_icc_create_default (CdIcc *icc, GError **error) { return cd_icc_create_default_full (icc, CD_ICC_LOAD_FLAGS_NONE, error); } /** * cd_icc_create_from_edid_data: * @icc: A valid #CdIcc * @edid: EDID data * @error: A #GError, or %NULL * * Creates an ICC profile from EDID data. * * Return value: %TRUE for success * * Since: 1.1.2 **/ gboolean cd_icc_create_from_edid_data (CdIcc *icc, CdEdid *edid, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); const gchar *data; /* not loaded */ if (priv->lcms_profile != NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_CREATE, "already loaded or generated"); return FALSE; } /* create from parsed object */ if (!cd_icc_create_from_edid (icc, cd_edid_get_gamma (edid), cd_edid_get_red (edid), cd_edid_get_green (edid), cd_edid_get_blue (edid), cd_edid_get_white (edid), error)) { return FALSE; } /* set copyright */ cd_icc_set_copyright (icc, NULL, /* deliberately not translated */ "This profile is free of known copyright restrictions."); /* set 'ICC meta Tag for Monitor Profiles' data */ data = cd_edid_get_checksum (edid); if (data != NULL) cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MD5, data); data = cd_edid_get_monitor_name (edid); if (data != NULL) cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MODEL, data); data = cd_edid_get_serial_number (edid); if (data != NULL) cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_SERIAL, data); data = cd_edid_get_pnp_id (edid); if (data != NULL) cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MNFT, data); data = cd_edid_get_vendor_name (edid); if (data != NULL) cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_VENDOR, data); return TRUE; } /** * cd_icc_create_from_edid: * @icc: A valid #CdIcc * @gamma_value: approximate device gamma * @red: primary color value * @green: primary color value * @blue: primary color value * @white: whitepoint value * @error: A #GError, or %NULL * * Creates an ICC profile from EDID data. * * Return value: %TRUE for success * * Since: 0.1.32 **/ gboolean cd_icc_create_from_edid (CdIcc *icc, gdouble gamma_value, const CdColorYxy *red, const CdColorYxy *green, const CdColorYxy *blue, const CdColorYxy *white, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsCIExyYTRIPLE chroma; cmsCIExyY white_point; cmsToneCurve *transfer_curve[3] = { NULL, NULL, NULL }; gboolean ret = FALSE; /* not loaded */ if (priv->lcms_profile != NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_CREATE, "already loaded or generated"); goto out; } /* copy data from our structures (which are the wrong packing * size for lcms2) */ chroma.Red.x = red->x; chroma.Red.y = red->y; chroma.Green.x = green->x; chroma.Green.y = green->y; chroma.Blue.x = blue->x; chroma.Blue.y = blue->y; white_point.x = white->x; white_point.y = white->y; white_point.Y = 1.0; /* estimate the transfer function for the gamma */ transfer_curve[0] = cmsBuildGamma (NULL, gamma_value); transfer_curve[1] = transfer_curve[0]; transfer_curve[2] = transfer_curve[0]; /* create our generated ICC */ priv->lcms_profile = cmsCreateRGBProfileTHR (priv->context_lcms, &white_point, &chroma, transfer_curve); if (priv->lcms_profile == NULL) { g_set_error (error, CD_ICC_ERROR, CD_ICC_ERROR_FAILED_TO_CREATE, "failed to create profile with chroma and gamma"); goto out; } /* set header options */ cmsSetHeaderRenderingIntent (priv->lcms_profile, INTENT_PERCEPTUAL); cmsSetDeviceClass (priv->lcms_profile, cmsSigDisplayClass); /* copy any important parts out of the lcms-generated profile */ ret = cd_icc_load (icc, CD_ICC_LOAD_FLAGS_NONE, error); if (!ret) goto out; /* set the data source so we don't ever prompt the user to * recalibrate (as the EDID data won't have changed) */ cd_icc_add_metadata (icc, CD_PROFILE_METADATA_DATA_SOURCE, CD_PROFILE_METADATA_DATA_SOURCE_EDID); /* success */ ret = TRUE; out: if (transfer_curve[0] != NULL) cmsFreeToneCurve (transfer_curve[0]); return ret; } /** * cd_icc_get_vcgt: * @icc: A valid #CdIcc * @size: the desired size of the table data * @error: A #GError or %NULL * * Gets the video card calibration data from the profile. * * Return value: (transfer container) (element-type CdColorRGB): VCGT data, or %NULL for error * * Since: 0.1.34 **/ GPtrArray * cd_icc_get_vcgt (CdIcc *icc, guint size, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); CdColorRGB *tmp; cmsFloat32Number in; const cmsToneCurve **vcgt; GPtrArray *array = NULL; guint i; g_return_val_if_fail (CD_IS_ICC (icc), NULL); g_return_val_if_fail (priv->lcms_profile != NULL, NULL); /* get tone curves from icc */ vcgt = cmsReadTag (priv->lcms_profile, cmsSigVcgtType); if (vcgt == NULL || vcgt[0] == NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA, "icc does not have any VCGT data"); goto out; } /* create array */ array = g_ptr_array_new_with_free_func ((GDestroyNotify) cd_color_rgb_free); for (i = 0; i < size; i++) { in = (gdouble) i / (gdouble) (size - 1); tmp = cd_color_rgb_new (); cd_color_rgb_set (tmp, cmsEvalToneCurveFloat(vcgt[0], in), cmsEvalToneCurveFloat(vcgt[1], in), cmsEvalToneCurveFloat(vcgt[2], in)); g_ptr_array_add (array, tmp); } out: return array; } /** * cd_icc_get_response: * @icc: A valid #CdIcc * @size: the size of the curve to generate * @error: a valid #GError, or %NULL * * Generates a response curve of a specified size. * * Return value: (transfer container) (element-type CdColorRGB): response data, or %NULL for error * * Since: 0.1.34 **/ GPtrArray * cd_icc_get_response (CdIcc *icc, guint size, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); CdColorRGB *data; CdColorspace colorspace; cmsHPROFILE srgb_profile = NULL; cmsHTRANSFORM transform = NULL; const guint component_width = 3; gdouble tmp; gfloat divadd; gfloat divamount; GPtrArray *array = NULL; guint i; g_autofree gdouble *values_in = NULL; g_autofree gdouble *values_out = NULL; /* run through the icc */ colorspace = cd_icc_get_colorspace (icc); if (colorspace != CD_COLORSPACE_RGB) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_INVALID_COLORSPACE, "Only RGB colorspaces are supported"); goto out; } /* create input array */ values_in = g_new0 (gdouble, size * 3 * component_width); divamount = 1.0f / (gfloat) (size - 1); for (i = 0; i < size; i++) { divadd = divamount * (gfloat) i; /* red */ values_in[(i * 3 * component_width) + 0] = divadd; values_in[(i * 3 * component_width) + 1] = 0.0f; values_in[(i * 3 * component_width) + 2] = 0.0f; /* green */ values_in[(i * 3 * component_width) + 3] = 0.0f; values_in[(i * 3 * component_width) + 4] = divadd; values_in[(i * 3 * component_width) + 5] = 0.0f; /* blue */ values_in[(i * 3 * component_width) + 6] = 0.0f; values_in[(i * 3 * component_width) + 7] = 0.0f; values_in[(i * 3 * component_width) + 8] = divadd; } /* create a transform from icc to sRGB */ values_out = g_new0 (gdouble, size * 3 * component_width); srgb_profile = cmsCreate_sRGBProfileTHR (priv->context_lcms); transform = cmsCreateTransformTHR (priv->context_lcms, priv->lcms_profile, TYPE_RGB_DBL, srgb_profile, TYPE_RGB_DBL, INTENT_PERCEPTUAL, 0); if (transform == NULL) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA, "Failed to setup transform"); goto out; } cmsDoTransform (transform, values_in, values_out, size * 3); /* create output array */ array = cd_color_rgb_array_new (); for (i = 0; i < size; i++) { data = cd_color_rgb_new (); cd_color_rgb_set (data, 0.0f, 0.0f, 0.0f); /* only save curve data if it is positive */ tmp = values_out[(i * 3 * component_width) + 0]; if (tmp > 0.0f) data->R = tmp; tmp = values_out[(i * 3 * component_width) + 4]; if (tmp > 0.0f) data->G = tmp; tmp = values_out[(i * 3 * component_width) + 8]; if (tmp > 0.0f) data->B = tmp; g_ptr_array_add (array, data); } out: if (transform != NULL) cmsDeleteTransform (transform); if (srgb_profile != NULL) cmsCloseProfile (srgb_profile); return array; } /** * cd_icc_set_vcgt: * @icc: A valid #CdIcc * @vcgt: (element-type CdColorRGB): video card calibration data * @error: A #GError or %NULL * * Sets the Video Card Gamma Table from the profile. * * Return vale: %TRUE for success. * * Since: 0.1.34 **/ gboolean cd_icc_set_vcgt (CdIcc *icc, GPtrArray *vcgt, GError **error) { CdIccPrivate *priv = GET_PRIVATE (icc); CdColorRGB *tmp; cmsToneCurve *curve[3]; gboolean ret; guint i; g_autofree guint16 *blue = NULL; g_autofree guint16 *green = NULL; g_autofree guint16 *red = NULL; g_return_val_if_fail (CD_IS_ICC (icc), FALSE); g_return_val_if_fail (priv->lcms_profile != NULL, FALSE); /* unwrap data */ red = g_new0 (guint16, vcgt->len); green = g_new0 (guint16, vcgt->len); blue = g_new0 (guint16, vcgt->len); for (i = 0; i < vcgt->len; i++) { tmp = g_ptr_array_index (vcgt, i); red[i] = tmp->R * (gdouble) 0xffff; green[i] = tmp->G * (gdouble) 0xffff; blue[i] = tmp->B * (gdouble) 0xffff; } /* build tone curve */ curve[0] = cmsBuildTabulatedToneCurve16 (NULL, vcgt->len, red); curve[1] = cmsBuildTabulatedToneCurve16 (NULL, vcgt->len, green); curve[2] = cmsBuildTabulatedToneCurve16 (NULL, vcgt->len, blue); /* smooth it */ for (i = 0; i < 3; i++) cmsSmoothToneCurve (curve[i], 5); /* write the tag */ ret = cmsWriteTag (priv->lcms_profile, cmsSigVcgtType, curve); if (!ret) { g_set_error_literal (error, CD_ICC_ERROR, CD_ICC_ERROR_NO_DATA, "failed to write VCGT data"); goto out; } out: for (i = 0; i < 3; i++) cmsFreeToneCurve (curve[i]); return ret; } static CdProfileWarning cd_icc_check_whitepoint (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); guint temp = priv->temperature; /* not set */ if (temp == 0) return CD_PROFILE_WARNING_NONE; /* hardcoded sanity check */ if (temp < 3000 || temp > 10000) return CD_PROFILE_WARNING_WHITEPOINT_UNLIKELY; return CD_PROFILE_WARNING_NONE; } static CdProfileWarning cd_icc_check_vcgt (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsFloat32Number in; cmsFloat32Number now[3]; cmsFloat32Number previous[3] = { -1, -1, -1}; const cmsToneCurve **vcgt; const guint size = 32; guint i; /* does profile have monotonic VCGT */ vcgt = cmsReadTag (priv->lcms_profile, cmsSigVcgtTag); if (vcgt == NULL) return CD_PROFILE_WARNING_NONE; for (i = 0; i < size; i++) { in = (gdouble) i / (gdouble) (size - 1); now[0] = cmsEvalToneCurveFloat(vcgt[0], in); now[1] = cmsEvalToneCurveFloat(vcgt[1], in); now[2] = cmsEvalToneCurveFloat(vcgt[2], in); /* check VCGT is increasing */ if (i > 0) { if (now[0] < previous[0] || now[1] < previous[1] || now[2] < previous[2]) { return CD_PROFILE_WARNING_VCGT_NON_MONOTONIC; } } previous[0] = now[0]; previous[1] = now[1]; previous[2] = now[2]; } return CD_PROFILE_WARNING_NONE; } static CdProfileWarning cd_profile_check_scum_dot (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); CdProfileWarning warning = CD_PROFILE_WARNING_NONE; cmsCIELab white; cmsHPROFILE profile_lab; cmsHTRANSFORM transform; guint8 rgb[3] = { 0, 0, 0 }; /* do Lab to RGB transform of 100,0,0 */ profile_lab = cmsCreateLab2ProfileTHR (priv->context_lcms, cmsD50_xyY ()); transform = cmsCreateTransformTHR (priv->context_lcms, profile_lab, TYPE_Lab_DBL, priv->lcms_profile, TYPE_RGB_8, INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_NOOPTIMIZE); if (transform == NULL) { g_warning ("failed to setup Lab -> RGB transform"); goto out; } white.L = 100.0; white.a = 0.0; white.b = 0.0; cmsDoTransform (transform, &white, rgb, 1); if (rgb[0] != 255 || rgb[1] != 255 || rgb[2] != 255) { warning = CD_PROFILE_WARNING_SCUM_DOT; goto out; } out: if (profile_lab != NULL) cmsCloseProfile (profile_lab); if (transform != NULL) cmsDeleteTransform (transform); return warning; } static CdProfileWarning cd_icc_check_primaries (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); cmsCIEXYZ *tmp; /* The values used to check are based on the following ultra-wide * gamut profile XYZ values: * * CIERGB: * Red: 0.486893 0.174667 -0.001251 * Green: 0.306320 0.824768 0.016998 * Blue: 0.170990 0.000565 0.809158 * ProPhoto RGB: * Red: 0.797546 0.288025 0.000000 * Green: 0.135315 0.711899 -0.000015 * Blue: 0.031342 0.000076 0.824921 */ /* check red */ tmp = cmsReadTag (priv->lcms_profile, cmsSigRedColorantTag); if (tmp == NULL) return CD_PROFILE_WARNING_NONE; if (tmp->X > 0.85f || tmp->Y < 0.15f || tmp->Z < -0.01) return CD_PROFILE_WARNING_PRIMARIES_INVALID; /* check green */ tmp = cmsReadTag (priv->lcms_profile, cmsSigGreenColorantTag); if (tmp == NULL) return CD_PROFILE_WARNING_NONE; if (tmp->X < 0.10f || tmp->Y > 0.85f || tmp->Z < -0.01f) return CD_PROFILE_WARNING_PRIMARIES_INVALID; /* check blue */ tmp = cmsReadTag (priv->lcms_profile, cmsSigBlueColorantTag); if (tmp == NULL) return CD_PROFILE_WARNING_NONE; if (tmp->X < 0.01f || tmp->Y < 0.0f || tmp->Z > 0.87f) return CD_PROFILE_WARNING_PRIMARIES_INVALID; return CD_PROFILE_WARNING_NONE; } static CdProfileWarning cd_icc_check_gray_axis (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); CdProfileWarning warning = CD_PROFILE_WARNING_NONE; cmsCIELab gray[16]; cmsHPROFILE profile_lab = NULL; cmsHTRANSFORM transform = NULL; const gdouble gray_error = 5.0f; gdouble last_l = -1; guint8 rgb[3*16]; guint8 tmp; guint i; /* only do this for display profiles */ if (cmsGetDeviceClass (priv->lcms_profile) != cmsSigDisplayClass) goto out; /* do Lab to RGB transform of 100,0,0 */ profile_lab = cmsCreateLab2ProfileTHR (priv->context_lcms, cmsD50_xyY ()); transform = cmsCreateTransformTHR (priv->context_lcms, priv->lcms_profile, TYPE_RGB_8, profile_lab, TYPE_Lab_DBL, INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_NOOPTIMIZE); if (transform == NULL) { g_warning ("failed to setup RGB -> Lab transform"); goto out; } /* run a 16 item gray ramp through the transform */ for (i = 0; i < 16; i++) { tmp = (255.0f / (16.0f - 1)) * i; rgb[(i * 3) + 0] = tmp; rgb[(i * 3) + 1] = tmp; rgb[(i * 3) + 2] = tmp; } cmsDoTransform (transform, rgb, gray, 16); /* check a/b is small */ for (i = 0; i < 16; i++) { if (gray[i].a > gray_error || gray[i].b > gray_error) { warning = CD_PROFILE_WARNING_GRAY_AXIS_INVALID; goto out; } } /* check it's monotonic */ for (i = 0; i < 16; i++) { if (last_l > 0 && gray[i].L < last_l) { warning = CD_PROFILE_WARNING_GRAY_AXIS_NON_MONOTONIC; goto out; } last_l = gray[i].L; } out: if (profile_lab != NULL) cmsCloseProfile (profile_lab); if (transform != NULL) cmsDeleteTransform (transform); return warning; } static CdProfileWarning cd_icc_check_d50_whitepoint (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); CdProfileWarning warning = CD_PROFILE_WARNING_NONE; cmsCIExyY tmp; cmsCIEXYZ additive; cmsCIEXYZ primaries[4]; cmsHPROFILE profile_lab; cmsHTRANSFORM transform; const cmsCIEXYZ *d50; const gdouble rgb_error = 0.05; const gdouble additive_error = 0.1f; const gdouble white_error = 0.05; guint8 rgb[3*4]; guint i; /* do Lab to RGB transform to get primaries */ profile_lab = cmsCreateXYZProfileTHR (priv->context_lcms); transform = cmsCreateTransformTHR (priv->context_lcms, priv->lcms_profile, TYPE_RGB_8, profile_lab, TYPE_XYZ_DBL, INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_NOOPTIMIZE); if (transform == NULL) { g_warning ("failed to setup RGB -> XYZ transform"); goto out; } /* Run RGBW through the transform */ rgb[0 + 0] = 255; rgb[0 + 1] = 0; rgb[0 + 2] = 0; rgb[3 + 0] = 0; rgb[3 + 1] = 255; rgb[3 + 2] = 0; rgb[6 + 0] = 0; rgb[6 + 1] = 0; rgb[6 + 2] = 255; rgb[9 + 0] = 255; rgb[9 + 1] = 255; rgb[9 + 2] = 255; cmsDoTransform (transform, rgb, primaries, 4); /* check red is in gamut */ cmsXYZ2xyY (&tmp, &primaries[0]); if (tmp.x - 0.735 > rgb_error || 0.265 - tmp.y > rgb_error) { warning = CD_PROFILE_WARNING_PRIMARIES_UNLIKELY; goto out; } /* check green is in gamut */ cmsXYZ2xyY (&tmp, &primaries[1]); if (0.160 - tmp.x > rgb_error || tmp.y - 0.840 > rgb_error) { warning = CD_PROFILE_WARNING_PRIMARIES_UNLIKELY; goto out; } /* check blue is in gamut */ cmsXYZ2xyY (&tmp, &primaries[2]); if (0.037 - tmp.x > rgb_error || tmp.y - 0.358 > rgb_error) { warning = CD_PROFILE_WARNING_PRIMARIES_UNLIKELY; goto out; } /* only do the rest for display profiles */ if (cmsGetDeviceClass (priv->lcms_profile) != cmsSigDisplayClass) goto out; /* check white is D50 */ d50 = cmsD50_XYZ(); if (fabs (primaries[3].X - d50->X) > white_error || fabs (primaries[3].Y - d50->Y) > white_error || fabs (primaries[3].Z - d50->Z) > white_error) { warning = CD_PROFILE_WARNING_WHITEPOINT_INVALID; goto out; } /* check primaries add up to D50 */ additive.X = 0; additive.Y = 0; additive.Z = 0; for (i = 0; i < 3; i++) { additive.X += primaries[i].X; additive.Y += primaries[i].Y; additive.Z += primaries[i].Z; } if (fabs (additive.X - d50->X) > additive_error || fabs (additive.Y - d50->Y) > additive_error || fabs (additive.Z - d50->Z) > additive_error) { warning = CD_PROFILE_WARNING_PRIMARIES_NON_ADDITIVE; goto out; } out: if (profile_lab != NULL) cmsCloseProfile (profile_lab); if (transform != NULL) cmsDeleteTransform (transform); return warning; } /** * cd_icc_get_warnings: * @icc: a #CdIcc instance. * * Returns any warnings with profiles * * Return value: (transfer container) (element-type CdProfileWarning): An array of warning values * * Since: 0.1.34 **/ GArray * cd_icc_get_warnings (CdIcc *icc) { CdIccPrivate *priv = GET_PRIVATE (icc); GArray *flags; gboolean ret; gchar ascii_name[1024]; CdProfileWarning warning; g_return_val_if_fail (CD_IS_ICC (icc), NULL); g_return_val_if_fail (priv->lcms_profile != NULL, NULL); flags = g_array_new (FALSE, FALSE, sizeof (CdProfileWarning)); /* check that the profile has a description and a copyright */ ret = cmsGetProfileInfoASCII (priv->lcms_profile, cmsInfoDescription, "en", "US", ascii_name, 1024); if (!ret || ascii_name[0] == '\0') { warning = CD_PROFILE_WARNING_DESCRIPTION_MISSING; g_array_append_val (flags, warning); } ret = cmsGetProfileInfoASCII (priv->lcms_profile, cmsInfoCopyright, "en", "US", ascii_name, 1024); if (!ret || ascii_name[0] == '\0') { warning = CD_PROFILE_WARNING_COPYRIGHT_MISSING; g_array_append_val (flags, warning); } /* not a RGB space */ if (cmsGetColorSpace (priv->lcms_profile) != cmsSigRgbData) goto out; /* does profile have an unlikely whitepoint */ warning = cd_icc_check_whitepoint (icc); if (warning != CD_PROFILE_WARNING_NONE) g_array_append_val (flags, warning); /* does profile have monotonic VCGT */ warning = cd_icc_check_vcgt (icc); if (warning != CD_PROFILE_WARNING_NONE) g_array_append_val (flags, warning); /* if Lab 100,0,0 does not map to RGB 255,255,255 for relative * colorimetric then white it will not work on printers */ warning = cd_profile_check_scum_dot (icc); if (warning != CD_PROFILE_WARNING_NONE) g_array_append_val (flags, warning); /* gray should give low a/b and should be monotonic */ warning = cd_icc_check_gray_axis (icc); if (warning != CD_PROFILE_WARNING_NONE) g_array_append_val (flags, warning); /* tristimulus values cannot be negative */ warning = cd_icc_check_primaries (icc); if (warning != CD_PROFILE_WARNING_NONE) g_array_append_val (flags, warning); /* check whitepoint works out to D50 */ warning = cd_icc_check_d50_whitepoint (icc); if (warning != CD_PROFILE_WARNING_NONE) g_array_append_val (flags, warning); out: return flags; } static void cd_icc_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { CdIcc *icc = CD_ICC (object); CdIccPrivate *priv = GET_PRIVATE (icc); switch (prop_id) { case PROP_SIZE: g_value_set_uint (value, priv->size); break; case PROP_FILENAME: g_value_set_string (value, priv->filename); break; case PROP_VERSION: g_value_set_double (value, priv->version); break; case PROP_KIND: g_value_set_uint (value, priv->kind); break; case PROP_COLORSPACE: g_value_set_uint (value, priv->colorspace); break; case PROP_CAN_DELETE: g_value_set_boolean (value, priv->can_delete); break; case PROP_CHECKSUM: g_value_set_string (value, priv->checksum); break; case PROP_WHITE: g_value_set_boxed (value, g_boxed_copy (CD_TYPE_COLOR_XYZ, &priv->white)); break; case PROP_RED: g_value_set_boxed (value, g_boxed_copy (CD_TYPE_COLOR_XYZ, &priv->red)); break; case PROP_GREEN: g_value_set_boxed (value, g_boxed_copy (CD_TYPE_COLOR_XYZ, &priv->green)); break; case PROP_BLUE: g_value_set_boxed (value, g_boxed_copy (CD_TYPE_COLOR_XYZ, &priv->blue)); break; case PROP_TEMPERATURE: g_value_set_uint (value, priv->temperature); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void cd_icc_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { CdIcc *icc = CD_ICC (object); switch (prop_id) { case PROP_KIND: cd_icc_set_kind (icc, g_value_get_uint (value)); break; case PROP_COLORSPACE: cd_icc_set_colorspace (icc, g_value_get_uint (value)); break; case PROP_VERSION: cd_icc_set_version (icc, g_value_get_double (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void cd_icc_class_init (CdIccClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cd_icc_finalize; object_class->get_property = cd_icc_get_property; object_class->set_property = cd_icc_set_property; /** * CdIcc:size: */ pspec = g_param_spec_uint ("size", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_SIZE, pspec); /** * CdIcc:filename: */ pspec = g_param_spec_string ("filename", NULL, NULL, NULL, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_FILENAME, pspec); /** * CdIcc:version: */ pspec = g_param_spec_double ("version", NULL, NULL, 0, G_MAXFLOAT, 0, G_PARAM_READWRITE); g_object_class_install_property (object_class, PROP_VERSION, pspec); /** * CdIcc:kind: */ pspec = g_param_spec_uint ("kind", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READWRITE); g_object_class_install_property (object_class, PROP_KIND, pspec); /** * CdIcc:colorspace: */ pspec = g_param_spec_uint ("colorspace", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READWRITE); g_object_class_install_property (object_class, PROP_COLORSPACE, pspec); /** * CdIcc:can-delete: */ pspec = g_param_spec_boolean ("can-delete", NULL, NULL, FALSE, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_CAN_DELETE, pspec); /** * CdIcc:checksum: */ pspec = g_param_spec_string ("checksum", NULL, NULL, NULL, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_CHECKSUM, pspec); /** * CdIcc:white: */ pspec = g_param_spec_boxed ("white", NULL, NULL, CD_TYPE_COLOR_XYZ, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_WHITE, pspec); /** * CdIcc:red: */ pspec = g_param_spec_boxed ("red", NULL, NULL, CD_TYPE_COLOR_XYZ, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_RED, pspec); /** * CdIcc:green: */ pspec = g_param_spec_boxed ("green", NULL, NULL, CD_TYPE_COLOR_XYZ, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_GREEN, pspec); /** * CdIcc:blue: */ pspec = g_param_spec_boxed ("blue", NULL, NULL, CD_TYPE_COLOR_XYZ, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_BLUE, pspec); /** * CdIcc:temperature: */ pspec = g_param_spec_uint ("temperature", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READABLE); g_object_class_install_property (object_class, PROP_TEMPERATURE, pspec); } static void cd_icc_init (CdIcc *icc) { guint i; CdIccPrivate *priv = GET_PRIVATE (icc); priv->context_lcms = cd_context_lcms_new (); priv->kind = CD_PROFILE_KIND_UNKNOWN; priv->colorspace = CD_COLORSPACE_UNKNOWN; priv->named_colors = g_ptr_array_new_with_free_func ((GDestroyNotify) cd_color_swatch_free); priv->metadata = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); priv->creation_time = -1; for (i = 0; i < CD_MLUC_LAST; i++) { priv->mluc_data[i] = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); } cd_color_xyz_clear (&priv->white); cd_color_xyz_clear (&priv->red); cd_color_xyz_clear (&priv->green); cd_color_xyz_clear (&priv->blue); } static void cd_icc_finalize (GObject *object) { CdIcc *icc = CD_ICC (object); CdIccPrivate *priv = GET_PRIVATE (icc); guint i; g_free (priv->filename); g_free (priv->checksum); g_free (priv->characterization_data); g_ptr_array_unref (priv->named_colors); g_hash_table_destroy (priv->metadata); for (i = 0; i < CD_MLUC_LAST; i++) g_hash_table_destroy (priv->mluc_data[i]); if (priv->lcms_profile != NULL) cmsCloseProfile (priv->lcms_profile); cd_context_lcms_free (priv->context_lcms); G_OBJECT_CLASS (cd_icc_parent_class)->finalize (object); } /** * cd_icc_new: * * Creates a new #CdIcc object. * * Return value: a new CdIcc object. * * Since: 0.1.32 **/ CdIcc * cd_icc_new (void) { CdIcc *icc; icc = g_object_new (CD_TYPE_ICC, NULL); return CD_ICC (icc); }