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

macOS Get UTI

Apple 関連で開発をしていると UTI を調べる必要がある場合が多々ある。

Uniform Type Identifier – Wikipedia

Uniform Type Identifier Concepts

検索をしていたら素敵なページを見つけた。

Mac や iOS でファイルの種類を表す識別子 Uniform Type Identifiers を拡張子から調べる(Swiftで1行で出来る) – niwatakoのはてなブログ

てゆーか JXA でも PyObjC でもできる。
しかし困ったことに JXA では CFString が NSString にキャストできない。

objective c – JXA: Accessing CFString constants from CoreServices – Stack Overflow

上記を見つけてようやく解決。
console.log って C 言語の char[] を出力できる、初めて知った。
UTF16LE に変換は不要、CJK 文字列でも問題ないようです。

#!/usr/bin/osascript -l JavaScript

let jp = $('スズキ GSX250R').UTF8String;
console.log(jp);
//=> スズキ GSX250R

// ex: ft=js.jxa

ということで JXA にて簡単に調べるコマンドを作ってみる。

#!/usr/bin/osascript -l JavaScript

ObjC.import('CoreServices');

function run(argv) {
    for (let ext of argv) {
        let uti = $.UTTypeCreatePreferredIdentifierForTag(
            $.kUTTagClassFilenameExtension, $(ext), null);
        let s = $.CFStringGetCStringPtr(uti, 0);
        console.log(`${ext}: ${s}`);
    }
}

// ex: ft=js.jxa

getuti.js

せっかくなので基底タイプも調べたいぞ。
JXA で得る方法が解らなかったので PyObjC で書いてみる。
PyObjC は CFDictionary や CFString も Python の型と等価なので超簡単。
他の言語を使うのが馬鹿馬鹿しくなってしまうので注意が必要。

#!/usr/bin/env python3

import sys, CoreServices

for ext in sys.argv[1:]:
    uti = CoreServices.UTTypeCreatePreferredIdentifierForTag(
        CoreServices.kUTTagClassFilenameExtension, ext, None)
    arr = CoreServices.UTTypeCopyDeclaration(uti)['UTTypeConformsTo']
    con = ','.join(arr)
    print(f'{ext}: {uti} [{con}]')

# ex: ft=py

@PyObjC

getuti という拡張子の無い名前で +x のパーミッションを付けパスの通った場所へ。

getuti

JXA でやりたかったけどまだまだ修行が足りない。

注意: macOS 13 Ventura からは上記が使えません。
macOS: Get UTI (Ventura) | Paepoi Blog

Atom 1.44 Script

筆者は Fedora と MacBook Air を 9:1 程度の割合で使っている。
えっ Windows ?もう Office と業務アプリ以外で使う人いないでしょ。

macOS で Atom の 1.44 アップデートがきたので更新。
設定のコミュニティパッケージが消えた、何でや!
いや atom-runner とかは普通に動作しているんですけど。

GitHub – rgbkrk/atom-script

原因は Script パッケージを無効にしたまま残していたからだった。
1.43 時からバグっていたけど、メンテナンスする人がいないのか…

Atom: Script to atom-runner | Paepoi Blog

削除したら設定のコミュニティパッケージは正常に戻った。
表示がおかしくなった人は無効にしているパッケージを削除してみて。

GitHub – lsegal/atom-runner

いやまあ、atom-runner も 4 年放置されているけど。
更新されないから逆に自分で書き換えたコードが上書きされないメリットも。
「Atom を Gedit のように使う」のページもそろそろ更新しなきゃ。

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 ビューアに必要かどうかは微妙。

1200px

PC での twitter の画像観覧が少し前から原寸になった。
twitter の 4 コマ漫画が大好きな筆者は少し困ったことに。

少し前まで普通にコンテキストメニューから落とすと縦 1200px 固定だった。
原寸を落とす方法も知っていたけど cbz にする場合このサイズが都合よかった。

つまりその時作った cbz が沢山ある。
それも完結していない続き物が多い。

続き物のページを cbz に追加する場合は縦 1200px に縮小しないと整合性が。
原寸は巨大なものが多いので表示に時間が掛かるというのもある。
途中のページから急に重く、なんて嫌だよ。

そういえば追加する時に以前追加した最後の名前は何だったか調べるのも面倒。
020.jpg だったら 021.jpg にして追加したい、ずっとそうしているし。

画像を縮小して勝手に cbz ファイルから名前を調べて追加。
なんて定型作業を自動化したいな。

だったら自分で作ればいいじゃないか!

#!/usr/bin/env python3

'''
    ダウンロードした続き物の twitter 4コマまんがを選択して使う
    ダイアログで選択した cbz ファイルの 001.jpg から始まる名前を取得
    名前順の最後になるよう 022.jpg 等の名前を付ける
    縦 1200px に画像をサイズダウンしキャッシュに保存
    それを cbz ファイルに追加
    までを自動化する Nautilus スクリプト
'''

import os, re, zipfile, gi
gi.require_version('Gtk', '3.0')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import GLib, Gtk, GdkPixbuf

# move ~/.catch
cachedir = GLib.get_user_cache_dir()
os.chdir(cachedir)

dlg = Gtk.FileChooserNative(title='Open', action=Gtk.FileChooserAction.OPEN)
dlg.set_current_folder(GLib.get_home_dir())
ft = Gtk.FileFilter()
ft.set_name('Comic Book Archive')
ft.add_mime_type('application/x-cbz')
ft.add_mime_type('application/vnd.comicbook+zip')
dlg.add_filter(ft)
r = dlg.run()
dlg.destroy()
if r == Gtk.ResponseType.ACCEPT:
    arc_name = dlg.get_file().get_path()
    with zipfile.ZipFile(arc_name, 'a') as z:
        # last name
        s = z.namelist()[-1]
        num = int(os.path.splitext(s)[0])
        # loop
        path_array = os.environ['NAUTILUS_SCRIPT_SELECTED_FILE_PATHS'].split('\n')
        for filepath in path_array:
            try:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file(filepath)
            except:
                continue
            # resize
            p_width = pixbuf.get_width()
            p_height = pixbuf.get_height()
            width = p_width * 1200 // p_height
            smallpix = pixbuf.scale_simple(width, 1200, GdkPixbuf.InterpType.BILINEAR)
            # create name
            ext = os.path.splitext(filepath)[1]
            num += 1
            name = f'{num:03d}{ext}'
            # create jpeg or png
            if re.search(r'\.(jpg|jpeg)$', filepath, re.I):
                smallpix.savev(name, 'jpeg', ['quality'], ['85'])
            elif re.search(r'\.png$', filepath, re.I):
                smallpix.savev(name, 'png', ['compression'], ['9'])
            z.write(name)

作ってみた。

file-roller で一旦 cbz 内のファイル名を調べる必要が無くなった。
1.jpg とか適当な名前の一時保存でも追加時に自動変名、我ながら超便利。
何故今までコレを思いつかなかったのだ俺よ。

ZipFile 作成にうっかり w 指定をして消えてしまったファイルも…
こういうのを作る時はバックアップをしてから、絶対だよ。

しかし特に Node.js 屋で見かけるんだけーが。
「コードを書くのが面倒だから npm でインストール!」
みたいなことを書く輩はいったい何故プログラミングを勉強しているのだ?
定型作業が面倒だからコードを書いて楽をするんじゃないのかと。
コードを書くのが面倒な人がそんなことするとは思えない。
定型作業はタイピング速度で解決、とか考えそう。