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

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 って。

Cairo Matrix

前回の続き。
Cairo.Matrix で GdkPixbuf をリサイズできるんだよね。
それなら draw シグナルのハンドラでそのまま描写すればいいジャン!

でも Matrix のリサイズは RGBA ベースじゃないだろうな?
ClutterImage ではスクリーントーンが縮小で潰れたんだよね。
screentone 2D and 3D | PaePoi

OpenGL ではないから大丈夫だと思う、ただリサイズは超ヌルヌルだった。
速度といっしょにそんな所も見ていこう。

#!/usr/bin/env python3

import gi, sys, cairo, time
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

class AWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.d = Gtk.DrawingArea()
        self.d.connect('draw', self.on_draw)
        self.add(self.d)
        self.pixbuf1 = GdkPixbuf.Pixbuf.new_from_file('003.png')
        self.pixbuf2 = GdkPixbuf.Pixbuf.new_from_file('004.png')
        self.resize(1600, 900)
        self.show_all()

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # __do__
        t = time.time()
        # Save Matrix
        cr.save()
        # Right Page
        w = self.pixbuf1.get_width()
        h = self.pixbuf1.get_height()
        n = ah/h
        matrix = cairo.Matrix(n, 0, 0, n, aw/2, n)
        cr.transform(matrix) # no! cr.set_matrix(matrix)
        Gdk.cairo_set_source_pixbuf(cr, self.pixbuf1, 0, 0)
        cr.paint()
        # Reset
        cr.restore()
        # Left Page
        w = self.pixbuf2.get_width()
        h = self.pixbuf2.get_height()
        n = ah/h
        matrix = cairo.Matrix(n, 0, 0, n, aw/2-w*n, n)
        cr.transform(matrix)
        Gdk.cairo_set_source_pixbuf(cr, self.pixbuf2, 0, 0)
        cr.paint()
        # __done__
        print('matrix  : {}'.format(time.time() - t))

class AApplication(Gtk.Application):
    __gtype_name__ = 'AApplication'
    def __init__(self):
        GLib.set_prgname('AApplication')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        AWindow(self)
    
    def do_activate(self):
        self.props.active_window.present()

AApplication().run(sys.argv)

Matrix 版

#!/usr/bin/env python3

import gi, sys, time
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

class AWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.d = Gtk.DrawingArea()
        self.d.connect('draw', self.on_draw)
        self.add(self.d)
        self.pixbuf1 = GdkPixbuf.Pixbuf.new_from_file('003.png')
        self.pixbuf2 = GdkPixbuf.Pixbuf.new_from_file('004.png')
        self.resize(1600, 900)
        self.show_all()

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # __do__
        t = time.time()
        # Right Page
        w = self.pixbuf1.get_width()
        h = self.pixbuf1.get_height()
        n = ah*w/h
        buf = self.pixbuf1.scale_simple(n, ah, GdkPixbuf.InterpType.BILINEAR)
        Gdk.cairo_set_source_pixbuf(cr, buf, aw/2, 0)
        cr.paint()
        # Left Page
        w = self.pixbuf2.get_width()
        h = self.pixbuf2.get_height()
        n = ah*w/h
        buf = self.pixbuf2.scale_simple(n, ah, GdkPixbuf.InterpType.BILINEAR)
        Gdk.cairo_set_source_pixbuf(cr, buf, aw/2-n, 0)
        cr.paint()
        # __done__
        print('scale:  {}'.format(time.time() - t))

class AApplication(Gtk.Application):
    __gtype_name__ = 'AApplication'
    def __init__(self):
        GLib.set_prgname('AApplication')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        AWindow(self)
    
    def do_activate(self):
        self.props.active_window.present()

AApplication().run(sys.argv)

scale_simple 版

差がなかった。
小さく描写だと scale_simple、大きく描写だと Matrix のほうがチビッと速い。
とはいえ人間の目で解るような違いは何をやっても出ない。

画質も違いが解らないレベル。
ただリサイズのスムースさは Matrix 版のほうが気のせいレベルで上かも。

cr.set_matrix(matrix) はしちゃだめよ。
原点がウインドウの枠どころか影までの所になってワケワカメになるよ。
GtkWindow の decorated property を False で使うならいいけど。

Matrix 版と ClutterImage 版 comipoli との比較、
スクリーントーンの縮小も問題無し、心配不要だった。

結論、変わらねー。
せっかく勉強したんだから次の comipoli は Matrix でいこう。

PyGObject cairo

こんなページを見つけた。
Ubuntu忘備録: GdkPixbufを高速にリサイズ(cairo)

早くなるなら使ってみようかなと。
comipoli を PyGObject に戻すので Python3 で。

Gjs はリソースの cairo を使うけど PyGObject はモジュールを使う。
Pycairo は Fedora にはデフォルトで入っている。
Pycairo

手持ちの 7952x5304px な巨大画像を同じディレクトリに置いて以下を。

#!/usr/bin/env python3

import gi, sys, cairo, time
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

class AWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.d = Gtk.DrawingArea()
        self.d.connect('draw', self.on_draw)
        self.add(self.d)
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file('7952x5304.jpg')
        self.resize(600, 300)
        self.show_all()

    def on_draw(self, widget, cr):
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # method
        n = time.time()
        buf = self.pixbuf.scale_simple(aw//2, ah, GdkPixbuf.InterpType.HYPER)
        print('method  : {}'.format(time.time() - n))
        Gdk.cairo_set_source_pixbuf(cr, buf, 0, 0)
        cr.paint()
        # function
        i = time.time()
        buf2 = self.pixbuf_scale(self.pixbuf, aw//2, ah)
        print('function: {}'.format(time.time() - i))
        Gdk.cairo_set_source_pixbuf(cr, buf2, aw//2, 0)
        cr.paint()

    def pixbuf_scale(self, pixbuf, w, h):
        with cairo.ImageSurface(cairo.Format.ARGB32, w, h) as surface:
            cr = cairo.Context(surface)
            pw = pixbuf.get_width()
            ph = pixbuf.get_height()
            matrix = cairo.Matrix(w/pw, 0, 0, h/ph, 0, 0)
            cr.set_matrix(matrix)
            Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
            cr.paint()
            return Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)

class AApplication(Gtk.Application):
    __gtype_name__ = 'AApplication'
    def __init__(self):
        GLib.set_prgname('AApplication')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        AWindow(self)
    
    def do_activate(self):
        self.props.active_window.present()

AApplication().run(sys.argv)

結果

三倍以上遅いんですけど、Python だからだろうか?
ちなみにこのマシンは i5-6500(skylake) 3.2GHz と内蔵グラフィックです。
このスペックでも 0.05 秒でリサイズできるのだから scale_simple で充分かと。