JXA」タグアーカイブ

JXA: Image Resize and Save

今回は JXA にて画像のリサイズと保存。

(旧) Cocoaの日々: キャプチャ画像を縮小して保存する

Cocoa は他の OS のグラフィック(デバイス)コンテキストと違っていて
現在フォーカスがあるグラフィックコンテキストに描写する、という手法。
つまり NSImage に NSImage をリサイズして描写するってことみたい。

各画像フォーマットのデータを作成する

NSDictionary を作るのが面倒だなぁ。
変換で楽したいし NSImageCompressionFactor は調べると NSString だし。
ってことで。

ObjC.import("Cocoa");

console.log($.NSImageCompressionFactor.js);
//=> NSImageCompressionFactor

まんまカヨ!

クオリティは 1.0 が最高値みたい
筆者はいつものように GIMP に合わせて 0.85 にしてみる。
ということでこんなコードになりました。

#!/usr/bin/osascript

ObjC.import("Cocoa");

function run(argv) {
    let srcImage = $.NSImage.alloc.initWithContentsOfFile(argv[0]);
    // Create 300x200 Image
    let newImage = $.NSImage.alloc.initWithSize($.NSMakeSize(300, 200));
    newImage.lockFocus;
    $.NSGraphicsContext.saveGraphicsState;
    $.NSGraphicsContext.currentContext.setImageInterpolation($.NSImageInterpolationHigh);
    srcImage.drawInRectFromRectOperationFraction(
        $.NSMakeRect(0, 0, 300, 200),
        $.NSZeroRect,
        $.NSCompositeSourceOver,
        1.0);
    $.NSGraphicsContext.restoreGraphicsState;
    newImage.unlockFocus;
    // JPEG Write;
    let bmp = $.NSBitmapImageRep.imageRepWithData(newImage.TIFFRepresentation);
    bmp.alpha = false;
    let prop = $({"NSImageCompressionFactor": 0.85});
    let data = bmp.representationUsingTypeProperties($.NSJPEGFileType, prop);
    data.writeToFileAtomically($(`${argv[0]}_300x200.jpg`), true);
}

prop の値を変更すると画像のサイズと画質が変わるのが確認できた。
これでサムネイル選択の作成に取りかかれる、最大の難関みたいな気がするけど。

JXA: NSApplication argv

JXA で osacompile にて app 化すると argv が仕事しない件。
NSProcessInfo を使うことで解決した。

function run(argv) {
    $.NSApplication.sharedApplication;
    const window = new MyWindow();
    $.NSApp.setActivationPolicy($.NSApplicationActivationPolicyRegular);
    ObjC.registerSubclass({
        name: "AppDelegate",
        protocols: ["NSApplicationDelegate"],
        methods: {
            "applicationDidFinishLaunching:": {
                types: ["void", ["id"]],
                implementation: (notification)=> {
                    let arr = $.NSProcessInfo.processInfo.arguments.js;
                    if (arr.length > 2) window.setPath(arr[2].js);
                }
            }
        }
    });
    //
    // etc..
    //
    // Not work after compiling
    //if (argv.length < 0) window.setPath(argv[0]);
    $.NSApp.delegate = $.AppDelegate.new;
    $.NSApp.run;
}

引数の数に注意。
[“osascript”, “src”, parm1, param2, …]

これで js のままでも app にコンパイルしても両方仕事する。
スクリプトのまま使うなら argv を使ったほうが簡単だけどね。

ところで前回の zipinfo の件だけど。
macOS の zip は -U オプションすら使えないじゃん。
パッチが当たっていないどころか UNICODE サポートですらないってことだ。
ということでこいつの出力からの変換は不可能であるようだ。

で、zipdetails という perl のコマンドを見つけた。
こいつの出力からなんとかならないかな?
かなり意地悪な zip ファイルを用意して試す。

改行が UNOCODE 未対応のようで化ける、駄目だ。
でも perl でできるということはスクリプトで得られるということだ。
バイナリを見ると、単純にファイル名は構造体にそのまんま入っているヤン。

って、だから JavaScript じゃバイナリは直接扱えないんだってばさ!
UInt8Array に変換すると超激遅なのは Gjs で経験済み。
Objective-c での手段も見つからない、みんな C でやっているの?

もう macOS 版は日本語アーカイブをサポートしない、で終わりにしよう。

JXA: is Command Exist

Comipoli JXA 版はココにきて色々問題が出て困っている。
Fedora で作成した日本語入り cbz が開けなかったり…
コンパイルすると起動引数を受け付けないとか…
他色々…

間違いを堂々と書いていたり、私ってほんとバカ(古い
JXA NSOpenPanel | PaePoi
後で修正するのも面倒なので確実なものだけ書くようにしたい。

そういえば unrar や 7za がインストールしてあるか確認しないと。
JXA では、えっと。

#!/usr/bin/osascript

ObjC.import("Cocoa");

function isCommandExist(cmd) {
    let task = $.NSTask.new;
    let pipe = $.NSPipe.pipe;
    task.standardOutput = pipe;
    task.standardError = pipe;
    task.launchPath = "/usr/bin/type";
    task.arguments = $([cmd]);
    task.launch;
    task.waitUntilExit;
    return task.terminationStatus === 0;
}
console.log(isCommandExist("unzip"));
console.log(isCommandExist("unrar"));

これでいいか。

stdout, stderror を吐かないように pipe に入れて捨てている。
> /dev/null を配列にいれるとエラーになった。

ところで zipinfo -1 という素敵な手段を今頃知った。
unzip -Z -1 でもいい。

 if (/\.(cbz|zip)$/i.test(path)) {
    this.status = 0;
    this.namelist = [];
    //let output = this.getString(["unzip", "-Z", path]);
    let output = this.getString(["zipinfo", "-1", path]);
    let list = output.split("\n");
    for (let line of list) {
        if (PICEXT.test(line)) {
            this.namelist.push(line);
        }
        /*if (line.startsWith("-")) {
            let name = line.slice(53);
            if (PICEXT.test(name)) {
                this.namelist.push(name);
            }
        }*/
    }
    this.namelist.sort();
}

今まで何を。。。。。

Fedora も同様だった。
オリジナルも次はこれにしよう。

JXA NSToolbar (HeaderBar)

Cocoa にて GNOME アプリの GtkHeaderBar を再現したい。
ようするに Safari のタイトルバーみたいにボタンを置きたい。
GNOME は完全に専用 widget だけど Cocoa ではどうやっているのだ?

調べてみるとアレは実は単なる NSToolbar だ。
配置してタイトルバーを消す設定にすると勝手にタイトルバー化。

よしやってみよう、と探しても Xcode で扱う手段しか見つからない。
えっと Xcode は否定しないけど、皆 Xcode の使い方ばかり覚える方向になっていない?
Visual Studio 使いをソレで笑っていたけど macOS 屋もたいして変わらなかった。

NSToolbarDelegate – AppKit | Apple Developer Documentation

つぎはぎだらけの情報で作り方をまとめると。
NSToolbarDelegate は必須。
Configuring a Toolbar の項目は全部 override 必須。
NSToolbarItem は NSToolbarDelegate 内で作る。
NSToolbarItem の view には NSButton, NSTextField 等を指定できる。
NSLabel なんて無いから NSTextField を使う。
右寄せや中央配置には NSToolbarFlexibleSpaceItemIdentifier を使う。
NSToolbarDelegate の関数名は合体せずそのまんま書く。

実際の作り方はコードで。

#!/usr/bin/osascript

ObjC.import("Cocoa");

ObjC.registerSubclass({
    name: "ToolbarDelegate",
    protocols: ["NSToolbarDelegate"],
    methods: {
        "toolbarAllowedItemIdentifiers:":{
            types: ["id", ["id"]],
            implementation: (toolbar)=> {
                return $([]);
            }
        },
        "toolbarSelectableItemIdentifiers:":{
            types: ["id", ["id"]],
            implementation: (toolbar)=> {
                return $([]);
            }
        },
        "toolbarDefaultItemIdentifiers:":{
            types: ["id", ["id"]],
            implementation: (toolbar)=> {
                console.log("toolbar Default");
                return $.NSArray.arrayWithObjects(
                    $("OPEN"),
                    $.NSToolbarFlexibleSpaceItemIdentifier,
                    $("TITLE"),
                    $.NSToolbarFlexibleSpaceItemIdentifier,
                    $("LR"),
                    $("RIGHT")
                );
            }
        },
        "toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:": {
            //types: ["id", ["id", "id", "bool"]],
            implementation: (toolbar, itemIdentifier, flag)=> {
                console.log(itemIdentifier.js);
                let item = $.NSToolbarItem.alloc.initWithItemIdentifier(itemIdentifier);
                if (itemIdentifier.isEqualToString($("OPEN"))) {
                    item.view = openButton;
                } else if (itemIdentifier.isEqualToString($("TITLE"))) {
                    item.view = label;
                } else if (itemIdentifier.isEqualToString($("LR"))) {
                    item.view = lrButton;
                } else if (itemIdentifier.isEqualToString($("RIGHT"))) {
                    item.view = thumbButton;
                }
                return item;
            }
        }
    }
});

// buttons Delegate
ObjC.registerSubclass({
    name: "ToolButtonDelegate",
    methods: {
        "onToolButtonClicked": {
            types: ["void", ["id"]],
            implementation: (button)=> {
                label.stringValue = button.title;
            }
        }
    }
});
let appDelegate = $.ToolButtonDelegate.new;

/**
 * ToolBar Item
 */
// open
let openButton = $.NSButton.alloc.initWithFrame($.NSMakeRect(10, 10, 60, 40));
openButton.title = "Open";
openButton.bezelStyle = $.NSRoundedBezelStyle;
openButton.target = appDelegate;
openButton.action = "onToolButtonClicked";
// title
label = $.NSTextField.alloc.initWithFrame($.NSMakeRect(10, 10, 300, 20));
label.drawsBackground = false;
//label.bordered = false;
label.editable = false;
label.selectable = false;
label.stringValue = "Title";
// L/R
let lrButton = $.NSButton.alloc.initWithFrame($.NSMakeRect(10, 10, 60, 40));
lrButton.title = "L<-R";
lrButton.bezelStyle = $.NSRoundedBezelStyle;
lrButton.buttonType = $.NSPushOnPushOffButton;
lrButton.target = appDelegate;
lrButton.action = "onToolButtonClicked";
// thumbnail
let thumbButton = $.NSButton.alloc.initWithFrame($.NSMakeRect(10, 10, 60, 40));
thumbButton.image = $.NSImage.imageNamed($.NSImageNameIconViewTemplate);
thumbButton.bezelStyle = $.NSRoundedBezelStyle;
thumbButton.target = appDelegate;
thumbButton.action = "onToolButtonClicked";

/**
 * ToolBar
 */
let toolbar = $.NSToolbar.alloc.initWithIdentifier($("ToolbarTest"));
//toolbar.allowsUserCustomization = true;
//toolbar.autosavesConfiguration = true;
toolbar.delegate = $.ToolbarDelegate.new;

/**
 * Window
 */
let window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(0, 0, 600, 400),
    $.NSTitledWindowMask
    | $.NSClosableWindowMask
    | $.NSMiniaturizableWindowMask
    | $.NSResizableWindowMask,
    $.NSBackingStoreBuffered,
    false
);
window.orderFrontRegardless;
// Like a GtkHeaderBar
window.titleVisibility = 1;
window.toolbar = toolbar;

/**
 * Application
 */
let app = $.NSApplication.sharedApplication;
app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
app.mainMenu = function() {
    const mainMenu = $.NSMenu.new;
    const itemApp  = $.NSMenuItem.new;
    const menuApp  = $.NSMenu.new;
    itemApp.submenu  = menuApp;
    mainMenu.addItem(itemApp);
    menuApp.addItem($.NSMenuItem.alloc.initWithTitleActionKeyEquivalent("Quit", "terminate:", "q") );
    return mainMenu;
}();
app.run;

LR ボタンを右に移したけどほぼ同じタイトルバーのできあがり。
JXA だけで作れることは証明できた。

toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:
の types はコレでいいと思うんだけど何故かエラー。
コメントアウトしても動くのでこれでいいや。

allowsUserCustomization 等を true にするとカスタムできる。
ツールバーを指二本タップで出る奴、って筆者は今まで知らなかったぞ!

だって mac ではボタンなんてほとんど使わないモン。
Dock ですら邪魔なので全部消して一番小さく設定しているくらい。

Toolbar は何のアプリかを一眼で見分ける目印、って感じ。
TextEdit.app と見た目が同じじゃつまんないもんね。

JXA NSUserDefaults

GUI アプリの再起動で前回終了した位置と大きさを再現したい。
Cocoa は NSUserDefaults という超便利なものを用意しているようだ。

EZ-NET: NSUserDefaults でアプリケーションの設定値を管理する : Objective-C プログラミング

まて、JXA で js のまま動かして値を保存するとどうなるんだ?
com.apple.osascript.plist に保存されてしまった、あーあやっぱり。

Windowの位置やサイズを前回終了時の状態に復元する – Xcode coding memo

debug は当然 js のまま行うので上記の超簡単手段は使えないなぁ。
osacompile 後はアプリ名になると思うけど、多分。
つか com.apple.***.plist みたいなのばかりだし誰もやっていないんじゃないの?

NSUserDefaults – Foundation | Apple Developer Documentation

initWithSuiteName という関数があるじゃないの。
これで js のままとコンパイル後で同じ設定が読み書きできるはず。
com.*** のほうがカッコイイし。

// Read
let defaults = $.NSUserDefaults.alloc.initWithSuiteName("com.sasakima.comipoli");
let aw = defaults.floatForKey("width");
if (aw == 0) aw = 400;
let ah = defaults.floatForKey("height");
if (ah == 0) ah = 400;
let x = defaults.floatForKey("x");
let y = defaults.floatForKey("y");
this.window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(x, y, aw, ah),
//etc...

// Write
let defaults = $.NSUserDefaults.alloc.initWithSuiteName("com.sasakima.comipoli");
defaults.setFloatForKey(this.window.frame.size.width, "width");
defaults.setFloatForKey(this.window.frame.size.height, "height");
defaults.setFloatForKey(this.window.frame.origin.x, "x");
defaults.setFloatForKey(this.window.frame.origin.y, "y");
defaults.synchronize;

イケた。

floatForKey は plist が存在しない場合はゼロが戻るだけみたい。
NSRect で保存する手段もあるみたいだけど plist が存在しない場合にチト困る。

ところで NSApp という定義でアプリケーションが参照できるのね。
sharedApplication の戻り値を得る必要は無かった。

function run(argv) {
    $.NSApplication.sharedApplication;
    // etc...
    $.NSApp.run;
}

んで。
キーボード操作は暫定で以下のように、space, delete でも同様に動くように。
space を使うので装飾は opthion キーしか選択肢が無くなったことは秘密。

icns を作る方法は以下のサイトを参考に、手抜きで画像一つだけでもイケた。
icnsファイルの作り方(Mac) – 2番煎じMEMO

現在ダイアログやもう一つウインドウを出す手段に苦しんでいる。
コレができないと同一ディレクトリ巡回やサムネイル選択が作れない。
あと何やったか忘れた、ここまでのバックアップ。
comimac-0.0.1.tar.gz