Hatena::ブログ(Diary)

今日覚えたこと RSSフィード

2017.11.07

iPhone Xのスクリーンショットをディスプレイの形に合わせて切り抜くツールを作った

作ってみたよ。

このリンクからすぐ利用できるのでどうぞ。

これは何か

iPhone Xは、ディスプレイが変わった形をしていて、四角形ではない。角が丸くなってるのと、スピーカーやインカメラが配置されてる部分だけ欠けている。

https://farm5.staticflickr.com/4456/38163154342_2269fed923.jpg

こんな形。

だけど、スクリーンショットを撮ると、普通の四角形の画像が生成される。画面では欠けている部分もスクリーンショット的には存在している。

このツールは、そんな四角形のスクリーンショットの画像を、iPhone Xディスプレイの形に合わせて切り抜いた画像に変換する。

f:id:nacookan:20171107233218p:image:w500

こんな感じ。普通にスクリーンショットを撮ると左側のように四角い画像になるけど、これを右側のように切り抜き加工する。

使い方

このツールブラウザ上で利用できる。もちろんiPhoneSafariでも使える。

  1. まず上記のリンクからページを開く。
  2. 「画像を選択」というボタンがあるのでそれをタップ。
  3. フォトライブラリから、元になるスクリーンショットを選ぶ。
  4. すぐに加工された画像が表示されるので、長押しして保存。

まあ簡単だと思う。

f:id:nacookan:20171108000148p:image

ちなみに、画像をサーバー等に送って加工してるわけじゃなくて、通信をせずにブラウザがその場で(ローカルで)処理している。だからこのツールの作者(nacookan)も、このツール配信してるjsdo.itもあなたの画像を見たりはできないので安心して。

どうしても心配なら、このツールHTMLソースコードをコピーして自分だけの場所でやってもいいよ。

仕組み

コードはJavaScriptで書いてjsdo.itに置いたので自由に見れる。

<input type="file"> でファイルを選んでもらったら、File APIでオープン。それをcanvasに描画している。さらにiPhone Xディスプレイの形に塗りつぶしてある画像を用意して(Data URIに変換してコード中に書いてる)、それをクリッピングマスクとして上から重ねることで切り抜く。あとはできあがったcanvas上の画像をData URIに変換して出力。

出力する際、canvasをそのまま見せるんじゃなく非表示にして、Data URIに変換したものをわざわざimgタグに入れてるのは、canvasのままだとブラウザによっては画像として保存しづらい予感がしたのと、元データを変えずに見かけ上のサイズをブラウザの幅に合わせたかったので。もしかしたらどちらもcanvasでやれるのかも知れないけど、よくわかんないので扱い慣れたimgタグにした。

あとコードを見るとわかるけど、一応は横画面にも対応してる。ただ、270°の回転(左に倒す)で固定なので、もし反対方向だったらごめん。そういうときは先に画像を逆さまにしておけばいい。

関連

おわり

どうぞご利用ください。

2016.12.09

Flickrの写真をExif併記でブログに貼るコードを生成するJavaScript for Automation

最近カメラを買ったのでそのことを書いたりしていた。

撮った写真を貼るときは、まず写真をFlickrアップロードして、与えられたURLを元にコードを書くわけだけど、FlickrURLにリンクしつつ、サムネイル的に縮小画像を貼りつけ、さらに一部のExif情報も一緒に載せたい。カメラやレンズに関する話を書いてるとき、そこに貼る写真については、それくらいやりたい。

これが、手動でやると相当メンドイし間違えたりするので、ツールを作ってみた。

ObjC.import('Cocoa');

var sys = Application("System Events");
sys.includeStandardAdditions = true;
var cmd = Application.currentApplication();
cmd.includeStandardAdditions = true;

var template =
    '|[$PHOTO_URL$:image=$THUMB_URL$:w512]||\n' +
    '|$CAMERA$, $LENS$<br>' +
    '$FOCAL_LENGTH$($FOCAL_LENGTH_IN35$), $EXPOSURE_TIME$, f$F_NUMBER$, ISO$ISO$||\n';

while(true){
    var url = sys.displayDialog(
        'Flickr URL',
        { defaultAnswer: 'https://www.flickr.com/photos/username/99999999999/' }
    ).textReturned;
    
    /^(.+\/photos\/.+?\/([0-9]+)\/)/.test(url);
    var photoUrl = RegExp.$1
    var photoId = RegExp.$2;
    var photoCurl = cmd.doShellScript('curl -L "' + photoUrl + '" | grep root.YUI_config.flickr.api.site_key ');
    /"(.+)"/.test(photoCurl);
    var apiKey = RegExp.$1;

    var apiUrl = 'https://api.flickr.com/services/rest?' + [
        'photo_id=' + photoId,
        'method=flickr.photos.getExif',
        'api_key=' + apiKey,
        'format=json'
    ].join('&');
    var apiCurl = cmd.doShellScript('curl -L "' + apiUrl + '"');

    /(\{.+\})/.test(apiCurl);
    var json = RegExp.$1;
    var data = JSON.parse(json);

    var map = {};
    map['PHOTO_URL'] = photoUrl;

    map['THUMB_URL'] = [
        'https://farm' + data.photo.farm + '.staticflickr.com',
        data.photo.server,
        data.photo.id + '_' + data.photo.secret + '_z.jpg'
    ].join('/');

    map['CAMERA'] = data.photo.camera;
    
    var exif = data.photo.exif;
    for(var i = 0; i < exif.length; i++){
        if(exif[i].tag == 'LensModel'){
            map['LENS'] = exif[i].raw._content;
        }
        if(exif[i].tag == 'FocalLength'){
            map['FOCAL_LENGTH'] = exif[i].clean._content.replace(/ /g, '');
        }
        if(exif[i].tag == 'FocalLengthIn35mmFormat'){
            map['FOCAL_LENGTH_IN35'] = exif[i].raw._content.replace(/ /g, '');
        }
        if(exif[i].tag == 'ExposureTime'){
            map['EXPOSURE_TIME'] = exif[i].raw._content;
        }
        if(exif[i].tag == 'FNumber'){
            map['F_NUMBER'] = parseFloat(exif[i].raw._content, 10).toString();
        }
        if(exif[i].tag == 'ISO'){
            map['ISO'] = exif[i].raw._content;
        }
    }
    var output = template.replace(/\$(.+?)\$/g, function(m, p1){
        return map[p1];
    });
    sys.displayDialog(output);
}

JavaScript for Automationで作ったので、Macスクリプトエディタにこのスクリプトコピペして、アプリケーション形式で保存する。以後はそのappファイルを起動することで動作する。

使い方

起動するとURLを聞かれるので、そこにFlickrURLを入力する。

そして3秒くらい待つと、ブログ貼り付け用のコードが出力される。

例えば、

https://www.flickr.com/photos/nacookan/31402730446/

こんなURLを入力したら、

|[https://www.flickr.com/photos/nacookan/31402730446/:image=https://farm6.staticflickr.com/5589/31402730446_08f21fb2a0_z.jpg:w512]||
|Sony ILCE-6500, E 35mm F1.8 OSS<br>35mm(52mm), 1/100, f3.5, ISO100||

こんなコードが生成される。

これを実際に貼り付けると、

https://www.flickr.com/photos/nacookan/31402730446/
Sony ILCE-6500, E 35mm F1.8 OSS
35mm(52mm), 1/100, f3.5, ISO100

こうなる。

はてなダイアリー用のコードが生成されるようになってる。他のブログとかに貼りたい人は、スクリプトの「var template =」ってなってる部分を自分が生成したい内容に合わせて書き換えればいい。

仕組み

まず入力されたURLの内容を取得して、そこに含まれるAPIアクセス用のキーを取り出す。

次に、そのキーも使ってExif取得APIURLを組み立てて、その内容を取得。

APIJSONで返ってくるのでパースして、テンプレートに埋め込んで出力用の文字列を完成させる。

詳細

APIのキーは、いったん普通に写真ページにWebアクセスして返ってきたページの中から取りだしてる。これはたぶんログインが不要な一般アクセス用のものなのかな、よくわかんないけど。

今回は、非公開情報へのアクセスも、更新系の操作もなく、公開されてる写真に関する情報を取得するだけで、わざわざ登録ユーザーでログインしてキーを発行してもらう必要がなかったのでこうした。

Exif取得のAPIについても、普通に写真ページにWebアクセスするとAjaxで呼んでたので、それと同じものを呼んでる。Exifだけじゃなくサムネイルが格納されてるサーバーの情報とかも含まれていたので、縮小版のURLを組み立てるのも簡単だった。

手動で直したりもしている

実は、自分が使ったときは、これだけじゃ不十分だった。

カメラやレンズの名前が、Exifに記録されてる内容はわかりにくい。なのでそこだけ手動で書き換えてる。

例えば「Sony ILCE-6500」ではわかりにくいので「Sony α6500」にしたりしてる。まあしょうがないよね。

2016.12.02

Exif情報を元にJPEGファイル名をリネームするJavaScript for Automation(2016年バージョン)

2014年に書いたこれ

を直した。

JPEGのファイル名を、Exif情報を元にリネームするスクリプト

iPhoneやデジカメで撮った写真のファイル名が、

img_20141024_012345_Apple_iPhone6_1.jpg

こんな感じになるスクリプト

「img + 年月日 + 時分秒 + カメラメーカー名 + カメラ名 + 連番 + .jpg」っていうルールでリネームする。連番は、カメラで連写などして年月日時分秒まで一致した場合にカウントアップする。

複数のカメラで撮った写真をひとつのフォルダにまとめて保存し、さらにレタッチしてファイルのタイムスタンプが変化してる場合などに、こういう風にファイル名がついていれば整理しやすい。

Exif情報を元にJPEGファイル名をリネームするJavaScript for Automation - 今日覚えたこと

当時の説明がこんな感じ。

コード

ソースコードは以下。

function openDocuments(docs) {
    var app = Application.currentApplication();
    app.includeStandardAdditions = true;
    
    var count = 0;
    for(var i = 0; i < docs.length; i++){
        var src = docs[i].toString();
        if(!/\.jpe?g$/i.test(src)) continue;
        
        // Get Exif
        var sips = app.doShellScript('sips -g all "' + src + '"');
        var lines = sips.split(/\r\n|\r|\n/).slice(1);
        var exif = {};
        for(var j = 0; j < lines.length; j++){
            /^\s*([^:]+): (.+)$/.test(lines[j]);
            exif[RegExp.$1] = RegExp.$2;
        }
        
        if(!exif.creation){
            // Get FileInfo
            var fileInfo = app.doShellScript('GetFileInfo "' + src + '" | grep created');
            exif.creation = fileInfo.replace(/^.+?([0-9]+)\/([0-9]+)\/([0-9]+) ([0-9:]+)/, '$3:$1:$2 $4');
        }
        if(!exif.make) exif.make = 'unknown';
        if(!exif.model) exif.model = 'unknown';
        
        // Generate Name
        var parent = src.replace(/[^\/]+$/, '');
        var dst;
        var index = 1;
        while(true){
            dst =
                parent +
                'img_' +
                exif.creation.replace(/:/g, '').replace(/ /, '_') + '_' +
                exif.make.replace(/[^a-z0-9-]/ig, '') + '_' +
                exif.model.replace(/[^a-z0-9-]/ig, '') + '_' +
                index.toString() +
                '.jpg';
            var ret = app.doShellScript('if test -e "' + dst + '"; then echo 1; else echo 0; fi');
            if(ret == '0') break;
            index++;
        }
        
        // Rename
        app.doShellScript('mv "' + src + '" "' + dst + '"');

        // Live Photos
        var lp_src = src.replace(/\.JPG$/i, '.MOV');
        var ret = app.doShellScript('if test -e "' + lp_src + '"; then echo 1; else echo 0; fi');
        if(ret == '1'){
            var lp_dst = dst.replace(/\.jpg$/i, '.mov');
            app.doShellScript('mv "' + lp_src + '" "' + lp_dst + '"');
        }

        count++;
    }
    
    app.displayAlert(count.toString() + ' file' + (2 <= count ? 's' : '') + ' renamed.');
}

macOS Sierraで動作確認した。特にOSの新機能とかを使ってるわけではないと思うので、JavaScript for Automationに対応しているYosemite以降なら動くと思う。

スクリプトエディタに上記のコードをコピペして、好きな場所にアプリケーション形式(.app)で保存する。

あとはそのappファイルに対してJPEGファイルをドラッグ&ドロップするとリネームされる。複数のファイルをドロップしてもOK。

2014年バージョンからの変更点

  • Exifに日付がない場合、代わりにファイルのタイムスタンプの日付を使うようにした
  • Exifにカメラメーカーやカメラ名が無い場合、エラーにならないでunknownとして動作するようにした
  • Live Photosに対応した。JPGと同じファイル名のMOVが存在している場合、MOVも同じようにリネームするようにした。

こんなところ。具体的には、SNOWで撮った写真にExifが入らないようなので、それでいちいちエラーにならないようにしたのと、iPhone 6s以降の新機能のLive Photosに対応した感じ。

まあ2年前には想定してなかった状況に対応したってこと。今後もまた何かあったら直すね。

2016.03.27

税込価格を計算するブックマークレット

Web上でなんかの商品の価格を見たとき、税別になってることがある。

https://farm2.staticflickr.com/1623/26046920115_a3cd8f4f8c.jpg

iPad Proを購入 - Apple(日本)

結局いくらなのかわかんないから不便だよね、こういうときに税込価格を簡単に確認できるように、ブックマークレットを作ってみた。

javascript:alert((parseInt(window.getSelection().toString().replace(/[^0-9]/g,''),10)*1.08).toString().replace(/^(-?[0-9]+)(?=\.|$)/,function(s){return s.replace(/([0-9]+?)(?=(?:[0-9]{3})+$)/g,'$1,');}));

これをブックマークに登録しておいて、税別価格を範囲選択した状態でそのブックマークをクリックすると、税込価格が表示される。

https://farm2.staticflickr.com/1681/26021077386_dd70325815.jpg

こんな感じ。

めちゃくちゃ単純なことなんだけど、今までいつも面倒だなーと思ってたので、これで少し便利になるのでうれしい。ChromeやSafariのExtensionとして作ればもっといいUIになるかも知れないけど、まあそんなに頻繁に使うわけでもないのでこれくらいでいいかなと思った。

ちなみに数字のカンマ編集は以下のコードを使いました。感謝です。

税率変わったらごめん

単純に1.08倍してるだけなので、将来税率が変わったら修正が必要。

商品によって税率が違うような日が来たら、どうしようもない。税率ごとに別々のブックマークレットに分けるとかしかないかな。

ドルのときもなんとかしたい

似たような状況で、ドルとか海外の通貨で表記されてるときも、だいたい日本円でいくらなのか知りたいんだけど、これは為替レートを取得しなきゃいけないので、面倒そう。。。

2016.03.25

JXA(JavaScript for Automation)で現在のマウスカーソルの位置を取得する

最近JXA(JavaScript for Automation)が気に入ってる。いろいろと情報もWeb上に出始めて、思いついたことは頑張ればだいたい実現できる感じになってきて、非常に便利で楽しい。

マウスカーソルの位置を取得する方法がわかったのでメモ。どうやらOS Xのバージョンによって使えるやり方が変わってきてるようなので、最新の10.11(El Capitan)で実際に使えたやり方が以下。

ObjC.import('Cocoa');
var pos = $.CGEventGetLocation($.CGEventCreate(null));

var sys = Application("System Events");
sys.includeStandardAdditions = true;
sys.displayDialog(JSON.stringify(pos));

これを実行すると、

{"x":1259.12890625,"y":629.25}

こんな感じの内容がダイアログで表示される。なぜか異様に細かい小数なんだけどまあいいや。

最初の2行で、マウスカーソルの位置を取得している。posに"x"と"y"のキーに持つオブジェクトの形で入る。後半の3行はそれをJSONに変換してダイアログで表示してる部分なので直接は関係ないよ。