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

@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!

Python3 PyObjC

Mojave を 10.14.3 にしたけど JXA が NSRect でバグるのはそのまま。
Apple は JXA で GUI を使ってほしくないようだ。
macOS 版 comipoli が全然進まない。。。。。

ならば macOS でも Python に戻してやる!
PyObjC がある、でもデフォルトで入っているのは Python 2.7 だ。
もうさぁ、いいかげんに Python3 にしようよ。
ガッツリと Python2 依存だった Fedora でさえ完全に切り替えたというのに。

Installing PyObjC ? PyObjC ? the Python ? Objective-C bridge

PyObjC は Python3 版もあるのか、3.5 までしか書いていないけど。
多分 3.7 でも動くだろう、よし Python3 をインストールして使うことにする。
何もインストールしないという筆者のポリシーがあるがこればかりはしょーがない。

Top – python.jp

あれ、こんな公式サイトだったかな?
Fedora には常に最新 stable が入っているから覗くこと無かったし。
とにかく macOS 64-bit installer をダウンロードとインストール。
速攻 pip で、Python3 は pip3 を使うのね。

$ pip3 install -U pyobjc

よし、これ以上は余程の機会が無いかぎり何もインストールしないぞ。
ちなみに筆者は Xcode は入れていない、デカすぎなんだよアホか!

よしコッチは NSRect の問題は無いぞ。

pyobjc example. ? GitHub

上記は Python2 では普通に動く。
Python3 では NSWindowDelegate がコンフリクトと出る。
単純に消したら動いたので不要ということみたい。

何故かは知らないけど Python3 で動かしたほうが圧倒的に早い。
新しいぶんチューニングされているのかな?

てか、NSRect はタプルのタプルでいいよってことみたい。
そんなこんなで comipoli のベースを作ってみた。

#!/usr/bin/env python3

import objc
from AppKit import *

class ComipoliWindow(NSWindow): #, NSWindowDelegate):
    #def __init__(self): # いらない
    def initWithFrame_(self, frame):
        self = objc.super(ComipoliWindow, self).initWithContentRect_styleMask_backing_defer_(
            frame,
            NSTitledWindowMask |
            NSClosableWindowMask |
            NSResizableWindowMask |
            NSMiniaturizableWindowMask,
            NSBackingStoreBuffered,
            False)
        self.setTitle_('日本語')
        self.setDelegate_(self)
        return self

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

class AppDelegate(NSObject):
    def applicationDidFinishLaunching_(self, notification):
        print('__init__')

NSApplication.sharedApplication()

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

main_menu = NSMenu.new() # alloc().init() は 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()

動くね。

Python だから後でファイル分割もできるしコードも短い。
JXA の無理矢理感が無いのもいい、ただ JXA より初期化は遅い。
これだけで Cocoa は使えるのに Apple は何故 6.1GB もある Xcode を無理矢理ダウンロードさせようとするのやら。

PDF to PNG

我がアプリに PDF 表示機能を追加するのは前回のとおり簡単だった。
問題はサムネイル、GtkDrawingArea を配列に入れればいいやと思っていた。

Matrix で表示させたら凄いメモリ食いなうえスクロールが遅過ぎで使えなかった。
GtkFlowBox を破棄したら突っ込んだ GtkDrawingArea も破棄されるのは当然だった。

結局は縮小 GdkPixbuf にするという何のヒネリもない実装になった。
cr.render は背景は何もしないのね、白で塗り潰す必要があった。
細かくは comipoli のソースを見てくれということで。

つまり PDF は結構簡単に画像にできるってことだ。
それなら変換アプリでも作ろうかなと思ったけど。

#!/usr/bin/env python3
'''
    pdf2png.py
'''

import sys, gi, cairo
gi.require_version('Gdk', '3.0')
gi.require_version('Poppler', '0.18')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gio, Gdk, GdkPixbuf, Poppler

if len(sys.argv) < 2:
    print('Usage: python3 {} PDFFILENAME'.format(sys.argv[0]))
    sys.exit()

f = Gio.File.new_for_path(sys.argv[1])
pdf = Poppler.Document.new_from_gfile(f)
l = pdf.get_n_pages()
for i in range(l):
    page = pdf.get_page(i)
    w, h = page.get_size()
    w = round(w)
    h = round(h)
    with cairo.ImageSurface(cairo.Format.ARGB32, w, h) as surface:
        cr = cairo.Context(surface)
        cr.set_source_rgb(1, 1, 1)
        cr.rectangle(0, 0, w, h)
        cr.fill()
        page.render(cr)
        pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)
        pixbuf.savev('{0:03d}.png'.format(i + 1), 'png', ['compression'], ['9'])

おしまい。

たったこれだけだったのでヤメ!
GNOME ってスゲェなぁ、何故使う人少ないんだろう?

PyGObject Poppler

Web で配布されているコミックは PDF であることもある。
我がアプリで PDF も読み込みできるといいんだけど。

Poppler – Wikipedia

こんなライブラリがあることを知った。

Fedora デフォルトには普通に Gir で入っていたりするし。
そりゃ Evince が使っているし。

Poppler 0.18 (0.69.0) – Poppler 0.18

ドキュメントもある。
関数名も解りやすいし適当に書いてみよう。

#!/usr/bin/env python3

import gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Poppler', '0.18')
from gi.repository import Gtk, Gio, Poppler

PDFFILE = 'りんごの色.pdf'
 
class PdfWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.connect('delete-event', Gtk.main_quit)
        # PDF file
        f = Gio.File.new_for_path(PDFFILE)
        self.pdf = Poppler.Document.new_from_gfile(f)
        self.max_page = self.pdf.get_n_pages()
        self.num = 0
        # button
        button = Gtk.Button.new_with_label('Next Page')
        button.connect('clicked', self.on_button_clicked)
        # DrawingArea
        self.canvas = Gtk.DrawingArea()
        self.canvas.connect('draw', self.on_draw)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox.pack_start(button, False, False, 0)
        vbox.pack_end(self.canvas, True, True, 0)
        self.add(vbox)
        self.resize(400, 400)
        self.show_all()
 
    def on_button_clicked(self, widget):
        if self.num < self.max_page-1:
            self.canvas.queue_draw()
            self.num += 1

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        page = self.pdf.get_page(self.num)
        w, h = page.get_size()
        if aw * h > ah * w:
            n = ah/h
            matrix = cairo.Matrix(n, 0, 0, n, aw/2-w*n/2, 0)
            cr.transform(matrix)
        else:
            n = aw/w
            matrix = cairo.Matrix(n, 0, 0, n, n, ah/2-h*n/2)
            cr.transform(matrix)
        page.render(cr)
 
PdfWin()
Gtk.main()

こんなにアッサリ。。。
何もインストールしないでも表示するだけならマジでこれだけ。

cairo のドキュメントを見ると PDFSurface なんてサーフェスがあるね。
こんなに面白かったのか cairo って。