GTK 4.8.1
GTK4を触り始めているのですが、最近気づいたことがある。
アレ、画像の表示が粗くない?
気のせいかな?と思っていたのだけど、GdkTexture
を使って画像を拡大・縮小して表示していると、従来のcairoを使ってレンダリングしていたときよりも明らかに画質が悪い。
ちょっと真剣に検証してみました。
# 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))
引数に画像ファイルのパスを指定すると、その画像を表示します。
画像は、「ぱくたそ」というサイトのこちらの画像を使わせていただきました。
GdkTexture
と言うのは、GTK4で導入された、GdkPaintable
というインターフェイスを実装した画像を扱うオブジェクトで、GtkPicture
やGtkImage
に貼り付ければ画像を表示されるというもの。
(gtk_picture_new_from_file
などでウィジェットを作成したとしても、間接的には結局GdkTexture
を使うことになるっぽい)
まぁこの辺は後々に記事を書きたいなぁと思ってますが…。
テクスチャでスケールといえば、大抵フィルターというものがあって、高速だが粗く描画するか(OpenGLでいうNEAREST)、ちょっと処理がかかるがいい感じに間引きしてキレイに見せて描画するか(LINEAR)、の方法があります。
どうもGdkTexture
を使った場合は前者で、cairoを使った場合は後者のような気がします。
GdkTexture
にその辺りの指定ができればいいのだけど、マニュアルを見る限りそのようなものはなさそう…。
なお、GdkPixbufでスケールをかける(gdk_pixbuf_scale_simple
)場合には、そのような指定があるのだけど、GdkPixbufだとCPUで処理してそうで、なんか重そうだし、それだったらcairoで描画したほうがいいかな…。
せっかくなので、GdkTexture
のソースコードを追ってみたら、以下の箇所にたどり着いた。
結局、cairo使ってんじゃん!?
自分の追い方が間違ったか、それともどこかでcairoの設定をしているのか…。
追記
なんかマニュアルを眺めていたら、gtk_snapshot_append_scaled_texture
なる関数が追加されてた。(4.10から)
これじゃん!と思って、早速試してみました。
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ウィジェットで表示してみると、…
(左から、GdkTexture
をそのまま使ったもの、cairoでレンダリングしたもの、上記のPaintableを使ったもの)
び、微妙…
やっぱりcairoで書いたものが一番キレイだと思うのだけど、GdkTexture
をそのまま使ったものよりかはちょっときれいになっているっぽい。(右耳のあたり)
この微妙な違いは、何なんだろう…。
ついでと言っちゃなんですが、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;
}