GTK+」タグアーカイブ

PyGObject Exif

わざわざ八田川まで通っていた日々は、意味あったジャン。
メジロやキセキレイを見つけた、これらも五条川で見たことない。

メジロってスズメより小さくてカワイイんだよ。
団体行動しかしないからスズメみたいにドコにでもいるわけじゃないんだよ。
しかも全然木から降りてこないし常にアクロバットに動き回っているんだよ。

mejiro

というわけで撮りにいってきた。
キセキレイはまだ一度しか見たことないな、普段ドコにいるんだろう?
ちなみにカワセミは前回とまったく同じ橋の日陰にいた。

ところで。

Fedora の場合 Exif 情報を得るには GExiv2 を使えばいい。
と書いたけど前回の PyObjC のように一覧表示する方法は知らなかった。
ということでチト調べよう。

C
gexiv2/gexiv2-metadata-exif.cpp ? master ? GNOME / gexiv2 ? GitLab

PyGObject
GExiv2.Metadata – Classes – GExiv2 0.10

get_exif_tags で配列を得て get_tag_string すればよさそう。
やってみたら Exif.Panasonic から始まる LUMIX 独自タグがズラリ。
Exif.Panasonic.LensType だけは取得したいな。
最後の LensType だけで見分けしたけど他のメーカーも同じなのかは知らない。

基本的には Exif.Image と Exif.Photo 接頭子のものだけ得られればよさげ。
あと邪魔なので省いたタグについてはソース内の URL で。
ということでこんなコードに。

#!/usr/bin/env python3

import sys, gi
gi.require_version('GExiv2', '0.10')
from gi.repository import GExiv2

def get_exif_dict(filename):
    metadata = GExiv2.Metadata()
    metadata.open_path(filename)
    tags = metadata.get_exif_tags()
    res = {}
    for tag in tags:
        if tag == 'Exif.Photo.MakerNote':
            # https://www.exiv2.org/makernote.html
            continue
        elif tag == 'Exif.Image.PrintImageMatching':
            # https://kotobank.jp/word/PRINT%20Image%20Matching-12107
            continue
        elif tag.startswith('Exif.Image.0x'):
            # https://exiftool.org/TagNames/EXIF.html
            continue
        elif tag.startswith('Exif.Image') or \
                tag.startswith('Exif.Photo') or \
                tag.endswith('LensType'):
            value = metadata.get_tag_string(tag)
            print(f'{tag}: {value}')

d = get_exif_dict(sys.argv[1])

exif

これと PyObjC 版の出力を比較すれば両方で使えるツールを作れそう。
ぶっちゃけデジイチの時点で mac 用しか需要なんて無いんだけど。

GtkDialog

GtkDialog のボタンをメッセージが長くても幅いっぱいに広げる方法が解った。
action_area って実は GtkButtonBox だった。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
 
dlg = Gtk.Dialog()
dlg.vbox.pack_start(Gtk.Label(label='a'*50, visible=True), False, False, 30)
dlg.add_button('_OK', Gtk.ResponseType.CLOSE)
dlg.add_button('_Cancel', Gtk.ResponseType.CLOSE)

# Expand
area = dlg.get_action_area()
area.props.layout_style = Gtk.ButtonBoxStyle.EXPAND

dlg.run()
dlg.destroy()

expand_buttons

それだけだったのか。。。。。

ついでに解った。
use-header-bar property を 1 にすれば buttons が headerbar になる。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
 
dlg = Gtk.Dialog(use_header_bar=1)
dlg.vbox.pack_start(Gtk.Label(label='Titlebar Buttons', visible=True), False, False, 30)
dlg.add_button('SUZUKI', Gtk.ResponseType.CLOSE)
dlg.add_button('MotorCycle', Gtk.ResponseType.CLOSE)
dlg.add_button('Cool', Gtk.ResponseType.CLOSE)

dlg.run()
dlg.destroy()

titlebar_buttons

そうだったのか。。。。。

それとタイトルバーを消す方法なんだが。
非表示の GtkTitleBar をセットすればとりあえず消すことはできると解った。
でも角が丸くならない、色々試したけど上手くいかない。
で思いついた、GtkMessageDialog のボタンを使わず add_button すればよくね?

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
 
dlg = Gtk.MessageDialog(text='SUZUKI MotorCycle is Bery Cool!', visible=True)
dlg.add_button('_OK', Gtk.ResponseType.CLOSE)
dlg.add_button('_Yes', Gtk.ResponseType.CLOSE)
dlg.add_button('_Agree', Gtk.ResponseType.CLOSE)

dlg.run()
dlg.destroy()

no_titlebar_dialog

何故今迄気が付かなかったんだ。。。。。

てか、統一感を出そうとするなら GtkMessageDialog 継承が一番じゃん。
そんなこんなで我がアプリは結局コレに書き換え。
prev のファイル名表示を追加しようと思ったけどゴチャゴチャするのでヤメ。
Comipoli 0.4.1 公開、本サイト更新のお知らせは久々だな。

ところで、この件でソースコードを見てやろうって思ったんだが。

gtk/gtkmessagedialog.c at master ? GNOME/gtk ? GitHub

gtk_widget_add_css_class という関数を見つけたんだけど。

GtkWidget: GTK 4 Reference Manual

コレって GTK4 からの関数なんですけど。
え、もしかして次の GNOME から GTK4 なんですか?

Gtk Next/Prev Dialog

Fedora を 32 (GNOME 3.36) にして一ヶ月たった。
今更気が付いたんだけーが GtkMessageDialog のバグが直っている。

GNOME 3.34 modal dialog bug | Paepoi Blog

この時に拙作 Comipoli は GtkInfoBar に変更した、そのまんまだ。
コレってフルスクリーンで cbz を見ていた時には解り辛いんだよね。
ダイアログのほうが視覚的に理解しやすいので元に戻すことにした。

戻すだけじゃつまらない、何か機能の追加か変更をしたい。
pixivコミックやニコニコ漫画のように「前の話」機能を追加することに。
って、GtkMessageDialog では YES/NO しか選択できないんだが。

ということで、GtkDialog を使って自作することにした。
Prev のショートカットはどうするか、Enter/Esc は勝手にやってくれるが。
ええい面倒だ、ニーモニックをそのまま描写しちゃえばいいや!

#!/usr/bin/env python3

from gi.repository import Gtk

class ComipolNextDialog(Gtk.Dialog):
    def __init__(self, text):
        Gtk.Dialog.__init__(self)
        self.set_resizable(False)
        # Buttons
        self.add_button('_Cancel (ESC)', Gtk.ResponseType.CLOSE)
        self.add_button('_Next (Enter)', Gtk.ResponseType.YES)
        self.add_button('_Prev (Alt+P)', Gtk.ResponseType.NO)
        self.set_default_response(Gtk.ResponseType.YES)
        # Message
        message = Gtk.Label(label=f'<span bgcolor="#CC0099">Next:</span> {text}', use_markup=True, visible=True)
        self.vbox.pack_start(message, False, False, 30)

こうなった。
YES/NO は最適なものが無いので簡易利用しただけ。
ボタン自体にショートカットが書いてある親切な仕様になってしまった。

んで呼び出し。
一つ前が存在しない時はボタンを無効にする必要があるなと。

dlg = ComipolNextDialog(html.escape(cbzs[next_index]))
dlg.set_transient_for(self)
if next_index < 2:
    dlg.set_response_sensitive(Gtk.ResponseType.NO, False)
res = dlg.run() 
if res == Gtk.ResponseType.YES:
    uri = GLib.filename_to_uri(GLib.build_filenamev([path, cbzs[next_index]]), None)
    self.set_uri(uri)
elif res == Gtk.ResponseType.NO:
    uri = GLib.filename_to_uri(GLib.build_filenamev([path, cbzs[next_index - 2]]), None)
    self.set_uri(uri)
dlg.destroy()

コレでイケた。
どんな感じに表示されるか。

dlg

うーん、GtkMessageDialog よりボタンの大きさが小さいなぁ。
勝手に等間隔にはなるけど長いファイル名だと右に寄ってしまうし。
アクションエリアにアクセスが非推奨ではどうにもできない。
タイトルバーも出るんだね、無しにしたいけど手段が解らない。
iPhone を考えるとあのスタイルが普通になるのよね今後は。

とはいえ動作は問題無しなのでコレでいこう。

GExiv2 and subprocess

前回の GExiv2 の件。
comipoli に実装しようとしてハマったので覚書。

GSubprocess から GUnixInputStream を得て GExiv2 に読ませる。
その GUnixInputStream からは GdkPixbuf を作ることができない。
順番を逆にしても駄目、Stream の再利用はできないみたい。

他の手段で GExiv2 を使おうと思ったけど上手くいかない。
他の手段で GdkPixbuf を得ることは無理。
exif 取得と画像取得で別にアクセスするしかないようだ。
遅くなるだろうけど気にならないレベルならいいかなって。

しかし GSubprocess をもう一つ使をうとするもエラー。
原因は解らない、GSubprocess の情報なんて皆無だ。

失敗をズラズラ書いても無意味なので結論。
GSubprocess と subprocess を両方使う強行手段でなんとかなった。

class ComipoliArchive:
    def __init__(self):
    	# etc...

    def _zip_escape(self, filename):
        ESCAPE = '[]*?!^-\\'
        res = ''
        for s in filename:
            if s in ESCAPE:
                res += '\\'
            res += s
        return res

    def _on_wait(self, proc, res):
        proc.wait_check_finish(res)

    def __getitem__(self, num):
        if num == self.max:
            raise IndexError
        try:
            args = ['unzip', '-pj', self.path, self._zip_escape(self.namelist[num])]
            # tag
            ori = 0
            pr = subprocess.Popen(args, encoding='UTF-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            fd = pr.stdout.fileno()
            with open(fd, 'rb') as f:
                metadata = GExiv2.Metadata()
                metadata.open_buf(f.read())
                ori = metadata.get_orientation()
            pr.wait()
            # pixbuf
            sp = Gio.Subprocess.new(args, Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE)
            sp.wait_check_async(None, self._on_wait)
            stream = sp.get_stdout_pipe()
            p = GdkPixbuf.Pixbuf.new_from_stream(stream)
            if ori == GExiv2.Orientation.HFLIP:
                p = p.flip(True)
            elif ori == GExiv2.Orientation.ROT_180:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.UPSIDEDOWN)
            elif ori == GExiv2.Orientation.VFLIP:
                p = p.flip(False)
            elif ori == GExiv2.Orientation.ROT_90_HFLIP:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.CLOCKWISE).flip(True)
            elif ori == GExiv2.Orientation.ROT_90:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.CLOCKWISE)
            elif ori == GExiv2.Orientation.ROT_90_VFLIP:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.CLOCKWISE).flip(False)
            elif ori == GExiv2.Orientation.ROT_270:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.COUNTERCLOCKWISE)
            stream.close()
            sp.force_exit()
            return p
        except Exception as e:
            raise e

前回の cbz を開いてみる。

orientation

よし回転されているな。
注意点は ROT_90 は 90 度傾いているから 270 度回転させるということ。
戻す度数という意味ではない、flip は手持ちサンプルが無いのでこれで正しいか未確認。
後 Popen は wait を忘れずに。

Scaling: GDK-PixBuf Reference Manual

展開速度も別に気にならない、てか何か変わったか?のレベル。
多分 GExiv2 が凄いだけなんだろうけど。

GExiv2

Nautilus のサムネイルはスマホの縦画像は縦に表示される。
けれど cbz にアーカイブすると横向きになってしまうようだ。

orientation1

iPhoneからアップロードしたJPEG写真が横向きになる問題(EXIF, Orientation) – Qiita

つまりスマホで撮影した写真は実は全部横向き。
EXIF 情報によりアプリ側が縦表示等に回転しているというだけの話。
アプリ側がそういう処理をしていないとそのまま表示される。

回転情報を得るにはこの EXIF 情報を得る必要がある。
GNOME は Exiv2 というライブラリがあり GExiv2 が GIR で提供されている。

Exiv2 – Image metadata library and tools

ghiro/gexiv.py at master ? Ghirensics/ghiro ? GitHub

GExiv2.Metadata – Classes – GExiv2 0.10

コレを参考に cbz アーカイブ内の画像からも取得してみる。

#!/usr/bin/env python3

import os, gi
gi.require_version('GExiv2', '0.10')
from gi.repository import GExiv2, Gio

FILENAME1 = os.path.expanduser('~/pic/exiv/IMG_0182.JPG')
FILENAME2 = os.path.expanduser('~/pic/exiv/cbz.cbz')

# filename
metadata = GExiv2.Metadata()
metadata.open_path(FILENAME1)
n = metadata.get_orientation()
print(n)

# cbz
cmd_array = ['unzip', '-pj', FILENAME2, 'IMG_0182.JPG']
sp = Gio.Subprocess.new(cmd_array, Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE)
stream = sp.get_stdout_pipe() # GUnixInputStream
fd = stream.get_fd()
with open(fd, 'rb') as f:
    metadata2 = GExiv2.Metadata()
    metadata2.open_buf(f.read())
    n = metadata2.get_orientation()
    print(n)

orientation2

GIR でアーカイブ内のバイナリ指定は迷ったけど上記でイケた。
Python の open ってファイルディスクリプタ指定でもイケるんだ。
GSubprocess を使ったけど subprocess モジュールでも同じだと思う。
で、とにかく値を得たら

gdk_pixbuf_rotate_simple
gdk_pixbuf_flip

で回転やフリップすればスマホ写真には対応できる。
ただ、この処理が cbz ビューアに必要かどうかは微妙。