zipfile module VS unzip command

Comipoli を PyGObject から Gjs に変更すると以前書いた。
実際に作っているんだけど困った、遅すぎる…

もしかして Gjs って遅いの?
いや単に zipfile モジュールが早いだけかも、実験だ。
zipfile を使うから当然 Python で。
blog で Python を書くのは久々のような。

#!/usr/bin/env python3

import time, zipfile, gi
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gio, GLib, GdkPixbuf

namelist = []
zipfile_list = []
unzip_list = []

sp = Gio.Subprocess.new(["unzip", "-Z", "test.cbz"], Gio.SubprocessFlags.STDOUT_PIPE);
istream = sp.get_stdout_pipe()
dstream = Gio.DataInputStream(base_stream=istream)
while True:
    line, l = dstream.read_line_utf8()
    if line == None: break
    if line.startswith('-'):
        name = line[53:]
        if GLib.Regex.match_simple("\.(jpe?g|png|gif)$", name, GLib.RegexCompileFlags.CASELESS, 0):
            namelist.append(name)

# zipfile speed
now = time.time()
for name in namelist:
    with zipfile.ZipFile("test.cbz") as o:
        data = o.read(name)
        stream = Gio.MemoryInputStream.new_from_data(data)
        p = GdkPixbuf.Pixbuf.new_from_stream(stream)
        zipfile_list.append(p)
        stream.close()
print(time.time() - now)

# unzip command speed
now2 = time.time()
for name in namelist:
    sp = Gio.Subprocess.new(["unzip", "-p", "test.cbz", name], Gio.SubprocessFlags.STDOUT_PIPE)
    stream = sp.get_stdout_pipe()
    p = GdkPixbuf.Pixbuf.new_from_stream(stream, None);
    unzip_list.append(p)
    stream.close()
print(time.time() - now2)

少し大きめの cbz を用意して。

やはり zipfile モジュールのほうが少し展開が早いんだね。
いや、Gjs で作りかえた Comipoli の遅さはこんなレベルじゃないんだが。
ぶっちゃけ二倍くらい表示に時間が掛かる、とても出せるシロモノではない。

やっぱり Gjs が遅いのかも。
Gjs で同様なサンプルをと思ったけど time.time() の代替は何だ?
g_timer_new とかってバインドされていないのね、GDateTime あたりかな。
それとも他に原因があるかもしれないし、今日はここまで。

ただ Gjs への書き換えをやったおかげで beta12 で多重展開していたのを見つけた。
修正したらスゲェ速くなったので PyGObject のまま beta13 公開。
たまにはこうやって丸ごと書き換えるといいこともあるもんだ。

Gjs unzip

筆者の blog ネタは最近 JavaScript ばかり。
なのに自作アプリの comipoli は Python3 製というのは変だ。

と随分前から思っていたけど、やはり Gjs で作り替えしようと考えた。
PyGObject 製だったのは zipfile モジュールを使っていたからで。

Gjs で作り替えとなるとその代替を考えないと。
cbr で unrar を使っているように unzip を使えばいいだけ、なんだけど。

unzip -Z FILENAME

コマンドでアーカイブの詳細は得られる。
最後の半角空白以降を抜き出せばいいかな。

いやまて、ファイル名に半角空白があった場合はどうなる?
ちゃっと実験用アーカイブを作って実験。

そうなるか、最後の半角空白という手は使えない。
色々試したけど 53 文字以降を取り出せば手持ちファイルは全部イケるようだ。
バージョンによって変わるかもだが、ISF 区切りの 9 番目以降という手もあるし。

ファイルは – で始まっているからコレの見分けは簡単だね。
unzip コマンドの最後に抜き出すファイル名を書けばソレを展開してくれる。
ということでこんなサンプルコードになりました。

#!/usr/bin/gjs

const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;

const archive = "test.zip";

let sp = Gio.Subprocess.new(["unzip", "-Z", archive], Gio.SubprocessFlags.STDOUT_PIPE);
let istream = sp.get_stdout_pipe();
let dstream = new Gio.DataInputStream({
    base_stream: istream
});
dstream.read_line_async(GLib.PRIORITY_DEFAULT, null, async_callback);

function async_callback(source_object, ares) {
    let [line, len] = source_object.read_line_finish_utf8(ares);
    if (line == null) {
        mainloop.quit();
    } else {
        if (line.startsWith('-')) {
            let name = line.slice(53);
            if (GLib.Regex.match_simple("\.js$", name, GLib.RegexCompileFlags.CASELESS, 0)) {
                Gio.Subprocess.new(["unzip", archive, name], Gio.SubprocessFlags.STDOUT_SILENCE);
            }
        }
        source_object.read_line_async(GLib.PRIORITY_DEFAULT, null, async_callback);
    }
}

let mainloop = new GLib.MainLoop(null, false);
mainloop.run();

read_line_async はメインループが必要だから面倒臭いなぁ。
for で回したほうが簡単だけど非同期にしたかったので。
GMainLoop はプロパティが無いのでこの引数指定にするらしい。

てゆーか、やっと上手くいったぞ。
実は随分前から試していて上手くいかなかっただけなんですけど。

Clutter Constraint

Clutter のドキュメントの中で Constraint がよく解らない。
Google 翻訳によると「制約」、っていったい何の制約なのだろう?
検索検索。

Clutter の Constraint (制約条件) – ふとしの日記

Gjs で動かないって new キーワードの場合引数はプロパティを JSON 指定ですし。
JSON 引数に書き換えたら普通に動いた、多分今は自身で解っていると思うけど。
五年前のこの当事はみんな手探り状態だったししかたがないね。

Clutter.SnapConstraint

えっとつまり。
Constraint ってアクションを他の Actor と連動させるって解釈でいいのかな?
この名前が不適切なのか、実は英語ではコレで合っているということなのかシラネ!

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const ConstraintWin = new Lang.Class({
    Name: 'ConstraintWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            x: 25,
            y: 25,
            width: 50,
            height: 50,
            reactive: true
        });
        this.blue = new Clutter.Actor({
            background_color: Clutter.Color.new(0, 0, 255, 255),
            width: 50,
            height: 50,
        });
        // DnD
        this.red.add_action(new Clutter.DragAction());
        // Constraint
        let bind = new Clutter.BindConstraint({
	        coordinate: Clutter.BindCoordinate.POSITION,
	        offset: 50,
            source: this.red
        });
        this.blue.add_constraint(bind);
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        stage.add_child(this.red);
        stage.add_child(this.blue);
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new ConstraintWin(app);
});
app.run(null);

で赤いブロックをドラッグする。
うん、見事にオフセットされたまま連動してドラッグされるんだね。

ClutterAlignConstraint は Totem のボタン類みたいな用途で使えそう。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const TotemLikeWin = new Lang.Class({
    Name: 'TotemLikeWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        //
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            width: 200,
            height: 50,
            reactive: true
        });
        stage.add_child(this.red);
        // Constraint
        let x = new Clutter.AlignConstraint({
	        align_axis: Clutter.AlignAxis.X_AXIS,
	        factor: 0.5,
            source: stage
        });
        let y = new Clutter.AlignConstraint({
	        align_axis: Clutter.AlignAxis.Y_AXIS,
	        factor: 0.8,
            source: stage
        });
        this.red.add_constraint(x);
        this.red.add_constraint(y);
        //
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new TotemLikeWin(app);
});
app.run(null);

いくらリサイズしても常に同じ位置に貼り付ける動作がこんなにアッサリ!
多分こういうのが本来の使い方だと思う。

Clutter Animation

ClutterActor はアニメーション機能を内蔵している。

clutter_actor_save_easing_state # pause
clutter_actor_set_easing_duration # time
# move, resize, opacity, etc…
clutter_actor_restore_easing_state # start

たったコレだけで様々な変更がアニメーションになって動く。
ということでサンプルコード、クリック毎に Actor が入れ替わります。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const AnimateWin = new Lang.Class({
    Name: 'AnimateWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            x: 25,
            y: 25,
            width: 50,
            height: 50
        });
        this.blue = new Clutter.Actor({
            background_color: Clutter.Color.new(0, 0, 255, 255),
            x: 75,
            y: 75,
            width: 50,
            height: 50
        });
        // action
        let click = new Clutter.ClickAction();
        click.connect("clicked", Lang.bind(this, function() {
            // pause
            this.red.save_easing_state();
            this.blue.save_easing_state();
            // animation time (default 250)
            this.red.set_easing_duration(2500);
            // move
            let [x, y] = this.red.get_position();
            let [x2, y2] = this.blue.get_position();
            this.red.set_position(x2, y2);
            this.blue.set_position(x, y);
            // play
            this.red.restore_easing_state();
            this.blue.restore_easing_state();
        }));
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        stage.add_child(this.red);
        stage.add_child(this.blue);
        stage.add_action(click);
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new AnimateWin(app);
});
app.run(null);

コレだけだと使いどころがあんまりなさそうだよね。
しかし、ClutterImage の resize が超滑らかになるメリットがあった!
ということで comipoli に早速採用、永遠に実験用アプリ…

JXA NSWindow

今回は JXA でウインドウを作ってみる。

実はウチの PyGObject Tips ページのアクセス数で解るんだけどさ。
ウインドウを作るのページだけブッチギリで多いのよ、初心者ってそんなもんだ。
なんだかなぁ、プログラミングが面白いのはその先からだと思うんですけど。
ということで画像も表示してみる。

検索すると色々な実装があるけど結果は全部同じだね。
それなら簡潔で理解が早い人が多いっぽい実装を選んでみよう。

でも気に入らないのは大半の人がウインドウを閉じると終了するコードだ。
おい macOS だぞ、メニューバーに command+Q は必須だろ?
それと無意味な即時実行多すぎ、多分よく解っていないんだろうけど。

NSApp.servicesMenu in JXA ? GitHub

なんだよ、command+Q メニューはこんなにアッサリ実装できるんかい。
まさかこんなところでバックコーテーションを使うとは。
alloc.init だけなら new メソッドでいいみたい、ふむふむ。

しかし一回しか使わない関数なら無名関数の即時実行でいいのに。
即時実行ってそういう場合に使うと思うんだが。
ということで、こんなサンプルコードになりました。

#!/usr/bin/osascript

ObjC.import("Cocoa");

const imgePath = $("/Users/sasakima-nao/Pictures/[シクシクおよよ]三嶋ゆらら(SSR).jpg");

/**
 * Contents
 */
let image = $.NSImage.alloc.initWithContentsOfFile(imgePath);
if (image.isNil()) {
    console.log("Image Not Found.");
}
let imageView = $.NSImageView.alloc.initWithFrame(
    $.NSMakeRect(0, 0, 320, 400)
);
imageView.setImageScaling($.NSScaleToFit);
imageView.setImage(image);

/**
 * Window
 */
let window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(0, 0, 320, 400),
    $.NSTitledWindowMask
    | $.NSClosableWindowMask
    | $.NSMiniaturizableWindowMask,
    //| $.NSResizableWindowMask,
    $.NSBackingStoreBuffered,
    false
);
window.title = $("[シクシクおよよ]三嶋ゆらら(SSR)");
window.center;
window.orderFrontRegardless;
window.contentView.addSubview(imageView);

/**
 * Application
 */
let app = $.NSApplication.sharedApplication;
app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
app.mainMenu = function() {
    
    function newMenu(title, action, key) {
       return $.NSMenuItem.alloc.initWithTitleActionKeyEquivalent(title, action, key);
    }
    
    const appName = "Test Window";
    
    const mainMenu = $.NSMenu.new;
    const itemApp  = $.NSMenuItem.new;
    const menuApp  = $.NSMenu.new;
    itemApp.submenu  = menuApp;
    mainMenu.addItem(itemApp);
    menuApp.addItem(newMenu(`Quit ${appName}`, 'terminate:', 'q') );

    return mainMenu;
}();
app.run;

やばい、ゆららちゃんがこんなにカワイイとは!
つい炭酸全部使ってフルマカロン取っ、、、、、いやそれはどうでもよくて。

macOS アプリって Application と Window に何も関連性が無いんだね。
あぁだから macOS では Window を全部閉じてもアプリは終了しないのか。

GTK+, WPF, VCL 等は全部 Application class が Window を管理している。
で、管理する Window が無くなったら Application が終了する、という流れ。
mac は本当に独特なんだなと。

しかしメソッドに括弧が無いのは相変わらず慣れない。
app.run にすら不要って意味ワカンネ!