summaryrefslogtreecommitdiffstats
path: root/libgimpbase/gimputils.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:13:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:13:10 +0000
commit3c57dd931145d43f2b0aef96c4d178135956bf91 (patch)
tree3de698981e9f0cc2c4f9569b19a5f3595e741f6b /libgimpbase/gimputils.c
parentInitial commit. (diff)
downloadgimp-3c57dd931145d43f2b0aef96c4d178135956bf91.tar.xz
gimp-3c57dd931145d43f2b0aef96c4d178135956bf91.zip
Adding upstream version 2.10.36.upstream/2.10.36
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'libgimpbase/gimputils.c')
-rw-r--r--libgimpbase/gimputils.c1603
1 files changed, 1603 insertions, 0 deletions
diff --git a/libgimpbase/gimputils.c b/libgimpbase/gimputils.c
new file mode 100644
index 0000000..2ee1338
--- /dev/null
+++ b/libgimpbase/gimputils.c
@@ -0,0 +1,1603 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
+ *
+ * gimputils.c
+ * Copyright (C) 2003 Sven Neumann <sven@gimp.org>
+ *
+ * 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 3 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, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#ifdef PLATFORM_OSX
+#include <AppKit/AppKit.h>
+#endif
+
+#ifdef HAVE_EXECINFO_H
+/* Allowing backtrace() API. */
+#include <execinfo.h>
+#endif
+
+#include <gio/gio.h>
+#include <glib/gprintf.h>
+
+#if defined(G_OS_WIN32)
+# include <windows.h>
+# include <shlobj.h>
+
+#else /* G_OS_WIN32 */
+
+/* For waitpid() */
+#include <sys/wait.h>
+#include <unistd.h>
+#include <errno.h>
+
+/* For thread IDs. */
+#include <sys/types.h>
+#include <sys/syscall.h>
+
+#ifdef HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
+
+#ifdef HAVE_SYS_THR_H
+#include <sys/thr.h>
+#endif
+
+#endif /* G_OS_WIN32 */
+
+#include "gimpbasetypes.h"
+#include "gimputils.h"
+
+#include "libgimp/libgimp-intl.h"
+
+
+/**
+ * SECTION: gimputils
+ * @title: gimputils
+ * @short_description: Utilities of general interest
+ *
+ * Utilities of general interest
+ **/
+
+static gboolean gimp_utils_generic_available (const gchar *program,
+ gint major,
+ gint minor);
+static gboolean gimp_utils_gdb_available (gint major,
+ gint minor);
+
+/**
+ * gimp_utf8_strtrim:
+ * @str: an UTF-8 encoded string (or %NULL)
+ * @max_chars: the maximum number of characters before the string get
+ * trimmed
+ *
+ * Creates a (possibly trimmed) copy of @str. The string is cut if it
+ * exceeds @max_chars characters or on the first newline. The fact
+ * that the string was trimmed is indicated by appending an ellipsis.
+ *
+ * Returns: A (possibly trimmed) copy of @str which should be freed
+ * using g_free() when it is not needed any longer.
+ **/
+gchar *
+gimp_utf8_strtrim (const gchar *str,
+ gint max_chars)
+{
+ /* FIXME: should we make this translatable? */
+ const gchar ellipsis[] = "...";
+ const gint e_len = strlen (ellipsis);
+
+ if (str)
+ {
+ const gchar *p;
+ const gchar *newline = NULL;
+ gint chars = 0;
+ gunichar unichar;
+
+ for (p = str; *p; p = g_utf8_next_char (p))
+ {
+ if (++chars > max_chars)
+ break;
+
+ unichar = g_utf8_get_char (p);
+
+ switch (g_unichar_break_type (unichar))
+ {
+ case G_UNICODE_BREAK_MANDATORY:
+ case G_UNICODE_BREAK_LINE_FEED:
+ newline = p;
+ break;
+ default:
+ continue;
+ }
+
+ break;
+ }
+
+ if (*p)
+ {
+ gsize len = p - str;
+ gchar *trimmed = g_new (gchar, len + e_len + 2);
+
+ memcpy (trimmed, str, len);
+ if (newline)
+ trimmed[len++] = ' ';
+
+ g_strlcpy (trimmed + len, ellipsis, e_len + 1);
+
+ return trimmed;
+ }
+
+ return g_strdup (str);
+ }
+
+ return NULL;
+}
+
+/**
+ * gimp_any_to_utf8:
+ * @str: The string to be converted to UTF-8.
+ * @len: The length of the string, or -1 if the string
+ * is nul-terminated.
+ * @warning_format: The message format for the warning message if conversion
+ * to UTF-8 fails. See the <function>printf()</function>
+ * documentation.
+ * @...: The parameters to insert into the format string.
+ *
+ * This function takes any string (UTF-8 or not) and always returns a valid
+ * UTF-8 string.
+ *
+ * If @str is valid UTF-8, a copy of the string is returned.
+ *
+ * If UTF-8 validation fails, g_locale_to_utf8() is tried and if it
+ * succeeds the resulting string is returned.
+ *
+ * Otherwise, the portion of @str that is UTF-8, concatenated
+ * with "(invalid UTF-8 string)" is returned. If not even the start
+ * of @str is valid UTF-8, only "(invalid UTF-8 string)" is returned.
+ *
+ * Return value: The UTF-8 string as described above.
+ **/
+gchar *
+gimp_any_to_utf8 (const gchar *str,
+ gssize len,
+ const gchar *warning_format,
+ ...)
+{
+ const gchar *start_invalid;
+ gchar *utf8;
+
+ g_return_val_if_fail (str != NULL, NULL);
+
+ if (g_utf8_validate (str, len, &start_invalid))
+ {
+ if (len < 0)
+ utf8 = g_strdup (str);
+ else
+ utf8 = g_strndup (str, len);
+ }
+ else
+ {
+ utf8 = g_locale_to_utf8 (str, len, NULL, NULL, NULL);
+ }
+
+ if (! utf8)
+ {
+ if (warning_format)
+ {
+ va_list warning_args;
+
+ va_start (warning_args, warning_format);
+
+ g_logv (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE,
+ warning_format, warning_args);
+
+ va_end (warning_args);
+ }
+
+ if (start_invalid > str)
+ {
+ gchar *tmp;
+
+ tmp = g_strndup (str, start_invalid - str);
+ utf8 = g_strconcat (tmp, " ", _("(invalid UTF-8 string)"), NULL);
+ g_free (tmp);
+ }
+ else
+ {
+ utf8 = g_strdup (_("(invalid UTF-8 string)"));
+ }
+ }
+
+ return utf8;
+}
+
+/**
+ * gimp_filename_to_utf8:
+ * @filename: The filename to be converted to UTF-8.
+ *
+ * Convert a filename in the filesystem's encoding to UTF-8
+ * temporarily. The return value is a pointer to a string that is
+ * guaranteed to be valid only during the current iteration of the
+ * main loop or until the next call to gimp_filename_to_utf8().
+ *
+ * The only purpose of this function is to provide an easy way to pass
+ * a filename in the filesystem encoding to a function that expects an
+ * UTF-8 encoded filename.
+ *
+ * Return value: A temporarily valid UTF-8 representation of @filename.
+ * This string must not be changed or freed.
+ **/
+const gchar *
+gimp_filename_to_utf8 (const gchar *filename)
+{
+ /* Simpleminded implementation, but at least allocates just one copy
+ * of each translation. Could check if already UTF-8, and if so
+ * return filename as is. Could perhaps (re)use a suitably large
+ * cyclic buffer, but then would have to verify that all calls
+ * really need the return value just for a "short" time.
+ */
+
+ static GHashTable *ht = NULL;
+ gchar *filename_utf8;
+
+ if (! filename)
+ return NULL;
+
+ if (! ht)
+ ht = g_hash_table_new (g_str_hash, g_str_equal);
+
+ filename_utf8 = g_hash_table_lookup (ht, filename);
+
+ if (! filename_utf8)
+ {
+ filename_utf8 = g_filename_display_name (filename);
+ g_hash_table_insert (ht, g_strdup (filename), filename_utf8);
+ }
+
+ return filename_utf8;
+}
+
+/**
+ * gimp_file_get_utf8_name:
+ * @file: a #GFile
+ *
+ * This function works like gimp_filename_to_utf8() and returns
+ * a UTF-8 encoded string that does not need to be freed.
+ *
+ * It converts a #GFile's path or uri to UTF-8 temporarily. The
+ * return value is a pointer to a string that is guaranteed to be
+ * valid only during the current iteration of the main loop or until
+ * the next call to gimp_file_get_utf8_name().
+ *
+ * The only purpose of this function is to provide an easy way to pass
+ * a #GFile's name to a function that expects an UTF-8 encoded string.
+ *
+ * See g_file_get_parse_name().
+ *
+ * Since: 2.10
+ *
+ * Return value: A temporarily valid UTF-8 representation of @file's name.
+ * This string must not be changed or freed.
+ **/
+const gchar *
+gimp_file_get_utf8_name (GFile *file)
+{
+ gchar *name;
+
+ g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+ name = g_file_get_parse_name (file);
+
+ g_object_set_data_full (G_OBJECT (file), "gimp-parse-name", name,
+ (GDestroyNotify) g_free);
+
+ return name;
+}
+
+/**
+ * gimp_file_has_extension:
+ * @file: a #GFile
+ * @extension: an ASCII extension
+ *
+ * This function checks if @file's URI ends with @extension. It behaves
+ * like g_str_has_suffix() on g_file_get_uri(), except that the string
+ * comparison is done case-insensitively using g_ascii_strcasecmp().
+ *
+ * Since: 2.10
+ *
+ * Return value: %TRUE if @file's URI ends with @extension,
+ * %FALSE otherwise.
+ **/
+gboolean
+gimp_file_has_extension (GFile *file,
+ const gchar *extension)
+{
+ gchar *uri;
+ gint uri_len;
+ gint ext_len;
+ gboolean result = FALSE;
+
+ g_return_val_if_fail (G_IS_FILE (file), FALSE);
+ g_return_val_if_fail (extension != NULL, FALSE);
+
+ uri = g_file_get_uri (file);
+
+ uri_len = strlen (uri);
+ ext_len = strlen (extension);
+
+ if (uri_len && ext_len && (uri_len > ext_len))
+ {
+ if (g_ascii_strcasecmp (uri + uri_len - ext_len, extension) == 0)
+ result = TRUE;
+ }
+
+ g_free (uri);
+
+ return result;
+}
+
+/**
+ * gimp_file_show_in_file_manager:
+ * @file: a #GFile
+ * @error: return location for a #GError
+ *
+ * Shows @file in the system file manager.
+ *
+ * Since: 2.10
+ *
+ * Return value: %TRUE on success, %FALSE otherwise. On %FALSE, @error
+ * is set.
+ **/
+gboolean
+gimp_file_show_in_file_manager (GFile *file,
+ GError **error)
+{
+ g_return_val_if_fail (G_IS_FILE (file), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+#if defined(G_OS_WIN32)
+
+ {
+ gboolean ret;
+ char *filename;
+ int n;
+ LPWSTR w_filename = NULL;
+ ITEMIDLIST *pidl = NULL;
+
+ ret = FALSE;
+
+ /* Calling this function multiple times should do no harm, but it is
+ easier to put this here as it needs linking against ole32. */
+ CoInitialize (NULL);
+
+ filename = g_file_get_path (file);
+ if (!filename)
+ {
+ g_set_error_literal (error, G_FILE_ERROR, 0,
+ _("File path is NULL"));
+ goto out;
+ }
+
+ n = MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS,
+ filename, -1, NULL, 0);
+ if (n == 0)
+ {
+ g_set_error_literal (error, G_FILE_ERROR, 0,
+ _("Error converting UTF-8 filename to wide char"));
+ goto out;
+ }
+
+ w_filename = g_malloc_n (n + 1, sizeof (wchar_t));
+ n = MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS,
+ filename, -1,
+ w_filename, (n + 1) * sizeof (wchar_t));
+ if (n == 0)
+ {
+ g_set_error_literal (error, G_FILE_ERROR, 0,
+ _("Error converting UTF-8 filename to wide char"));
+ goto out;
+ }
+
+ pidl = ILCreateFromPathW (w_filename);
+ if (!pidl)
+ {
+ g_set_error_literal (error, G_FILE_ERROR, 0,
+ _("ILCreateFromPath() failed"));
+ goto out;
+ }
+
+ SHOpenFolderAndSelectItems (pidl, 0, NULL, 0);
+ ret = TRUE;
+
+ out:
+ if (pidl)
+ ILFree (pidl);
+ g_free (w_filename);
+ g_free (filename);
+
+ return ret;
+ }
+
+#elif defined(PLATFORM_OSX)
+
+ {
+ gchar *uri;
+ NSString *filename;
+ NSURL *url;
+ gboolean retval = TRUE;
+
+ uri = g_file_get_uri (file);
+ filename = [NSString stringWithUTF8String:uri];
+
+ url = [NSURL URLWithString:filename];
+ if (url)
+ {
+ NSArray *url_array = [NSArray arrayWithObject:url];
+
+ [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:url_array];
+ }
+ else
+ {
+ g_set_error (error, G_FILE_ERROR, 0,
+ _("Cannot convert '%s' into a valid NSURL."), uri);
+ retval = FALSE;
+ }
+
+ g_free (uri);
+
+ return retval;
+ }
+
+#else /* UNIX */
+
+ {
+ GDBusProxy *proxy;
+ GVariant *retval;
+ GVariantBuilder *builder;
+ gchar *uri;
+
+ proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.FileManager1",
+ "/org/freedesktop/FileManager1",
+ "org.freedesktop.FileManager1",
+ NULL, error);
+
+ if (! proxy)
+ {
+ g_prefix_error (error,
+ _("Connecting to org.freedesktop.FileManager1 failed: "));
+ return FALSE;
+ }
+
+ uri = g_file_get_uri (file);
+
+ builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
+ g_variant_builder_add (builder, "s", uri);
+
+ g_free (uri);
+
+ retval = g_dbus_proxy_call_sync (proxy,
+ "ShowItems",
+ g_variant_new ("(ass)",
+ builder,
+ ""),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, error);
+
+ g_variant_builder_unref (builder);
+ g_object_unref (proxy);
+
+ if (! retval)
+ {
+ g_prefix_error (error, _("Calling ShowItems failed: "));
+ return FALSE;
+ }
+
+ g_variant_unref (retval);
+
+ return TRUE;
+ }
+
+#endif
+}
+
+/**
+ * gimp_strip_uline:
+ * @str: underline infested string (or %NULL)
+ *
+ * This function returns a copy of @str stripped of underline
+ * characters. This comes in handy when needing to strip mnemonics
+ * from menu paths etc.
+ *
+ * In some languages, mnemonics are handled by adding the mnemonic
+ * character in brackets (like "File (_F)"). This function recognizes
+ * this construct and removes the whole bracket construction to get
+ * rid of the mnemonic (see bug 157561).
+ *
+ * Return value: A (possibly stripped) copy of @str which should be
+ * freed using g_free() when it is not needed any longer.
+ **/
+gchar *
+gimp_strip_uline (const gchar *str)
+{
+ gchar *escaped;
+ gchar *p;
+ gboolean past_bracket = FALSE;
+
+ if (! str)
+ return NULL;
+
+ p = escaped = g_strdup (str);
+
+ while (*str)
+ {
+ if (*str == '_')
+ {
+ /* "__" means a literal "_" in the menu path */
+ if (str[1] == '_')
+ {
+ *p++ = *str++;
+ str++;
+ continue;
+ }
+
+ /* find the "(_X)" construct and remove it entirely */
+ if (past_bracket && str[1] && *(g_utf8_next_char (str + 1)) == ')')
+ {
+ str = g_utf8_next_char (str + 1) + 1;
+ p--;
+ }
+ else
+ {
+ str++;
+ }
+ }
+ else
+ {
+ past_bracket = (*str == '(');
+
+ *p++ = *str++;
+ }
+ }
+
+ *p = '\0';
+
+ return escaped;
+}
+
+/**
+ * gimp_escape_uline:
+ * @str: Underline infested string (or %NULL)
+ *
+ * This function returns a copy of @str with all underline converted
+ * to two adjacent underlines. This comes in handy when needing to display
+ * strings with underlines (like filenames) in a place that would convert
+ * them to mnemonics.
+ *
+ * Return value: A (possibly escaped) copy of @str which should be
+ * freed using g_free() when it is not needed any longer.
+ *
+ * Since: 2.2
+ **/
+gchar *
+gimp_escape_uline (const gchar *str)
+{
+ gchar *escaped;
+ gchar *p;
+ gint n_ulines = 0;
+
+ if (! str)
+ return NULL;
+
+ for (p = (gchar *) str; *p; p++)
+ if (*p == '_')
+ n_ulines++;
+
+ p = escaped = g_malloc (strlen (str) + n_ulines + 1);
+
+ while (*str)
+ {
+ if (*str == '_')
+ *p++ = '_';
+
+ *p++ = *str++;
+ }
+
+ *p = '\0';
+
+ return escaped;
+}
+
+/**
+ * gimp_canonicalize_identifier:
+ * @identifier: The identifier string to canonicalize.
+ *
+ * Turns any input string into a canonicalized string.
+ *
+ * Canonical identifiers are e.g. expected by the PDB for procedure
+ * and parameter names. Every character of the input string that is
+ * not either '-', 'a-z', 'A-Z' or '0-9' will be replaced by a '-'.
+ *
+ * Return value: The canonicalized identifier. This is a newly
+ * allocated string that should be freed with g_free()
+ * when no longer needed.
+ *
+ * Since: 2.4
+ **/
+gchar *
+gimp_canonicalize_identifier (const gchar *identifier)
+{
+ gchar *canonicalized = NULL;
+
+ if (identifier)
+ {
+ gchar *p;
+
+ canonicalized = g_strdup (identifier);
+
+ for (p = canonicalized; *p != 0; p++)
+ {
+ gchar c = *p;
+
+ if (c != '-' &&
+ (c < '0' || c > '9') &&
+ (c < 'A' || c > 'Z') &&
+ (c < 'a' || c > 'z'))
+ *p = '-';
+ }
+ }
+
+ return canonicalized;
+}
+
+/**
+ * gimp_enum_get_desc:
+ * @enum_class: a #GEnumClass
+ * @value: a value from @enum_class
+ *
+ * Retrieves #GimpEnumDesc associated with the given value, or %NULL.
+ *
+ * Return value: the value's #GimpEnumDesc.
+ *
+ * Since: 2.2
+ **/
+GimpEnumDesc *
+gimp_enum_get_desc (GEnumClass *enum_class,
+ gint value)
+{
+ const GimpEnumDesc *value_desc;
+
+ g_return_val_if_fail (G_IS_ENUM_CLASS (enum_class), NULL);
+
+ value_desc =
+ gimp_enum_get_value_descriptions (G_TYPE_FROM_CLASS (enum_class));
+
+ if (value_desc)
+ {
+ while (value_desc->value_desc)
+ {
+ if (value_desc->value == value)
+ return (GimpEnumDesc *) value_desc;
+
+ value_desc++;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * gimp_enum_get_value:
+ * @enum_type: the #GType of a registered enum
+ * @value: an integer value
+ * @value_name: return location for the value's name (or %NULL)
+ * @value_nick: return location for the value's nick (or %NULL)
+ * @value_desc: return location for the value's translated description (or %NULL)
+ * @value_help: return location for the value's translated help (or %NULL)
+ *
+ * Checks if @value is valid for the enum registered as @enum_type.
+ * If the value exists in that enum, its name, nick and its translated
+ * description and help are returned (if @value_name, @value_nick,
+ * @value_desc and @value_help are not %NULL).
+ *
+ * Return value: %TRUE if @value is valid for the @enum_type,
+ * %FALSE otherwise
+ *
+ * Since: 2.2
+ **/
+gboolean
+gimp_enum_get_value (GType enum_type,
+ gint value,
+ const gchar **value_name,
+ const gchar **value_nick,
+ const gchar **value_desc,
+ const gchar **value_help)
+{
+ GEnumClass *enum_class;
+ GEnumValue *enum_value;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (G_TYPE_IS_ENUM (enum_type), FALSE);
+
+ enum_class = g_type_class_ref (enum_type);
+ enum_value = g_enum_get_value (enum_class, value);
+
+ if (enum_value)
+ {
+ if (value_name)
+ *value_name = enum_value->value_name;
+
+ if (value_nick)
+ *value_nick = enum_value->value_nick;
+
+ if (value_desc || value_help)
+ {
+ GimpEnumDesc *enum_desc;
+
+ enum_desc = gimp_enum_get_desc (enum_class, value);
+
+ if (value_desc)
+ {
+ if (enum_desc && enum_desc->value_desc)
+ {
+ const gchar *context;
+
+ context = gimp_type_get_translation_context (enum_type);
+
+ if (context) /* the new way, using NC_() */
+ *value_desc = g_dpgettext2 (gimp_type_get_translation_domain (enum_type),
+ context,
+ enum_desc->value_desc);
+ else /* for backward compatibility */
+ *value_desc = g_strip_context (enum_desc->value_desc,
+ dgettext (gimp_type_get_translation_domain (enum_type),
+ enum_desc->value_desc));
+ }
+ else
+ {
+ *value_desc = NULL;
+ }
+ }
+
+ if (value_help)
+ {
+ *value_help = ((enum_desc && enum_desc->value_help) ?
+ dgettext (gimp_type_get_translation_domain (enum_type),
+ enum_desc->value_help) :
+ NULL);
+ }
+ }
+
+ success = TRUE;
+ }
+
+ g_type_class_unref (enum_class);
+
+ return success;
+}
+
+/**
+ * gimp_enum_value_get_desc:
+ * @enum_class: a #GEnumClass
+ * @enum_value: a #GEnumValue from @enum_class
+ *
+ * Retrieves the translated description for a given @enum_value.
+ *
+ * Return value: the translated description of the enum value
+ *
+ * Since: 2.2
+ **/
+const gchar *
+gimp_enum_value_get_desc (GEnumClass *enum_class,
+ GEnumValue *enum_value)
+{
+ GType type = G_TYPE_FROM_CLASS (enum_class);
+ GimpEnumDesc *enum_desc;
+
+ enum_desc = gimp_enum_get_desc (enum_class, enum_value->value);
+
+ if (enum_desc && enum_desc->value_desc)
+ {
+ const gchar *context;
+
+ context = gimp_type_get_translation_context (type);
+
+ if (context) /* the new way, using NC_() */
+ return g_dpgettext2 (gimp_type_get_translation_domain (type),
+ context,
+ enum_desc->value_desc);
+ else /* for backward compatibility */
+ return g_strip_context (enum_desc->value_desc,
+ dgettext (gimp_type_get_translation_domain (type),
+ enum_desc->value_desc));
+ }
+
+ return enum_value->value_name;
+}
+
+/**
+ * gimp_enum_value_get_help:
+ * @enum_class: a #GEnumClass
+ * @enum_value: a #GEnumValue from @enum_class
+ *
+ * Retrieves the translated help for a given @enum_value.
+ *
+ * Return value: the translated help of the enum value
+ *
+ * Since: 2.2
+ **/
+const gchar *
+gimp_enum_value_get_help (GEnumClass *enum_class,
+ GEnumValue *enum_value)
+{
+ GType type = G_TYPE_FROM_CLASS (enum_class);
+ GimpEnumDesc *enum_desc;
+
+ enum_desc = gimp_enum_get_desc (enum_class, enum_value->value);
+
+ if (enum_desc && enum_desc->value_help)
+ return dgettext (gimp_type_get_translation_domain (type),
+ enum_desc->value_help);
+
+ return NULL;
+}
+
+/**
+ * gimp_enum_value_get_abbrev:
+ * @enum_class: a #GEnumClass
+ * @enum_value: a #GEnumValue from @enum_class
+ *
+ * Retrieves the translated abbreviation for a given @enum_value.
+ *
+ * Return value: the translated abbreviation of the enum value
+ *
+ * Since: 2.10
+ **/
+const gchar *
+gimp_enum_value_get_abbrev (GEnumClass *enum_class,
+ GEnumValue *enum_value)
+{
+ GType type = G_TYPE_FROM_CLASS (enum_class);
+ GimpEnumDesc *enum_desc;
+
+ enum_desc = gimp_enum_get_desc (enum_class, enum_value->value);
+
+ if (enum_desc &&
+ enum_desc[1].value == enum_desc->value &&
+ enum_desc[1].value_desc)
+ {
+ return g_dpgettext2 (gimp_type_get_translation_domain (type),
+ gimp_type_get_translation_context (type),
+ enum_desc[1].value_desc);
+ }
+
+ return NULL;
+}
+
+/**
+ * gimp_flags_get_first_desc:
+ * @flags_class: a #GFlagsClass
+ * @value: a value from @flags_class
+ *
+ * Retrieves the first #GimpFlagsDesc that matches the given value, or %NULL.
+ *
+ * Return value: the value's #GimpFlagsDesc.
+ *
+ * Since: 2.2
+ **/
+GimpFlagsDesc *
+gimp_flags_get_first_desc (GFlagsClass *flags_class,
+ guint value)
+{
+ const GimpFlagsDesc *value_desc;
+
+ g_return_val_if_fail (G_IS_FLAGS_CLASS (flags_class), NULL);
+
+ value_desc =
+ gimp_flags_get_value_descriptions (G_TYPE_FROM_CLASS (flags_class));
+
+ if (value_desc)
+ {
+ while (value_desc->value_desc)
+ {
+ if ((value_desc->value & value) == value_desc->value)
+ return (GimpFlagsDesc *) value_desc;
+
+ value_desc++;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * gimp_flags_get_first_value:
+ * @flags_type: the #GType of registered flags
+ * @value: an integer value
+ * @value_name: return location for the value's name (or %NULL)
+ * @value_nick: return location for the value's nick (or %NULL)
+ * @value_desc: return location for the value's translated description (or %NULL)
+ * @value_help: return location for the value's translated help (or %NULL)
+ *
+ * Checks if @value is valid for the flags registered as @flags_type.
+ * If the value exists in that flags, its name, nick and its
+ * translated description and help are returned (if @value_name,
+ * @value_nick, @value_desc and @value_help are not %NULL).
+ *
+ * Return value: %TRUE if @value is valid for the @flags_type,
+ * %FALSE otherwise
+ *
+ * Since: 2.2
+ **/
+gboolean
+gimp_flags_get_first_value (GType flags_type,
+ guint value,
+ const gchar **value_name,
+ const gchar **value_nick,
+ const gchar **value_desc,
+ const gchar **value_help)
+{
+ GFlagsClass *flags_class;
+ GFlagsValue *flags_value;
+
+ g_return_val_if_fail (G_TYPE_IS_FLAGS (flags_type), FALSE);
+
+ flags_class = g_type_class_peek (flags_type);
+ flags_value = g_flags_get_first_value (flags_class, value);
+
+ if (flags_value)
+ {
+ if (value_name)
+ *value_name = flags_value->value_name;
+
+ if (value_nick)
+ *value_nick = flags_value->value_nick;
+
+ if (value_desc || value_help)
+ {
+ GimpFlagsDesc *flags_desc;
+
+ flags_desc = gimp_flags_get_first_desc (flags_class, value);
+
+ if (value_desc)
+ *value_desc = ((flags_desc && flags_desc->value_desc) ?
+ dgettext (gimp_type_get_translation_domain (flags_type),
+ flags_desc->value_desc) :
+ NULL);
+
+ if (value_help)
+ *value_help = ((flags_desc && flags_desc->value_desc) ?
+ dgettext (gimp_type_get_translation_domain (flags_type),
+ flags_desc->value_help) :
+ NULL);
+ }
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * gimp_flags_value_get_desc:
+ * @flags_class: a #GFlagsClass
+ * @flags_value: a #GFlagsValue from @flags_class
+ *
+ * Retrieves the translated description for a given @flags_value.
+ *
+ * Return value: the translated description of the flags value
+ *
+ * Since: 2.2
+ **/
+const gchar *
+gimp_flags_value_get_desc (GFlagsClass *flags_class,
+ GFlagsValue *flags_value)
+{
+ GType type = G_TYPE_FROM_CLASS (flags_class);
+ GimpFlagsDesc *flags_desc;
+
+ flags_desc = gimp_flags_get_first_desc (flags_class, flags_value->value);
+
+ if (flags_desc->value_desc)
+ {
+ const gchar *context;
+
+ context = gimp_type_get_translation_context (type);
+
+ if (context) /* the new way, using NC_() */
+ return g_dpgettext2 (gimp_type_get_translation_domain (type),
+ context,
+ flags_desc->value_desc);
+ else /* for backward compatibility */
+ return g_strip_context (flags_desc->value_desc,
+ dgettext (gimp_type_get_translation_domain (type),
+ flags_desc->value_desc));
+ }
+
+ return flags_value->value_name;
+}
+
+/**
+ * gimp_flags_value_get_help:
+ * @flags_class: a #GFlagsClass
+ * @flags_value: a #GFlagsValue from @flags_class
+ *
+ * Retrieves the translated help for a given @flags_value.
+ *
+ * Return value: the translated help of the flags value
+ *
+ * Since: 2.2
+ **/
+const gchar *
+gimp_flags_value_get_help (GFlagsClass *flags_class,
+ GFlagsValue *flags_value)
+{
+ GType type = G_TYPE_FROM_CLASS (flags_class);
+ GimpFlagsDesc *flags_desc;
+
+ flags_desc = gimp_flags_get_first_desc (flags_class, flags_value->value);
+
+ if (flags_desc->value_help)
+ return dgettext (gimp_type_get_translation_domain (type),
+ flags_desc->value_help);
+
+ return NULL;
+}
+
+/**
+ * gimp_flags_value_get_abbrev:
+ * @flags_class: a #GFlagsClass
+ * @flags_value: a #GFlagsValue from @flags_class
+ *
+ * Retrieves the translated abbreviation for a given @flags_value.
+ *
+ * Return value: the translated abbreviation of the flags value
+ *
+ * Since: 2.10
+ **/
+const gchar *
+gimp_flags_value_get_abbrev (GFlagsClass *flags_class,
+ GFlagsValue *flags_value)
+{
+ GType type = G_TYPE_FROM_CLASS (flags_class);
+ GimpFlagsDesc *flags_desc;
+
+ flags_desc = gimp_flags_get_first_desc (flags_class, flags_value->value);
+
+ if (flags_desc &&
+ flags_desc[1].value == flags_desc->value &&
+ flags_desc[1].value_desc)
+ {
+ return g_dpgettext2 (gimp_type_get_translation_domain (type),
+ gimp_type_get_translation_context (type),
+ flags_desc[1].value_desc);
+ }
+
+ return NULL;
+}
+
+/**
+ * gimp_stack_trace_available:
+ * @optimal: whether we get optimal traces.
+ *
+ * Returns #TRUE if we have dependencies to generate backtraces. If
+ * @optimal is #TRUE, the function will return #TRUE only when we
+ * are able to generate optimal traces (i.e. with GDB or LLDB);
+ * otherwise we return #TRUE even if only backtrace() API is available.
+ *
+ * On Win32, we return TRUE if Dr. Mingw is built-in, FALSE otherwise.
+ *
+ * Note: this function is not crash-safe, i.e. you should not try to use
+ * it in a callback when the program is already crashing. In such a
+ * case, call gimp_stack_trace_print() or gimp_stack_trace_query()
+ * directly.
+ *
+ * Since: 2.10
+ **/
+gboolean
+gimp_stack_trace_available (gboolean optimal)
+{
+#ifndef G_OS_WIN32
+ gchar *lld_path = NULL;
+ gboolean has_lldb = FALSE;
+
+ /* Similarly to gdb, we could check for lldb by calling:
+ * gimp_utils_generic_available ("lldb", major, minor).
+ * We don't do so on purpose because on macOS, when lldb is absent, it
+ * triggers a popup asking to install Xcode. So instead, we just
+ * search for the executable in path.
+ * This is the reason why this function is not crash-safe, since
+ * g_find_program_in_path() allocates memory.
+ * See issue #1999.
+ */
+ lld_path = g_find_program_in_path ("lldb");
+ if (lld_path)
+ {
+ has_lldb = TRUE;
+ g_free (lld_path);
+ }
+
+ if (gimp_utils_gdb_available (7, 0) || has_lldb)
+ return TRUE;
+#ifdef HAVE_EXECINFO_H
+ if (! optimal)
+ return TRUE;
+#endif
+#else /* G_OS_WIN32 */
+#ifdef HAVE_EXCHNDL
+ return TRUE;
+#endif
+#endif /* G_OS_WIN32 */
+ return FALSE;
+}
+
+/**
+ * gimp_stack_trace_print:
+ * @prog_name: the program to attach to.
+ * @stream: a #FILE * stream.
+ * @trace: location to store a newly allocated string of the trace.
+ *
+ * Attempts to generate a stack trace at current code position in
+ * @prog_name. @prog_name is mostly a helper and can be set to NULL.
+ * Nevertheless if set, it has to be the current program name (argv[0]).
+ * This function is not meant to generate stack trace for third-party
+ * programs, and will attach the current process id only.
+ * Internally, this function uses `gdb` or `lldb` if they are available,
+ * or the stacktrace() API on platforms where it is available. It always
+ * fails on Win32.
+ *
+ * The stack trace, once generated, will either be printed to @stream or
+ * returned as a newly allocated string in @trace, if not #NULL.
+ *
+ * In some error cases (e.g. segmentation fault), trying to allocate
+ * more memory will trigger more segmentation faults and therefore loop
+ * our error handling (which is just wrong). Therefore printing to a
+ * file description is an implementation without any memory allocation.
+
+ * Return value: #TRUE if a stack trace could be generated, #FALSE
+ * otherwise.
+ *
+ * Since: 2.10
+ **/
+gboolean
+gimp_stack_trace_print (const gchar *prog_name,
+ gpointer stream,
+ gchar **trace)
+{
+ gboolean stack_printed = FALSE;
+
+ /* This works only on UNIX systems. */
+#ifndef G_OS_WIN32
+ GString *gtrace = NULL;
+ gchar gimp_pid[16];
+ gchar buffer[256];
+ ssize_t read_n;
+ int sync_fd[2];
+ int out_fd[2];
+ pid_t fork_pid;
+ pid_t pid = getpid();
+ gint eintr_count = 0;
+#if defined(G_OS_WIN32)
+ DWORD tid = GetCurrentThreadId ();
+#elif defined(PLATFORM_OSX)
+ uint64 tid64;
+ long tid;
+
+ pthread_threadid_np (NULL, &tid64);
+ tid = (long) tid64;
+#elif defined(SYS_gettid)
+ long tid = syscall (SYS_gettid);
+#elif defined(HAVE_THR_SELF)
+ long tid = 0;
+ thr_self (&tid);
+#endif
+
+ g_snprintf (gimp_pid, 16, "%u", (guint) pid);
+
+ if (pipe (sync_fd) == -1)
+ {
+ return FALSE;
+ }
+
+ if (pipe (out_fd) == -1)
+ {
+ close (sync_fd[0]);
+ close (sync_fd[1]);
+
+ return FALSE;
+ }
+
+ fork_pid = fork ();
+ if (fork_pid == 0)
+ {
+ /* Child process. */
+ gchar *args[9] = { "gdb", "-batch",
+ "-ex", "info threads",
+ "-ex", "thread apply all backtrace full",
+ (gchar *) prog_name, NULL, NULL };
+
+ if (prog_name == NULL)
+ args[6] = "-p";
+
+ args[7] = gimp_pid;
+
+ /* Wait until the parent enabled us to ptrace it. */
+ {
+ gchar dummy;
+
+ close (sync_fd[1]);
+ while (read (sync_fd[0], &dummy, 1) < 0 && errno == EINTR);
+ close (sync_fd[0]);
+ }
+
+ /* Redirect the debugger output. */
+ dup2 (out_fd[1], STDOUT_FILENO);
+ close (out_fd[0]);
+ close (out_fd[1]);
+
+ /* Run GDB if version 7.0 or over. Why I do such a check is that
+ * it turns out older versions may not only fail, but also have
+ * very undesirable side effects like terminating the debugged
+ * program, at least on FreeBSD where GDB 6.1 is apparently
+ * installed by default on the stable release at day of writing.
+ * See bug 793514. */
+ if (! gimp_utils_gdb_available (7, 0) ||
+ execvp (args[0], args) == -1)
+ {
+ /* LLDB as alternative if the GDB call failed or if it was in
+ * a too-old version. */
+ gchar *args_lldb[15] = { "lldb", "--attach-pid", NULL, "--batch",
+ "--one-line", "thread list",
+ "--one-line", "thread backtrace all",
+ "--one-line", "bt all",
+ "--one-line-on-crash", "bt",
+ "--one-line-on-crash", "quit", NULL };
+
+ args_lldb[2] = gimp_pid;
+
+ execvp (args_lldb[0], args_lldb);
+ }
+
+ _exit (0);
+ }
+ else if (fork_pid > 0)
+ {
+ /* Main process */
+ int status;
+
+ /* Allow the child to ptrace us, and signal it to start. */
+ close (sync_fd[0]);
+#ifdef PR_SET_PTRACER
+ prctl (PR_SET_PTRACER, fork_pid, 0, 0, 0);
+#endif
+ close (sync_fd[1]);
+
+ /* It is important to close the writing side of the pipe, otherwise
+ * the read() will wait forever without getting the information that
+ * writing is finished.
+ */
+ close (out_fd[1]);
+
+ while ((read_n = read (out_fd[0], buffer, 256)) != 0)
+ {
+ if (read_n < 0)
+ {
+ /* LLDB on macOS seems to trigger a few EINTR error (see
+ * !13), though read() finally ends up working later. So
+ * let's not make this error fatal, and instead try again.
+ * Yet to avoid infinite loop (in case the error really
+ * happens at every call), we abandon after a few
+ * consecutive errors.
+ */
+ if (errno == EINTR && eintr_count <= 5)
+ {
+ eintr_count++;
+ continue;
+ }
+ break;
+ }
+ eintr_count = 0;
+ if (! stack_printed)
+ {
+#if defined(PLATFORM_OSX)
+ if (stream)
+ g_fprintf (stream,
+ "\n# Stack traces obtained from PID %d - Thread 0x%lx #\n\n",
+ pid, tid);
+#elif defined(G_OS_WIN32) || defined(SYS_gettid) || defined(HAVE_THR_SELF)
+ if (stream)
+ g_fprintf (stream,
+ "\n# Stack traces obtained from PID %d - Thread %lu #\n\n",
+ pid, tid);
+#endif
+ if (trace)
+ {
+ gtrace = g_string_new (NULL);
+#if defined(PLATFORM_OSX)
+ g_string_printf (gtrace,
+ "\n# Stack traces obtained from PID %d - Thread 0x%lx #\n\n",
+ pid, tid);
+#elif defined(G_OS_WIN32) || defined(SYS_gettid) || defined(HAVE_THR_SELF)
+ g_string_printf (gtrace,
+ "\n# Stack traces obtained from PID %d - Thread %lu #\n\n",
+ pid, tid);
+#endif
+ }
+ }
+ /* It's hard to know if the debugger was found since it
+ * happened in the child. Let's just assume that any output
+ * means it succeeded.
+ */
+ stack_printed = TRUE;
+
+ buffer[read_n] = '\0';
+ if (stream)
+ g_fprintf (stream, "%s", buffer);
+ if (trace)
+ g_string_append (gtrace, (const gchar *) buffer);
+ }
+ close (out_fd[0]);
+
+#ifdef PR_SET_PTRACER
+ /* Clear ptrace permission set above */
+ prctl (PR_SET_PTRACER, 0, 0, 0, 0);
+#endif
+
+ waitpid (fork_pid, &status, 0);
+ }
+ /* else if (fork_pid == (pid_t) -1)
+ * Fork failed!
+ * Just continue, maybe the backtrace() API will succeed.
+ */
+
+#ifdef HAVE_EXECINFO_H
+ if (! stack_printed)
+ {
+ /* As a last resort, try using the backtrace() Linux API. It is a bit
+ * less fancy than gdb or lldb, which is why it is not given priority.
+ */
+ void *bt_buf[100];
+ int n_symbols;
+
+ n_symbols = backtrace (bt_buf, 100);
+ if (trace && n_symbols)
+ {
+ char **symbols;
+ int i;
+
+ symbols = backtrace_symbols (bt_buf, n_symbols);
+ if (symbols)
+ {
+ for (i = 0; i < n_symbols; i++)
+ {
+ if (stream)
+ g_fprintf (stream, "%s\n", (const gchar *) symbols[i]);
+ if (trace)
+ {
+ if (! gtrace)
+ gtrace = g_string_new (NULL);
+ g_string_append (gtrace,
+ (const gchar *) symbols[i]);
+ g_string_append_c (gtrace, '\n');
+ }
+ }
+ free (symbols);
+ }
+ }
+ else if (n_symbols)
+ {
+ /* This allows to generate traces without memory allocation.
+ * In some cases, this is necessary, especially during
+ * segfault-type crashes.
+ */
+ backtrace_symbols_fd (bt_buf, n_symbols, fileno ((FILE *) stream));
+ }
+ stack_printed = (n_symbols > 0);
+ }
+#endif /* HAVE_EXECINFO_H */
+
+ if (trace)
+ {
+ if (gtrace)
+ *trace = g_string_free (gtrace, FALSE);
+ else
+ *trace = NULL;
+ }
+#endif /* G_OS_WIN32 */
+
+ return stack_printed;
+}
+
+/**
+ * gimp_stack_trace_query:
+ * @prog_name: the program to attach to.
+ *
+ * This is mostly the same as g_on_error_query() except that we use our
+ * own backtrace function, much more complete.
+ * @prog_name must be the current program name (argv[0]).
+ * It does nothing on Win32.
+ *
+ * Since: 2.10
+ **/
+void
+gimp_stack_trace_query (const gchar *prog_name)
+{
+#ifndef G_OS_WIN32
+ gchar buf[16];
+
+ retry:
+
+ g_fprintf (stdout,
+ "%s (pid:%u): %s: ",
+ prog_name,
+ (guint) getpid (),
+ "[E]xit, show [S]tack trace or [P]roceed");
+ fflush (stdout);
+
+ if (isatty(0) && isatty(1))
+ fgets (buf, 8, stdin);
+ else
+ strcpy (buf, "E\n");
+
+ if ((buf[0] == 'E' || buf[0] == 'e')
+ && buf[1] == '\n')
+ _exit (0);
+ else if ((buf[0] == 'P' || buf[0] == 'p')
+ && buf[1] == '\n')
+ return;
+ else if ((buf[0] == 'S' || buf[0] == 's')
+ && buf[1] == '\n')
+ {
+ if (! gimp_stack_trace_print (prog_name, stdout, NULL))
+ g_fprintf (stderr, "%s\n", "Stack trace not available on your system.");
+ goto retry;
+ }
+ else
+ goto retry;
+#endif
+}
+
+
+/* Private functions. */
+
+static gboolean
+gimp_utils_generic_available (const gchar *program,
+ gint major,
+ gint minor)
+{
+#ifndef G_OS_WIN32
+ pid_t pid;
+ int out_fd[2];
+
+ if (pipe (out_fd) == -1)
+ {
+ return FALSE;
+ }
+
+ /* XXX: I don't use g_spawn_sync() or similar glib functions because
+ * to read the contents of the stdout, these functions would allocate
+ * memory dynamically. As we know, when debugging crashes, this is a
+ * definite blocker. So instead I simply use a buffer on the stack
+ * with a lower level fork() call.
+ */
+ pid = fork ();
+ if (pid == 0)
+ {
+ /* Child process. */
+ gchar *args[3] = { (gchar *) program, "--version", NULL };
+
+ /* Redirect the debugger output. */
+ dup2 (out_fd[1], STDOUT_FILENO);
+ close (out_fd[0]);
+ close (out_fd[1]);
+
+ /* Run version check. */
+ execvp (args[0], args);
+ _exit (-1);
+ }
+ else if (pid > 0)
+ {
+ /* Main process */
+ gchar buffer[256];
+ ssize_t read_n;
+ int status;
+ gint installed_major = 0;
+ gint installed_minor = 0;
+ gboolean major_reading = FALSE;
+ gboolean minor_reading = FALSE;
+ gint i;
+ gchar c;
+
+ waitpid (pid, &status, 0);
+
+ if (! WIFEXITED (status) || WEXITSTATUS (status) != 0)
+ return FALSE;
+
+ /* It is important to close the writing side of the pipe, otherwise
+ * the read() will wait forever without getting the information that
+ * writing is finished.
+ */
+ close (out_fd[1]);
+
+ /* I could loop forever until EOL, but I am pretty sure the
+ * version information is stored on the first line and one call to
+ * read() with 256 characters should be more than enough.
+ */
+ read_n = read (out_fd[0], buffer, 256);
+
+ /* This is quite a very stupid parser. I only look for the first
+ * numbers and consider them as version information. This works
+ * fine for both GDB and LLDB as far as I can see for the output
+ * of `${program} --version` but this should obviously not be
+ * considered as a *really* generic version test.
+ */
+ for (i = 0; i < read_n; i++)
+ {
+ c = buffer[i];
+ if (c >= '0' && c <= '9')
+ {
+ if (minor_reading)
+ {
+ installed_minor = 10 * installed_minor + (c - '0');
+ }
+ else
+ {
+ major_reading = TRUE;
+ installed_major = 10 * installed_major + (c - '0');
+ }
+ }
+ else if (c == '.')
+ {
+ if (major_reading)
+ {
+ minor_reading = TRUE;
+ major_reading = FALSE;
+ }
+ else if (minor_reading)
+ {
+ break;
+ }
+ }
+ else if (c == '\n')
+ {
+ /* Version information should be in the first line. */
+ break;
+ }
+ }
+ close (out_fd[0]);
+
+ return (installed_major > 0 &&
+ (installed_major > major ||
+ (installed_major == major && installed_minor >= minor)));
+ }
+#endif
+
+ /* Fork failed, or Win32. */
+ return FALSE;
+}
+
+static gboolean
+gimp_utils_gdb_available (gint major,
+ gint minor)
+{
+ return gimp_utils_generic_available ("gdb", major, minor);
+}