Clean up and modernize PurpleImage

Thu, 07 Aug 2025 21:32:18 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Thu, 07 Aug 2025 21:32:18 -0500
changeset 43300
0604c6839974
parent 43299
9454cc5cd5fb
child 43301
0e43dc8462e8

Clean up and modernize PurpleImage

Testing Done:
Ran the tests under valgrind and called in the turtles.

Reviewed at https://reviews.imfreedom.org/r/4074/

libpurple/image.c file | annotate | diff | comparison | revisions
libpurple/image.h file | annotate | diff | comparison | revisions
libpurple/meson.build file | annotate | diff | comparison | revisions
libpurple/purpleimage.c file | annotate | diff | comparison | revisions
libpurple/purpleimage.h file | annotate | diff | comparison | revisions
libpurple/tests/data/test-image.png file | annotate | diff | comparison | revisions
libpurple/tests/image/meson.build file | annotate | diff | comparison | revisions
libpurple/tests/image/test.png file | annotate | diff | comparison | revisions
libpurple/tests/image/test_image.c file | annotate | diff | comparison | revisions
libpurple/tests/image/test_image.gresource.xml file | annotate | diff | comparison | revisions
libpurple/tests/meson.build file | annotate | diff | comparison | revisions
libpurple/tests/test_image.c file | annotate | diff | comparison | revisions
pidgin/pidginaccounteditor.c file | annotate | diff | comparison | revisions
po/POTFILES.in file | annotate | diff | comparison | revisions
--- a/libpurple/image.c	Thu Jul 31 20:57:19 2025 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,463 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here. Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * This library is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "debug.h"
-#include "image.h"
-#include "util.h"
-
-typedef struct {
-	gchar *path;
-
-	GBytes *contents;
-
-	const gchar *extension;
-	const gchar *mime;
-	gchar *gen_filename;
-	gchar *friendly_filename;
-} PurpleImagePrivate;
-
-enum {
-	PROP_0,
-	PROP_PATH,
-	PROP_CONTENTS,
-	PROP_SIZE,
-	N_PROPERTIES,
-};
-
-static GParamSpec *properties[N_PROPERTIES] = {NULL, };
-
-G_DEFINE_TYPE_WITH_PRIVATE(PurpleImage, purple_image, G_TYPE_OBJECT);
-
-/******************************************************************************
- * Helpers
- ******************************************************************************/
-static void
-_purple_image_set_path(PurpleImage *image, const gchar *path) {
-	PurpleImagePrivate *priv = purple_image_get_instance_private(image);
-
-	g_set_str(&priv->path, path);
-}
-
-static void
-_purple_image_set_contents(PurpleImage *image, GBytes *bytes) {
-	PurpleImagePrivate *priv = purple_image_get_instance_private(image);
-
-	if(priv->contents)
-		g_bytes_unref(priv->contents);
-
-	priv->contents = (bytes) ? g_bytes_ref(bytes) : NULL;
-}
-
-/******************************************************************************
- * Object stuff
- ******************************************************************************/
-static void
-purple_image_init(G_GNUC_UNUSED PurpleImage *image) {
-}
-
-static void
-purple_image_finalize(GObject *obj) {
-	PurpleImage *image = PURPLE_IMAGE(obj);
-	PurpleImagePrivate *priv = purple_image_get_instance_private(image);
-
-	if(priv->contents)
-		g_bytes_unref(priv->contents);
-
-	g_free(priv->path);
-	g_free(priv->gen_filename);
-	g_free(priv->friendly_filename);
-
-	G_OBJECT_CLASS(purple_image_parent_class)->finalize(obj);
-}
-
-static void
-purple_image_set_property(GObject *obj, guint param_id,
-                          const GValue *value, GParamSpec *pspec)
-{
-	PurpleImage *image = PURPLE_IMAGE(obj);
-
-	switch (param_id) {
-		case PROP_PATH:
-			_purple_image_set_path(image, g_value_get_string(value));
-			break;
-		case PROP_CONTENTS:
-			_purple_image_set_contents(image, g_value_get_boxed(value));
-			break;
-		default:
-			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-			break;
-	}
-}
-
-static void
-purple_image_get_property(GObject *obj, guint param_id, GValue *value,
-                          GParamSpec *pspec)
-{
-	PurpleImage *image = PURPLE_IMAGE(obj);
-
-	switch (param_id) {
-		case PROP_PATH:
-			g_value_set_string(value, purple_image_get_path(image));
-			break;
-		case PROP_CONTENTS:
-			g_value_set_boxed(value, purple_image_get_contents(image));
-			break;
-		case PROP_SIZE:
-			g_value_set_uint64(value, purple_image_get_data_size(image));
-			break;
-		default:
-			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-			break;
-	}
-}
-
-static void
-purple_image_class_init(PurpleImageClass *klass) {
-	GObjectClass *gobj_class = G_OBJECT_CLASS(klass);
-
-	gobj_class->finalize = purple_image_finalize;
-	gobj_class->get_property = purple_image_get_property;
-	gobj_class->set_property = purple_image_set_property;
-
-	/**
-	 * PurpleImage:path:
-	 *
-	 * The file path for the image if one was provided.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_PATH] = g_param_spec_string(
-		"path", NULL, NULL,
-		NULL,
-		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * PurpleImage:contents:
-	 *
-	 * The contents of the image.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_CONTENTS] = g_param_spec_boxed(
-		"contents", NULL, NULL,
-		G_TYPE_BYTES,
-		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * PurpleImage:size:
-	 *
-	 * The size of the image in bytes.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_SIZE] = g_param_spec_uint64(
-		"size", NULL, NULL,
-		0, G_MAXUINT64, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	g_object_class_install_properties(gobj_class, N_PROPERTIES, properties);
-}
-
-/******************************************************************************
- * API
- ******************************************************************************/
-PurpleImage *
-purple_image_new_from_bytes(GBytes *bytes) {
-	return g_object_new(
-		PURPLE_TYPE_IMAGE,
-		"contents", bytes,
-		NULL
-	);
-}
-
-PurpleImage *
-purple_image_new_from_file(const gchar *path, GError **error) {
-	PurpleImage *image = NULL;
-	GBytes *bytes = NULL;
-	gchar *contents = NULL;
-	gsize length = 0;
-
-	if(!g_file_get_contents(path, &contents, &length, error)) {
-		return NULL;
-	}
-
-	bytes = g_bytes_new_take(contents, length);
-
-	image = g_object_new(
-		PURPLE_TYPE_IMAGE,
-		"contents", bytes,
-		"path", path,
-		NULL
-	);
-
-	g_bytes_unref(bytes);
-
-	return image;
-}
-
-PurpleImage *
-purple_image_new_from_data(const guint8 *data, gsize length) {
-	PurpleImage *image;
-	GBytes *bytes = NULL;
-
-	bytes = g_bytes_new(data, length);
-
-	image = purple_image_new_from_bytes(bytes);
-
-	g_bytes_unref(bytes);
-
-	return image;
-}
-
-PurpleImage *
-purple_image_new_take_data(guint8 *data, gsize length) {
-	PurpleImage *image;
-	GBytes *bytes = NULL;
-
-	bytes = g_bytes_new_take(data, length);
-
-	image = purple_image_new_from_bytes(bytes);
-
-	g_bytes_unref(bytes);
-
-	return image;
-}
-
-gboolean
-purple_image_save(PurpleImage *image, const gchar *path) {
-	PurpleImagePrivate *priv = NULL;
-	gconstpointer data;
-	gsize len;
-	gboolean succ;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), FALSE);
-	g_return_val_if_fail(path != NULL, FALSE);
-	g_return_val_if_fail(path[0] != '\0', FALSE);
-
-	priv = purple_image_get_instance_private(image);
-	data = purple_image_get_data(image);
-	len = purple_image_get_data_size(image);
-
-	g_return_val_if_fail(data != NULL, FALSE);
-	g_return_val_if_fail(len > 0, FALSE);
-
-	succ = g_file_set_contents(path, data, len, NULL);
-	if (succ && priv->path == NULL)
-		priv->path = g_strdup(path);
-
-	return succ;
-}
-
-GBytes *
-purple_image_get_contents(PurpleImage *image)
-{
-	PurpleImagePrivate *priv = NULL;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
-
-	priv = purple_image_get_instance_private(image);
-
-	if(priv->contents)
-		return g_bytes_ref(priv->contents);
-
-	return NULL;
-}
-
-const gchar *
-purple_image_get_path(PurpleImage *image) {
-	PurpleImagePrivate *priv = NULL;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
-
-	priv = purple_image_get_instance_private(image);
-
-	return priv->path ? priv->path : purple_image_generate_filename(image);
-}
-
-gsize
-purple_image_get_data_size(PurpleImage *image) {
-	PurpleImagePrivate *priv;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), 0);
-
-	priv = purple_image_get_instance_private(image);
-
-	if(priv->contents)
-		return g_bytes_get_size(priv->contents);
-
-	return 0;
-}
-
-gconstpointer
-purple_image_get_data(PurpleImage *image) {
-	PurpleImagePrivate *priv = NULL;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
-
-	priv = purple_image_get_instance_private(image);
-
-	if(priv->contents)
-		return g_bytes_get_data(priv->contents, NULL);
-
-	return NULL;
-}
-
-const gchar *
-purple_image_get_extension(PurpleImage *image) {
-	PurpleImagePrivate *priv = NULL;
-	gconstpointer data;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
-
-	priv = purple_image_get_instance_private(image);
-
-	if (priv->extension)
-		return priv->extension;
-
-	if (purple_image_get_data_size(image) < 4)
-		return NULL;
-
-	data = purple_image_get_data(image);
-	g_assert(data != NULL);
-
-	if (memcmp(data, "GIF8", 4) == 0)
-		return priv->extension = "gif";
-	if (memcmp(data, "\xff\xd8\xff", 3) == 0) /* 4th may be e0 through ef */
-		return priv->extension = "jpg";
-	if (memcmp(data, "\x89PNG", 4) == 0)
-		return priv->extension = "png";
-	if (memcmp(data, "MM", 2) == 0)
-		return priv->extension = "tif";
-	if (memcmp(data, "II", 2) == 0)
-		return priv->extension = "tif";
-	if (memcmp(data, "BM", 2) == 0)
-		return priv->extension = "bmp";
-	if (memcmp(data, "\x00\x00\x01\x00", 4) == 0)
-		return priv->extension = "ico";
-
-	return NULL;
-}
-
-const gchar *
-purple_image_get_mimetype(PurpleImage *image) {
-	PurpleImagePrivate *priv = NULL;
-	const gchar *ext = purple_image_get_extension(image);
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
-
-	priv = purple_image_get_instance_private(image);
-
-	if (priv->mime)
-		return priv->mime;
-
-	g_return_val_if_fail(ext != NULL, NULL);
-
-	if (g_strcmp0(ext, "gif") == 0)
-		return priv->mime = "image/gif";
-	if (g_strcmp0(ext, "jpg") == 0)
-		return priv->mime = "image/jpeg";
-	if (g_strcmp0(ext, "png") == 0)
-		return priv->mime = "image/png";
-	if (g_strcmp0(ext, "tif") == 0)
-		return priv->mime = "image/tiff";
-	if (g_strcmp0(ext, "bmp") == 0)
-		return priv->mime = "image/bmp";
-	if (g_strcmp0(ext, "ico") == 0)
-		return priv->mime = "image/vnd.microsoft.icon";
-
-	return NULL;
-}
-
-const gchar *
-purple_image_generate_filename(PurpleImage *image) {
-	PurpleImagePrivate *priv = NULL;
-	gconstpointer data;
-	gsize len;
-	const gchar *ext = NULL;
-	gchar *checksum;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
-
-	priv = purple_image_get_instance_private(image);
-
-	if (priv->gen_filename)
-		return priv->gen_filename;
-
-	/* grab the image's data and size of that data */
-	data = purple_image_get_data(image);
-	len = purple_image_get_data_size(image);
-
-	/* create a checksum of it and use it as the start of our filename */
-	checksum = g_compute_checksum_for_data(G_CHECKSUM_SHA1, data, len);
-
-	/* if the image has a known format, set the extension appropriately */
-	ext = purple_image_get_extension(image);
-	if(ext != NULL) {
-		priv->gen_filename = g_strdup_printf("%s.%s", checksum, ext);
-		g_free(checksum);
-	} else {
-		priv->gen_filename = checksum;
-	}
-
-	return priv->gen_filename;
-}
-
-void
-purple_image_set_friendly_filename(PurpleImage *image, const gchar *filename) {
-	PurpleImagePrivate *priv = NULL;
-	gchar *newname;
-	const gchar *escaped;
-
-	g_return_if_fail(PURPLE_IS_IMAGE(image));
-
-	priv = purple_image_get_instance_private(image);
-
-	newname = g_path_get_basename(filename);
-	escaped = purple_escape_filename(newname);
-	g_free(newname);
-	newname = NULL;
-
-	if (g_strcmp0(escaped, "") == 0 || g_strcmp0(escaped, ".") == 0 ||
-		g_strcmp0(escaped, G_DIR_SEPARATOR_S) == 0 ||
-		g_strcmp0(escaped, "/") == 0 || g_strcmp0(escaped, "\\") == 0)
-	{
-		escaped = NULL;
-	}
-
-	g_set_str(&priv->friendly_filename, escaped);
-}
-
-const gchar *
-purple_image_get_friendly_filename(PurpleImage *image) {
-	PurpleImagePrivate *priv = NULL;
-
-	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
-
-	priv = purple_image_get_instance_private(image);
-
-	if(priv->friendly_filename) {
-		return priv->friendly_filename;
-	}
-
-	return purple_image_generate_filename(image);
-}
- 
--- a/libpurple/image.h	Thu Jul 31 20:57:19 2025 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,274 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here. Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * This library is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
-# error "only <purple.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IMAGE_H
-#define PURPLE_IMAGE_H
-
-#include <glib-object.h>
-
-#include "purpleversion.h"
-
-#define PURPLE_TYPE_IMAGE  purple_image_get_type()
-
-struct _PurpleImageClass {
-	/*< private >*/
-	GObjectClass parent_class;
-
-	void (*purple_reserved1)(void);
-	void (*purple_reserved2)(void);
-	void (*purple_reserved3)(void);
-	void (*purple_reserved4)(void);
-};
-
-G_BEGIN_DECLS
-
-/**
- * PurpleImage:
- *
- * #PurpleImage object is a container for raw image data. It doesn't manipulate
- * image data, just stores it in its binary format - png, jpeg etc. Thus, it's
- * totally independent from the UI.
- *
- * This class also provides certain file-related features, like: friendly
- * filenames (not necessarily real filename for displaying); remote images
- * (which data is not yet loaded) or guessing file format from its header.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-G_DECLARE_DERIVABLE_TYPE(PurpleImage, purple_image, PURPLE, IMAGE, GObject)
-
-/**
- * purple_image_new_from_bytes:
- * @bytes: (transfer none): A #GBytes containing the raw image data.
- *
- * Loads a raw image data as a new #PurpleImage object.
- *
- * Returns: the new #PurpleImage.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-PurpleImage *purple_image_new_from_bytes(GBytes *bytes);
-
-/**
- * purple_image_new_from_file:
- * @path: the path to the image file.
- * @error: Return address for a #GError, or %NULL.
- *
- * Loads an image file as a new #PurpleImage object. The @path must exists, be
- * readable and should point to a valid image file. If you don't set @be_eager
- * parameter, there will be a risk that file will be removed from disk before
- * you access its data.
- *
- * Returns: the new #PurpleImage.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-PurpleImage *purple_image_new_from_file(const gchar *path, GError **error);
-
-/**
- * purple_image_new_from_data:
- * @data: the pointer to the image data buffer.
- * @length: the length of @data.
- *
- * Creates a new #PurpleImage object with contents of @data buffer.
- *
- * The @data buffer is owned by #PurpleImage object, so you might want
- * to g_memdup2() it first.
- *
- * Returns: the new #PurpleImage.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-PurpleImage *purple_image_new_from_data(const guint8 *data, gsize length);
-
-/**
- * purple_image_new_take_data:
- * @data: (transfer full): the pointer to the image data buffer.
- * @length: the length of @data.
- *
- * Creates a new #PurpleImage object with contents of @data buffer.
- *
- * The @data buffer is owned by #PurpleImage object, so you might want
- * to g_memdup2() it first.
- *
- * Returns: the new #PurpleImage.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-PurpleImage *purple_image_new_take_data(guint8 *data, gsize length);
-
-/**
- * purple_image_save:
- * @image: the image.
- * @path: destination of a saved image file.
- *
- * Saves an @image to the disk.
- *
- * Returns: %TRUE if succeeded, %FALSE otherwise.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-gboolean purple_image_save(PurpleImage *image, const gchar *path);
-
-/**
- * purple_image_get_contents:
- * @image: The #PurpleImage.
- *
- * Returns a new reference to the #GBytes that contains the image data.
- *
- * Returns: (transfer full): A #GBytes containing the image data.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-GBytes *purple_image_get_contents(PurpleImage *image);
-
-
-/**
- * purple_image_get_path:
- * @image: the image.
- *
- * Returns the physical path of the @image file. It is set only, if the @image is
- * really backed by an existing file. In the other case it returns %NULL.
- *
- * Returns: the physical path of the @image, or %NULL.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-const gchar *purple_image_get_path(PurpleImage *image);
-
-/**
- * purple_image_get_data_size:
- * @image: the image.
- *
- * Returns the size of @image's data.
- *
- * Returns: the size of data, or 0 in case of failure.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-gsize purple_image_get_data_size(PurpleImage *image);
-
-/**
- * purple_image_get_data:
- * @image: the image.
- *
- * Returns the pointer to the buffer containing image data.
- *
- * Returns: (transfer none): the @image data.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-gconstpointer purple_image_get_data(PurpleImage *image);
-
-/**
- * purple_image_get_extension:
- * @image: the image.
- *
- * Guesses the @image format based on its contents.
- *
- * Returns: (transfer none): the file extension suitable for @image format.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-const gchar *purple_image_get_extension(PurpleImage *image);
-
-/**
- * purple_image_get_mimetype:
- * @image: the image.
- *
- * Guesses the @image mime-type based on its contents.
- *
- * Returns: (transfer none): the mime-type suitable for @image format.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-const gchar *purple_image_get_mimetype(PurpleImage *image);
-
-/**
- * purple_image_generate_filename:
- * @image: the image.
- *
- * Calculates almost-unique filename by computing checksum from file contents
- * and appending a suitable extension. You should not assume the checksum
- * is SHA-1, because it may change in the future.
- *
- * Returns: (transfer none): the generated file name.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-const gchar *purple_image_generate_filename(PurpleImage *image);
-
-/**
- * purple_image_set_friendly_filename:
- * @image: the image.
- * @filename: the friendly filename.
- *
- * Sets the "friendly filename" for the @image. This don't have to be a real
- * name, because it's used for displaying or as a default file name when the
- * user wants to save the @image to the disk.
- *
- * The provided @filename may either be a full path, or contain
- * filesystem-unfriendly characters, because it will be reformatted.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-void purple_image_set_friendly_filename(PurpleImage *image, const gchar *filename);
-
-/**
- * purple_image_get_friendly_filename:
- * @image: the image.
- *
- * Returns the "friendly filename" for the @image, to be displayed or used as
- * a default name when saving a file to the disk.
- * See #purple_image_set_friendly_filename.
- *
- * If the friendly filename was not set, it will be generated with
- * #purple_image_generate_filename.
- *
- * Returns: (transfer none): the friendly filename.
- *
- * Since: 3.0
- */
-PURPLE_AVAILABLE_IN_3_0
-const gchar *purple_image_get_friendly_filename(PurpleImage *image);
-
-G_END_DECLS
-
-#endif /* PURPLE_IMAGE_H */
--- a/libpurple/meson.build	Thu Jul 31 20:57:19 2025 -0500
+++ b/libpurple/meson.build	Thu Aug 07 21:32:18 2025 -0500
@@ -2,7 +2,6 @@
 	'accounts.c',
 	'core.c',
 	'debug.c',
-	'image.c',
 	'network.c',
 	'plugins.c',
 	'prefs.c',
@@ -41,6 +40,7 @@
 	'purplehistoryadapter.c',
 	'purplehistorymanager.c',
 	'purpleidlemanager.c',
+	'purpleimage.c',
 	'purplekeyvaluepair.c',
 	'purplemarkup.c',
 	'purplemenu.c',
@@ -97,7 +97,6 @@
 	'accounts.h',
 	'core.h',
 	'debug.h',
-	'image.h',
 	'network.h',
 	'plugins.h',
 	'prefs.h',
@@ -136,6 +135,7 @@
 	'purplehistoryadapter.h',
 	'purplehistorymanager.h',
 	'purpleidlemanager.h',
+	'purpleimage.h',
 	'purplekeyvaluepair.h',
 	'purplemarkup.h',
 	'purplemenu.h',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purpleimage.c	Thu Aug 07 21:32:18 2025 -0500
@@ -0,0 +1,332 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here. Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <birb.h>
+
+#include "purpleimage.h"
+
+struct _PurpleImage {
+	GObject parent;
+
+	char *filename;
+
+	GBytes *contents;
+};
+
+enum {
+	PROP_0,
+	PROP_CONTENTS,
+	PROP_DATA,
+	PROP_FILENAME,
+	PROP_SIZE,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_image_set_filename(PurpleImage *image, const char *filename) {
+	g_return_if_fail(PURPLE_IS_IMAGE(image));
+
+	if(g_set_str(&image->filename, filename)) {
+		g_object_notify_by_pspec(G_OBJECT(image), properties[PROP_FILENAME]);
+	}
+}
+
+static void
+purple_image_set_contents(PurpleImage *image, GBytes *contents) {
+	g_return_if_fail(PURPLE_IS_IMAGE(image));
+
+	if(image->contents != contents) {
+		GObject *obj = G_OBJECT(image);
+
+		g_clear_pointer(&image->contents, g_bytes_unref);
+		if(contents != NULL) {
+			image->contents = g_bytes_ref(contents);
+		}
+
+		g_object_freeze_notify(obj);
+		g_object_notify_by_pspec(G_OBJECT(image), properties[PROP_CONTENTS]);
+		g_object_notify_by_pspec(G_OBJECT(image), properties[PROP_DATA]);
+		g_object_notify_by_pspec(G_OBJECT(image), properties[PROP_SIZE]);
+		g_object_thaw_notify(obj);
+	}
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(PurpleImage, purple_image, G_TYPE_OBJECT);
+
+static void
+purple_image_finalize(GObject *obj) {
+	PurpleImage *image = PURPLE_IMAGE(obj);
+
+	g_clear_pointer(&image->contents, g_bytes_unref);
+	g_clear_pointer(&image->filename, g_free);
+
+	G_OBJECT_CLASS(purple_image_parent_class)->finalize(obj);
+}
+
+static void
+purple_image_get_property(GObject *obj, guint param_id, GValue *value,
+                          GParamSpec *pspec)
+{
+	PurpleImage *image = PURPLE_IMAGE(obj);
+	gsize size = 0;
+
+	switch (param_id) {
+	case PROP_CONTENTS:
+		g_value_set_boxed(value, purple_image_get_contents(image));
+		break;
+	case PROP_DATA:
+		g_value_set_pointer(value,
+		                    (gpointer)purple_image_get_data(image, NULL));
+		break;
+	case PROP_FILENAME:
+		g_value_set_string(value, purple_image_get_filename(image));
+		break;
+	case PROP_SIZE:
+		purple_image_get_data(image, &size);
+		g_value_set_uint64(value, size);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_image_set_property(GObject *obj, guint param_id, const GValue *value,
+                          GParamSpec *pspec)
+{
+	PurpleImage *image = PURPLE_IMAGE(obj);
+
+	switch (param_id) {
+	case PROP_FILENAME:
+		purple_image_set_filename(image, g_value_get_string(value));
+		break;
+	case PROP_CONTENTS:
+		purple_image_set_contents(image, g_value_get_boxed(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_image_init(G_GNUC_UNUSED PurpleImage *image) {
+}
+
+static void
+purple_image_class_init(PurpleImageClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_image_finalize;
+	obj_class->get_property = purple_image_get_property;
+	obj_class->set_property = purple_image_set_property;
+
+	/**
+	 * PurpleImage:contents:
+	 *
+	 * The contents of the image.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_CONTENTS] = g_param_spec_boxed(
+		"contents", NULL, NULL,
+		G_TYPE_BYTES,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
+		G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleImage:data:
+	 *
+	 * The raw image data.
+	 *
+	 * Generally the [property@Image:contents] property should be used, but if
+	 * just the data is necessary this saves a step.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_DATA] = g_param_spec_pointer(
+		"data", NULL, NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleImage:filename:
+	 *
+	 * The filename for the image if one was provided.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_FILENAME] = g_param_spec_string(
+		"filename", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
+		G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleImage:size:
+	 *
+	 * The size of the image in bytes.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_SIZE] = g_param_spec_uint64(
+		"size", NULL, NULL,
+		0, G_MAXUINT64, 0,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+GBytes *
+purple_image_get_contents(PurpleImage *image) {
+	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
+
+	return image->contents;
+}
+
+gconstpointer
+purple_image_get_data(PurpleImage *image, gsize *size) {
+	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
+
+	if(image->contents != NULL) {
+		return g_bytes_get_data(image->contents, size);
+	}
+
+	return NULL;
+}
+
+const char *
+purple_image_get_filename(PurpleImage *image) {
+	g_return_val_if_fail(PURPLE_IS_IMAGE(image), NULL);
+
+	return image->filename;
+}
+
+PurpleImage *
+purple_image_new_from_bytes(GBytes *bytes) {
+	g_return_val_if_fail(bytes != NULL, NULL);
+
+	return g_object_new(
+		PURPLE_TYPE_IMAGE,
+		"contents", bytes,
+		NULL);
+}
+
+PurpleImage *
+purple_image_new_from_data(gconstpointer data, gsize size) {
+	PurpleImage *image = NULL;
+	GBytes *contents = NULL;
+
+	g_return_val_if_fail(data != NULL, NULL);
+	g_return_val_if_fail(size > 0, NULL);
+
+	contents = g_bytes_new(data, size);
+
+	image = g_object_new(
+		PURPLE_TYPE_IMAGE,
+		"contents", contents,
+		NULL);
+
+	g_bytes_unref(contents);
+
+	return image;
+}
+
+PurpleImage *
+purple_image_new_from_filename(const char *filename, GError **error) {
+	PurpleImage *image = NULL;
+	GBytes *bytes = NULL;
+	GError *local_error = NULL;
+	char *contents = NULL;
+	gsize length = 0;
+
+	if(!g_file_get_contents(filename, &contents, &length, &local_error)) {
+		g_propagate_error(error, local_error);
+
+		return NULL;
+	}
+
+	bytes = g_bytes_new_take(contents, length);
+
+	image = g_object_new(
+		PURPLE_TYPE_IMAGE,
+		"contents", bytes,
+		"filename", filename,
+		NULL
+	);
+
+	g_bytes_unref(bytes);
+
+	return image;
+}
+
+PurpleImage *
+purple_image_new_from_resource(const char *path, GError **error) {
+	PurpleImage *image = NULL;
+	GBytes *contents = NULL;
+	GError *local_error = NULL;
+
+	g_return_val_if_fail(!birb_str_is_empty(path), NULL);
+
+	contents = g_resources_lookup_data(path, G_RESOURCE_LOOKUP_FLAGS_NONE,
+	                                   &local_error);
+
+	if(local_error != NULL) {
+		g_clear_pointer(&contents, g_bytes_unref);
+
+		g_propagate_error(error, local_error);
+
+		return NULL;
+	}
+
+	image = g_object_new(
+		PURPLE_TYPE_IMAGE,
+		"contents", contents,
+		NULL);
+
+	g_clear_pointer(&contents, g_bytes_unref);
+
+	return image;
+}
+
+gboolean
+purple_image_save(PurpleImage *image, const char *filename, GError **error) {
+	gconstpointer data = NULL;
+	gsize size = 0;
+
+	g_return_val_if_fail(PURPLE_IS_IMAGE(image), FALSE);
+	g_return_val_if_fail(!birb_str_is_empty(filename), FALSE);
+
+	data = g_bytes_get_data(image->contents, &size);
+
+	return g_file_set_contents(filename, data, size, error);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purpleimage.h	Thu Aug 07 21:32:18 2025 -0500
@@ -0,0 +1,163 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here. Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_IMAGE_H
+#define PURPLE_IMAGE_H
+
+#include <glib-object.h>
+
+#include "purpleversion.h"
+
+
+G_BEGIN_DECLS
+
+#define PURPLE_TYPE_IMAGE (purple_image_get_type())
+
+/**
+ * PurpleImage:
+ *
+ * A container for raw image data. It doesn't manipulate the image data, it
+ * just stores it in its binary format - png, jpeg etc.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+G_DECLARE_FINAL_TYPE(PurpleImage, purple_image, PURPLE, IMAGE, GObject)
+
+/**
+ * purple_image_get_contents:
+ *
+ * Gets the contents of the image.
+ *
+ * Returns: (transfer none): The contents.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GBytes *purple_image_get_contents(PurpleImage *image);
+
+/**
+ * purple_image_get_data:
+ * @size: (out) (nullable): a return address for the length of the data
+ *
+ * Gets the data of the image.
+ *
+ * Optionally the size can be returned as well via @size parameter.
+ *
+ * Returns: (transfer none): The data.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gconstpointer purple_image_get_data(PurpleImage *image, gsize *size);
+
+/**
+ * purple_image_get_filename:
+ *
+ * Gets the filename to the image
+ *
+ * Returns: (nullable): The filename of the image.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_image_get_filename(PurpleImage *image);
+
+/**
+ * purple_image_new_from_bytes:
+ * @bytes: (transfer none): the new image data
+ *
+ * Creates an image from bytes.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleImage *purple_image_new_from_bytes(GBytes *bytes);
+
+/**
+ * purple_image_new_from_data:
+ * @data: (transfer none): the raw image data
+ * @size: the size of the raw image data
+ *
+ * Creates a new image from raw data.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleImage *purple_image_new_from_data(gconstpointer data, gsize size);
+
+/**
+ * purple_image_new_from_filename:
+ * @filename: the filename of the image file
+ * @error: (out) (nullable): a return address for a #GError
+ *
+ * Creates an image from a file.
+ *
+ * The @filename must exist, be readable, and have valid image contents.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleImage *purple_image_new_from_filename(const char *filename, GError **error);
+
+/**
+ * purple_image_new_from_resource:
+ * @path: the path of the resource
+ * @error: (out) (nullable): a return address for a #GError
+ *
+ * Creates a new image from a resource.
+ *
+ * Returns: (transfer full) (nullable): The new image on success; otherwise
+ *          null with @error set.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleImage *purple_image_new_from_resource(const char *path, GError **error);
+
+/**
+ * purple_image_save:
+ * @filename: the filename to save to
+ * @error: (out) (nullable): a return address for a #GError
+ *
+ * Saves an @image to the disk.
+ *
+ * Returns: %TRUE if succeeded, %FALSE otherwise.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_image_save(PurpleImage *image, const char *filename, GError **error);
+
+G_END_DECLS
+
+#endif /* PURPLE_IMAGE_H */
Binary file libpurple/tests/data/test-image.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/image/meson.build	Thu Aug 07 21:32:18 2025 -0500
@@ -0,0 +1,20 @@
+TEST_PURPLE_IMAGE_SOURCES = [
+  'test_image.c'
+]
+
+TEST_PURPLE_IMAGE_RESOURCES = gnome.compile_resources(
+  'test_image_resources',
+  'test_image.gresource.xml',
+  source_dir : '.',
+  c_name : 'test_image')
+TEST_PURPLE_IMAGE_SOURCES += TEST_PURPLE_IMAGE_RESOURCES
+
+test_image = executable(
+  'test_image',
+  TEST_PURPLE_IMAGE_SOURCES,
+  c_args : [
+    '-DTEST_DATA_DIR="@0@"'.format(meson.current_source_dir()),
+  ],
+  dependencies : [libpurple_dep, glib])
+
+test('image', test_image)
Binary file libpurple/tests/image/test.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/image/test_image.c	Thu Aug 07 21:32:18 2025 -0500
@@ -0,0 +1,170 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here. Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <birb.h>
+
+#include <purple.h>
+
+// generated via:
+// $ cat test-image.png | hexdump -v -e '1 1 "0x%02x," " "' | xargs -n 8 echo
+static gsize test_image_data_len = 160;
+static const guint8 test_image_data[] = {
+	0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
+	0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
+	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
+	0x08, 0x02, 0x00, 0x00, 0x00, 0xfd, 0xd4, 0x9a,
+	0x73, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59,
+	0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b,
+	0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00,
+	0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xe0,
+	0x0a, 0x02, 0x16, 0x30, 0x22, 0x28, 0xa4, 0xc9,
+	0xdd, 0x00, 0x00, 0x00, 0x1d, 0x69, 0x54, 0x58,
+	0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x72, 0x65,
+	0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74,
+	0x68, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x64, 0x2e,
+	0x65, 0x07, 0x00, 0x00, 0x00, 0x16, 0x49, 0x44,
+	0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff,
+	0x3f, 0x03, 0x03, 0x03, 0xe3, 0xb3, 0x4c, 0xb5,
+	0x9b, 0x4e, 0x0b, 0x00, 0x2f, 0xa9, 0x06, 0x2f,
+	0x8a, 0xd1, 0xc6, 0xb3, 0x00, 0x00, 0x00, 0x00,
+	0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+test_image(PurpleImage *image, const char *filename,
+           const guint8 *expected_data, gsize expected_size)
+{
+	GBytes *actual_contents = NULL;
+	gconstpointer actual_data = NULL;
+	gconstpointer contents_data = NULL;
+	char *actual_filename = NULL;
+	gsize actual_size = 0;
+	gsize contents_size = 0;
+
+	birb_assert_type(image, PURPLE_TYPE_IMAGE);
+
+	g_object_get(
+		G_OBJECT(image),
+		"contents", &actual_contents,
+		"data", &actual_data,
+		"filename", &actual_filename,
+		"size", &actual_size,
+		NULL);
+
+	g_assert_cmpmem(actual_data, actual_size,
+	                expected_data, expected_size);
+
+	g_assert_nonnull(actual_contents);
+	contents_data = g_bytes_get_data(actual_contents, &contents_size);
+	g_assert_cmpmem(contents_data, contents_size,
+	                expected_data, expected_size);
+	g_clear_pointer(&actual_contents, g_bytes_unref);
+
+	g_assert_cmpstr(actual_filename, ==, filename);
+	g_clear_pointer(&actual_filename, g_free);
+
+	g_assert_finalize_object(image);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_image_new_from_bytes(void) {
+	PurpleImage *image = NULL;
+	GBytes *contents = NULL;
+
+	contents = g_bytes_new((gconstpointer)test_image_data,
+	                       test_image_data_len);
+
+	image = purple_image_new_from_bytes(contents);
+	g_clear_pointer(&contents, g_bytes_unref);
+
+	test_image(image, NULL, (gconstpointer)test_image_data,
+	           test_image_data_len);
+}
+
+static void
+test_image_new_from_data(void) {
+	PurpleImage *image = NULL;
+
+	image = purple_image_new_from_data(test_image_data, test_image_data_len);
+
+	test_image(image, NULL, (gconstpointer)test_image_data,
+	           test_image_data_len);
+}
+
+static void
+test_image_new_from_filename(void) {
+	PurpleImage *image = NULL;
+	GError *error = NULL;
+	char *filename = NULL;
+	char *expected_data = NULL;
+	gsize expected_size = 0;
+
+	filename = g_build_filename(TEST_DATA_DIR, "test.png", NULL);
+	image = purple_image_new_from_filename(filename, &error);
+	g_assert_no_error(error);
+
+	g_file_get_contents(filename, &expected_data, &expected_size, &error);
+	g_assert_no_error(error);
+
+	test_image(image, filename, (gconstpointer)expected_data, expected_size);
+
+	g_clear_pointer(&filename, g_free);
+	g_clear_pointer(&expected_data, g_free);
+}
+
+static void
+test_image_new_from_resource(void) {
+	PurpleImage *image = NULL;
+	GError *error = NULL;
+
+	image = purple_image_new_from_resource("/im/pidgin/libpurple/tests/image/test.png",
+	                                       &error);
+	g_assert_no_error(error);
+
+	test_image(image, NULL, (gconstpointer)test_image_data,
+	           test_image_data_len);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char **argv) {
+	g_test_init(&argc, &argv, NULL);
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/image/new/from-bytes", test_image_new_from_bytes);
+	g_test_add_func("/image/new/from-data", test_image_new_from_data);
+	g_test_add_func("/image/new/from-filename", test_image_new_from_filename);
+	g_test_add_func("/image/new/from-resource", test_image_new_from_resource);
+
+	return g_test_run();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/image/test_image.gresource.xml	Thu Aug 07 21:32:18 2025 -0500
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/im/pidgin/libpurple/tests/image/">
+    <file>test.png</file>
+  </gresource>
+</gresources>
--- a/libpurple/tests/meson.build	Thu Jul 31 20:57:19 2025 -0500
+++ b/libpurple/tests/meson.build	Thu Aug 07 21:32:18 2025 -0500
@@ -27,7 +27,6 @@
     'history_adapter',
     'history_manager',
     'idle_manager',
-    'image',
     'keyvaluepair',
     'markup',
     'menu',
@@ -79,7 +78,6 @@
 foreach prog : PROGS
     e = executable(f'test_@prog@', f'test_@prog@.c',
                    c_args : [
-                       '-DTEST_DATA_DIR="@0@/data"'.format(meson.current_source_dir()),
                        '-DTEST_CACHE_DIR="@0@/cache"'.format(meson.current_build_dir()),
                    ],
                    dependencies : [libpurple_dep, glib],
@@ -91,3 +89,4 @@
 endforeach
 
 subdir('avatar')
+subdir('image')
--- a/libpurple/tests/test_image.c	Thu Jul 31 20:57:19 2025 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,173 +0,0 @@
-/*
- * Purple
- *
- * Purple is the legal property of its developers, whose names are too
- * numerous to list here. Please refer to the COPYRIGHT file distributed
- * with this source distribution
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or (at
- * your option) any later version.
- *
- * This program 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
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
- */
-
-#include <glib.h>
-#include <string.h>
-
-#include <purple.h>
-
-// generated via:
-// $ cat test-image.png | hexdump -v -e '1 1 "0x%02x," " "' | xargs -n 8 echo
-static const gsize test_image_data_len = 160;
-static const guint8 test_image_data[] = {
-	0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
-	0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
-	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
-	0x08, 0x02, 0x00, 0x00, 0x00, 0xfd, 0xd4, 0x9a,
-	0x73, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59,
-	0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b,
-	0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00,
-	0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xe0,
-	0x0a, 0x02, 0x16, 0x30, 0x22, 0x28, 0xa4, 0xc9,
-	0xdd, 0x00, 0x00, 0x00, 0x1d, 0x69, 0x54, 0x58,
-	0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
-	0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x72, 0x65,
-	0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74,
-	0x68, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x64, 0x2e,
-	0x65, 0x07, 0x00, 0x00, 0x00, 0x16, 0x49, 0x44,
-	0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff,
-	0x3f, 0x03, 0x03, 0x03, 0xe3, 0xb3, 0x4c, 0xb5,
-	0x9b, 0x4e, 0x0b, 0x00, 0x2f, 0xa9, 0x06, 0x2f,
-	0x8a, 0xd1, 0xc6, 0xb3, 0x00, 0x00, 0x00, 0x00,
-	0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
-};
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static void
-_test_image(PurpleImage *image,
-            const guint8 *edata,
-            gsize elen,
-            const char *path,
-            const char *ext,
-            const char *mimetype)
-{
-	GBytes *bytes = NULL;
-	const guint8 *adata = NULL;
-	gsize alen;
-
-	g_assert(PURPLE_IS_IMAGE(image));
-
-	bytes = purple_image_get_contents(image);
-	adata = g_bytes_get_data(bytes, &alen);
-	g_assert_cmpmem(adata, alen, edata, elen);
-	g_bytes_unref(bytes);
-
-	/* if the caller provided a path, check it, otherwise just make sure we
-	 * have something.
-	 */
-	if(path != NULL) {
-		g_assert_cmpstr(purple_image_get_path(image), ==, path);
-	} else {
-		const char *apath = purple_image_get_path(image);
-
-		g_assert(apath);
-		g_assert_cmpstr(apath, !=, "");
-	}
-
-	g_assert_cmpstr(purple_image_get_extension(image), ==, ext);
-	g_assert_cmpstr(purple_image_get_mimetype(image), ==, mimetype);
-
-	g_object_unref(image);
-}
-
-/******************************************************************************
- * Tests
- *****************************************************************************/
-static void
-test_image_new_from_bytes(void) {
-	GBytes *bytes = g_bytes_new(test_image_data, test_image_data_len);
-	PurpleImage *image = purple_image_new_from_bytes(bytes);
-
-	_test_image(
-		image,
-		g_bytes_get_data(bytes, NULL),
-		g_bytes_get_size(bytes),
-		NULL,
-		"png",
-		"image/png"
-	);
-
-	g_bytes_unref(bytes);
-}
-
-
-static void
-test_image_new_from_data(void) {
-	PurpleImage *image = purple_image_new_from_data(
-		test_image_data,
-		test_image_data_len
-	);
-
-	_test_image(
-		image,
-		test_image_data,
-		test_image_data_len,
-		NULL,
-		"png",
-		"image/png"
-	);
-}
-
-static void
-test_image_new_from_file(void) {
-	PurpleImage *image = NULL;
-	GError *error = NULL;
-	char *path = NULL;
-	char *edata = NULL;
-	gsize elen;
-
-	path = g_build_filename(TEST_DATA_DIR, "test-image.png", NULL);
-	image = purple_image_new_from_file(path, &error);
-	g_assert_no_error(error);
-
-	g_file_get_contents(path, &edata, &elen, &error);
-	g_assert_no_error(error);
-
-	_test_image(
-		image,
-		(guint8 *)edata,
-		elen,
-		path,
-		"png",
-		"image/png"
-	);
-
-	g_free(edata);
-	g_free(path);
-}
-
-/******************************************************************************
- * Main
- *****************************************************************************/
-int
-main(int argc, char **argv) {
-	g_test_init(&argc, &argv, NULL);
-	g_test_set_nonfatal_assertions();
-
-	g_test_add_func("/image/new-from-bytes", test_image_new_from_bytes);
-	g_test_add_func("/image/new-from-data", test_image_new_from_data);
-	g_test_add_func("/image/new-from-file", test_image_new_from_file);
-
-	return g_test_run();
-}
--- a/pidgin/pidginaccounteditor.c	Thu Jul 31 20:57:19 2025 -0500
+++ b/pidgin/pidginaccounteditor.c	Thu Aug 07 21:32:18 2025 -0500
@@ -279,7 +279,7 @@
 		editor->avatar_texture = gdk_texture_new_from_bytes(contents, &error);
 		if(error != NULL) {
 			g_warning("Failed to create texture from file %s: %s",
-			          purple_image_get_path(image),
+			          purple_image_get_filename(image),
 			          error->message);
 			g_clear_error(&error);
 		} else {
--- a/po/POTFILES.in	Thu Jul 31 20:57:19 2025 -0500
+++ b/po/POTFILES.in	Thu Aug 07 21:32:18 2025 -0500
@@ -25,7 +25,6 @@
 libpurple/accounts.c
 libpurple/core.c
 libpurple/debug.c
-libpurple/image.c
 libpurple/network.c
 libpurple/plugins.c
 libpurple/prefs.c
@@ -63,6 +62,7 @@
 libpurple/purplehistoryadapter.c
 libpurple/purplehistorymanager.c
 libpurple/purpleidlemanager.c
+libpurple/purpleimage.c
 libpurple/purplekeyvaluepair.c
 libpurple/purplemarkup.c
 libpurple/purplemessage.c

mercurial