Python」タグアーカイブ

f-string

PEP 498 — Literal String Interpolation | Python.org

f-string なんてものが Python 3.6 から使えるのか。

#!/usr/bin/env python3

suzuki = 'スズキ'

# Python2 互換
print('%sのバイクはカッコイイ' % suzuki)
# Python3 format
print('{0}はスクーターもカッコイイ'.format(suzuki))
# f-string (3.6 or lator)
print(f'{suzuki}は自動車もカッコイイ')

# raw f-string
lf = '元からある改行はダメだけど\n'
print(fr'{lf}この \n は改行されない')

# docstring
doc = 'ヒアドキュメント'
print(f'''これなら ' も " も普通に使える
まるで{doc}だね''')

# ゼロ詰め
for n in range(3):
    print(f'{n:#03d}')

なるほど。

当然だけど f-string が一番短く書ける、かつ理解しやすい。
ゼロ詰めが可能なので js のテンプレートリテラルより便利。
今後 Python はコレでいこう。

vscode だとヘンテコな色分けをしてくれます。
てか f’ と打ったら閉じクォートを勝手に補完するのをヤメてくれ!
閉じクォート補完設定は OFF にしているのに、別設定なのかよ。
本当に余計なことばかりするクソエディタだな。

私的には $ 記号が欲しかった、sh, php, js 互換になるし。

__getitem__ for in

Python で自作クラスに __getitem__ を定義すれば for 文が使えるようになる。
しかし場合によっては無限大になったり存在しない値を戻したりしかねない。

len から自前で最大値を取得して、という方法ではないようだ。
for 文はいったい何を基準で抜けているのか。

配列の範囲外を得ようとすると IndexError の例外を投げてくる。
ということはコレを利用して for 文を抜けているのだろうか。
下記の __getitem__ が無限になってしまうコードに入れて実験。

#!/usr/bin/env python3

class Test():
    def __getitem__(self, n):
        if n > 3:
            raise IndexError
        return n * 2

t = Test()

for n in t:
    print(n)

'''output
0
2
4
6
'''

なるほど。

Poppler で範囲外を指定すると None を戻すだけで例外にならず親でエラーになっていた。
cbz 等は self.namelist が IndexError を吐いて親に raise していただけ。
Python が用意してくれた例外に頼らず自身で raise していこう。

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 必須になるけど、いやそのほうがいいでしょ普通。
たまには公式サイトも見ることにしよう。

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 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 で充分かと。