Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2011-11-17

最高の電子書籍はどこにあるのか?

伝記スティーブ・ジョブズ1・2、ようやく読み終わった。今回は電子書籍で購入した。かつて、アラン・ケイが夢見たDynabook、それを実現してしまったジョブズ渾身の傑作iPadで、長編書籍(しかもジョブズ自身の伝記という内容)を果たしてどこまで快適に読めるのか、身を持って体感するため。

iPadで、伝記スティーブ・ジョブズが読める電子書籍リーダーにもいろいろある。このブログを書いている時点で7つの電子書籍リーダーを選択できた。果たして自分はどの電子書籍リーダーで読めば良いのだろう?探ってみた。

一覧比較

  • 最初だらだらと調べていたのだけど、いろいろな要素があって非常に分かりにくい...。
  • とりあえず、iPadで伝記スティーブ・ジョブズを読む状況に限定して調べてみた。
ebiReaderKinoppyhontoパブリBookLiveVoyagerアプリ
価格1995円2000円1995円1995円1995円1995円1900円
ストア機能SafariありSafariSafariSafariあり*1あり
フォーマットebi.j.book.book.bookMCBook
フォント画像扱い秀英明朝秀英横太明朝秀英横太明朝凸版明朝秀英横太明朝リュウミンL
ページの記録前回ページと栞1つ前回ページ前回ページ前回ページ前回ページ前回ページ前回ページと栞複数
文章のマーキングなしあり(メモ書き可)なしなしなしなしあり(6色・メモ書き可)
検索本文本文・GoogleWikipedia・辞書本文本文なし本文本文・Google・Wikipedia・Yahoo!・辞書など
複数端末対応3台まで3台まで3台まで
再ダウンロード期限無期限無期限1年1年無期限無期限無期限
      • ○=iPad・iPhoneどちらも同時に閲覧できた。
      • 但し、無期限であっても、販売中止になってしまうと再ダウンロードできない。

フォント比較

  • フォントはiPadで読むか、iPhoneで読むかによって印象も変わってくると思う。
    • iPhone3GSや、さらにはRetinaディスプレイ化されたiPhone4以降なら、また違った印象になるはず。
  • 個人的には、iPadで読むなら秀英横太明朝が横のラインも鮮明で読みやすいと思った。
秀英横太明朝

f:id:zariganitosh:20111117110737p:image

秀英明朝

f:id:zariganitosh:20111117110738p:image

凸版明朝

f:id:zariganitosh:20111117110739p:image

リュウミンL

f:id:zariganitosh:20111117110740p:image

現状の選択

  • 実は、このように詳細に比較することもなく、フォントの読みやすさの印象だけで「パブリ」で購入してしまったのだった。(早く読みたかったので)
    • iPadで読む場合、秀英横太明朝は自分好みのフォントである。
    • 紙の本と同じ感覚で読み進めることができた。
  • 実際に電子書籍で読み始めると、当初は考えていなかった、いろいろな欲求が湧いてくる。
    • 当初は、快適に読む以上のことは考えていなかったが、伝記スティーブ・ジョブズは、単なる本以上にAppleを知る資料となる。
    • そうなってくると、マーカーを引きたいとか、付箋を貼ってメモ書きしたいとか、思うようになる。
    • また、知らない単語は今までと同じように辞書を引けば良いのだけど、書籍リーダーが辞書と連携していると快適である。
    • Google検索等のWeb検索とも連携していれば、なお嬉しい。
  • 電子書籍には、紙の本にはない素晴らしい機能もある。本文検索である。
    • 紙の本は検索できないので、索引を設けて参照ページを記載している。
    • 電子書籍の良いところは、自分の興味のある単語をキーに、自由に検索できることである。
    • 特にこの本を資料と考えれば、この検索機能は絶大なメリットになる。
  • 電子書籍は著作権保護のためか、コピー機能が無効にされている。
    • コピー機能がない環境では、検索や辞書機能と連携するこの仕組みがとっても有り難いのだ。
  • 以上のことを考えると、「Kinoppy」で購入しておくのがベストだったのではないかと、後悔している...。
    • 読み易さを優先すると、秀英横太明朝の「パブリ」が良さそう。
    • しかし、資料となる書籍ならマーキングやメモ書き機能も欲しい。
    • となると、Kinoppyかアプリ版なのだが...
      • フォントはKinoppyの秀英明朝の方が好み。
      • Kinoppyには、アプリ内にショップ機能(立ち読み・購入する機能)がある。
      • Kinoppyでは、購入した書籍以外にも、自炊したPDF・テキストなども本棚で管理し、閲覧もできる。

近い将来

  • ところで、上記の電子書籍には大御所が二つ抜けている。
  • AppleのiBooksと、AmazonのKindleである。
  • iBooksとKindleは、現状では日本語・縦書きフォーマットに対応していないのだ...。
  • あるいは対応しているかもしれないが、何らかの事情で日本語の書籍を取り扱っていない。
  • 実は、英語版のSteve JobsもKindleで購入している。
  • amazon.comで$11.99(≒924円)と非常に安価だ。
  • Amazonで購入すると即、iPadとiPhoneのKindleに反映され、読めるようになった。(どうゆう仕組みなんだ?)
  • iPadのKindleで読んでいるページをiPhoneのKindleに同期して続きを読むなんてことも当然のようにできた。
  • もちろん、本文検索・Google検索・Wikipedia検索・辞書・マーキング・メモ書き・Tweetなどの機能もある。
  • iOSに準拠したUIであり、その操作感は期待を裏切らず、迷わない。
  • Kindleの日本語書籍ストアができたら、間違いなく繁盛しそうな気がする。
  • iBooksの日本語書籍ストアもしかり。

その他雑感

  • 1000ページ以上めくっていると、紙の本を模したページめくりアニメーションより、シンプルなスライドアニメーションの方が好きになった。
  • ページを移動する時のアニメーションは重要。
    • 瞬時にページが切り替わってしまうと、ページをめくった気がせず、不満が残る。
    • 誤操作によるページめくりにも気付きにくく、次に進んだか、前に戻ったのかも分からない。
  • 読書に集中していても、ふとした時に時間を知りたくなる。
    • 全画面表示の電子書籍リーダーでは、一旦終了しないと時間が確認できなくてイライラする。
    • iBooks・Kindle・Kinoppyは常にステータスバーに時刻が表示されていて、安心できる。
  • 今、全体のどこまで読んでいるのか?という情報も気になる。
    • 紙の本であれば、見開きした左右の紙の厚みで、暗黙に確認できていた情報である。
    • 電子書籍でも、常に、あるいはページをめくる時には、表示して欲しい情報である。
  • パブリで購入したのは.bookフォーマットのスティーブ・ジョブズ。
  • .bookはKinoppyでも閲覧可能なフォーマットだ。
  • しかし、現状はパブリで購入したものは、パブリでしか読む術がない。
  • Kinoppyでも読めれば良いのに。
    • 例えKinoppyで読めたとしても、T-TimeのUIを超える操作性は期待できないのだろうか?
    • マーキングやメモ書き機能に憧れる。


以下、一覧比較の元となる調査データ

電子書籍・コミックリーダー ebiReader

立ち読み・購入(1・2 各1995円)
ファイルフォーマット
  • ebi.j(.ebija)
    • イーブック・イニシアティブ・ジャパンが開発したフォーマット。
  • 文字データも画像として扱っている。
  • 漫画の品揃えが豊富。
フォント
  • 画像なので紙の書籍のフォントに準ずるのか?
機能
  • 栞(記録されるページ)
    • 前回開いていたページ
    • 栞を挟んだページ(栞は1つしかない)

以下の機能はすべて不可能(文字は画像扱いなので)

  • 本文検索 なし
  • 辞書 なし
  • メモ書き なし
  • マーキング なし
複数デバイス対応
  • トランクルーム本棚からダウンロードすることで、一つの電子書籍を複数デバイスで読めるようだ。

紀伊國屋書店Kinoppy

立ち読み・購入(1・2 各2000円)
  • 電子書籍リーダー内ですべて完結する。
ファイルフォーマット
  • .book、XMDF、EPUB3.0の縦書きに対応
フォント
  • 秀英明朝
機能
  • 栞(記録されるページ)
    • 前回開いていたページ
  • 本文検索
  • Google検索
  • Wikipedia検索
  • 辞書
  • マーキング
  • メモ書き
複数デバイス対応
  • 電子書籍を一度購入すれば、複数の端末で再購入なしに再ダウンロードできる。

honto BOOK

立ち読み・購入(1・2 各1995円)
対応フォーマット
  • .book、XMDF、MCBook、イメージビューア形式
フォント
  • 秀英横太明朝
機能
  • 栞(記録されるページ)
    • 前回開いていたページ
  • 本文検索
    • 単語を選択して検索できない。
    • キー入力する必要があり面倒。

以下の機能はすべて不可能

  • 辞書 なし
  • マーキング なし
  • メモ書き なし
複数デバイス対応
  • アプリの会員IDを登録した一つの端末でのみ閲覧できる。
    • 同時に複数端末では閲覧できないらしい。(試してないが、説明を読んで理解したこと)
ダウンロード期限
  • 伝記スティーブ・ジョブズには、365日のダウンロード期限が設定されている。
  • ダウンロード期限内であれば、何度でもダウンロードできる。
  • ダウンロード期限を過ぎると、ダウンロードできなくなる。
  • ダウンロード期限を過ぎても、ダウンロード済の書籍については継続して閲覧できる。

電子文庫パブリ

立ち読み・購入(1・2 各1995円)
対応フォーマット
  • .book、XMDF
フォント
  • 秀英横太明朝
機能
  • 栞(記録されるページ)
    • 前回開いていたページ
  • 本文検索
    • 単語を選択して検索できない。
    • キー入力する必要があり面倒。

以下の機能は.bookフォーマットではすべて不可能(但し、XMDFフォーマットならすべて可能になる)

  • 辞書 なし
  • マーキング なし
  • メモ書き なし
  • Google検索
  • Wikipedia検索
  • Yahoo!検索
複数デバイス対応
  • アプリの会員IDを登録した複数端末(iPad・iPhone)で閲覧できた。
ダウンロード期限
  • 伝記スティーブ・ジョブズには、2012/11/08のダウンロード期限が設定されている。

BookLive! Reader

立ち読み・購入(1・2 各1995円)
対応フォーマット
  • .book、XMDF
フォント
  • 凸版明朝
機能
  • 栞(記録されるページ)
    • 前回開いていたページ

以下の機能はすべて不可能

  • 本文検索 なし
  • 辞書 なし
  • マーキング なし
  • メモ書き なし
複数デバイス対応
  • 端末登録して、最大3台の端末で閲覧できる。

Voyager Books

立ち読み・購入(1・2 各1995円)
対応フォーマット
  • .book
フォント
  • 秀英横太明朝
機能
  • 目次は本文へのリンク
  • 栞(記録されるページ)
    • 前回開いていたページ
  • 本文検索
    • 単語を選択して検索できない。
    • キー入力する必要があり面倒。

以下の機能はすべて不可能

  • 辞書 なし
  • マーキング なし
  • メモ書き なし
複数デバイス対応
  • ログインした複数端末で閲覧できる。

スティーブ・ジョブズ(アプリ)

立ち読み・購入(1・2 各1900円)
  • アプリ内ですべて完結する。
    • 無料アプリをダウンロードして、アプリ内決済によって行う。
    • このアプリが、スティーブ・ジョブズ専用の電子書籍リーダーなのだ。
対応フォーマット
  • MCBook
フォント
  • リュウミンL
機能
  • 栞(記録されるページ)
    • 前回開いていたページ
    • 栞を挟んだページ(栞は複数挟める)
  • 本文検索
  • Google検索
  • 辞書
  • マーキング(6色)

以下の機能は不可能

  • メモ書き なし
複数デバイス対応
  • アプリをインストールした複数端末で閲覧できる。

参考ページ

以下のページがたいへん参考になりました。感謝です!

*1:立ち読みはMacBookSafariでしかできなかった。

2011-10-01

iPhone・iPadのSafariでもあらゆるページで自動入力したい

前回までにMacBookにおいては、手軽に、安全で、確実なSafariの自動入力環境になった。

しかし、本来はiPhoneiPadでこそ、自動入力したいのではないか?MacBookならキーチェーン.appの自分のメモを見れば、どのページであっても手入力して、どうにかログインはできる。ところが、iPhone・iPadにはキーチェーン.appがないので、ID・パスワードを確認する術がないのだ。

出先でちょこっと金融機関のページにログインしたいと思っても、自動入力できず、ID・パスワードもあやふやでログインできず、諦めた経験はないだろうか。結局、家に帰って、MacBookの前に座って、ようやくログイン。そんな経験が自分には少なからずある。また、iPhoneの小さなソフトウェアキーボードで入力するのも面倒くさい。

自動入力は、iPhone・iPadで使えてこそ、その真価を発揮するのだ。但し、ロック解除の動作中に無制限に自動入力されてしまうのは問題である。また、ID・パスワードを単純なテキストとして保存しておくのも問題である。例えiPhoneを落としたとしても、そして悪意のある第三者に拾われたとしても、自動入力やファイル解析で簡単にアクセスされないセキュリティも必要である。

そんなiPhone・iPadの自動入力環境を目指して、いろいろやってみた。

プラットホームの違い

MacBookでは...
  • AppleScriptを実行できる。
  • Safariの機能拡張を利用できる。
  • また、AppleScript経由で、標準インストールされるUNIX由来の環境やスクリプト言語など、あらゆる環境にアクセスして活用できる。
    • シェルコマンド、RubyPerlPHPPython、C、ObjectiveC、Cocoaなど。
iPhone・iPadでは...
  • AppleScriptは実行できない。
  • Safariの機能拡張は使えない。
  • Mobile Safariで手軽に活用できるのは、JavaScriptのみである。

方針

  • つまり、自分にとっての選択肢はJavaScriptのみ。
  • 前回AppleScriptで作った自動入力環境を、今度はJavaScriptで実装すれば良いはず。
  • 但し、JavaScriptはブラウザの中の世界しかアクセスできないと思っているので、opensslコマンドに頼っていたこともすべてJavaScriptで実装する必要あり。
  • 具体的には、SHA-1ハッシュの生成と、aes_128_cbcによる暗号化も、JavaScriptで実装するのだ。
  • しかし、自分にはその知識も技術もないので、ライブラリ的な何かを探す。
    • きっと、世界のどこかで、その道の達人が、素晴らしい実装をしてくれているはず。
  • JavaScriptはブックマークレットとして保存して活用する。
  • マスターパスワードはSHA-1ハッシュにして、ログイン情報はaes_128_cbcで暗号化して、すべてJavaScriptコードに含める。
  • 自分が実装するのは、マスターパスワード認証と、復号したログイン情報をフォームに入力する部分。
  • 自動入力以外の処理は、MacBookのauto_login環境(AppleScriptのスクリプト)を利用する。
  • MacBookのauto_login環境のマスターパスワード、ログイン情報を利用して、JavaScriptコードを生成する。
  • 生成したJavaScriptコードをSafariでブックマークして、ブックマークレットとして活用する。

利用させて頂いたコード

以下の素晴らしいコードを使わせて頂きました。感謝です!


上記JavaScriptコードをClosure Compiler Serviceを利用してコード圧縮して、AppleScriptに取り込んだ。

 property aes_code : "var Aes={};Aes.cipher=function(b,e){for(var a=e.length/4-1,d=],[],[],[?,c=0;c<16;c++)d[c%4][Math.floor(c/4)]=b[c];d=Aes.addRoundKey(d,e,0,4);for(c=1;c<a;c++)d=Aes.subBytes(d,4),d=Aes.shiftRows(d,4),d=Aes.mixColumns(d,4),d=Aes.addRoundKey(d,e,c,4);d=Aes.subBytes(d,4);d=Aes.shiftRows(d,4);d=Aes.addRoundKey(d,e,a,4);a=Array(16);for(c=0;c<16;c++)a[c]=d[c%4][Math.floor(c/4)];return a};Aes.keyExpansion=function(b){for(var e=b.length/4,a=e+6,d=Array(4*(a+1)),c=Array(4),f=0;f<e;f++)d[f]=[b[4*f],b[4*f+1],b[4*f+2],b[4*f+3;for(f=e;f<4*(a+1);f++){d[f]=Array(4);for(b=0;b<4;b++)c[b]=d[f-1][b];if(f%e==0){c=Aes.subWord(Aes.rotWord(c));for(b=0;b<4;b++)c[b]^=Aes.rCon[f/e][b]}else e>6&&f%e==4&&(c=Aes.subWord(c));for(b=0;b<4;b++)d[f][b]=d[f-e][b]^c[b]}return d};Aes.subBytes=function(b,e){for(var a=0;a<4;a++)for(var d=0;d<e;d++)b[a][d]=Aes.sBox[b[a][d;return b};Aes.shiftRows=function(b,e){for(var a=Array(4),d=1;d<4;d++){for(var c=0;c<4;c++)a[c]=b[d][(c+d)%e];for(c=0;c<4;c++)b[d][c]=a[c]}return b};Aes.mixColumns=function(b){for(var e=0;e<4;e++){for(var a=Array(4),d=Array(4),c=0;c<4;c++)a[c]=b[c][e],d[c]=b[c][e]&128?b[c][e]<<1^283:b[c][e]<<1;b[0][e]=d[0]^a[1]^d[1]^a[2]^a[3];b[1][e]=a[0]^d[1]^a[2]^d[2]^a[3];b[2][e]=a[0]^a[1]^d[2]^a[3]^d[3];b[3][e]=a[0]^d[0]^a[1]^a[2]^d[3]}return b};Aes.addRoundKey=function(b,e,a,d){for(var c=0;c<4;c++)for(var f=0;f<d;f++)b[c][f]^=e[a*4+f][c];return b};Aes.subWord=function(b){for(var e=0;e<4;e++)b[e]=Aes.sBox[b[e;return b};Aes.rotWord=function(b){for(var e=b[0],a=0;a<3;a++)b[a]=b[a+1];b[3]=e;return b};Aes.sBox=[99,124,119,123,242,107,111,197,48,1,103,43,254,215,171,118,202,130,201,125,250,89,71,240,173,212,162,175,156,164,114,192,183,253,147,38,54,63,247,204,52,165,229,241,113,216,49,21,4,199,35,195,24,150,5,154,7,18,128,226,235,39,178,117,9,131,44,26,27,110,90,160,82,59,214,179,41,227,47,132,83,209,0,237,32,252,177,91,106,203,190,57,74,76,88,207,208,239,170,251,67,77,51,133,69,249,2,127,80,60,159,168,81,163,64,143,146,157,56,245,188,182,218,33,16,255,243,210,205,12,19,236,95,151,68,23,196,167,126,61,100,93,25,115,96,129,79,220,34,42,144,136,70,238,184,20,222,94,11,219,224,50,58,10,73,6,36,92,194,211,172,98,145,149,228,121,231,200,55,109,141,213,78,169,108,86,244,234,101,122,174,8,186,120,37,46,28,166,180,198,232,221,116,31,75,189,139,138,112,62,181,102,72,3,246,14,97,53,87,185,134,193,29,158,225,248,152,17,105,217,142,148,155,30,135,233,206,85,40,223,140,161,137,13,191,230,66,104,65,153,45,15,176,84,187,22];Aes.rCon=0,0,0,0],[1,0,0,0],[2,0,0,0],[4,0,0,0],[8,0,0,0],[16,0,0,0],[32,0,0,0],[64,0,0,0],[128,0,0,0],[27,0,0,0],[54,0,0,0?;Aes.Ctr={};Aes.Ctr.encrypt=function(b,e,a){if(!(a==128||a==192||a==256))return'';for(var b=Utf8.encode(b),e=Utf8.encode(e),d=a/8,c=Array(d),a=0;a<d;a++)c[a]=isNaN(e.charCodeAt(a))?0:e.charCodeAt(a);for(var c=Aes.cipher(c,Aes.keyExpansion(c)),c=c.concat(c.slice(0,d-16)),e=Array(16),a=(new Date).getTime(),d=a%1E3,f=Math.floor(a/1E3),i=Math.floor(Math.random()*65535),a=0;a<2;a++)e[a]=d>>>a*8&255;for(a=0;a<2;a++)e[a+2]=i>>>a*8&255;for(a=0;a<4;a++)e[a+4]=f>>>a*8&255;d='';for(a=0;a<8;a++)d+=String.fromCharCode(e[a]);for(var c=Aes.keyExpansion(c),f=Math.ceil(b.length/16),i=Array(f),j=0;j<f;j++){for(a=0;a<4;a++)e[15-a]=j>>>a*8&255;for(a=0;a<4;a++)e[15-a-4]=j/4294967296>>>a*8;for(var g=Aes.cipher(e,c),k=j<f-1?16:(b.length-1)%16+1,h=Array(k),a=0;a<k;a++)h[a]=g[a]^b.charCodeAt(j*16+a),h[a]=String.fromCharCode(h[a]);i[j]=h.join('')}b=d+i.join('');return b=Base64.encode(b)};Aes.Ctr.decrypt=function(b,e,a){if(!(a==128||a==192||a==256))return'';for(var b=Base64.decode(b),e=Utf8.encode(e),d=a/8,c=Array(d),a=0;a<d;a++)c[a]=isNaN(e.charCodeAt(a))?0:e.charCodeAt(a);c=Aes.cipher(c,Aes.keyExpansion(c));c=c.concat(c.slice(0,d-16));e=Array(8);ctrTxt=b.slice(0,8);for(a=0;a<8;a++)e[a]=ctrTxt.charCodeAt(a);for(var d=Aes.keyExpansion(c),c=Math.ceil((b.length-8)/16),a=Array(c),f=0;f<c;f++)a[f]=b.slice(8+f*16,f*16+24);for(var b=a,i=Array(b.length),f=0;f<c;f++){for(a=0;a<4;a++)e[15-a]=f>>>a*8&255;for(a=0;a<4;a++)e[15-a-4]=(f+1)/4294967296-1>>>a*8&255;for(var j=Aes.cipher(e,d),g=Array(b[f].length),a=0;a<b[f].length;a++)g[a]=j[a]^b[f].charCodeAt(a),g[a]=String.fromCharCode(g[a]);i[f]=g.join('')}b=i.join('');return b=Utf8.decode(b)};var Base64={code:'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',encode:function(b,e){var a,d,c,f,i=,j='',g,k,h=Base64.code;k=(typeof e=='undefined'?0:e)?b.encodeUTF8():b;g=k.length%3;if(g>0)for(;g++<3;)j+='=',k+='\\x00';for(g=0;g<k.length;g+=3)a=k.charCodeAt(g),d=k.charCodeAt(g+1),c=k.charCodeAt(g+2),f=a<<16|d<<8|c,a=f>>18&63,d=f>>12&63,c=f>>6&63,f&=63,i[g/3]=h.charAt(a)+h.charAt(d)+h.charAt(c)+h.charAt(f);i=i.join('');return i=i.slice(0,i.length-j.length)+j},decode:function(b,e){var e=typeof e=='undefined'?!1:e,a,d,c,f,i,j=,g,k=Base64.code;g=e?b.decodeUTF8():b;for(var h=0;h<g.length;h+=4)a=k.indexOf(g.charAt(h)),d=k.indexOf(g.charAt(h+1)),f=k.indexOf(g.charAt(h+2)),i=k.indexOf(g.charAt(h+3)),c=a<<18|d<<12|f<<6|i,a=c>>>16&255,d=c>>>8&255,c&=255,j[h/4]=String.fromCharCode(a,d,c),i==64&&(j[h/4]=String.fromCharCode(a,d)),f==64&&(j[h/4]=String.fromCharCode(a));f=j.join('');return e?f.decodeUTF8():f}},Utf8={encode:function(b){b=b.replace(/[\\u0080-\\u07ff]/g,function(b){b=b.charCodeAt(0);return String.fromCharCode(192|b>>6,128|b&63)});return b=b.replace(/[\\u0800-\\uffff]/g,function(b){b=b.charCodeAt(0);return String.fromCharCode(224|b>>12,128|b>>6&63,128|b&63)})},decode:function(b){b=b.replace(/[\\u00e0-\\u00ef][\\u0080-\\u00bf][\\u0080-\\u00bf]/g,function(b){b=(b.charCodeAt(0)&15)<<12|(b.charCodeAt(1)&63)<<6|b.charCodeAt(2)&63;return String.fromCharCode(b)});return b=b.replace(/[\\u00c0-\\u00df][\\u0080-\\u00bf]/g,function(b){b=(b.charCodeAt(0)&31)<<6|b.charCodeAt(1)&63;return String.fromCharCode(b)})}};"
 property sha1_code : "sha1=new function(){var l=[1732584193,4023233417,2562383102,271733878,3285377520],k=l.length;this.hex=function(c){var c=g(c),a,e='';for(a=0;a<c.length;a++)e+=(c[a]>15?'':'0')+c[a].toString(16);return e};this.dec=function(c){return g(c)};this.bin=function(c){var c=g(c),a,e='';for(a in c)e+=String.fromCharCode(c[a]);return e};var g=function(c){var o;var a=;if(c&&c.constructor===.constructor)a=c;else if(typeof c=='string'){var e,f,b=;for(e=a=0;a<c.length;a++)f=c.charCodeAt(a),f<=255?b[e++]=f:(b[e++]=f>>>8,b[e++]=f&255);a=b}c=a;e=a=c.length;for(c[e++]=128;e%64!=56;)c[e++]=0;a*=8;o=a=c.concat(0,0,0,0,m([a])),c=o;a=;e=;for(var h,d=,b=0;b<k;b++)a[b]=l[b];for(f=0;f<c.length;f+=64){for(b=0;b<k;b++)e[b]=a[b];b=c.slice(f,f+64);h=;for(var g=d=void 0,g=d=0;d<b.length;d+=4,g++)h[g]=b[d]<<24|b[d+1]<<16|b[d+2]<<8|b[d+3];d=h;for(b=16;b<80;b++)d[b]=(d[b-3]^d[b-8]^d[b-14]^d[b-16])<<1|(d[b-3]^d[b-8]^d[b-14]^d[b-16])>>>31;for(b=0;b<80;b++)h=b<20?(a[1]&a[2]^~a[1]&a[3])+j[0]:b<40?(a[1]^a[2]^a[3])+j[1]:b<60?(a[1]&a[2]^a[1]&a[3]^a[2]&a[3])+j[2]:(a[1]^a[2]^a[3])+j[3],h+=(a[0]<<5|a[0]>>>27)+d[b]+a[4],a[4]=a[3],a[3]=a[2],a[2]=a[1]<<30|a[1]>>>2,a[1]=a[0],a[0]=h;for(b=0;b<k;b++)a[b]+=e[b]}return m(a)},m=function(c){var a=;for(n=i=0;i<c.length;i++)a[n++]=c[i]>>>24&255,a[n++]=c[i]>>>16&255,a[n++]=c[i]>>>8&255,a[n++]=c[i]&255;return a},j=[1518500249,1859775393,2400959708,3395469782]};"

JavaScriptコードを生成する

  • MacBookの自動入力環境auto_loginを使って、自動ログイン.scptと同様の動作をするJavaScriptコードを生成する。

 set BS to load script POSIX file ((do shell script "dirname " & quoted form of ((path to me)'s POSIX path)) & "/_login_base.scpt")
 
 tell BS
   authenticate("自動ログインのブックマークレットを生成します。" & return)
   try
     openssl_decode(MASTER_PASS, login_info_path("/"), login_tmp_path("/"))
     set current_record to read_file(login_tmp_path(""))
   end try
   remove_file(login_tmp_path("/"))
   
   set json_text to json_from(text_from(current_record))
 end tell
 
 
 
 --ライブラリ関数的コード
 aes_code & sha1_code
 
 --マスターパスワード認証
 result & "var passwd_sha1=" & quoted form of sha1(MASTER_PASS) & ";"
 result & "var passwd=prompt('自動ログインのマスターパスワードを入力してください。','');"
 result & "if(sha1.hex(passwd) != passwd_sha1){alert('パスワードが一致しません。');}"
 
 --aes_128暗号化したログイン情報を設定
 result & "var json_text_cipher=" & quoted form of encode_aes_128(json_text, MASTER_PASS) & ";"
 
 --ログイン情報を復号化
 result & "var json_text=" & "Aes.Ctr.decrypt(json_text_cipher, passwd, 128);"
 result & "var json=eval('('+json_text+')');"
 
 --URLキーを取得する
 result & "var url_key=document.location.protocol.replace(/[^A-Za-z]/g,'_')+'__'+document.location.hostname.replace(/[^A-Za-z]/g,'_')+document.location.pathname.replace(/\\W/g,'_');"
 
 --URLキーからログイン情報を取得する
 result & "var j=json[url_key];"
 
 --ログイン情報を自動入力する
 result & "for(i=0;i<j.length;i++){if(j[i].type=='text'||j[i].type=='password'){document.getElementsByName(j[i].name)[0].value=j[i].value}else if(j[i].type=='radio'||j[i].type=='checkbox'){document.getElementsByTagName('input')[j[i].index].checked=j[i].checked}else if(j[i].type=='option'){if(document.getElementsByTagName('option')[j[i].index].value==j[i].value){document.getElementsByTagName('option')[j[i].index].selected=j[i].selected}}}"
 result & "alert('入力完了');"
 
 --エスケープ処理(encodeURIではエラーになってしまう)
 BS's replace(result, "%", "%25")
 
 --生成したJavaScriptコードをクリップボードに保存する
 set the clipboard to bookmarklet(result)
 
 "クリップボードにJavaScriptをコピーしました。\nブックマークレットとして、保存してください。"
 display dialog result buttons {"OK"} default button "OK" giving up after 5
 
 
 
 on sha1(str)
   tell application "Safari"
     do JavaScript sha1_code & "sha1.hex(" & quoted form of str & ");" in document 1
   end tell
 end sha1
 
 on encode_aes_128(plain_text, passwd)
   tell application "Safari"
     do JavaScript aes_code & "Aes.Ctr.encrypt(\"" & my escape_dbqt(plain_text) & "\", " & quoted form of passwd & ", 128);" in document 1
   end tell
 end encode_aes_128
 
 on decode_aes_128(cipher_text, passwd)
   tell application "Safari"
     do JavaScript aes_code & "Aes.Ctr.decrypt(\"" & cipher_text & "\", " & quoted form of passwd & ", 128);" in document 1
   end tell
 end decode_aes_128
 
 on escape_dbqt(str)
   do shell script "echo " & quoted form of str & "|sed -e 's/\\\"/\\\\\\\"/g'"
 end escape_dbqt
 
 on bookmarklet(str)
   "javascript:" & "(function(){" & str & "})()"
 end bookmarklet
 
 on encode_uri(str)
   tell application "Safari"
     do JavaScript "encodeURI(\"" & str & "\")" in document 1
   end tell
 end encode_uri

ブックマークレットを登録する

  • 上記スクリプトを実行すると、JavaScriptコードがクリップボードにコピーされる。
  • ブックマークを何か一つ追加して、ブックマーク管理ページからアドレス欄にペーストして、ブックマークレットとする。

f:id:zariganitosh:20111001013156p:image

  • Safariのブックマークは、iPhone・iPadと同期する設定にしておく。
  • iTunesとの接続による同期、あるいはMobileMeによる同期を実行。

ブックマークレットを使ってみる

  • 同期したら、ゆうちょダイレクトのページを開いて、上記ブックマークレットを実行してみると...

f:id:zariganitosh:20110930180221p:image

  • ちなみに、prompt()関数を使っているので、入力中の文字はしっかり見える。(黒丸にならない)

f:id:zariganitosh:20110930180220p:image

f:id:zariganitosh:20110930180219p:image

見事に自動入力された!

auto_loginのダウンロード

  • ブックマークレット作成機能の追加にあたり、従来のコードも整合性を合わせるため若干修正した。
  • 以前のログイン情報は auto_login/_login.info に保存されているので、ファイルをコピーすると引き継げる。
    • マスターパスワードも、以前と同じに設定する必要あり。

スクリプトの役割

f:id:zariganitosh:20111001081849p:image

  • _で始まるファイル名は、通常は触らないファイル。(auto_login内部、あるいは開発で利用している)
  • 日本語名のファイルを実行して、各種操作を行う。

  • _edit_login_info.scpt
    • DB編集スクリプト生成.scptが利用する雛形。ロックされたファイル。
  • _login_base.scpt
    • 各スクリプトから参照する共通のコードライブラリ。
  • _login_pass.scpt
    • マスターパスワードをSHA-1形式で保存している。
  • _login.info
    • ログイン情報(Webページのフォームの内容)を暗号化して保存している。
  • _show_login_info.scpt
    • ログイン情報_login.infoの内容をレコード形式で表示する。
  • DB編集スクリプト生成.scpt
    • ログイン情報_login.infoをレコード形式に変換し、編集するスクリプトを生成する。
  • パスワードリセット.scpt
    • パスワードの変更を行う。
  • ブックマークレット作成.scpt
    • 自動入力するJavaScriptコードをクリップボードにコピーする。
  • ログイン情報取得.scpt
    • webページからフォームの内容を取り込み、保存する。
  • 自動ログイン.scpt
    • 自動入力を実行する。

iPhone・iPad専用ページで自動入力するには?

  • 最近はiPhone・iPad専用のページが用意されている場合が多い。
  • iPhone・iPad専用ページのURLは、MacBookとは異なっている。
  • auto_loginでは、ログイン情報の取得は、MacBookしかできない仕様なので、
  • そのままでは、iPhone・iPad専用ページのログインが永遠にできない。

  • その問題を解決するには、DB編集スクリプト生成で、ログイン情報の編集を行う。
  • DB編集スクリプト生成を実行すると、現在のログイン情報をlogin_info_data()に持ったスクリプト編集プログラムが起動する。

f:id:zariganitosh:20111001081850p:image

  • レコードの中から、対応するURLキーを見つけて、その部分をコピーして追加する。
    • 対応するURLキー:https___direct_smbc_co_jp_aib_aibgsjsw5001_jsp
  • そして、一方の..._aibgsjsw5001_jspの部分を..._aibgsjsw1001_jspに変更する。
    • 太字の部分が追加・変更したレコード。

 on login_info_data()
   {https___direct_smbc_co_jp_aib_aibgsjsw1001_jsp:{{checked:false, value:"01234", type:"text", |name|:"USRID1", |index|:"12"}, {checked:false, value:"56789", type:"text", |name|:"USRID2", |index|:"13"}, {checked:false, value:"1234", type:"password", |name|:"PASSWORD", |index|:"14"}, {checked:false, value:"ログイン", type:"submit", |name|:"bLogon.y", |index|:"21"}}, https___direct_smbc_co_jp_aib_aibgsjsw5001_jsp:{{checked:false, value:"01234", type:"text", |name|:"USRID1", |index|:"12"}, {checked:false, value:"56789", type:"text", |name|:"USRID2", |index|:"13"}, {checked:false, value:"1234", type:"password", |name|:"PASSWORD", |index|:"14"}, {checked:false, value:"ログイン", type:"submit", |name|:"bLogon.y", |index|:"21"}}}
 end login_info_data

  • この状態でAppleScriptエディタの実行ボタンを押すと、

f:id:zariganitosh:20111001081851p:image

  • 確認されるので、OKボタンを押すと、変更した内容で書き込まれる。

これでiPhoneでもログインできるようになった!


  • AppleScriptエディタには、重要なログインパスワードが単純なテキストで表示されている。
  • セキュリティ確保のため、くれぐれも保存しないで終了するべき。

2011-06-10

徹底的にキャレットを追跡する

iPadのソフトウェアキーボードに不足している矢印キーを補うため、前回までに以下のブックマークレットを追加して凌いできた。

  • キャレットを左右に移動する。(移動単位は1文字毎)
  • 選択範囲を左右に伸縮する。(移動単位は1文字毎)
  • キャレットを段落の先頭・末尾へジャンプさせる。

キャレット(文字カーソル)を1文字毎に動かしている時はほとんど気にならなかったが、段落の先頭・末尾にジャンプさせるようになって、どうにも気になる問題が出てきた。

それは、キャレットを次々とジャンプさせて上下に移動させると、スクロールが固定されているのですぐに見えなくなってしまうこと。特にiPadを横長のポジションで操作している時が困りもの。ソフトウェアキーボードが編集領域の半分以上を覆ってしまい、キーボードあるいはブックマークバーの下に、キャレットはすぐ隠れてしまう...。

普段何気なく行っているテキスト入力操作も、快適な入力環境を保つため、その裏では実に様々な補助的な処理が行われていたのだ。キャレットを移動したら、それが見える適切な位置までスクロールするというのは当然の挙動と思っていたが、それはOSX環境のテキストエディタが裏で一生懸命にスクロールさせた努力の結果なのであった。

矢印キーのないiPadで、矢印キーのように振る舞うブックマークレットを作ってみて気づかされた事実。キャレットだけ動かしていてはダメで、常に視界に入るようにスクロールさせて、追跡する必要があったのだ。

では、どうやってキャレット移動とスクロールをシンクロさせれば良いのだろう?幾多の試行錯誤が始まるのであった。

  • 以下は、キャレットのみ移動する現状のブックマークレット。
      • 半角¥は、半角\に置き換える必要あり。
//段落の先頭へジャンプ
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str = el.value.substr(0, el.selectionStart-1);
var offset = str.split(/\n|\r/).pop().length+1;
el.setSelectionRange(el.selectionStart-offset, el.selectionStart-offset);

//段落の末尾へジャンプ
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str = el.value.substr(el.selectionEnd+1);
var offset = str.split(/\n|\r/).shift().length+1;
el.setSelectionRange(el.selectionEnd+offset, el.selectionEnd+offset);

改行コードでスクロール

  • まずは単純に改行コードを目印にして、キャレットがそれを越えたら1行分の高さをスクロールするようにしてみた。
  • テキストエリアの高さは、textarea.offsetHeight で求められる。556pxだった。
  • その高さで何行入力できるか、実際にiPadで入力して確認した。25行だった。
  • ゆえに、556÷25=22.24。つまり、1行=22.24pxなのだ。
  • キャレットが改行コードを通過する場合に22.24px、上か下にスクロールするように修正してみた。
      • 半角¥は、半角\に置き換える必要あり。
//段落の先頭へジャンプ2
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str1 = el.value.substr(0, el.selectionStart-1);
var offset = str1.split(/\n|\r/).pop().length+1;
var letter = el.value.substr(el.selectionStart-1, 1);
el.setSelectionRange(el.selectionStart-offset, el.selectionStart-offset);
if ("\n"==letter) window.scrollBy(0, -22.24);

//段落の末尾へジャンプ2
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str2 = el.value.substr(el.selectionEnd+1);
var offset = str2.split(/\n|\r/).shift().length+1;
var letter = el.value.substr(el.selectionStart, 1);
el.setSelectionRange(el.selectionEnd+offset, el.selectionEnd+offset);
if ("\n"==letter) window.scrollBy(0, +22.24);
  • できた。が、当然ながら改行コードでしか、スクロールしない。
  • 一方、現実の文章は改行でなくとも、テキストエリアの右端に達すると、文章は折り返されて次の行に表示されている。
  • 常に1行ごとに改行した箇条書きのような文章ではある程度有効だが、そんな限定的な条件では実際、使い物にならない。

文字を挿入してスクロール

  • これまでの経験から、iPadでキャレットが見えない位置にあっても、何か文字を入力すると、キャレットの見える最適な位置までスクロールすることは分かっている。
  • ならば、この性質を利用して、キャレットが先頭あるいは末尾にジャンプした時に、何か1文字を入力して、またすぐ削除する処理を追加してみたら...どうだろうか?
キャレット位置に挿入する その1
  • まずは、キャレット位置に挿入する方法を模索する必要がある。
  • 検索してみると、一般的には以下の方法が使われているようだ。
//textareaのキャレット位置にtextを挿入する
//範囲選択されている場合は、その部分をtextに置き換える
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
insert_text(el, "__挿入する文字列__");
function insert_text(textarea, text){  
  textarea.value = textarea.value.substr(0, textarea.selectionStart) + text + textarea.value.substr(textarea.selectionEnd);
}
  • つまり、キャレット位置を境に「手前の文字列 + 挿入する文字列 + 後側の文字列」を求めて、それを丸ごとテキストエリアに代入しているのだ。
  • ところが、この方法で処理しても、挿入はされるが、スクロールはしないのである。
  • 期待したようなキャレットの見える最適な位置へは、スクロールしてくれないのだ。
    • スクロールは固定されたまま全く動かず。
    • キャレットは最後尾へジャンプしてしまう。
  • 問題は、キャレット位置に挿入するのではなく、テキストエリア全体の値を書き換えている所にあるようだ。
  • 代入する瞬間は、全体の値を再設定しているだけなので、キャレット位置とは無関係な処理となるようだ。
キャレット位置に挿入する その2
  • 文字を挿入して最適な位置にスクロールさせるには、GUIを実際に操作するような方法で処理する必要があるようだ。
  • AppleScriptで言うGUIスクリプティングのような仕様が、果たしてJavaScriptにもあるのだろうか? → 実はあった!
  • GUIの操作は、それに伴うイベントの発生によって処理される。
  • だから、目指す処理のイベントをコードの中で作成して、それをテキストエリアに投げてあげれば、GUIを操作したのと全く同じ結果が得られるのだ。
  • イベントを投げるには、3つの手順が必要だ。
    • イベントを生成して、
    • そのイベントの内容を設定して、
    • 操作対象のオブジェクトに渡す。
  • 早速、以下のように実装してみた。
//GUI操作と同等の処理で、textareaのキャレット位置にtextを挿入する
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
insert_text(el, "__挿入する文字列__");
function insert_text(textarea, text){
  var textEvent = document.createEvent('TextEvent');
  textEvent.initTextEvent ('textInput', true, true, window, text);
  textarea.dispatchEvent(textEvent);
}
  • テストしてみた。キャレットを視界の外にスクロールさせてから、実行してみると...
  • 文字は挿入され、そして見事に最適な位置までスクロールして、視界の中に現れた!

上手くいった!


  • これらの機能を段落の先頭にジャンプと組み合わせて実装してみる。
  • 移動後、半角スペースを入力して、すぐに削除する処理を追加した。
      • 半角¥は、半角\に置き換える必要あり。
//段落の先頭へジャンプ3
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str1 = el.value.substr(0, el.selectionStart-1);
var offset = str1.split(/\n|\r/).pop().length+1;
el.setSelectionRange(el.selectionStart-offset, el.selectionStart-offset);
insertText(el, " ");
backDelete(el);
function insertText(textarea, str){
  var textEvent = document.createEvent('TextEvent');
  textEvent.initTextEvent ('textInput', true, true, window, str);
  textarea.dispatchEvent(textEvent);
}
function backDelete(textarea){
  var caret = textarea.selectionStart;
  textarea.value = textarea.value.substr(0, caret-1) + textarea.value.substr(caret);
  textarea.setSelectionRange(caret-1, caret-1);
}

//段落の末尾へジャンプ3
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str2 = el.value.substr(el.selectionEnd+1);
var offset = str2.split(/\n|\r/).shift().length+1;
el.setSelectionRange(el.selectionEnd+offset, el.selectionEnd+offset);
insertText(el, " ");
backDelete(el);
function insertText(textarea, str){
  var textEvent = document.createEvent('TextEvent');
  textEvent.initTextEvent ('textInput', true, true, window, str);
  textarea.dispatchEvent(textEvent);
}
function backDelete(textarea){
  var caret = textarea.selectionStart;
  textarea.value = textarea.value.substr(0, caret-1) + textarea.value.substr(caret);
  textarea.setSelectionRange(caret-1, caret-1);
}
  • できた、できた!
  • スペースの挿入・削除を繰り返しながら、キャレットは常に見える位置にスクロールされる。
  • 動きは遅いけど...。
削除について・KeyboardEventについて
  • 当初back deleteキーのエスケープコードである\bの入力で削除されることを期待したが、ダメだった。
  • 文字は削除されず、フォント幅0の見えない文字と文字コード8が入力される結果となった。
  • おそらく、'textInput'よりもさらに低レベルな'keydown'イベントを生成する必要があるのだと思う。
    • 'textInput'はその名のとおり、テキストに入力される段階のイベントのようだ。だからそのままテキストエリアに記録されるのかもしれない。
    • 'keydown'ならキーコードを生成する段階のイベントなので、キーコード8をback delete(1文字削除)として解釈してくれるかもしれない。
  • しかし、'keydown'イベントを生成して、テキストエリアに渡しても、何も起こらなかった。
  • KeyboardEvent系の処理については、Sfariではまだ処理できないのだろうか?あるいは、自分の実装の方法が悪いのか?謎である...。
javascript:
function log(e){
  for (i in e) console.log(i+" = "+e[i]);
}
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
el.addEventListener("keydown",log,false);

var keyboardEvent = document.createEvent("KeyboardEvent");
keyboardEvent.initKeyboardEvent("keydown",false,false,window,'U+0041',0,false,false,false,false);
el.dispatchEvent(keyboardEvent);
  • 上記を実行してみると、確かにkeydownイベントは生成され、テキストエリアで反応しているようだが...
    • 実際にキー入力した時と違って、文字が入力されない。
    • keyIdentifierをどうやって設定('a'、'A'、'\u0041'、'U+0041')しても、which と keyCode は、常に 0 になってしまう...。
  • イベントは生成できても、テキストエリアに文字が入力されない原因は、これだろうか?
keyLocation = 0
ctrlKey = false
shiftKey = false
keyIdentifier = U+0041
altKey = false
metaKey = false
altGraphKey = false
pageY = 0
layerY = 0
pageX = 0
charCode = 0
view = [object DOMWindow]
which = 0
keyCode = 0
detail = 0
layerX = 0
returnValue = true
timeStamp = 1307594994602
eventPhase = 2
target = [object HTMLTextAreaElement]
defaultPrevented = false
srcElement = [object HTMLTextAreaElement]
type = keydown
clipboardData = undefined
cancelable = false
currentTarget = [object HTMLTextAreaElement]
bubbles = false
cancelBubble = false
initKeyboardEvent = function initKeyboardEvent() {
    [native code]
}
initUIEvent = function initUIEvent() {
    [native code]
}
initEvent = function initEvent() {
    [native code]
}
MOUSEOUT = 8
preventDefault = function preventDefault() {
    [native code]
}
FOCUS = 4096
CHANGE = 32768
MOUSEMOVE = 16
AT_TARGET = 2
stopPropagation = function stopPropagation() {
    [native code]
}
SELECT = 16384
BLUR = 8192
KEYUP = 512
MOUSEDOWN = 1
MOUSEDRAG = 32
BUBBLING_PHASE = 3
MOUSEUP = 2
CAPTURING_PHASE = 1
MOUSEOVER = 4
CLICK = 64
DBLCLICK = 128
KEYDOWN = 256
KEYPRESS = 1024
DRAGDROP = 2048
stopImmediatePropagation = function stopImmediatePropagation() {
    [native code]
}
  • できないことに悩んで立ち止まるより、できる方法でどんどん前に進んだ方がいい。
  • そんな訳で「キャレット位置に挿入する その1」の方法で削除することにしている。
      • それにしても、なぜkeydownイベントで文字入力できないのか?謎である。ちゃんと入力できる方法を知りたい!
参考ページ

以下のページがたいへん参考になりました。感謝です!

フォント幅を計算してスクロール

  • 文字の入力と削除を繰り返しながら、キャレットが上下に移動しても、それを視界に追跡できるようになった。
  • しかし、無駄に文字の入力と削除を繰り返すこのやり方は、やはり邪道な気がする。
  • ここまで試行錯誤する中で、究極的にはフォント幅を緻密に計算してスクロールさせたいと考え始めた。
  • でも、JavaScriptからフォント幅を求める関数って、あるのだろうか?多分ない。
  • プロポーショナルなフォント幅は様々である。
  • フォントの種類やサイズも固定とは限らない。

考え出すと否定的になってしまうが、もっと前向きに考えてみると...

  • 自分のiPad環境では、はてなダイアリーの編集ページのフォントは、おそらくヒラギノ角ゴシックW3、12ポイントだと思う。
  • スタイルシートで、テキストエリアのフォントだって指定できる。
  • プロポーショナルと言っても、日本語フォントの幅は常に固定である。
  • それほど多くないASCII文字についてのフォント幅の情報があれば、1段落の行数をかなり正確に計算できるかもしれない。
  • で、ASCII文字のフォント幅情報って、どこにある?フォントデータから取り出す方法なんて知らない...。
  • ならば、たかが100文字弱のASCIIフォント、スクリーンショットで撮影してピクセル数を数えてしまえば良いのだ。

勢いのあるうちに、早速やってみた。

  • フォントを10文字ずつ書き出し、そのピクセル数を数えた。
//iPadプロポーショナル文字幅チェック用
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
for (var n=32, str = ""; n<128; n++){
  for( var i = 0, buf = ""; i < 10; i++ ) buf += String.fromCharCode(n);
  str += "W" + buf + "W\n"
}
el.value = str;
  • 両端の W は、左右の端を計測するときの目安。

f:id:zariganitosh:20110609163146p:image

f:id:zariganitosh:20110609163749p:image

  • このように地道にピクセル数を数えて、フォント幅を求める関数fontWidth(str)を得た。
      • 半角¥は、半角\に置き換える必要あり。
function fontWidth(str){
return {
' ' : 4.3,
'!' : 4.7,
'"' : 6.0,
'#' : 9.3,
'$' : 9.3,
'%' : 14.9,
'&' : 11.2,
"'" : 3.2,
'(' : 5.6,
')' : 5.6,
'*' : 6.5,
'+' : 9.8,
',' : 4.7,
'-' : 6.4,
'.' : 4.7,
'/' : 4.7,
'0' : 9.3,
'1' : 9.3,
'2' : 9.3,
'3' : 9.3,
'4' : 9.3,
'5' : 9.3,
'6' : 9.3,
'7' : 9.3,
'8' : 9.3,
'9' : 9.3,
':' : 4.7,
';' : 4.7,
'<' : 9.8,
'=' : 9.8,
'>' : 9.8,
'?' : 9.5,
'@' : 17.0,
'A' : 11.2,
'B' : 11.2,
'C' : 12.1,
'D' : 12.1,
'E' : 11.2,
'F' : 10.2,
'G' : 13.0,
'H' : 12.1,
'I' : 4.7,
'J' : 8.4,
'K' : 11.2,
'L' : 9.3,
'M' : 14.0,
'N' : 12.1,
'O' : 13.0,
'P' : 11.2,
'Q' : 13.0,
'R' : 12.1,
'S' : 11.2,
'T' : 10.3,
'U' : 12.1,
'V' : 11.2,
'W' : 15.8,
'X' : 11.2,
'Y' : 11.2,
'Z' : 10.2,
'[' : 4.7,
'\\' : 9.3,
'\u00a5' : 9.3,
']' : 4.7,
'^' : 7.9,
'_' : 9.3,
'`' : 5.6,
'a' : 9.3,
'b' : 9.3,
'c' : 8.4,
'd' : 9.3,
'e' : 9.3,
'f' : 4.7,
'g' : 9.3,
'h' : 9.3,
'i' : 3.7,
'j' : 3.7,
'k' : 8.4,
'l' : 3.7,
'm' : 14.0,
'n' : 9.3,
'o' : 9.3,
'p' : 9.3,
'q' : 9.3,
'r' : 5.6,
's' : 8.4,
't' : 4.7,
'u' : 9.3,
'v' : 8.4,
'w' : 12.1,
'x' : 8.4,
'y' : 8.4,
'z' : 8.4,
'{' : 5.6,
'|' : 4.4,
'}' : 5.6,
'~' : 9.8}[str] || 16.7;
}
  • 上記の関数fontWidth(str)と、キャレットが段落の先頭・末尾へジャンプする処理とを組み合わせてみた。
      • 半角¥は、半角\に置き換える必要あり。
//段落の先頭へジャンプ4
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str1 = el.value.substr(0, el.selectionStart-1);
var jumpstr = str1.split(/\n|\r/).pop();
var offset = jumpstr.length+1;
var letter = el.value.substr(el.selectionStart-1, 1);
el.setSelectionRange(el.selectionStart-offset, el.selectionStart-offset);
var line = Math.floor(textWidth(jumpstr) / el.offsetWidth);
if ("\n"==letter) line++;
window.scrollBy(0, -22.24*line);
function textWidth(text){
  for(var i=0, w=0; i < text.length; i++) w += fontWidth(text[i]);
  return w;
}
function fontWidth(str){return{" ":4.3,"!":4.7,'"':6,"#":9.3,$:9.3,"%":14.9,"&":11.2,"'":3.2,"(":5.6,")":5.6,"*":6.5,"+":9.8,",":4.7,"-":6.4,".":4.7,"/":4.7,0:9.3,1:9.3,2:9.3,3:9.3,4:9.3,5:9.3,6:9.3,7:9.3,8:9.3,9:9.3,":":4.7,";":4.7,"<":9.8,"=":9.8,">":9.8,"?":9.5,"@":17,A:11.2,B:11.2,C:12.1,D:12.1,E:11.2,F:10.2,G:13,H:12.1,I:4.7,J:8.4,K:11.2,L:9.3,M:14,N:12.1,O:13,P:11.2,Q:13,R:12.1,S:11.2,T:10.3,U:12.1,V:11.2,W:15.8,X:11.2,Y:11.2,Z:10.2,"[":4.7,"\\":9.3,"\u00a5":9.3,"]":4.7,"^":7.9,_:9.3,"`":5.6,
a:9.3,b:9.3,c:8.4,d:9.3,e:9.3,f:4.7,g:9.3,h:9.3,i:3.7,j:3.7,k:8.4,l:3.7,m:14,n:9.3,o:9.3,p:9.3,q:9.3,r:5.6,s:8.4,t:4.7,u:9.3,v:8.4,w:12.1,x:8.4,y:8.4,z:8.4,"{":5.6,"|":4.4,"}":5.6,"~":9.8}[str]||16.7};

//段落の末尾へジャンプ4
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str1 = el.value.substr(0, el.selectionEnd+1);
var str2 = el.value.substr(el.selectionEnd+1);
var jumpstr = str2.split(/\n|\r/).shift();
var linestr = str1.split(/\n|\r/).pop() + jumpstr;
var offset = jumpstr.length+1;
var letter = el.value.substr(el.selectionStart, 1);
el.setSelectionRange(el.selectionEnd+offset, el.selectionEnd+offset);
var line = Math.floor(textWidth(linestr) / el.offsetWidth);
if ("\n"==letter) line++;
window.scrollBy(0, 22.24*line);
console.log(line);
function textWidth(text){
  for(var i=0, w=0; i < text.length; i++) w += fontWidth(text[i]);
  return w;
}
function fontWidth(str){return{" ":4.3,"!":4.7,'"':6,"#":9.3,$:9.3,"%":14.9,"&":11.2,"'":3.2,"(":5.6,")":5.6,"*":6.5,"+":9.8,",":4.7,"-":6.4,".":4.7,"/":4.7,0:9.3,1:9.3,2:9.3,3:9.3,4:9.3,5:9.3,6:9.3,7:9.3,8:9.3,9:9.3,":":4.7,";":4.7,"<":9.8,"=":9.8,">":9.8,"?":9.5,"@":17,A:11.2,B:11.2,C:12.1,D:12.1,E:11.2,F:10.2,G:13,H:12.1,I:4.7,J:8.4,K:11.2,L:9.3,M:14,N:12.1,O:13,P:11.2,Q:13,R:12.1,S:11.2,T:10.3,U:12.1,V:11.2,W:15.8,X:11.2,Y:11.2,Z:10.2,"[":4.7,"\\":9.3,"\u00a5":9.3,"]":4.7,"^":7.9,_:9.3,"`":5.6,
a:9.3,b:9.3,c:8.4,d:9.3,e:9.3,f:4.7,g:9.3,h:9.3,i:3.7,j:3.7,k:8.4,l:3.7,m:14,n:9.3,o:9.3,p:9.3,q:9.3,r:5.6,s:8.4,t:4.7,u:9.3,v:8.4,w:12.1,x:8.4,y:8.4,z:8.4,"{":5.6,"|":4.4,"}":5.6,"~":9.8}[str]||16.7};
  • できた、できた!
  • 繰り返し実行していると若干ズレるけど、許せる範囲の誤差だと思う。
  • そもそも何十行も上下に移動したいなら、フリックした方が早いのだ。
  • 2、3段落を上下に移動したい時に、キャレットがすぐに視界から消えてしまうのを防止する効果は十分ある。
  • 但し、iPadのヒラギノ角ゴシックW3、12ピクセルのフォントサイズに限定されたテキストエリア専用(はてなダイアリーの編集・下書きページ専用)になってしまう。

所感

一生懸命、ピクセル数を数えたけど...

  • イベントで1文字追加と削除の操作をしてスクロール位置を調整する方が良いかもしれない。
    • どんな時でも確実にキャレットが見える最適なポジションまでスクロールしてくれる。
    • スクロールも滑らかで、動きが目に優しい。

  • 先日のWWDCでiOS5の概要が公開された。200を超える新機能があるのだと言う。
  • 果たしてその中に、ソフトウェアキーボードの矢印キーは含まれるだろうか?
  • あるいは、ソフトウェアキーボードにショートカット操作は追加されるのだろうか?
  • それらの機能が追加されて初めて、ソフトウェアキーボードは現実のキーボードと対等になれるのだと思う。
  • あるいは、矢印キーやショートカット操作無しでも快適と感じられる、自分の中の意識改革が必要なのだろうか?

今後のソフトウェアキーボードの方向性が気になる。

2011-06-02

より賢くサイズ調整するテキストエリアに改良する過程

前回からの続き。

その後使ってみて、テキストエリアの高さが行数によって自動調整されると、素晴らしく便利なことは分かった。特にソフトウェアキーボードを活用するiPad環境では必携の機能になりそう。

      • 半角¥nは、半角\nに置き換える必要あり。
javascript:(function(){
  var els = document.getElementsByTagName('textarea');
  for (i = 0; i < els.length; i++){
    els[i].style.height = 'auto';
    els[i].style.overflow = 'auto';
    els[i].addEventListener('input', _resize, false);
    els[i].addEventListener('focus', _resize, false);
  }
  function _resize(){
    this.setAttribute('rows', this.value.split('\n').length+25);
  }
})();

上記のコードは、文字が入力されるごとに、テキストエリアの改行コード数えて、rows属性に 改行数+25 を設定して、高さ調整している。これでも最初のうちは感動して使っていたのだが、すぐにいくつかの不満が出てきた。

  • 長くなったテキストエリアに喜んで、ずっと下の方(編集したい部分)をタッチする。すると、ソフトウェアキーボードが表示される時に、毎回テキストエリアの先頭に戻ってしまうのだ。編集したい箇所は遥か下の方なのに。困った...。
    • せっかく1本指スクロールで快適になっても、これでは煩わしい。
    • 但し、文字カーソルはタッチした位置に設定されているので、何か入力すればその位置までスクロールして再び見える位置になるのだけど。
    • でも、入力位置が見えない状態で最初の一文字を入力するのは、不安だし、毎回だとストレスになる。どうにかしたい。
  • また、改行数に頼った高さ調整は不完全で、長い文がテキストエリアの右端で折り返されて行数が増えた場合は、当然だが自動調整してくれない。
    • 特に、「記事を書く」ページで開いた時はテキストエリアの幅が狭いので、改行数+25 の調整では足りなくて結局2本指でスクロールする羽目になってしまうのだ。困った...。
    • かと言って 改行数×2+1 で調整すると、「下書き編集」ページでは無駄に下の空白が長くなってしまう。どうにかしたい。

入力モードの時に最適な位置を表示する

  • 想像する理想は、iPadデフォルトアプリの「メモ」のような挙動。
無駄なスクロールを排除する
  • まずはテキストエリアをタッチして、入力モードにした瞬間の位置を動かさないように出来ないものか、やってみた。
      • 半角¥nは、半角\nに置き換える必要あり。
javascript:(function(){
  var scrollTop;
  var els = document.getElementsByTagName('textarea');
  for (var i = 0; i < els.length; i++){
    els[i].style.height = 'auto';
    els[i].style.overflow = 'auto';
    els[i].addEventListener('input', resize, false);
    els[i].addEventListener('focus', resize, false);
    els[i].addEventListener('focus', scroll, false);
    els[i].addEventListener('mousedown', savePoint, false);
  }
  function resize(){
    this.setAttribute('rows', this.value.split('\n').length+25);
  }
  function scroll(){
    window.scrollTo(0, scrollTop);
  }
  function savePoint(){
    scrollTop = document.body.scrollTop;
  }
})();
  • 調べてみると、focusイベントよりmousedownイベントの方が先に発生することが分かった。
  • mousedownでタッチした瞬間のスクロール座標を記録して、
  • focusでそのスクロール座標を再設定することで無駄なスクロールを抑えた。

これで無駄なスクロールはなくなり、タッチした位置はその場に固定されるようになった!

ソフトウェアキーボードに隠れないようにスクロールさせる
  • 満足するのはまだ早い...。現状では画面の下半分でタッチして入力モードにした場合、間違いなくソフトウェアキーボードに隠れて、見えなくなってしまうのだ。
  • これを解消するためには、その分を考慮してスクロールする量を調整してあげれば良いのだが、果たしてどれだけ調整するべきなのか?

f:id:zariganitosh:20110602155001p:image

  • ここにスクリーンショットを撮影して、入力モード時の座標をピクセル単位で調べてみた。
  • event.pageYでmousedownした際のページ座標が取得できる。ページ座標は、webページを表示する領域から始まるので、Sfariのブックマークバー直下の表示領域が基点。
  • 一方、ページ座標window.event.pageY - スクロール量document.body.scrollTop を計算することで、iPadの画面1024×768ピクセルで考えた時のmousedownした座標が求められる。
  • iPadを横にした入力モードでは、190ピクセルの高さより下の位置ではキーボードに隠れてしまうのだ。(上図参考)
  • よって、window.event.pageY - document.body.scrollTop - 190ピクセルさらに上にスクロールさせれば、キーボードに隠れた位置がちょうど表示されるようになるはず!
      • 半角¥nは、半角\nに置き換える必要あり。
javascript:(function(){
  var scrollTop, offsetH;
  var els = document.getElementsByTagName('textarea');
  for (var i = 0; i < els.length; i++){
    els[i].style.height = 'auto';
    els[i].style.overflow = 'auto';
    els[i].addEventListener('input', resize, false);
    els[i].addEventListener('focus', resize, false);
    els[i].addEventListener('focus', scroll, false);
    els[i].addEventListener('mousedown', savePoint, false);
  }
  function resize(){
    this.setAttribute('rows', this.value.split('\n').length+25);
  }
  function scroll(){
    window.scrollTo(0, scrollTop+offsetH);
  }
  function savePoint(e){
    scrollTop = document.body.scrollTop;
    offsetH = e.pageY-scrollTop-190;
    offsetH = (offsetH>0) ? offsetH : 0;
  }
})();

これで、文字カーソルの位置がキーボードの下に隠れることはなくなった!

ジャストフィットな高さ調節

改行コードに頼った高さ調節には限界があると分かった。こうなったら、段落ごとに文字数を数えて右端で折り返される部分を予想してみるか?なんて思い始める。しかし、そんなことしても最近のプロポーショナルなフォントの前では、所詮ごまかしに過ぎないことが分かっていた。

そんなことを考えながら、しばらく悩んで調べていたら、テキストエリアの高さ調節をする全く別の手段があることに気づいた。

素晴らしい!(感謝です!)

  • テキストエリアにはscrollHeighとoffsetHeightの高さがあって、
    • scrollHeigh:テキストエリア内に表示されるテキスト全体の高さ。
    • offsetHeight:テキストエリアそのものの高さ。
  • テキストエリアのスタイルシート設定で、textarea.style.height = textarea.scrollHeigh とすれば、右端の折り返しまで考慮したジャストフィットな高さになるのだ!
  • 但し、テキストエリアが十分長くて下部に余白スペースがあっても、scrollHeigh = offsetHeight となる。
  • つまり、文字を削除して下部に余白スペースができても、scrollHeighを合わせるだけでは短くなる方向には調整できないのである。ちょっと工夫が必要。
javascript:(function(){
  var scrollTop, offsetH;
  var els = document.getElementsByTagName('textarea');
  for (var i = 0; i < els.length; i++){
    els[i].style.height = 'auto';
    els[i].style.overflow = 'auto';
    els[i].addEventListener('input', fit, false);
    els[i].addEventListener('focus', fit, false);
    els[i].addEventListener('focus', scroll, false);
    els[i].addEventListener('mousedown', savePoint, false);
  }
  function fit(){
    while(this.scrollHeight <= this.offsetHeight){
      this.style.height = this.offsetHeight-20 + "px";
    }
    this.style.height = this.scrollHeight + "px";
  }
  function scroll(){
    window.scrollTo(0, scrollTop+offsetH);
  }
  function savePoint(e){
    scrollTop = document.body.scrollTop;
    offsetH = e.pageY-scrollTop-190;
    offsetH = (offsetH>0) ? offsetH : 0;
  }
})();
  • scrollHeigh と offsetHeight に差がない時は、差異が発生するまで、1行分程度の高さ20pxを差し引いて、短くなくなる方向に設定している。
  • その後、scrollHeightに合わせて、ジャストフィット。

以上で、iPadのテキストエリアはかなり賢く行動してくれるようになった!

  • キー入力毎にテキストエリアの高さを弄るので、キー入力に対するレスポンスは若干悪くなる。
  • 特に、back deleteキーのリピート動作で感じるかも。それ以外は、ほとんど気にならないと思った。

不具合修正等

  • 一行目を入力している時、無限ループに陥ってしまう不具合を修正した。
  • ブックマークレットを実行したら即、テキストエリアが拡大するように変更した。
javascript:(function(){
  var ROW_HEIGHT = 22.24;
  var scrollTop, offsetH;
  var els = document.getElementsByTagName('textarea');
  for (var i = 0; i < els.length; i++){
    els[i].style.height = 'auto';
    els[i].style.overflow = 'auto';
    els[i].style.height = els[i].scrollHeight + "px";
    els[i].addEventListener('input', fit, false);
    els[i].addEventListener('focus', scroll, false);
    els[i].addEventListener('mousedown', savePoint, false);
  }
  function fit(){
    while(this.scrollHeight <= this.offsetHeight && this.offsetHeight > ROW_HEIGHT){
       this.style.height = this.offsetHeight-ROW_HEIGHT + "px";
    }
    this.style.height = this.scrollHeight + "px";
  }
  function scroll(){
    window.scrollTo(0, scrollTop+offsetH);
  }
  function savePoint(e){
    scrollTop = document.body.scrollTop;
    offsetH = e.pageY-scrollTop-190;
    offsetH = (offsetH>0) ? offsetH : 0;
  }
})();

JavaScript高速化

  • さらに多少なりとも高速化を目指して、Closure Compiler Serviceでコンパイルしてみた。
  • 人が読み易いJavaScriptから、コンピュータにとって読み易いJavaScriptに変換されるようだ。
javascript:(function() {
  function f() {
    for(;this.scrollHeight <= this.offsetHeight && this.offsetHeight > e;) {
      this.style.height = this.offsetHeight - e + "px"
    }
    this.style.height = this.scrollHeight + "px"
  }
  function g() {
    window.scrollTo(0, d + c)
  }
  function h(a) {
    d = document.body.scrollTop;
    c = a.pageY - d - 190;
    c = c > 0 ? c : 0
  }
  for(var e = 22.24, d, c, b = document.getElementsByTagName("textarea"), a = 0;a < b.length;a++) {
    b[a].style.height = "auto", b[a].style.overflow = "auto", b[a].style.height = b[a].scrollHeight + "px", b[a].addEventListener("input", f, !1), b[a].addEventListener("focus", g, !1), b[a].addEventListener("mousedown", h, !1)
  }
})();
  • 改行を省くと...
javascript:(function(){function f(){for(;this.scrollHeight<=this.offsetHeight&&this.offsetHeight>e;)this.style.height=this.offsetHeight-e+"px";this.style.height=this.scrollHeight+"px"}function g(){window.scrollTo(0,d+c)}function h(a){d=document.body.scrollTop;c=a.pageY-d-190;c=c>0?c:0}for(var e=22.24,d,c,b=document.getElementsByTagName("textarea"),a=0;a<b.length;a++)b[a].style.height="auto",b[a].style.overflow="auto",b[a].style.height=b[a].scrollHeight+"px",b[a].addEventListener("input",f,!1),b[a].addEventListener("focus",
g,!1),b[a].addEventListener("mousedown",h,!1)})();
  • 気持ち反応が早くなったような、なっていないような...。

処理方法を見直して高速化

  • そもそも1文字入力毎にfit()処理をしているから、反応が怠くなるのだ。
  • そんなに頻繁にfit()する必要はないわけで、returnキーを押した時だけ処理するようにしてみた。
  • キーコードを取得するため、処理のタイミングを'input'から'keyup'に変更した。
  • また、テキストエリアの高さが1行になってしまうのも見苦しいので、最低でも2行分の高さは確保するようにしてみた。
javascript:(function(){
  var ROW_HEIGHT = 22.24;
  var scrollTop, offsetH;
  var els = document.getElementsByTagName('textarea');
  for (var i = 0; i < els.length; i++){
    els[i].style.height = 'auto';
    els[i].style.overflow = 'auto';
    els[i].style.height = els[i].scrollHeight + "px";
    els[i].addEventListener('keyup', fit, false);
    els[i].addEventListener('focus', scroll, false);
    els[i].addEventListener('mousedown', savePoint, false);
  }
  function fit(e){
    if (e.keyCode!=13) return;
    while(this.scrollHeight <= this.offsetHeight && this.offsetHeight > ROW_HEIGHT){
      this.style.height = this.offsetHeight - ROW_HEIGHT + "px";
    }
    this.style.height = (this.scrollHeight<ROW_HEIGHT*2) ? "auto" : this.scrollHeight + ROW_HEIGHT + "px";
  }
  function scroll(){
    window.scrollTo(0, scrollTop+offsetH);
  }
  function savePoint(e){
    scrollTop = document.body.scrollTop;
    offsetH = e.pageY-scrollTop-190;
    offsetH = (offsetH>0) ? offsetH : 0;
  }
})();

かなり反応が良くなった!

暫く、これで使ってみる。


ペースト・アンドゥー対応

  • returnキーのみの反応だと、ある程度長いテキストをペーストしても、テキストエリアのサイズは変化せずそのままになってしまう...。
  • 結果、次の入力先を見失って、テキストエリアを拡大したいが為に、無駄にreturnキーを入力して、削除してを繰り返すことになる。
  • そんな無駄な操作を繰り返さない為に、さらにもうちょっと工夫してみた。
  • ついでに、テキスト未入力時の初期値を'auto'からROW_HEIGHT*2に変更した。
javascript:(function(){
  var ROW_HEIGHT = 22.24;
  var scrollTop, offsetH, keyCode = 13;
  var els = document.getElementsByTagName('textarea');
  for (var i = 0; i < els.length; i++){
    els[i].style.overflow = 'auto';
    els[i].style.height = ROW_HEIGHT*2;
    els[i].style.height = els[i].scrollHeight + "px";
    els[i].addEventListener('keydown', saveKeyCode, false);
    els[i].addEventListener('input', fit, false);
    els[i].addEventListener('focus', scroll, false);
    els[i].addEventListener('mousedown', savePoint, false);
  }
  function saveKeyCode(e){
    keyCode = e.keyCode;
  }
  function fit(e){
    if (keyCode==13 || keyCode==86) {  /*return:13、command-v:86*/
      while(this.scrollHeight <= this.offsetHeight && this.offsetHeight > ROW_HEIGHT){
        this.style.height = this.offsetHeight - ROW_HEIGHT + "px";
      }
      this.style.height = (this.scrollHeight<ROW_HEIGHT*2) ? "auto" : this.scrollHeight + ROW_HEIGHT + "px";
    }
    keyCode = 13;
  }
  function scroll(){
    window.scrollTo(0, scrollTop+offsetH);
  }
  function savePoint(e){
    scrollTop = document.body.scrollTop;
    offsetH = e.pageY-scrollTop-190;
    offsetH = (offsetH>0) ? offsetH : 0;
  }
})();

これでキー入力を伴わないテキスト変化(ペースト・アンドゥー等)にも対応して、サイズ調整されるようになった!

2011-05-30

テキスト編集を強力にサポートするブックマークレット作り

前回、カーソル移動キーのないiPadのソフトウェアキーボードを補うために、文字カーソルを左右に移動するブックマークレットを作ってみた。実際に試してみると、自分の中では好感触!悪くない。

しかし、現状はシンプルすぎる(いや、一般性のない限定的な条件でしか使えない)スクリプトなので、使い勝手がすこぶる悪い...。はてなダイアリー本文(タイトルは駄目)の編集でしかカーソル移動できないのだ。あり得ん。

javascript:
var el = document.getElementById('textarea-edit');
el.setSelectionRange(el.selectionStart-1, el.selectionStart-1);

カーソル移動

欲しいのは、id属性に依存しない、編集中のテキストなら何処でもカーソル移動できるボタンなのだ。そこで...

  • フォーカスの当たっているエレメントを取得して、操作するように変更してみた。
  • テキストが範囲選択されている場合は、範囲の始点または終点に移動する。(現実のキーボードと同じ仕様)
戻る
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
if(el.selectionStart == el.selectionEnd){
el.setSelectionRange(el.selectionStart-1, el.selectionStart-1);
}else{
el.setSelectionRange(el.selectionStart, el.selectionStart);
}
進む
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
if(el.selectionStart == el.selectionEnd){
el.setSelectionRange(el.selectionEnd+1, el.selectionEnd+1);
}else{
el.setSelectionRange(el.selectionEnd, el.selectionEnd);
}

ブックマークレットの登録方法

  • 好みの名称で何処かのページをブックマークバーに一旦登録する。
  • 上記のブックマークを編集モードにして、必要なJavaScriptコードをURL欄にペーストする。
  • (注意)半角の\nが、半角の¥nになってしまっているので、以下のブックマークレットでは一旦メモ帳にペーストして、修正する必要あり。
    • 段落の先頭・末尾へジャンプ。
    • テキストエリアの自動リサイズ。

以上で完了。

カーソルジャンプ

また、テキスト入力中に先頭・末尾に移動したいこともよくある。そこで...

  • 改行コードを目印に、段落の先頭・末尾に移動するスクリプトを作ってみた。
  • 現実のキーボードショートカットcontrol-A・control-Eに相当する。
    • 但し、連続操作した時は、次の先頭・末尾を目指して、カーソルが上または下へ次々と移動する。
    • 上下の矢印キーがない環境では、この方が便利と思ったので。
段落の先頭へジャンプ
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
el.setSelectionRange(el. selectionStart-1, el.selectionStart-1);
for(i=0;i<200;i++){
  el.setSelectionRange(el.selectionStart-1, el.selectionStart);
  var sel = document.getSelection();
  if(sel=="\n"){
    el.setSelectionRange(el.selectionStart+1, el.selectionStart+1);
    break;
  }
}
  • 以下、改良バージョン
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str = el.value.substr(0, el.selectionStart-1);
var offset = str.split(/\n|\r/).pop().length+1;
el.setSelectionRange(el.selectionStart-offset, el.selectionStart-offset);
段落の末尾へジャンプ
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
el.setSelectionRange(el.selectionEnd+1, el.selectionEnd+1);
for(i=0;i<200;i++){
  el.setSelectionRange(el.selectionEnd, el.selectionEnd+1);
  var sel = document.getSelection();
  if(sel=="\n"){
    el.setSelectionRange(el.selectionEnd-1, el.selectionEnd-1);
    break;
  }
}
  • 以下、改良バージョン
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
var str = el.value.substr(el.selectionEnd+1);
var offset = str.split(/\n|\r/).shift().length+1;
el.setSelectionRange(el.selectionEnd+offset, el.selectionEnd+offset);

選択範囲

さらには、カーソル移動しながら範囲を選択したいこともしばしば。そこで...

  • 選択範囲の終点を操作して、拡大・縮小するスクリプトを作ってみた。
  • 現実のキーボードショートカットshift-←・shift-→に相当する。
範囲を縮小、あるいは戻る方向へ移動する
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
el.setSelectionRange(el.selectionStart, el.selectionEnd-1);
範囲を拡大する
javascript:
var fn = document.getSelection().focusNode;
var el = fn.getElementsByTagName('textarea')[0] || fn.getElementsByTagName('input')[0];
el.setSelectionRange(el.selectionStart, el.selectionEnd+1);

テキストエリアを自動リサイズする

iPadでテキストエリア内をスクロールするには、2本指でフリックする。しかし、1本指のフリックの時と違って慣性がほとんどなく、滑らかさがない。長い範囲のスクロールではなかなか進まずイライラする。

そもそも、テキストエリアに十分な高さがあれば、テキストエリア内のスクロールは不要になり、すべては1本指で滑らかに操作できるのだ。

でも、テキストエリアの最適な高さって、一体どれくらいだろう?無駄に高過ぎるとやはり苦労するし、短過ぎるとまた2本指でイライラすることになる。そこで...

  • テキスト入力に応じて、テキストエリアの高さを自動的に調整するスクリプトを作ってみた。
  • 文字入力毎にテキストエリア内の改行を数えて、高さを調整している。(3行目)
    • 改行数+25の設定。this.value.split('\n').length+25。
  • 一段落が複数行になりがちなタイプなら、以下のように修正しても良さそう。
    • 改行数×2+1の設定。this.value.split('\n').length*2+1。
javascript:(function(){
  function _resize(){
    this.setAttribute('rows', this.value.split('\n').length+25);
  }
  var els = document.getElementsByTagName('textarea');
  for (i = 0; i < els.length; i++){
    els[i].style.height = 'auto';
    els[i].style.overflow = 'auto';
    els[i].addEventListener('input', _resize, false);
    els[i].addEventListener('focus', _resize, false);
  }
})();

ブックマークバーについて

以上のブックマークレットを前回までの環境に追加修正すると、iPadのSafariは、このような状態となる。

f:id:zariganitosh:20110530161802p:image

  • ブックマークバーはブックマークレットだらけ。
  • iPadを横長に利用すれば、ギリギリ一覧できる。
  • ブックマークレットの名称は、最低でも日本語フォント3文字分の幅は欲しい。
    • 3文字未満(指の幅より狭い名称)だと、押し間違いが多くなる。
    • タッチした時の反応が顕著に悪くなる気がした。
  • また、小文字よりは大文字の方がタッチした時の反応が良い気がした。
  • カーソル操作のブックマークレットは、バー右側に寄せた方が操作しやすかった。
    • 中央付近では、文字カーソルの動きが手に隠れてしまい使いにくい。
    • 右手でのタッチ操作がメインなので、操作の移動が少ない右側に落ち着いたのだ。

課題と所感

  • ブックマークバーや戻る・進む・ページ選択等のコントロールの位置が、iPhoneのSafariのように画面下部に配置された方が操作しやすいと思う。
  • しかし、iPadのSafariでは、画面上部に配置される仕様。操作する時に手の移動距離が多くなってしまい、かったるい。
  • ブックマークレットを操作した時の明確なリアクションが欲しい。
  • タッチした時の音もなく、色の変化も分かりにくいので、正しくタッチできたかどうか不安が残る。
  • ソフトウェアキーボードのback deleteキーのような、押し続けた時のリピート機能も欲しい。
  • シフトキーを併用した状態を感知して、ブックマークレットを実行したい。

  • 以上の課題は、JavaScriptで対応できそうなこともあれば、iOS側の対応に期待するしかない部分もある。今後も、できることから地道に改良して、テキスト編集の操作性の向上を目指して行く予定。

  • 懲りずに、この日記もまたAll iPadでの投稿。JavaScriptの検索・コード書き・テスト等も含めてすべてをiPadで作業した。
  • まだまだ課題は多いが、ブックマークレットとマルチタスク用ジェスチャで、iPadはかなり実用的なテキスト編集環境になってきた!

2011-05-27

iPadで快適にブログを書く方法を模索する

今回の試みは、マルチタスク用ジェスチャの存在を知ったのと、カーソルキーのあるエディタの存在を知ったことに端を発する。さらに、ブックマークレットでも文字カーソルを移動できないかと思い調べてみると、はてなダイアリー(id="textarea-edit")限定なら、自分の知識でも作成できてしまった。

自分の中で今まで懸念事項だった問題(素早いアプリ切り替えと、カーソルキーによる移動)が解決してしまったのである。

この日記は、All iPadで編集した。

操作

  • テキストエリアのスクロールは二本指で上下にフリックするよ。
  • テキスト入力時、ダブルタップで単語が選択されるよ。
  • テキスト入力時、二本指タップで段落が選択されるよ。(段落 = 改行で区切られた文字の集合)
  • iPadを揺さぶると、undo(直前の操作の取り消し)ができるよ。

f:id:zariganitosh:20110526162927p:image


  • [.?123]ボタン で、undo・取り消す が表示されるよ。

f:id:zariganitosh:20110526162209p:image


  • [#+=]ボタン で、redo・やり直す が表示されるよ。(redo = undoの取り消し)

f:id:zariganitosh:20110526162210p:image

ブックマークレット

iPadにとって、ブックマークレットは、独自機能を追加できる最も手軽な方法。AppleScriptの使えないiPadでは、JavaScriptが大活躍する。

f:id:zariganitosh:20110527125111j:image

  • iPadにブックマークレットを登録するには、見出しのリンクにタッチする。
  • 「ページを開けません。」と警告されるが、構わずOKをタッチする。
  • 開けなかった白紙のページをブックマークバーに一旦、追加するよ。
  • その後、ブックマークの編集で「http://zariganitosh/?」部分を削除すれば、ブックマークレットの出来上がり。
CLOSE
  • 表示中のページを閉じるよ。そして、その前に表示していたページに移動するよ。
AP&LD
  • Autopagerizeを起動するよ。(スクロールが下部に達すると次ページを自動ロードする)
LINK
  • 表示中のページ内の http・ttp で始まるテキストをすべてリンク化するよ。
NOTE
  • 選択中のテキストや画像をEvernoteに追加するよ。
pageLINK
  • 表示中のページへのリンクを生成するよ。(aタグ&はてなブックマーク数リンク)
BOTTOM
  • ページの最下部にジャンプするよ。
<<<<>>>>
  • はてなダイアリー本文のテキストエリア(id="textarea-edit")で、文字カーソルを左右に移動するよ。
    • カーソルキーのないキーボードを補うために、苦肉の策として作ってみた。

マルチタスク用ジェスチャー

  • Xcode 3.2.6 & iOS SDK 4.3をインストールして、開発用の端末とすることで以下の機能が有効になる。
4 or 5本指で左右にスワイプ
アプリケーション切り替え。(タスクリストを表示せずに、ダイレクトに素早く切り替えられる)
4 or 5本指で上にスワイプ
タスクリストを表示する。(=ホームボタンのダブル押し)
4 or 5本指でピンチイン
アプリを閉じてホーム画面を表示する。(=ホームボタンを押す操作)
  • 特に「4or5本指で左右にスワイプ」はたいへん便利。キーボードのcommand-tabに匹敵する軽快さ!
有効にする方法
  • インストールしたらXcodeを起動して、ウィンドウ >> オーガナイザ を開く。
  • MacBookにiPadを接続する。
  • 左側のDEVICESのiPadを選択して、「Use for Development」ボタンを押す。
  • その後のアラートでは、すべて「キャンセル」を選択する。
  • iPadを再起動して、設定 >> 一般 >> マルチタスク用ジェスチャがオン になっていればOK。

f:id:zariganitosh:20110527142011j:image

はてなフォトライフ

  • 画像はメールに添付してアップロードするよ。
  • 「Cc/Bcc, 差出人: Sample@mail.com 画像:84.5 KB」の欄をタップすると、画像サイズを調整できるよ。(宛先と件名の間の行)

f:id:zariganitosh:20110527095620j:image

f:id:zariganitosh:20110527100045j:image

  • 最小の84.5 KB以下の画像が添付されている時は、画像サイズは選択できないよ。
  • 逆に言えば、画像サイズの選択項目が表示されない時は、最小の84.5 KB以下の画像が添付されている状態なのだ。

画像の加工

ブログに説明用のスクリーンショットや写真を載せるために。

  • ちなみに、画像の回転は、はてなフォトライフにアップロードしてからでも出来るよ。
Fotolr写真編集HD

f:id:zariganitosh:20110527095620j:image

  • 画像の回転(編集 >> 回転)、個人情報の部分を塗り潰す(編集 >> 絵画)のに使っているよ。
    • その他にも多数のレタッチ機能あり。
iPhotonic(iPhoneアプリ)

f:id:zariganitosh:20110527101031j:image

  • 個人情報の部分にモザイクを掛けるのに使っているよ。
    • 文字や矢印の書き込み、スポットライトやレンズの機能あり。
Adobe Photoshop Express
  • 基本的な写真のレタッチに使っているよ。(iPhotoのレタッチ機能に近い)

iA Writer(有料)

  • iPadのソフトウェアキーボード最大の不満であるカーソルキーを追加したエディタ。
  • 1文字ごとの移動に加え、1単語(日本語では品詞区切りになる)ごとの移動もできる!
  • ファイルはDropboxと同期される。
  • 日本語入力モードで開きカッコ ( が入力できない時は、スペースを空けてから入力すると良さそう。

f:id:zariganitosh:20110527162341p:image

所感

  • かつて、下書きだけはiPadで書くことはあったが、画像やリンクを含めた仕上げまで全てiPadで編集したのは今回が初めて。

結構しんどい...。

  • まだ、画像やリンクを作成する操作に身体が慣れていないから、かもしれない。
  • MacBookなら、やりたいことは無意識のショートカット操作で素早く完了する。
  • iPadだと、次に何を操作すべきか、いちいち頭で考えてから操作している。

でも、ここで思い出してみた。

  • はてなダイアリーを始めたばかりの頃は、MacBookでも操作にモタついていたはず。
  • 今のiPadで操作するのと同じような状況だったはず。
  • 数百回の日記を投稿する中で、確立されたMacBookの操作スタイルが出来上がった。
  • ならばiPadでも経験を積めば、MacBookのような素早い操作が可能かもしれない、と思い始めた。

悩み

  • 現在は、id="textarea-edit"限定のカーソル移動しかできない。これでは本文以外の入力、例えばタイトル欄ではカーソル移動できないのだ。
    • フォーカス中のテキストエリア、あるいはテキストボックスでカーソル移動するブックマークレットに出来ないだろうか?。
  • MacBookのSafariでは、NinjaKit用savetext.user.jsを使っていた。このスクリプトは強力で、うっかりタブを閉じようが、Safariが強制終了しようが、直前の入力テキストをかなりの確率で復活してくれた。
    • iPadのSafariでは、編集中のテキストをどの程度保持してくれるのだろう?小まめに保存するしかないのだろうか?

2011-05-10

シンクサービスからデータクラス情報を読み込めませんでしたエラー対策

突如、iPhoneiPadMacBookにUSB接続すると、エラー警告されるようになってしまった...。

      • シンクサービスからデータクラス情報を読み込めませんでした。

f:id:zariganitosh:20110510192105p:image


無視して同期しようとすると、さらにエラー警告される。

      • セッションが失敗したため、iPadのバックアップを作成できませんでした。

f:id:zariganitosh:20110510192106p:image

ならば復元しようとするも、またしてもエラー警告。復元もできない...。


iTunes >> 環境設定 >> デバイス >> デバイスのバックアップ:を確認すると、空っぽ。そんな馬鹿な!と思って、~/Library/Application Support/MobileSync/Backup/ を確認すると、バックアップファイルはちゃんと存在していた。ひとまず安心。(ホッとした)

しかし、同期できず、復元も出来ないとなると、打つ手がない...。そもそも、バックアップファイルはちゃんと存在しているのに、iTunesがデバイスのバックアップ:として認識しないのが問題だ。

そこで、Google検索で「シンクサービス」と入力してみると、即「...からデータクラス情報を読み込めませんでした」と候補が補完されてしまった。恐るべし補完機能。きっと、それだけ困っている人が多いという証だと思う。

ところが、検索結果はリストアップされたが、この問題に対するズバリの解決にはなかなか辿り着けなかった。結局、いくつかのリンクを辿って、以下のページから問題解決の手順を発見できた。(感謝です!)

手順

多くの人が掲題のエラー対策を一発で見つけることができますように!願いを込めてメモしておく。

  • もしかしたら単純に最新のiTunesを再インストールするだけでOKかもしれないが、自分が実施したのは以下の手順。
  1. 最新のiTunesをダウンロードする。
    • メールアドレス未入力のままでもダウンロード可能だった。
  2. iPhone・iPadをUSB接続から取り外す。
  3. iTunesを終了する。
  4. 以下のファイルをゴミ箱に移動する。
    • /Applications/iTunes.app
    • /System/Library/Extensions/AppleMobileDevice.kext
    • /Library/Receipts/AppleMobileDeviceSupport.pkg
  5. コンピュータを再起動する。(iTunesのダウンロードが完了してから)
  6. 最新のiTunesを再インストールする。
  7. iPhone・iPadをUSB接続する。
  8. iTunesを起動する。

以上で、エラー警告されることなく、正常に接続できるようになった。

  • iTunes >> 環境設定 >> デバイス >> デバイスのバックアップ: でもちゃんと認識されていた。
  • 同期も問題なく出来た。

想像する発生原因

  • おそらく、最新のXcodeとiOS SDKをインストールした後に、掲題のエラーが発生したように思う。

2010-12-05

AirPrint・CUPS-PDF・Dropboxを賢く使う方法(まとめ)

重要:一括ダウンロードに含まれるスクリプトを修正しました。

  • CUPS-PDF_permissions_default.scpt を実行すると/etc/cups/cups-pdf.confの内容が消えてしまう不具合を直しました。(すいません!
  • もし、/etc/cups/cups-pdf.confの内容が消えてしまった場合は、CUPS-PDFを再インストールすることでデフォルト設定が戻ります。
  • 2010/12/6 18:00以前にダウンロードした方は、最新版をダウンロードしてください。(お手数おかけして、申し訳ないです!

過去3回、AirPrintに試行錯誤してきて、

さらに分かったことも含めて、AirPrintにまつわる操作とか設定等のまとめ。

AirPrint可能なプリンタ共有にするよ

  • /usr/share/cups/mime/airprint.typesを追加する。(以前はダウンロードして、3ファイル追加・修正していたが、実はこれ1行だけでOKだった)
sudo sh -c "echo 'image/urf urf string(0,UNIRAST<00>)' > /usr/share/cups/mime/airprint.types"
  • 既存のファイルを修正しないので、ファイルを削除するだけで確実に、OSX標準の状態に戻せるのだ。

  • cupsdを再起動する。(以前はOSを再起動していたが、実はcupsdを再起動するだけで有効になった)
sudo killall cupsd
  • cupsdはlaunchdによって常時起動プロセスとなっているので、killしてもすぐ再起動するのだ。

  • あとは、システム環境設定 >> プリントとファクスで、プリンタを一旦削除、その後追加し直せば、AirPrinterの出来上がり!

>>>> サポートスクリプトあり

PDFに出力するよ

f:id:zariganitosh:20101204143953p:image

  • 追加したCUPS-PDFを選択して、「ネットワークでこのプリンタを共有」にチェックを入れる。

f:id:zariganitosh:20101204152404p:image

  • AirPrintでプリンタにCUPS-PDFを選択すれば、PDFで出力される。

f:id:zariganitosh:20101204144336p:image f:id:zariganitosh:20101204144402p:image

  • AirPrintで出力されたPDFは、デフォルトでは以下のパスどちらかに保存される。
    • /ユーザ/共有/CUPS-PDF/guest
    • /ユーザ/共有/CUPS-PDF/ANONYMOUS
  • /ユーザ/共有/CUPS-PDF/ はエイリアスであり、その実体は /var/spool/cups-pdf/ である。

guestとANONYMOUS

  • 「ゲストにこのコンピュータへのログインを許可」にチェックを入れたことがあるかどうかによって、以下のように反映される。
    • 過去に一度でもゲストアカウントを有効にしたことがあると、AirPrintで出力されるPDFは guest フォルダに出力される。
    • 今まで全くゲストアカウントを有効にしたことがなければ、AirPrintで出力されるPDFは ANONYMOUS フォルダに出力される。
  • guest フォルダと ANONYMOUS フォルダには、以下の違いがある。
    • guest フォルダは、guestアカウントでしか読み書きできない。他のユーザではアクセスできないので不便。
    • ANONYMOUS フォルダは、誰もが読み書きできる権限。誰もが簡単にアクセスできるのでとっても便利。
悩み
  • 自宅のようなプライベートな環境で利用するAirPrintなら、ANONYMOUS フォルダに出力されると大変扱い易いのだが、
  • ゲストアカウントをたまに利用する自分のMacBookでは、無情にも guest フォルダに出力されてしまっていた...。
  • ならばと、システム環境設定 >> アカウント で、ゲストアカウントを無効にしてみたが、相変わらず guest フォルダに出力される。(なぜだ!)
実験
  • 別の環境にOSX 10.6.5を新規インストールして試してみると...
    • インストール直後の「ゲストにこのコンピュータへのログインを許可」にチェック無しの状態なら ANONYMOUS フォルダに出力された。
    • 一度でも「ゲストにこのコンピュータへのログインを許可」にチェックを入れると、その後は常に guest フォルダに出力されてしまうのだ。
  • その条件の違いは何かと思って調べてみると...
    • インストール直後、ログインまで完了した状態。
$ sudo ls -lR /var/db/dslocal/nodes/Default/users
Password:
total 480
-rw-------  1 root  wheel    776  7  3  2009 _amavisd.plist
-rw-------  1 root  wheel    777  7  3  2009 _appowner.plist
-rw-------  1 root  wheel    846  7  3  2009 _appserver.plist
...(中略)...
-rw-------  1 root  wheel    728  9  3  2009 root.plist
    • システム環境設定 >> アカウント >> 「ゲストにこのコンピュータへのログインを許可」にチェックを入れた状態。
$ sudo ls -lR /var/db/dslocal/nodes/Default/users
Password:
total 480
-rw-------  1 root  wheel  14090 12  2 17:22 Guest.plist
-rw-------  1 root  wheel    776  7  3  2009 _amavisd.plist
-rw-------  1 root  wheel    777  7  3  2009 _appowner.plist
-rw-------  1 root  wheel    846  7  3  2009 _appserver.plist
...(中略)...
-rw-------  1 root  wheel    728  9  3  2009 root.plist

1行目にGuest.plistが追加された!

>>>> サポートスクリプトあり

Guestアカウントの削除
  • ならばと、思い切ってコマンドから Guest アカウントを削除してしまうと、その後は ANONYMOUS フォルダに出力されるようになった!
sudo dscl . -delete /users/Guest
  • もう一度、ユーザの設定ファイルを見ると、一番上の Guest.plist が消えていた。
  • 果たして、このようにGuestを削除してしまって、問題ないのだろうか?ちょっと不安...。(詳しい方、ぜひ助言をお願いします!
    • その後、OSを再起動して、
    • 「ゲストにこのコンピュータへのログインを許可」にチェックを入れると、
    • Guest.plist は復活して、再びゲストアカウントでもログイン可能になった。
  • どうしても ANONYMOUS フォルダを利用したい時には、試してみる価値はある。
  • 但し、TimeMachine等でバックアップして、いつでも元の環境に戻せる状況にしてから。

>>>> サポートスクリプトあり

プリンタ共有でユーザ管理するよ

  • ANONYMOUS フォルダは使えるようになったが、ゲストアカウントを有効にしてしまうと、とたんに使えなくなってしまう...。
  • もっと確実にユーザ管理する方法はないのかと思っていたら、実は昔からちゃんとあった。
  • システム環境設定 >> プリントとファクス で、CUPS-PDFの「ネットワークでこのプリンタを共有」をチェックありにすると、
  • システム環境設定 >> 共有 >> プリンタ共有 のプリンタ:に CUPS-PDF が追加されて、ユーザ:が「全員 プリント可能」となる。
  • この状態だと guest か ANONYMOUS として出力される。

f:id:zariganitosh:20101204151933p:image

  • ユーザ:で[+]ボタンを押すと、ユーザやグループを自由に追加できる。
  • 試しに ログインユーザである zari を追加してみると、以下の状態になった。
    • 「zari プリント可能」
    • 「全員 アクセスなし」

f:id:zariganitosh:20101204144941p:image

  • AirPrintのプリンタ選択ページでは、CUPS-PDFに鍵のマークが付く。

f:id:zariganitosh:20101204145102p:image

  • 選択すると、ユーザ名とパスワードの認証を求められた!

f:id:zariganitosh:20101204145202p:image

  • そして、印刷結果は /var/spool/cups-pdf/zari/ フォルダに出力された。
  • zari フォルダ以下のアクセス権は、zari ユーザだけが読み書きできる。
  • 自分自身がzariなので、自由に読み書きできるのだ。これなら使い易い!

出力されるPDFの権限をコントロールするよ

  • ところで、多くの人がAirPrintを利用する環境で、zariユーザのパスワードをみんなに教えてしまうのは、ちょっと考えものだ。
  • そんな場合は、新規メンバー ボタンを押すと、独自のユーザとパスワードを設定できるので、それを利用してもらうのも良さそう。
  • 試しに printer ユーザを追加してみると、AirPrintする際にprinterユーザも認証されるようになった。*1

f:id:zariganitosh:20101204145501p:image f:id:zariganitosh:20101204145546p:image


  • ちなみに、printerユーザは、システム環境設定 >> アカウント で「共有のみ」ユーザとして登録されることになる。

f:id:zariganitosh:20101204151934p:image

  • もし、printerユーザを削除したい時は、システム環境設定 >> アカウント で[−]ボタンを押して削除すればいいのだ。

  • しかし、ログインユーザ以外のprinterやguestでは、アクセス権がないのでPDFを出力したはいいが、それに触るのが困難になってしまう。
  • そこで、CUPS-PDFの設定ファイル、/etc/cups/cups-pdf.conf の「UserUMask」を設定する。
    • 「UserUMask 0077」は、所有者だけに「読み」「書き」を許可する。(デフォルト)
    • 「UserUMask 0022」で、グループと全てのユーザに「読み」を許可する。
    • 「UserUMask 0000」で、グループと全てのユーザに「読み」「書き」を許可する。

  • UserUMask 0123 とした場合の意味
    • 最初の0は、8進数であることを表現するプレフィックス
    • 1は、所有者 の権限を制限する。(2進表現で001となり、読み・書き・実行の権限のうち、実行が制限される)
    • 2は、グループの権限を制限する。(2進表現で010となり、読み・書き・実行の権限のうち、書きが制限される)
    • 3は、全ユーザの権限を制限する。(2進表現で011となり、読み・書き・実行の権限のうち、書きと実行が制限される)

  • 自分の場合、UserUMask 0022を設定して、/var/spool/cups-pdf/ 以下のユーザ名フォルダは、一旦全て削除してしまった。
  • その後のAirPrintで出力されるPDFやユーザ名のフォルダは、全てのユーザで読み込み可能になった。
  • これで、guestだろうが、printerだろうが、自由にアクセスできるようになった!

>>>> サポートスクリプトあり

Dropboxと同期するよ


  • Dropboxは、MacBookやiPhoneiPadなどでファイルを共有する手段として、たいへん使い勝手がいい。
  • Dropboxの素晴らしいところは、ファイルの実体をコピーしなくても、シンボリックリンク*2さえ入れておけば、そこにリンクされるファイルが同期されるところ。
  • フォルダのシンボリックリンクであれば、そのフォルダ以下のすべてのファイルとフォルダが同期される。
    • 一方、MobileMeのiDisk内では...
      • シンボリックリンクの作成は拒絶されてしまった...。
      • エイリアスにリンクされるファイルやフォルダが同期されることもなく、
      • エイリアスはエイリアスを表現する1ファイルとして同期されだけだった。
  • よって、/var/spool/cups-pdf/ のシンボリックリンクをDropboxに入れることで、CUPS-PDFの印刷結果が自動的に同期される。
ln -s /var/spool/cups-pdf ~/Dropbox/Public/
  • AirPrint後、Dropboxで同期されると即、iPhone・iPadのDropboxアプリからも、アクセス可能になる。
  • 同期されたPDFを選択して、リンクアイコンにタッチ、Email Linkを選択すれば、PDFにアクセス可能なURLを書き込んでメールを起動してくれる。

f:id:zariganitosh:20101129141145p:image

  • 共有したい人宛にそのメールを送信すれば、印刷したPDFを確認できる。(URLだけなので、メールはとっても軽いのだ)
  • ちなみに、Publicフォルダ内か、外かによって、以下のような違いがある。
    • ~/Dropbox/Public/cups-pdf/ の場合、URLを知っている人なら誰もが自由に閲覧可能なPDFとなる。
    • ~/Dropbox/cups-pdf/ の場合、PDFにアクセスする前に自分のDropboxへのログインを求められる。(つまり、自分だけのプライベートなPDFとなる。他人のログインでは、そのPDFにアクセスできない。)

  • その他にも、DropboxにShareフォルダを作ってを作って、よりシンプルに共有する方法もある。(メールによるURLの通知が不要)

プリントジョブを管理するよ

iPhone・iPadから
  • ホームボタンをダブル押し、タスクリストを表示して、プリント アプリを選択する。
  • 「プリントをキャンセル」ボタンを押すことで、簡単にキャンセルできる。
プリンタ共有しているMacBookから
  • http://localhost:631/ にアクセスして、CUPSの管理ページから操作する。
  • プリントジョブのキャンセル、プリント履歴の確認、プリンタの追加・削除などができる。

所感

  • なぜAppleは、ヒューレッド・パッカード限定のAirPrintととしてリリースしたのだろう?
  • たった1行の設定ファイルを追加するだけで、AirPrint対応のプリンタ共有が可能になり、現状のプリンタがそのまま使える、というのに。
  • それに、OSXはPDFベースの描画エンジンなのだから、その系譜のiOS自体もPDFを出力するのは簡単なはず。
  • その証拠にOSXの印刷では、遥かに昔からシステム標準でPDF出力に対応している。
  • なのにAirPrintでは、紙に印刷する手段しか提供してくれないのはなぜなのだろう?
  • なぜだ...なぜだ、Apple...。

PS

  • おかげで、AirPrintの日記が書けた(書くことになった?)けど...ね。

AirPrintをサポートするAppleScript

  • 以上のAirPrintに関する様々な便利技は、iPhone・iPadを利用するなら是非知っておきたいこと。
  • しかし、このように日記に書いても、数ヵ月後にはたぶん完全に忘れてる。
  • そうなった時に、無知な自分をサポートしてくれるAppleScriptを作ってみた。

airprint_sharing_ON.scpt
  • AirPrint可能なプリンタ共有を開始するサポートをするよ。

 (*
  *    AirPrintを有効にする
  *)
 activate
 "プリンタ共有でAirPrintを有効にします。"
 display dialog result with icon 1
 
 "echo 'image/urf urf string(0,UNIRAST<00>)' > /usr/share/cups/mime/airprint.types"
 do shell script result with administrator privileges
 "killall cupsd"
 do shell script result with administrator privileges
 
 (*
  *    プリンタの削除と追加
  *)
 tell application "System Preferences"
   activate
   set current pane to pane "com.apple.preference.printfax"
 end tell
 delay 1
 activate
 "AirPrintから利用したいプリンタを、[−]ボタンで一旦削除します。
 
 その後、[+]ボタンを押して再度追加してください。"
 display dialog result with icon 1
 delay 1
 activate
 "「ネットワークでこのプリンタを共有」にもチェックを入れます。"
 display dialog result with icon 1
 
 (*
  *    完了
  *)
 delay 1
 activate
 "以上で AirPrint 対応のプリンタ共有が開始されました。"
 display dialog result with icon 1

CUPS-PDF_anonymous_enable.scpt
  • CUPS-PDFで確実に ANONYMOUS を利用するために、ゲストアカウントを完全に削除するよ。

 (*
  *    CUPS-PDF ANONYMOUS を有効にする
  *)
 "実行する前に必ず、TimeMchine等で
 OS全体をバックアップしてください。
 
 ANONYMOUS を有効にするため、
 Guest アカウントを削除します。
 
 再び Guest アカウントを利用するには、OSの再起動が必要です。"
 display dialog result with icon 1
 
 "dscl . -delete /users/Guest"
 do shell script result with administrator privileges

CUPS-PDF_permissions_755.scpt
  • CUPS-PDFの出力ファイルの権限を、所有者:読み書き、グループ:読み、全ユーザ:読み、に設定するよ。

 (*
  *    CUPS-PDFの出力ファイルの権限を以下の設定にする
  *      所有者 :読み・書き
  *      グループ:読み
  *      全ユーザ:読み
  *)
 activate
 "出力するPDFの権限を以下の設定にします。
   所有者 :読み・書き
   グループ:読み
   全ユーザ:読み"
 display dialog result with icon 1
 
 "sed -e \"s/.*UserUMask [0-9]\\{4\\}/UserUMask 0022/\" /etc/cups/cups-pdf.conf > /tmp/cups-pdf.conf"
 do shell script result with administrator privileges
 "cp /tmp/cups-pdf.conf /etc/cups/cups-pdf.conf"
 do shell script result with administrator privileges

USERS_ls.scpt
  • OSに保持されているユーザリストを確認するよ。

 (*
  *    ユーザ確認コマンド
  *      1行目のGuest.plistを確認する
  *      存在していると、CUPS-PDFは guest フォルダに出力する
  *      存在しなければ、CUPS-PDFは ANONYMOUS フォルダに出力する
  *)
 tell application "Terminal"
   activate
   do script with command "sudo ls -lR /var/db/dslocal/nodes/Default/users"
 end tell

一括ダウンロード

  • ユーザ・スクリプト・フォルダに入れておくと便利そう。

重要:一括ダウンロードに含まれるスクリプトを修正しました。

  • CUPS-PDF_permissions_default.scpt を実行すると/etc/cups/cups-pdf.confの内容が消えてしまう不具合を直しました。(すいません!
  • もし、/etc/cups/cups-pdf.confの内容が消えてしまった場合は、CUPS-PDFを再インストールすることでデフォルト設定が戻ります。
  • 2010/12/6 18:00以前にダウンロードした方は、最新版をダウンロードしてください。(お手数おかけして、申し訳ないです!

参考ページ

以下のページが大変参考になりました。感謝です!

*1:最初にログインユーザであるzariで認証してしまったiOS端末は、zariユーザを削除して、printerユーザだけ登録した状態でも、zariユーザとして認証された。ログインユーザは登録が有る無しに関わらず、常にアクセス可能なようだ。

*2:Finderのエイリアスではダメ。

2010-11-30

AirPrint中にキャンセルする方法

  • AirPrint中に用紙切れ、インク切れ等の何らかのエラーが発生すると、ダイアログが表示される。

f:id:zariganitosh:20101130094007p:image

  • そのエラーの要因を取り除いてあげれば、ダイアログは勝手に消え、自動的に印刷は再開される。
  • 「キャンセル」を押せば、その印刷は中止される。
  • では、キャンセルしたいのに誤って「続ける」を押してしまい、いつまでも印刷が終わらなくなってしまった時の対応とは?
  • AirPrint中には、ホームボタンのダブル押しで表示されるタスクリストに、プリント アプリが表示されていた。

f:id:zariganitosh:20101130093642p:image:w160


  • プリント アプリにタッチすると「プリントをキャンセル」ボタンが赤々と輝いているではないか!

f:id:zariganitosh:20101130093709p:image:w160


  • プリント アプリは、未完了のAirPrintジョブが存在する場合に起動するようだ。
  • 間違って100枚で印刷してしまった時、途中でキャンセルしたい時にも役立つ。

2010-11-29

AirPrintでPDFをDropboxに送る

AirPrintは、ヒューレッド・パッカード製の限られた対応プリンタでしか印刷できない仕様で登場して、最初はガッカリした。しかし、前回の日記でやったように設定すれば、MacBookでプリンタ共有しているプリンタからも印刷可能になる。これでCanonでも、EPSONでも、その他どんなメーカーのプリンタでも、プリンタ共有さえ出来れば、そこから出力されるようになったのだ。

しかし、出力先は所詮、紙...。今やお金を払ってまでも書籍を懸命に自炊(PDF化)する時代に、さらに紙代・インク代にお金をかけて、印刷するのは馬鹿げている。もしかしたら、どこかで誰かがその印刷された紙をスキャンして、せっせとPDF化することになるかもしれない。AirPrint、便利かもしれないけど、でも結局使わない、ということになってしまうと思った。

ところが、このAirPrint、実は素晴らしい可能性を秘めていた。UNIX環境には、CUPS-PDFというプリンタ設定がある。これは、印刷する時のプリンタにCUPS-PDFを選択すると、PDFを出力してくれる。言わばドライバだけの仮想プリンタのようなものである。大変便利そうなのだが、OSX環境においてはQuartzがPDFベースの描画エンジンで、画面に見えているものすべては簡単にPDF化できる。だから、CUPS-PDFを使うまでもなく、今までほとんど使っていなかった。

そこに、AirPrintが登場した。今や自分のMacBookでは、プリンタ共有さえ出来れば、どんなプリンタからも出力できるはず。どんなプリンタでも...そう、CUPS-PDFからもきっと出力できるはずだ!そのことに気付いて、早速やってみた。

利用環境

CUPS-PDF

ダウンロード
インストール
  • ダウンロードした CUPS-PDF 2.5.0 Installer.zip を解凍し、CUPS-PDF.mpkg をダブルクリック、手順に従ってインストールした。
システム環境設定
  • システム環境設定 >> プリントとファックス で+ボタンを押すと、プリンタの追加 ウィンドウには CUPS-PDF が表示されている。

f:id:zariganitosh:20101129061842p:image

  • CUPS-PDF を選択して、追加ボタンを押した。

f:id:zariganitosh:20101129061841p:image

  • 「ネットワークでこのプリンタを共有」にもチェックを入れた。
印刷
  • さっそく、MacBookからプリンタにCUPS-PDFを指定して印刷してみる。

f:id:zariganitosh:20101129061843p:image

  • 印刷結果として生成されたPDFは、/ユーザ/共有/CUPS-PDF/ からアクセスできるようになっていた。
  • 上記はエイリアスで、その実体は、/var/spool/cups-pdf/ というFinderからは不可視な階層になっていた。
  • そのCUPS-PDFフォルダの中に、印刷者であるログインユーザ名のフォルダが生成されて、そこにPDFファイルが追加されていくようだ。
  • PDFのファイル名には、ジョブ番号が付番されていた。
  • ジョブ番号は、CUPS-PDFで印刷する度に+1されて行く。
  • PDFを開いてみると、さっき印刷した内容がちゃんと表示された。

f:id:zariganitosh:20101129061844p:image

満足、満足。

AirPrint
  • 次に、iPhone・iPadからAirPrintで印刷してみた。

f:id:zariganitosh:20101129061845p:image f:id:zariganitosh:20101129091648p:image

  • AirPrintの場合、ログインユーザ名はどうなるのだろうと思っていたら、guest ユーザとなった。
  • プリンタ共有しているMacBookで /var/spool/cups-pdf/guest フォルダが生成され、その中に印刷結果のPDFが追加されていく。
  • ところで、/var/spool/cups-pdf/guest フォルダには、guest ユーザしかアクセスできない権限が設定されていた。
  • よって、guestユーザでログインした時だけ、Finderで自由にアクセスできるようになる。
  • いつも使っているユーザでアクセスできないのは、ちょっと不便だ。どうにかしたい...。

Dropbox

  • 今やAirPrintによって、/var/spool/cups-pdf/guest フォルダにPDFが保存されるようになったことについては大満足。
  • しかし、このままではguestユーザしかアクセスできない、秘密のPDFになってしまう...。
  • 印刷結果は、自分が望む人に自由に渡せるようになって初めて、その利用価値が高まる。
  • 紙の場合は、手渡し、FAX、郵送など、手間はかかるが確実に渡せる方法を知っている。
  • それでは、guestユーザしか閲覧できないPDFファイルの場合は、どうするべきか?

そんな時こそ、Dropboxが大活躍する!

  • もはや、あまりにも便利、かつ有名になってしまったDropboxについての説明は、ここでは省略。
シンボリックリンク
  • Dropboxの素晴らしいところは、ファイルの実体をコピーしなくても、シンボリックリンクさえ入れておけば、そこにリンクされるファイルが同期されるところ。
  • フォルダのシンボリックリンクであれば、そのフォルダ以下のすべてのファイルとフォルダが同期される。
  • つまり、/var/spool/cups-pdf/ のシンボリックリンクをDropboxに入れておけば、CUPS-PDFの印刷結果が自動的に同期されることになるのだ!
  • 具体的には、ターミナル.appを起動して、以下のコマンドを実行すればOK。
ln -s /var/spool/cups-pdf ~/Dropbox/
  • ~/Dropbox/cups-pdf というシンボリックリンクが追加され、/var/spool/cups-pdf/ 以下のファイルとフォルダが同期されるようになった。

f:id:zariganitosh:20101129102011p:image


  • ちなみに、Finderで確認すると、シンボリックリンクもエイリアスとなってしまうが、
  • シンボリックリンクとエイリアスは、違う。
  • Dropboxに追加して、それ以下のファイルとフォルダを同期してくれるのはシンボリックリンクのみである。
  • Finderで作成可能なエイリアスでは、同じように同期してくれないので、注意が必要。

/var/spool/cups-pdf/guestのアクセス権の変更

  • これでAirPrintしたすべてのPDFが同期されるようになったかと思えば、そうではなかった。
  • そう、アクセス権のない /var/spool/cups-pdf/guest 以下のファイルやフォルダは、同期されないのであった。(アクセス権がないのだから当然の結果なのだ)
  • どうしようかと暫し悩んで、/var/spool/cups-pdf/guest のアクセス権 everyone=読み出しのみ を追加してしまった。
  • 内包している項目にも適用して、現状のすべてのファイルは同期されるようになった。

CUPS-PDFの設定ファイルの変更

  • 今度こそAirPrintしたPDFがDropboxと同期されるようになったかと思いきや、まだ問題があった。
  • /var/spool/cups-pdf/guestと、その中に既に存在するPDFにはアクセスできるようになったが、
  • 新規にAirPrintしたPDFにはアクセスできない状態。
  • 新規生成されるPDFのアクセス権は相変わらず everyone=アクセス不可 で、guestしかアクセスできないのであった...。
  • ファイルは見えているのに開くことが出来ないという、もどかしいことになっている。
  • 生成されるPDFのアクセス権はCUPS-PDFが決めているはず。では、どうすればいいのか?
  • 調べてみると、CUPS-PDFには設定ファイルがあった。
    • /etc/cups/cups-pdf.conf
  • その中の Key: UserUMask を設定することで、PDF生成時のアクセス権を変更できた。
    • コメントマーク#を外して「UserUMask 0022」とすることで、GroupとEveryoneのアクセス権に「読み出しのみ」が追加された。
...(中略)...
###########################################################################
#									  #
# Security Settings							  #
#									  #
###########################################################################

### Key: AnonUMask
##  umask for anonymous output
##  these are the _inverse_ permissions to be granted
### Default: 0000

#AnonUMask 0000

### Key: UserUMask
##  umask for user output of known users
##  changing this can introduce security leaks if confidential
##  information is processed!
### Default: 0077

UserUMask 0022

### Key: Grp
##  group cups-pdf is supposed to run as - this will also be the gid for all
##  created directories and log files
### Default: lp

Grp staff
...(中略)...

ちなみに、設定の意味は以下のように理解している。

  • UserUMask 1234 となっていた場合、
    1. 不明(何の権限だろう?)
    2. 所有者の権限を制限
    3. グループの権限を制限
    4. 全員の権限を制限
  • 上記は権限を制限する設定なので、それぞれの権限は以下のように設定されるようだ。
    • 7だと、すべての権限が制限される。(つまり、アクセス権なしの状態)
    • 0だと、権限の制限なし。(つまり、フルアクセスな状態)
    • 2(2進数で 010)だと、読み・書き・実行の書き込み権限が制限される状態。(つまり、読み出しのみ可能な状態)
  • つまり、chmodコマンドとは反対の意味になるのだ。

共有

  • 一旦Dropboxで同期できれば、そのPDFは素晴らしい使い勝手になる。
  • リンクアイコンにタッチして、Email Linkを選択すれば、PDFにアクセス可能なURLを書き込んでメールを起動してくれる。

f:id:zariganitosh:20101129141145p:image

  • 共有したい人宛にそのメールを送信すれば、印刷したPDFを確認できる。(URLだけなので、メールはとっても軽いのだ)
  • 誰もが自由に閲覧するためには、シンボリックな cups-pdf を Public フォルダに入れておく必要があった。
    • ~/Dropbox/Public/cups-pdf/

  • その他にも、DropboxにShareフォルダを作って、よりシンプルに共有する方法もある。(メールによるURLの通知が不要)

あとは創意工夫でDropboxの機能をフル活用して、さらなる便利な使い方が見つかるかもしれない。

Printopia

  • 以上のことは、Printopiaというアプリケーションによって、ものすごくシンプルに実現できたのであった...。
  • 但し、9.95ドルの有料アプリケーションである。(デモ版は1週間だけ自由に試用できる)

f:id:zariganitosh:20101129141016p:image f:id:zariganitosh:20101129142322p:image


お金を払って機能を買うか、お金を払わずに知恵を付けるか、それが問題だ。