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

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

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 でインストール!」
みたいなことを書く輩はいったい何故プログラミングを勉強しているのだ?
定型作業が面倒だからコードを書いて楽をするんじゃないのかと。
コードを書くのが面倒な人がそんなことするとは思えない。
定型作業はタイピング速度で解決、とか考えそう。

Gst Metadata

スマホ動画等の回転情報の件。
そういう埋め込み情報ってメタデータというんだね。
gstreamer metadata で検索したら公式のサンプルコードが見付かった。

Metadata

上部の Language から JavaScript を選択しても C のままなんだけど。
いやそれは別にいい、C のサンプルがあるだけで親切という世界。

for (let s in OBJECT) print(s);

すれば今の gjs はメソッドの存在は解る、以前はできなかったような?
別の端末で Python を立ち上げて dir していた記憶があるけど、まあいいか。

そんなことより困ったぞ。
GST_CLOCK_TIME_NONE は 18446744073709551615 だ。
JavaScript の Number は 16 桁までしか使えない。
Python なら桁数無制限だから気にしなくてもいいんだけど。

BigInt – JavaScript | MDN

コレが使えるかなと思ったけど現行 gjs は未対応。
そもそも bus.timed_pop_filtered の引数が Number 指定だった。
あぁコイツも PyGObject で作り替えするしかないのか。。。。。
って、gst_bus_timed_pop_filtered の第一引数はタイムアウト指定じゃん。

GstBus

GST_CLOCK_TIME_NONE 固定では無いみたい、一千万ナノ秒にして問題無しだった。
これで Gjs のまま書き換え作業に移れるぞ。

そんなことより、困ったのが GstElement の取得。
ハンドラの中で get_pipeline にて得た変数は当然ガベージコレクションされる。
すると GstElement の参照元まで破棄される、と気がつくのに半日かかった。
SpiderMonkey ってガベージコレクションのタイミングわかり辛ぇ!
コンストラクタで this に付けるという回避策を気がつくのに二日も使った筆者は…

それとタグって結構重複している、値も試したかぎりでは全部同じだった。
Map を使って重複タグは一つにまとめるようにしてみた。

var Y901xWindow = GObject.registerClass({
    GTypeName: 'Y901xWindow'
}, class Y901xWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        // var
        this.player = new ClutterGst.Playback();
        this.pipeline = this.player.get_pipeline();
        // etc...
        this.player.connect('ready', (playbin)=> {
            this.player.set_playing(false);
            // Get Origin size
            let vsink = playbin.get_video_sink();
            // Only ClutterGst
            let frame = vsink.get_frame();
            let d = frame.resolution.par_d;
            let n = frame.resolution.par_n;
            this.src_width = frame.resolution.width * n / d;
            this.src_height = frame.resolution.height;
            /**
             * get Tag
             */
            //let bus = vsink.get_bus(); // Not this.
            //let bus = playbin.get_pipeline().get_bus(); // GC...
            let bus = this.pipeline.get_bus();
            let meta = new Map();
            let t = GLib.path_get_basename(playbin.get_uri());
            meta.set('Title', decodeURI(t));
            for (;;) {
                let msg = bus.timed_pop_filtered(10000000, //Gst.CLOCK_TIME_NONE 
                    Gst.MessageType.ASYNC_DONE | Gst.MessageType.TAG | Gst.MessageType.ERROR);
                if (msg == null) {
                    break;
                } else if (msg.type != Gst.MessageType.TAG) {
                    break;
                }
                let tag_list = msg.parse_tag();
                tag_list.foreach((ls, tag)=> {
                    let num = ls.get_tag_size(tag);
                    for (let i=0; i<num; ++i) {
                        let val = ls.get_value_index(tag, i);
                        if (tag == 'datetime') {
                            let t = val.to_iso8601_string()
                            meta.set(tag, t);
                        } else {
                            meta.set(tag, val);
                        }
                    }
                });
            }
            for (let [key, val] of meta) print(`${key}: ${val}`);

って。

回転情報出てこないジャン!
勉強にはなったけど何も進まず正月休みが終わってしまった。