GdkTextureはスケールに弱い？


GTK 4.8.1

GTK4を触り始めているのですが、最近気づいたことがある。
アレ、画像の表示が粗くない？

気のせいかな？と思っていたのだけど、`GdkTexture`を使って画像を拡大・縮小して表示していると、従来のcairoを使ってレンダリングしていたときよりも明らかに画質が悪い。
ちょっと真剣に検証してみました。

```python
# coding: utf-8

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gio, Gdk, GdkPixbuf, Graphene, GObject

class MyCairoPaintable(GObject.GObject, Gdk.Paintable):
	def __init__(self, *args, **kwargs):
		self._pixbuf = None
		super().__init__(*args, **kwargs)

	@GObject.Property(type=GdkPixbuf.Pixbuf)
	def pixbuf(self):
		return self._pixbuf
	@pixbuf.setter
	def pixbuf(self, value):
		self._pixbuf = value
        self.invalidate_contents()

	def do_get_flags(self):
		return Gdk.PaintableFlags.SIZE

	def do_snapshot(self, snapshot, width, height):
		if self._pixbuf is not None:
			cr = snapshot.append_cairo(Graphene.Rect().init(0, 0, width, height))
			image_width = self._pixbuf.get_width()
			image_height = self._pixbuf.get_height()

			s = width / image_width
			if image_height * s > height:
				s = height / image_height

			cr.save()

			cr.translate(width / 2.0, height / 2.0)
			cr.scale(s, s)
			cr.translate(-image_width / 2.0, -image_height / 2.0)

			Gdk.cairo_set_source_pixbuf(cr, self._pixbuf, 0, 0)
			cr.paint()

			cr.restore()

def on_open(app, files, n_files, hint):
	if n_files > 0:
		window = Gtk.Window(application=app, default_width=400, default_height=300)

		hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True)
		window.set_child(hbox)

		texture = Gdk.Texture.new_from_file(files[0])
		picture1 = Gtk.Picture(paintable=texture, halign=Gtk.Align.FILL, hexpand=True)
		hbox.append(picture1)

		pixbuf = GdkPixbuf.Pixbuf.new_from_file(files[0].get_path())
		paintable = MyCairoPaintable(pixbuf=pixbuf)
		picture2 = Gtk.Picture(paintable=paintable, halign=Gtk.Align.FILL, hexpand=True)
		hbox.append(picture2)

		window.show()

if __name__ == '__main__':
	app = Gtk.Application(flags=Gio.ApplicationFlags.HANDLES_OPEN)
	app.connect('open', on_open)
	sys.exit(app.run(sys.argv))

```

引数に画像ファイルのパスを指定すると、その画像を表示します。

![gdk_texture_scale.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41431/64b37f22-e60a-6553-1697-3e2e284a2a77.png)

画像は、「ぱくたそ」というサイトの[こちらの画像](https://www.pakutaso.com/20220534140post-40387.html)を使わせていただきました。

`GdkTexture`と言うのは、GTK4で導入された、`GdkPaintable`というインターフェイスを実装した画像を扱うオブジェクトで、`GtkPicture`や`GtkImage`に貼り付ければ画像を表示されるというもの。
(`gtk_picture_new_from_file`などでウィジェットを作成したとしても、間接的には結局`GdkTexture`を使うことになるっぽい)
まぁこの辺は後々に記事を書きたいなぁと思ってますが…。

テクスチャでスケールといえば、大抵フィルターというものがあって、高速だが粗く描画するか(OpenGLでいうNEAREST)、ちょっと処理がかかるがいい感じに間引きしてキレイに見せて描画するか(LINEAR)、の方法があります。
どうも`GdkTexture`を使った場合は前者で、cairoを使った場合は後者のような気がします。
`GdkTexture`にその辺りの指定ができればいいのだけど、マニュアルを見る限りそのようなものはなさそう…。

なお、GdkPixbufでスケールをかける([`gdk_pixbuf_scale_simple`](https://docs.gtk.org/gdk-pixbuf/method.Pixbuf.scale_simple.html))場合には、そのような指定があるのだけど、GdkPixbufだとCPUで処理してそうで、なんか重そうだし、それだったらcairoで描画したほうがいいかな…。

----------------------------------------------------------------------

せっかくなので、`GdkTexture`のソースコードを追ってみたら、以下の箇所にたどり着いた。

https://github.com/GNOME/gtk/blob/main/gsk/gskrendernodeimpl.c#L1487

**結局、cairo使ってんじゃん！？**

自分の追い方が間違ったか、それともどこかでcairoの設定をしているのか…。

----------------------------------------------------------------------

## 追記

なんかマニュアルを眺めていたら、[`gtk_snapshot_append_scaled_texture`](https://docs.gtk.org/gtk4/method.Snapshot.append_scaled_texture.html)なる関数が追加されてた。(4.10から)
これじゃん！と思って、早速試してみました。

```python
class MyScaledTexturePaintable(GObject.GObject, Gdk.Paintable):
	def __init__(self, *args, **kwargs):
		self._texture = None
		super().__init__(*args, **kwargs)

	@GObject.Property(type=Gdk.Texture)
	def texture(self):
		return self._texture
	@texture.setter
	def texture(self, value):
		self._texture = value

	def do_get_flags(self):
		return Gdk.PaintableFlags.SIZE

	def do_snapshot(self, snapshot, width, height):
		if self._texture is not None:
			image_width = self._texture.get_width()
			image_height = self._texture.get_height()

			s = width / image_width
			if image_height * s > height:
				s = height / image_height

			rect = Graphene.Rect().init((width - image_width * s) / 2, (height - image_height * s) / 2,
						image_width * s, image_height * s)
			snapshot.append_scaled_texture(self._texture, Gsk.ScalingFilter.LINEAR, rect)
```

こんな感じのPaintableを作って、Pictureウィジェットで表示してみると、…

![ss230510a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41431/6c42a26d-4d69-4220-0aae-2147fefab454.png)
(左から、`GdkTexture`をそのまま使ったもの、cairoでレンダリングしたもの、上記のPaintableを使ったもの)

**び、微妙…**

やっぱりcairoで書いたものが一番キレイだと思うのだけど、`GdkTexture`をそのまま使ったものよりかはちょっときれいになっているっぽい。(右耳のあたり)
この微妙な違いは、何なんだろう…。

----------------------------------------------------------------------

ついでと言っちゃなんですが、C言語で先のプログラムを書き直してみました。

```c
#include <gtk/gtk.h>
#include <cairo/cairo.h>
#include <graphene.h>

G_BEGIN_DECLS

#define	MY_TYPE_CAIRO_PAINTABLE	my_cairo_paintable_get_type()
G_DECLARE_FINAL_TYPE(MyCairoPaintable, my_cairo_paintable, MY, CAIRO_PAINTABLE, GObject)

MyCairoPaintable * my_cairo_paintable_new(GdkPixbuf *pixbuf);

G_END_DECLS

static void my_cairo_paintable_interface_init(GdkPaintableInterface *iface);

struct _MyCairoPaintable {
	GObject parent_instance;
};
typedef	struct _MyCairoPaintablePrivate {
	GdkPixbuf *pixbuf;
}	MyCairoPaintablePrivate;
G_DEFINE_TYPE_WITH_CODE(MyCairoPaintable, my_cairo_paintable, G_TYPE_OBJECT,
		G_ADD_PRIVATE(MyCairoPaintable)
		G_IMPLEMENT_INTERFACE(GDK_TYPE_PAINTABLE, my_cairo_paintable_interface_init))

enum {
	MY_CAIRO_PAINTABLE_PROP_PIXBUF = 1,
	MY_CAIRO_PAINTABLE_N_PROPERTIES
};
GParamSpec *my_cairo_paintable_properties[MY_CAIRO_PAINTABLE_N_PROPERTIES] = { NULL, };

static GdkPaintableFlags my_cairo_paintable_get_flags(GdkPaintable *self)
{
	return GDK_PAINTABLE_STATIC_SIZE;
}

static void my_cairo_paintable_snapshot(GdkPaintable *self, GdkSnapshot *snapshot, double width, double height)
{
	MyCairoPaintablePrivate *priv = my_cairo_paintable_get_instance_private(MY_CAIRO_PAINTABLE(self));
	cairo_t *cr;
	graphene_rect_t rect = GRAPHENE_RECT_INIT(0, 0, width, height);
	double s;
	gint image_width;
	gint image_height;

	if (priv->pixbuf != NULL) {
		cr = gtk_snapshot_append_cairo(GTK_SNAPSHOT(snapshot), &rect);
		image_width = gdk_pixbuf_get_width(priv->pixbuf);
		image_height = gdk_pixbuf_get_height(priv->pixbuf);

		s = width / (double)image_width;
		if ((double)image_height * s > height) {
			s = height / (double)image_height;
		}

		cairo_save(cr);

		cairo_translate(cr, width / 2.0, height / 2.0);
		cairo_scale(cr, s, s);
		cairo_translate(cr, -(double)image_width / 2.0, -(double)image_height / 2.0);

		gdk_cairo_set_source_pixbuf(cr, priv->pixbuf, 0, 0);
		cairo_paint(cr);

		cairo_restore(cr);
	}
}

static void my_cairo_paintable_get_property(GObject *self, guint prop_id, GValue *value, GParamSpec *pspec)
{
	MyCairoPaintablePrivate *priv = my_cairo_paintable_get_instance_private(MY_CAIRO_PAINTABLE(self));

	switch (prop_id) {
	case MY_CAIRO_PAINTABLE_PROP_PIXBUF :
		g_value_set_object(value, priv->pixbuf);
		break;

	default :
		G_OBJECT_WARN_INVALID_PROPERTY_ID(self, prop_id, pspec);
		break;
	}
}

static void my_cairo_paintable_set_property(GObject *self, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	MyCairoPaintablePrivate *priv = my_cairo_paintable_get_instance_private(MY_CAIRO_PAINTABLE(self));

	switch (prop_id) {
	case MY_CAIRO_PAINTABLE_PROP_PIXBUF :
		g_clear_object(&priv->pixbuf);
		priv->pixbuf = g_value_dup_object(value);
		gdk_paintable_invalidate_contents(GDK_PAINTABLE(self));
		break;

	default :
		G_OBJECT_WARN_INVALID_PROPERTY_ID(self, prop_id, pspec);
		break;
	}
}

static void my_cairo_paintable_dispose(GObject *self)
{
	MyCairoPaintablePrivate *priv = my_cairo_paintable_get_instance_private(MY_CAIRO_PAINTABLE(self));

	g_clear_object(&priv->pixbuf);

	(*G_OBJECT_CLASS(my_cairo_paintable_parent_class)->dispose)(self);
}

static void my_cairo_paintable_class_init(MyCairoPaintableClass *klass)
{
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->get_property = my_cairo_paintable_get_property;
	obj_class->set_property = my_cairo_paintable_set_property;
	obj_class->dispose = my_cairo_paintable_dispose;

	my_cairo_paintable_properties[MY_CAIRO_PAINTABLE_PROP_PIXBUF] = g_param_spec_object("pixbuf", "Pixbuf", "",
			GDK_TYPE_PIXBUF, G_PARAM_READWRITE);
	g_object_class_install_properties(obj_class, MY_CAIRO_PAINTABLE_N_PROPERTIES, my_cairo_paintable_properties);
}

static void my_cairo_paintable_init(MyCairoPaintable *self)
{
	MyCairoPaintablePrivate *priv = my_cairo_paintable_get_instance_private(self);

	priv->pixbuf = NULL;
}

static void my_cairo_paintable_interface_init(GdkPaintableInterface *iface)
{
	iface->get_flags = my_cairo_paintable_get_flags;
	iface->snapshot = my_cairo_paintable_snapshot;
}

MyCairoPaintable * my_cairo_paintable_new(GdkPixbuf *pixbuf)
{
	return MY_CAIRO_PAINTABLE(g_object_new(MY_TYPE_CAIRO_PAINTABLE, "pixbuf", pixbuf, NULL));
}

/*----------------------------------------------------------------------*/

static void on_open(GApplication *self, GFile **files, gint n_files, gchar *hint, gpointer user_data)
{
	GtkWidget *window;
	GtkWidget *hbox;
	GtkWidget *picture1;
	GtkWidget *picture2;
	GdkTexture *texture;
	MyCairoPaintable *paintable;
	GdkPixbuf *pixbuf;

	if (n_files > 0) {
		window = gtk_window_new();
		gtk_window_set_application(GTK_WINDOW(window), GTK_APPLICATION(self));
		gtk_window_set_default_size(GTK_WINDOW(window), 400, 300)

		hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
		gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
		gtk_window_set_child(GTK_WINDOW(window), hbox);

		texture = gdk_texture_new_from_file(files[0], NULL);
		picture1 = gtk_picture_new_for_paintable(GDK_PAINTABLE(texture));
		gtk_widget_set_halign(picture1, GTK_ALIGN_FILL);
		gtk_box_append(GTK_BOX(hbox), picture1);
		g_object_unref(G_OBJECT(texture));

		pixbuf = gdk_pixbuf_new_from_file(g_file_get_path(files[0]), NULL);
		paintable = my_cairo_paintable_new(pixbuf);
		picture2 = gtk_picture_new_for_paintable(GDK_PAINTABLE(paintable));
		gtk_widget_set_halign(picture2, GTK_ALIGN_FILL);
		gtk_box_append(GTK_BOX(hbox), picture2);
		g_object_unref(G_OBJECT(paintable));
		g_object_unref(G_OBJECT(pixbuf));

		gtk_widget_show(window);
	}
}

int main(int argc, char *argv[])
{
	GtkApplication *app;
	gint exit_code;

	app = gtk_application_new(NULL, G_APPLICATION_HANDLES_OPEN);
	g_signal_connect(G_OBJECT(app), "open", G_CALLBACK(on_open), NULL);
	exit_code = g_application_run(G_APPLICATION(app), argc, argv);
	g_object_unref(G_OBJECT(app));
	return exit_code;
}

```