Programming」カテゴリーアーカイブ

GTask

Python の threading はあまり意味が無いので有名。
じゃあマルチスレッドは GLib でやればいいんでないの?
GLib (Gio) でのマルチスレッドは GTask で簡単にできるらしい。

早速 PyGObject で…
g_task_run_in_thread 関数がバインドされていないヤン!

no_run_in_thread

Gjs からも当然使えないってことだよね。
ならば Vala で、って valadoc のどこにも書いていない…

しかたがない、久々に C 言語でやってみるか。
画像が沢山あるディレクトリから GdkPixbuf をリサイズして取り込む例。
マルチスレッドで取り込み次第順次表示していく感じで。

GF_PATH 定数は自前で画像の多いディレクトリに書き換えてね。

#include <gtk/gtk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

/*
gcc tasktest.c `pkg-config --cflags --libs gtk+-3.0`
*/

#define GF_PATH "/home/sasakima-nao/pic/test"

GtkWidget *flowbox;

void
task_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) {

    GdkPixbuf *pixbuf;
    GtkWidget *image;

    pixbuf = gdk_pixbuf_new_from_file_at_size (task_data, 80, 100, NULL);
    image = gtk_image_new_from_pixbuf (pixbuf);
    gtk_widget_show (image);
    gtk_container_add (GTK_CONTAINER (flowbox), image);
    g_free (task_data);
    g_object_unref (task);
}

void
button_click_cb () {

    GFile *file;
    GFileEnumerator *dirlist;
    gssize result;

    file = g_file_new_for_path (GF_PATH);
    dirlist = g_file_enumerate_children(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, G_FILE_QUERY_INFO_NONE, NULL, NULL);
    while (TRUE) {
        GFileInfo *info;
        if (!g_file_enumerator_iterate (dirlist, &info, NULL, NULL, NULL))
            break;
        if (!info)
            break;
        GTask *task;
        gchar *fullpath;
        fullpath = g_strdup_printf ("%s/%s", GF_PATH, g_file_info_get_display_name (info));
        task = g_task_new (NULL, NULL, NULL, NULL);
        g_task_set_task_data (task, fullpath, NULL);
        g_task_run_in_thread (task, task_cb);
    }
    g_object_unref (dirlist);
}

int
main (int argc, char *argv[]) {

    GtkWidget *window;
    GtkWidget *button;
    GtkWidget *vbox;
    GtkWidget *scrolled;

    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    button = gtk_button_new_with_label ("clicke!");
    g_signal_connect (G_OBJECT (button), "clicked", button_click_cb, NULL);
    flowbox = gtk_flow_box_new ();
    gtk_widget_set_valign (GTK_WIDGET (flowbox), GTK_ALIGN_START);
    vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
    gtk_box_pack_start (GTK_BOX (vbox), flowbox, TRUE, TRUE, 0);
    scrolled = gtk_scrolled_window_new (NULL, NULL);
    gtk_container_add (GTK_CONTAINER (scrolled), vbox);
    gtk_container_add (GTK_CONTAINER (window), scrolled);
    g_signal_connect (G_OBJECT (window), "delete_event", gtk_main_quit, NULL);
    gtk_window_resize (GTK_WINDOW (window), 600, 400);
    gtk_widget_show_all (window);
    gtk_main ();

    return 0;
}

面倒臭え!
何故 C で作りたい初心者が多いのか理解できネェ!!
GTask の破棄はコレでいいのかよくワカンネェ(ぅぉい!

とにかく起動してボタンを押してみる。

murti_core

うん、これだけで見事に CPU コアをフルに使って動いている。
画像も一枚ずつ順次表示される、なるほど。

これで本格的マルチスレッドもバッチリ。
って、Comipoli で使いたいんで Python でないと困るんだけーが。

GNOME Gsf

こんなの見つけた。

Zip files: GSF Reference Manual

何だよ、GNOME には ZIP ファイルを扱うライブラリがあったのか。
GLib や Gio ばかり見ていて気が付かなかった。

gsf_gir

Fedora 24 には普通に最初から libgsf が入っていますね。
GObject Introspection からも使える、これは使わない手はない。
以下は Vala だけどサンプルコードも見つかった。

Projects/Vala/GSFSample – GNOME Wiki!

早速コードを書いてみたけど色々困った。
gsf_infile_num_children はアーカイブの一階層分しか得ることができない。
階層がある場合は gsf_infile_num_children の戻り値を再帰するしかない。

しかも二階層以下はソートが名前の逆順に。
Python の ZIP ファイルもそうだったけど何故だ?
しかたがないので一階層以降は逆順の再帰で。

ついでに、サイズ取得が何故かプロパティ。
gst_input_size は size_t で size property は int64 だし。
意味ワカンネェけど動いたからいいや。

#!/usr/bin/env python3

import sys, gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gsf', '1')
from gi.repository import Gtk, Gio, GdkPixbuf, Gsf

PATH = "ziptest.cbz";
#PATH = "なえコレ.zip"

class GsfTest(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.datas = []
        # unzip
        f = Gsf.InputStdio.new(PATH)
        infile = Gsf.InfileZip.new(f)
        self.read_zipfile(infile)
        # add
        fbox = Gtk.FlowBox(valign=Gtk.Align.START, min_children_per_line=5)
        for data in self.datas:
            fbox.add(data)
        self.add(fbox)
        self.show_all()

    def read_zipfile(self, infile):
        num = infile.num_children()
        for i in range(num):
            item = infile.child_by_index(i)
            if item.size == 0:
                self.read_childfile(item)
            else:
                self.append_data(infile, item)

    def read_childfile(self, infile):
        num = infile.num_children()
        for i in range(num-1, -1, -1):
            item = infile.child_by_index(i)
            if item.size == 0:
                # recursive function
                self.read_childfile(item)
            else:
                self.append_data(infile, item)

    def append_data(self, infile, item):
        data = item.read(item.size)
        try:
            stream = Gio.MemoryInputStream.new_from_data(data)
            p = GdkPixbuf.Pixbuf.new_from_stream(stream)
            minp = p.scale_simple(80, 100, GdkPixbuf.InterpType.BILINEAR)
            image = Gtk.Image(pixbuf=minp)
            self.datas.append(image)
        except Exception as e:
            print(e)


class GsfApp(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

    def do_activate(self):
        GsfTest(self)

app = GsfApp()
app.run(sys.argv)

コレでイケたけど。
階層の名前を取り出してソートしたほうが無難だろうか?

それより問題は速度がどうなるかだ。
Comipoli beta3 | PaePoi
の時からどう変わったか Comipoli に組み込んで同じ巨大ファイルを。

gsf_cpu

あんまり変わんねえヤン…
期待したのに。
せっかく安定して動いているのを変えるべきか悩む。

get_modifier_state

我がアプリに矢印キーの代わりにマウスで押すボタンを付けた。
出した後で気が付いた、矢印キーの設定と連動していない。

まあ普通に GdkEvent を作って emit すれば…じゃない!
このアプリでは Ctrl 等の装飾キー状態も必要だった。

ボタンクリックのハンドラにて現在どの装飾キーが押されているかを取得。
それって手段があるのか?ってレベルの話だな。
ガチでアプリを作っている人でないと一生気にならなそうだ。

get_modifier

なんだ devhelp で get_modifier と検索したらアッサリ。
早速試してみよう。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
 
class ButtonWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        button = Gtk.Button(label="modifier = ?")
        button.connect("clicked", self.on_button_clicked)
        self.connect("delete-event", Gtk.main_quit)
        self.add(button)
        self.show_all()

    def on_button_clicked(self, button):
        kmp = Gdk.Keymap.get_default()
        state = kmp.get_modifier_state()
        if state == 0:
            button.set_label("modifier = NULL")
        elif state == 1:
            button.set_label("modifier = Shift")
        elif state == 4:
            button.set_label("modifier = Ctrl")
        elif state == 5:
            button.set_label("modifier = Shift+Ctrl")
        elif state == 8:
            button.set_label("modifier = Alt")
        elif state == 9:
            button.set_label("modifier = Shift+Alt")
        elif state == 12:
            button.set_label("modifier = Ctrl+Alt")
        elif state == 13:
            button.set_label("modifier = Shift+Ctrl+Alt")
        else:
            button.set_label("modifier = ???")

ButtonWin()
Gtk.main()

test1

これでボタンからでも装飾キーの状態が判別できるね。
後は GdkEvent を作って key-press-event に投げれば…

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
 
class ButtonWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        button = Gtk.Button(label="Press Key 'A'")
        button.connect("clicked", self.on_button_clicked)
        self.connect("delete-event", Gtk.main_quit)
        self.connect("key-press-event", self.on_key_press_event)
        self.add(button)
        self.show_all()

    def on_button_clicked(self, button):
        """
            Gdk-WARNING **:
            Event with type 8 not holding a GdkDevice.
            It is most likely synthesized outside Gdk/GTK+

        """
        kmp = Gdk.Keymap.get_default()
        event = Gdk.Event()
        event.type = Gdk.EventType.KEY_PRESS
        event.state = kmp.get_modifier_state()
        event.keyval = Gdk.KEY_a
        event.window = self.props.window
        event.put()

    def on_key_press_event(self, widget, event):
        if event.state == Gdk.ModifierType.CONTROL_MASK:
            self.set_title("Ctrl+{0}".format(chr(event.keyval)))
        else:
            self.set_title("{0}".format(chr(event.keyval)))

ButtonWin()
Gtk.main()

test2

これで Ctrl を押しながらボタンクリックも Ctrl+a にはなるけど。
GdkDevice がどうのとワーニングになってしまう。

キーボードの指定か何か必要なのか?よくワカンネェYO!
よし必殺技だ、ハンドラを関数として呼び出ししてまえ。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
 
class ButtonWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        button = Gtk.Button(label="Press Key 'A'")
        button.connect("clicked", self.on_button_clicked)
        self.connect("delete-event", Gtk.main_quit)
        self.connect("key-press-event", self.on_key_press_event)
        self.add(button)
        self.show_all()

    def on_button_clicked(self, button):
        """
            call on_key_press_event
        """
        kmp = Gdk.Keymap.get_default()
        event = Gdk.Event()
        event.state = kmp.get_modifier_state()
        event.keyval = Gdk.KEY_a
        #event.put()
        self.on_key_press_event(None, event)

    def on_key_press_event(self, widget, event):
        """
            OK
        """
        if event.state == Gdk.ModifierType.CONTROL_MASK:
            self.set_title("Ctrl+{0}".format(chr(event.keyval)))
        else:
            self.set_title("{0}".format(chr(event.keyval)))

ButtonWin()
Gtk.main()

なんというアホな処理。
でもこれで Ctrl+Click でも同じハンドラという狙ったとおりになる。
実装する時はキチンと関数に分離しとこう。

Next Button

前々回の更新で Comipoli にマウスのスワイプで次ページ機能を付けた。
二週間使ってみたけど駄目だこりゃ、どうしても使い辛い。

スワイプはタッチパネルだから直感的に使えるとよく解った。
マウスはやはり移動とクリック以外をさせてはいけない。
eog の次画像表示がボタンになったのは多分散々議論した結果なのだろう。

それとフルスクリーン化はやはりダブルクリックですよね。
スワイプ操作とバッティングするので外したけどあったほうが良さげ。
なんたって操作性を eog 同様にしたいんだからそうするべき。

ということで Comipoli にもストック画像でボタンを追加。
具体的には go-next-symbolic 等を。

GTK3 Demo – L’Isola di Niente

ってコレはどうやって ClutterImage に取り込みするのだ?
GtkButton なら簡単なんだけど、手段が解らない。

しかたがないので自作画像で。
XPM は透過属性があるので RGBA_8888 を指定。
#2E2E2E でダークテーマの色とほぼ一致するみたい。

#!/usr/bin/env python3

from gi.repository import GdkPixbuf, Clutter, Cogl

ICON_RIGHT = [
"32 32 3 1",
" 	c None",
".	c #2E2E2E",
"+	c #FFFFFF",
".........+......................",
".........++.....................",
".........+++....................",
".........++++...................",
".........+++++..................",
".........++++++.................",
".........+++++++................",
".........++++++++...............",
".........+++++++++..............",
".........++++++++++.............",
".........+++++++++++............",
".........++++++++++++...........",
".........+++++++++++++..........",
".........++++++++++++++.........",
".........+++++++++++++++........",
".........+++++++++++++++........",
".........+++++++++++++++........",
".........+++++++++++++++........",
".........++++++++++++++.........",
".........+++++++++++++..........",
".........++++++++++++...........",
".........+++++++++++............",
".........++++++++++.............",
".........+++++++++..............",
".........++++++++...............",
".........+++++++................",
".........++++++.................",
".........+++++..................",
".........++++...................",
".........+++....................",
".........++.....................",
".........+......................"]

ICON_LEFT = [
"32 32 3 1",
" 	c None",
".	c #2E2E2E",
"+	c #FFFFFF",
"......................+.........",
".....................++.........",
"....................+++.........",
"...................++++.........",
"..................+++++.........",
".................++++++.........",
"................+++++++.........",
"...............++++++++.........",
"..............+++++++++.........",
".............++++++++++.........",
"............+++++++++++.........",
"...........++++++++++++.........",
"..........+++++++++++++.........",
".........++++++++++++++.........",
"........+++++++++++++++.........",
"........+++++++++++++++.........",
"........+++++++++++++++.........",
"........+++++++++++++++.........",
".........++++++++++++++.........",
"..........+++++++++++++.........",
"...........++++++++++++.........",
"............+++++++++++.........",
".............++++++++++.........",
"..............+++++++++.........",
"...............++++++++.........",
"................+++++++.........",
".................++++++.........",
"..................+++++.........",
"...................++++.........",
"....................+++.........",
".....................++.........",
"......................+........."]

class ComipoliGoButton(Clutter.Actor):
    def __init__(self, right):
        Clutter.Actor.__init__(self)
        if right:
            pixbuf = GdkPixbuf.Pixbuf.new_from_xpm_data(ICON_RIGHT)
        else:
            pixbuf = GdkPixbuf.Pixbuf.new_from_xpm_data(ICON_LEFT)
        image = Clutter.Image()
        image.set_data(
            pixbuf.get_pixels(),
            Cogl.PixelFormat.RGBA_8888,
            pixbuf.get_width(),
            pixbuf.get_height(),
            pixbuf.get_rowstride()
        )
        self.set_content(image)
        self.connect("enter-event", self.on_enter_event)
        self.connect("leave-event", self.on_leave_event)
        self.props.opacity = 0x00
        self.set_reactive(True)

    def on_enter_event(self, actor, event):
        self.props.opacity = 0xff

    def on_leave_event(self, actor, event):
        self.props.opacity = 0x00

で。
eog の表示はウザいのでマウスカーソルが乗った時だけ表示させる。
ボタンを大きくしたいので縦に引伸し、これはウインドウ側に実装。

Linux アプリケーション – L’Isola di Niente

で上記スクリーンショットのようになりました。
いや、バージョンが上がったら画像はまた変更するけど。
ストックの取り込みが解ったら変更するかもしれない。

ウインドウの隅にカーソルを移動するだけだから直感的に使えると思う。
そういえばマウスジェスチャってすっかり廃れたよね。
マウスボタンを押しながら移動ってやっぱり全然直感的じゃないのよ。

キモヲタならロッカージェスチャとかホイールのほうがいいとか言うかも。
それを言うのは Windows しか使えない人だけですから。
特に右クリックはコンテキストメニュー以外には使ってはいけない。
というのが現在の Mac と GNOME が共通する思想です。

Comipoli beta3

Comipoli beta2 のメモリ展開に凄い問題が見つかった。
3000x4000px over の画像を役二百枚まとめた CBZ を試しに作ってみた。
ソレを読み込むと…

memory_swap

8GB もあるメモリがアッサリ埋まって swap 領域に突入。
SSD だから我慢できる程度の遅さだけど HDD だと死ねるレベル。

JPEG 圧縮を zip 圧縮しているからファイルサイズは 150MB 程度なのに。
GdkPixbuf に展開するとこんなサイズになってしまうのか。

16GB あれば、、、、、いやそうじゃない。
とにかくメモリ使用量をなんとかしなきゃ。

都度読み込みならたとえ 100000x100000px でも問題無い…
そこまでいくと展開が超遅くなるから誰もやらないってばさ。

JPEG を GdkPixbuf に変換せずにバイナリのまま配列なら…
前後処理丸ごと作り替えになるし表示効率も悪くなるな。
そもそも Python でバイナリの配列も実体はポインタなのかな?
ここらは C でやったほうがモヤモヤしなくて済むのだが。

単純に一定の大きさ以上は縮小が一番効率良さげ。
現在のディスプレイ市場は 1080p が主流。
ならば 1080p よりデカいなら縮小という手段が多分最高効率だと思う。

def iter_zip(self):
    # unzip
    with zipfile.ZipFile(self.zip.get_path()) as o:
        l = o.namelist()
        l.sort()
        self.datas.clear()
        for name in l:
            ext = os.path.splitext(name)[1].lower()
            if ext == ".jpg" or ext == ".jpeg" or ext == ".png":
                data = o.read(name)
                stream = Gio.MemoryInputStream.new_from_data(data)
                p = GdkPixbuf.Pixbuf.new_from_stream(stream)
                # Escape Swap
                if p.get_height() > 1080:
                    rate = p.get_width() / p.get_height()
                    p = GdkPixbuf.Pixbuf.scale_simple(p, 1080 * rate, 1080, GdkPixbuf.InterpType.BILINEAR)
                stream.close()
                yield p

手抜きに見えるけどフルスクリーン時の描写処理が速くなるメリットがある。
と思えるけど Comipoli ではソコは OpenGL がやるので無意味なことは秘密だよ。

いや問題はメモリ使用量だってば!
再起動して同じファイルを読み込んでみる

memory_after

1/10 以下に、まさかこんなに違うとは。
これだったらメモリは 4GB あれば問題無さそう、今時なら普通だよね。

でもやはり展開時間は倍近くになる。
バックグラウンド展開だから気にならないとはいえ、やはり遅い。
czipfile なんて早いらしいモジュールは Python2 用しか見当たらない。

ただデータの転送量が減るおかげか動作が少し速くなるメリットも。
とりあえず又問題が出るまでコレでいこう、まだベータだし。