Comipoli for macOS

Safari からのアクセスが微妙に増えている気がする。
筆者は Fedora メインで mac はサブなので mac ネタなんてほとんど無いのに。
でもそういえば macOS も bash だし fedora よりユーザーは当然多いし。

Comipoli の macOS 版を JXA で作る計画は一応あるんだけど。
cbz を Finder でサムネイル表示できない macOS で意味あるんだろうか?
てか筆者は Cocoa の知識はほとんど無い。
しかも Objective-c から JavaScript へ変換という情報がほぼ無いことが必要。

そもそも NSWindow の配置はレイアウタなのか絶対位置なのかさえ知らない。
よし解らないなら実験だ、NSImageView を 2 つ置いて並べてみよう。

#!/usr/bin/osascript

ObjC.import("Cocoa");

const P_LEFT = $("/Users/sasakima-nao/Pictures/三嶋ゆらら.jpg");
const P_RIGHT = $("/Users/sasakima-nao/Pictures/優木苗.jpg");

ObjC.registerSubclass({
    name: 'WinDelegate',
    //superclass: 'NSObject',
    protocols: ['NSWindowDelegate'],
    methods: {
        // メソッド名は合体しない
        'windowDidResize:': {
            types: ['void', ['id']],
            implementation: function(aNotification) {
                fitImage();
            }
        },
        'windowWillClose:': {
            types: ['void', ['id']],
            implementation: function(notification) {
                return app.terminate(0);
            }
        }
    }
});

/**
 * func
 */
var fitImage = ()=> {
    let aw = window.frame.size.width;
    let ah = window.frame.size.height;
    let iw = leftView.image.size.width;
    let ih = leftView.image.size.height;
    // left
    let f = leftView.frame;
    f.size = $.NSMakeSize(aw/2, ah);
    leftView.frame = f;
    // right
    f = rightView.frame;
    f.size = $.NSMakeSize(aw/2, ah);
    f.origin = $.CGPointMake(aw/2, 0);
    rightView.frame = f;
}
var createImageView = (path)=> { 
    let image = $.NSImage.alloc.initWithContentsOfFile(path);
    let view = $.NSImageView.new;
    view.setImageScaling($.NSScaleToFit);
    view.setImage(image);
    return view;
}

/**
 * image
 */
leftView = createImageView(P_LEFT);
rightView = createImageView(P_RIGHT);

/**
 * Window
 */
let window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(0, 0, 320, 400),
    $.NSTitledWindowMask
    | $.NSClosableWindowMask
    | $.NSMiniaturizableWindowMask
    | $.NSResizableWindowMask,
    $.NSBackingStoreBuffered,
    false
);
window.title = $("comipoli");
//window.center;
window.orderFrontRegardless;
window.delegate = $.WinDelegate.new;
window.contentView.addSubview(leftView);
window.contentView.addSubview(rightView);
window.makeKeyAndOrderFront(window);
fitImage();

/**
 * 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;

class 使いたい…

うん絶対位置配置なんだね。
macOS にも unzip はあるし意外に移植は簡単そうだ。
osacompile でアプリにすることもできるはず。

問題はコード分割が現状できないことなんだが。

Gmail…

google アカウントから「ブロックしました」と度々通知が来るようになった。
何だよ、筆者のアカウントなんかハッキングしてもたいしたもんは無いぞ?

何度も来るのでさすがにおかしいと思い通知の時間を元に色々検証。
って、その通知メールを受信している Sylpheed 自身が原因だった。
imap で既読を付けようとする毎にブロックされているようだ。
3.6 は Gmail が要求するセキュリティに合致していないってことかな。

Sylpheed – 軽快で使いやすいオープンソースのメールソフト

3.7 が出ているけどこの件には関係なさそうだなぁ。
安全性の低いアプリへのアクセスを許可もできるみたいだけど。。。。。
今は全アカウントで imap なので何を使ってもいいし。
他を探してもまたブロックされるかもしれないし。

ということで数年ぶりに Evolution を起動。
こいつはブロックされないようだ、当然だと思うけど。

あぁ、GNOME 純正でないのは KeePassX と google-chrome だけになってしまった。
実際 macOS ではこれに Visual Studio Code が入るだけで使ってるし。

十年くらい前に Linux を使い始めてからずっと使い続けているメーラーなのに。
まさかこんな理由で乗り換えするはめになるとはね。
今後もこんな感じでサードパーティの淘汰みたいな流れなんだろうな。

unzip escape

comipoli を gjs 化するので python3 の zipfile が使えない。
ということで unzip コマンドを使う方法に切り替えるのだが
展開する内部ファイル名にブラケット [, ] があると展開できないと気が付いた。

検索するとアスタリスクが駄目なことは皆気がついているようだ、他にもあるかな?
file-roller の GPL ソースを見るのが一番速い。

file-roller/fr-command-zip.c at master ? GNOME/file-roller ? GitHub

[]*?!^-\

こんなにあった、てか記号全部ではないのね。
駄目文字の前にバックスラッシュを付けるだけだし変換は簡単だね。

var ComipoliArchive = class ComipoliArchive {
    // etc...
    _zipEscape(str) {
        const ESCAPE = "[]*?!^-\\";
        let res = "";
        for (let s of str) {
            if (ESCAPE.includes(s)) res += "\\";
            res += s;
        }
        return res;
    }
    getItem(num) {
        let cmdArray = null;
        let pixbuf = null;
        switch (this.status) {
        case 1:
            cmdArray = ["unrar", "p", "-inul", "-@", "--", this.path, this.namelist[num]];
            break;
        case 2:
            cmdArray = ["7za", "x", "-so", this.path, this.namelist[num]];
            break;
        default:
            //cmdArray = ["unzip", "-pj", this.path, this.namelist[num]];
            cmdArray = ["unzip", "-pj", this.path, this._zipEscape(this.namelist[num])];
        }
        let sp = Gio.Subprocess.new(cmdArray, Gio.SubprocessFlags.STDOUT_PIPE);
        let stream = sp.get_stdout_pipe();
        try {
            pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null);
        }
        catch(e) {
            print("Error: " + this.namelist[num] + "\n" + e);
        }
        stream.close(null);
        return pixbuf;
    }
}

展開できた。

しかし cairo 描写に書き換えたら 3D で動く毎に draw シグナルが…
これでは clutter_actor_save_easing_state が使えないや。

yield @ Python3 and JavaScript

我が comipoli はどうせ cairo に書き換えるなら Gjs 化してしまおう。
ということで以前 ClutterImage 作成が遅過ぎでお蔵入りしたソースを引っ張りだした。
ES6 仕様に書き換えをしていて yield でハマったので覚書。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib

class Dlg(Gtk.Dialog):
    def __init__(self):
        Gtk.Dialog.__init__(self, transient_for=None, title="Comipoli Thumbnail")
        self.show_all()
        # idle
        self.iter = self.iter_func()
        GLib.idle_add(self.iter_idle)

    def iter_idle(self):
        try:
            n = next(self.iter)
            print(n)
            return True
        except Exception as e:
            return False

    def iter_func(self):
        for num in range(100):
            yield num

dlg = Dlg()
dlg.run()
dlg.destroy()

PyGObject

#!/usr/bin/gjs

const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;

var Dlg = GObject.registerClass({
    GTypeName: "Dlg"
}, class Dlg extends Gtk.Dialog {
    _init() {
        super._init({
            transient_for: null,
            title: "Comipoli Thumbnail"
        });
        this.show_all();
        // idle
        this.iter = this.iter_func();
        GLib.idle_add(GLib.PRIORITY_LOW, ()=> {
            let r = this.iter.next();
            if (r.done) return false;
            print(r.value);
            return true;
        });
    }
    * iter_func() {
        for (let i=0; i<100; i++) {
            yield i;
        }
    }
});
Gtk.init(null);
let dlg = new Dlg();
dlg.run();
dlg.destroy();

Gjs

g_idle_add の引数が違うことは置いておいて。
g_idle_add は何も処理を行っていない時に与えられた関数を実行し続ける関数。
false を戻さないかぎり動き続ける。

Python は例外で判別、JavaScript は next メソッドの戻り値で判別。
yield, next の使い方はまったく同じなので混乱してもーたがね。

検索すると個別で next() を呼んでいる人ばかりで困ったよ。
どう考えても yield でそんな使い方はしないと思うんですけど。
複数ファイル処理で一つ読み終わったらとっとと表示みたいな場合が主だよね。

あと JavaScript のジェネレーター関数はアスタリスクが必要。
無名関数にする手段は無いっぽい、JavaScript を筆者が使うようになった切っ掛けなのに。
でも Python には無名関数はいらないな、そんなの Python じゃない。
適材適所ってことですよ、何でもアリはコードを読み辛くするだけだ。

screentone 2D and 3D

コミックブックアーカイブビューアを作っている筆者でありますが。
スクリーントーンの描写が変になる場合があることに困っていた。

Clutter(OpenGL ES) でリサイズしているのが悪いのかな?
と思い GdkPixbuf をリサイズしてからレンダリングで解決した。
よかったよかった、じゃない!もう少し調べよう。

Jトーン 網点(ドットパターン) J-00|スクリーントーン 通販【ジェイトーン・ネットショッピング】

トーンは上記のサンプル画像をお借りしてと。

#!/usr/bin/env python3

import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Clutter", "1.0")
gi.require_version("GtkClutter", "1.0")
from gi.repository import Gtk, Gdk, GdkPixbuf, Clutter, GtkClutter, Cogl

class DrawWindow(Gtk.Window):
    """
        2D and 3D View Check
    """
    def __init__(self):
        Gtk.Window.__init__(self)
        # tone
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file("tone.jpg")
        # cairo (2D)
        self.da = Gtk.DrawingArea()
        self.da.connect("draw", self.on_draw)
        # OpenGL ES (3D)
        embed = GtkClutter.Embed()
        stage = embed.get_stage()
        stage.set_background_color(Clutter.Color.new(0, 0, 0, 255))
        stage.connect("allocation-changed", self.on_stage_allocation_changed)
        self.actor = Clutter.Actor()
        image = Clutter.Image()
        image.set_data(
            self.pixbuf.get_pixels(),
            Cogl.PixelFormat.RGBA_8888 if self.pixbuf.get_has_alpha() else Cogl.PixelFormat.RGB_888,
            self.pixbuf.get_width(),
            self.pixbuf.get_height(),
            self.pixbuf.get_rowstride()
        )
        self.actor.set_content(image)
        stage.add_child(self.actor)
        # set
        vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
        vbox.pack_start(self.da, True, True, 0)
        vbox.pack_start(embed, True, True, 0)
        self.add(vbox)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_stage_allocation_changed(self, actor, box, flags):
        aw = box.x2
        ah = box.y2
        w = self.pixbuf.get_width()
        h = self.pixbuf.get_height()
        if aw * h > ah * w:
            width = w * ah / h
            height = ah
            x = (aw - width) / 2
            y = 0
        else:
            width = aw
            height = h * aw / w
            x = 0
            y = (ah - height) / 2
        self.actor.set_size(width , height)
        self.actor.set_position(x, y)

    def on_draw(self, widget, cr):
        d_width = widget.get_allocated_width()
        d_height = widget.get_allocated_height()
        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, d_width, d_height)
        cr.fill()
        p_width = self.pixbuf.get_width()
        p_height = self.pixbuf.get_height()
        if (d_width * p_height) > (d_height * p_width):
            width = p_width * d_height / p_height
            height = d_height
        else:
            width = d_width
            height = p_height * d_width / p_width
        pixbuf = GdkPixbuf.Pixbuf.scale_simple(self.pixbuf, width, height, GdkPixbuf.InterpType.BILINEAR)
        Gdk.cairo_set_source_pixbuf(cr, pixbuf, (d_width-width)/2, (d_height-height)/2)
        cr.paint()

GtkClutter.init()
DrawWindow()
Gtk.main()

動かす。

上半分が cairo で下が Clutter です。
拡大はまったく同じ描写になるけど縮小は全然違うものになることが解る。
なんだよこの 3D 描写。。。。。

画像処理や 3D に詳しいわけではないので細かい解説はできないけど。
単なるラスターデータとして扱う 2D と RGB として扱う 3D の違いだろう。
網点を RGB 色として認識した結果こんな悲惨なことに。

とにかく 3D では小さめな画像かアニメ調のベタ塗りでないとこんなことになる。
スクリーントーンを多用するコミック表示には徹底的に向いていないようです。

Clutter で cairo が使える手段があるのはそういうことか。
cairo で作り替えしよ。