月別アーカイブ: 2019年2月

Python3 subprocess

筆者のアプリは python3 なのに Gio の subprocess を使っていた。
Gjs, Vala, C 等からでも参考になるよう、てか途中まで Gjs 製だった。

JXA では後で Swift をやるかもと NSTask を使っていた。
やらねーし、Xcode デカすぎだし Python のほうが面白いし。
PyObjC 版は Python3 の subprocess モジュールでいいんでね?

17.5. subprocess ? サブプロセス管理 ? Python 3.6.5 ドキュメント

知らない間に subprocess が超単純化されていた。
Popen とか call とか迷わず run だけでいい、破棄も wait もいらない。
unrar, 7za を使うコードを subprocess.run に書き換えしてみる。

#!/usr/bin/env python3

import objc, zipfile, re, subprocess
from AppKit import *

PICEXT = '\.(jpe?g|png|gif)$'

class ComipoliArchive:
    def __init__(self):
        self.status = 0
        self.namelist = []
        self.path = ''

    def new_archive(self, path, is_unrar, is_7za):
        self.path = path
        if re.search(r'\.(cbz|zip)$', path, re.I):
            self.status = 0
            self.namelist.clear()
            with zipfile.ZipFile(path) as o:
                l = o.namelist()
                l.sort()
                for name in l:
                    if re.search(PICEXT, name, re.I):
                        self.namelist.append(name)
        elif is_unrar and re.search(r'\.(cbr|rar)$', path, re.I):
            self.status = 1
            self.namelist.clear()
            cp = subprocess.run(['unrar', 'vt', '-p-', '--', path], stdout=subprocess.PIPE)
            lines = cp.stdout.decode('utf-8').split('\n')
            for line in lines:
                s = line.lstrip()
                if s.startswith('Name: '):
                    name = s[6:]
                    if re.search(PICEXT, name, re.I):
                        self.namelist.append(name)
        elif is_7za and re.search(r'\.(cb7|7z)$', path, re.I):
            self.status = 2
            self.namelist.clear()
            cp = subprocess.run(['7za', 'l', '-slt', path], stdout=subprocess.PIPE)
            lines = cp.stdout.decode('utf-8').split('\n')
            for line in lines:
                if line.startswith('Path'):
                    name = line[7:]
                    if re.search(PICEXT, name, re.I):
                        self.namelist.append(name)
        else:
            return False
        return True

    def __getitem__(self, num):
        if self.status == 0:
            with zipfile.ZipFile(self.path) as o:
                b = o.read(self.namelist[num])
                data = NSData.dataWithBytes_length_(b, len(b))
                return NSImage.alloc().initWithData_(data)
        elif self.status == 1:
            cp = subprocess.run(['unrar', 'p', '-inul', '-@', '--', self.path, self.namelist[num]], stdout=subprocess.PIPE)
            data = NSData.dataWithBytes_length_(cp.stdout, len(cp.stdout))
            return NSImage.alloc().initWithData_(data)
        elif self.status == 2:
            cp = subprocess.run(['7za', 'x', '-so', self.path, self.namelist[num]], stdout=subprocess.PIPE)
            data = NSData.dataWithBytes_length_(cp.stdout, len(cp.stdout))
            return NSImage.alloc().initWithData_(data)

    def __len__(self):
        return len(self.namelist)

丸ごと。

unrar の存在確認も subprocess でやろうと思ったけど。

Pythonで外部コマンドの存在チェック(`which`的な) – Qiita

self.is_unrar = shutil.which('unrar') != None

こっちのほうがいいや。
早速試すことにしよう。

WinRAR archiver, a powerful tool to process RAR and ZIP files

から macOS 版 rar のダウンロード。
インストーラーも Makefile も無いので手動でパスを通す。
終わったら cbr にしたい画像の入ったディレクトリで以下を。

rar a test.cbr -- *.jpg

直接 CBR ファイルを作れるよ。

イケた。

Python3 自体の知識が 3.2 くらいで止まっているわ。
最新 Python3 必須になるけど、いやそのほうがいいでしょ普通。
たまには公式サイトも見ることにしよう。

sips

Retina Display の mac でスクリーンショットする。
mac で見る分にはいいけど Blog にアップするとデカくてビビる。
前回のスクショの詳細を見ると 144dpi もある、通常の二倍だ。

全画面だと 2880x1800px になるからたしかに二倍だね。
色々検索するとみんな困っているようで、何故こうしたのやら。

macos – How can I stop my retina display from taking 2x sized screenshots? – Ask Different

sips という素敵なコマンドがあるようだ。
man を見ると BSD がとか出るけど最後に Darwin の文字が。
Fedora の dnf list にも出ないしやはり macOS 専用のようです。

早速クイックアクションに登録。
コピペだと上書きになるので別名保存処理を入れてみた。
コマンドではないからディレクトリ名を得る必要があるな。

for f in "$@"
do
    width=$((`sips -g pixelWidth "$f" | cut -s -d ':' -f 2 | cut -c 2-` / 2))
    dir=${f%/*}
    name=`date "+72dpi-%Y%m%d-%H:%M.%S.png"`
    sips -s dpiHeight 72.0 -s dpiWidth 72.0 -Z $width "$f" --out "$dir/$name"
done

name はお好みに。

値は正常になるけどなんかボケるなぁ。
でもこんなもんでいいだろう、今回よりコレ使います。

@objc.python_method

macOS に入っている unzip はパッチが当たっていない Unicode 非対応な奴。
NSTask で使うと日本語でエラーになるので制限を付ける必要があった。

PyObjC では Python3 の zipfile モジュールを使えば何も問題なくね?
と思って使ってみたら、やっぱり文字化けしたw
でもコッチなら文字化けしたままエラーにならず画像を取り出しできるじゃないの。
よし unzip 問題は解決だ。

しかし PyObjC プログラミングは結構きつい!
まったく例外を吐いてくれない、エラーになったらフリーズするだけ。
チビッと書いて動かしてを繰り替えさないとドコでエラーになったか全然ワカンネ。

例外がウルトラ親切な PyGObject ってマジ神仕様だよ。
アレで何も作れなかったら他の環境なんて絶対に無理と断言できるくらい。

それとサブクラスにはまいった。

FieldGraph ? PyObjC ? the Python ? Objective-C bridge

NSObject を継承するとメソッドの定義に制限を食らう。
つまりメソッドの定義は Objective-c の関数変換と同様にする必要あり。
引数の数だけアンダーバーが必須、最後にアンダーバーが必要だとか。

#def change_pixbuf(self, num): # Error
def changePixbuf_(self, num):

みたいな。

@objc.IBAction はデフォルトだから書く必要は無いみたい。
Python のメソッドらしいスネークケースにするには

@objc.python_method
def change_pixbuf(self, num):

のデコレーターが必須。

ハンドラと関数はやっぱり別表記にしたいよね。
それを見つけるまで半日近く、日本語情報は皆無に近いし。

Python で作っても NSOpenPanel で警告が出るじゃん。
jxa てか osascript のせいでは無かったようで。

そんなんでもなんとかココまで作れた、ページめくりもできる。
まだ超作りかけだけど参考用とバックアップを兼ねて置いとく。
comipoli_pyobjc1.tar.gz

PyObjC NSView

comipoli JXA 版の PyObjC 化を進めないと。

An introduction to PyObjC ? PyObjC ? the Python ? Objective-C bridge

Objective-c の関数は JXA ではコロンをキャメルケースで合体だった。
PyObjC ではスネークケース、最後のコロンもアンダーバー。

alloc は関数、JXA では関数に括弧不要だったけどこのほうが理解しやすい。
class の継承は Python の掟どおり、PyGObject と同様。

プロパティは何故か全部 ReadOnly になっている。
getter は関数のように括弧で呼び出し。
setter は全部 set*** メソッドを使ってくれということみたい。

上記には書いていないけど NSArray は添字アクセスできた。
もう少し詳しく解ったらまとめページでも作る、かも。

だいたい解ったところで JXA で書いた ComipoliView を書き換えてみる。

#!/usr/bin/env python3

import objc
from AppKit import *

class ComipoliView(NSView):
    def init(self):
        objc.super(ComipoliView, self).init()
        self.first_page = None
        self.second_page = None
        self.spread = False
        self.l_to_r = False
        return self

    def drawRect_(self, rect):
        NSColor.blackColor().set()
        NSRectFill(rect)
        if self.first_page != None:
            aw = rect.size.width
            ah = rect.size.height
            w = self.first_page.size().width
            h = self.first_page.size().height
            # Horizontal?
            if w - h > 0 or not self.spread:
                # Single Page
                width, height, x, y = 0
                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
                r1 = NSMakeRect(x, y, width, height)
                self.first_page.drawInRect_(r1)
            else:
                if self.second_page != None:
                    left = ah * w / h
                    if self.l_to_r:
                        r1 = NSMakeRect(aw / 2 - left, 0, left, ah)
                        self.first_page.drawInRect_(r1)
                        #
                        w2 = self.second_page.size().width
                        h2 = self.second_page.size().height
                        right = ah * w2 / h2
                        r2 = NSMakeRect(aw / 2, 0, right, ah)
                        self.second_page.drawInRect_(r2)
                    else:
                        r1 = NSMakeRect(aw / 2, 0, left, ah)
                        self.first_page.drawInRect_(r1)
                        #
                        w2 = self.second_page.size().width
                        h2 = self.second_page.size().height
                        right = ah * w2 / h2
                        r2 = NSMakeRect(aw / 2 - right, 0, right, ah)
                        self.second_page.drawInRect_(r2)

comipoli_view.py

#!/usr/bin/env python3

import objc
from AppKit import *
from comipoli_view import ComipoliView

class ComipoliWindow(NSWindow):
    def initWithFrame_(self, frame):
        #self = objc.super(ComipoliWindow, self).initWithContentRect_styleMask_backing_defer_(
        objc.super(ComipoliWindow, self).initWithContentRect_styleMask_backing_defer_(
            frame,
            NSTitledWindowMask |
            NSClosableWindowMask |
            NSResizableWindowMask |
            NSMiniaturizableWindowMask,
            NSBackingStoreBuffered,
            False)
        self.setTitle_('日本語')
        self.setDelegate_(self)
        # NSView
        self.view = ComipoliView.new()
        self.view.setFrameSize_(frame.size)
        self.view.first_page = NSImage.alloc().initWithContentsOfFile_('003.png')
        self.view.second_page = NSImage.alloc().initWithContentsOfFile_('004.png')
        self.view.spread = True
        self.contentView().addSubview_(self.view)
        #
        return self

    def windowDidResize_(self, sender):
        aw = self.contentView().frame().size.width
        ah = self.contentView().frame().size.height
        size = NSMakeSize(aw, ah)
        self.view.setFrameSize_(size)

    def windowWillClose_(self, sender):
        NSApp.terminate_(sender)

class AppDelegate(NSObject):
    def applicationDidFinishLaunching_(self, notification):
        pass

NSApplication.sharedApplication()

frame = NSMakeRect(100, 400, 320, 160)
window = ComipoliWindow.alloc().initWithFrame_(frame)
window.makeKeyAndOrderFront_(window)

main_menu = NSMenu.new()
item_app  = NSMenuItem.new()
main_menu.addItem_(item_app)
menu_app = NSMenu.new()
item_app.setSubmenu_(menu_app)
item_quit = NSMenuItem.new()
item_quit.initWithTitle_action_keyEquivalent_('Quit Comipoli', 'terminate:', 'q')
menu_app.addItem_(item_quit)
NSApp.setMainMenu_(main_menu)

NSApp.setDelegate_(AppDelegate.new())
NSApp.activateIgnoringOtherApps_(True)
NSApp.run()

comipoli_window.py

なるほど、これでいいんだ。
ついでに発見したけど、init で self に代入しなくてもいいのね。
というか、self に代入って変だろ。。。。。

どうでもいいおまけ。
NSMatrix ってのがあるんだ、おぉ!って思ったけど。
GTK+ でいう GtkRadioButton の grope 化だった、まぎらわしいYO!