PDF の応用

とりあえず、ここまでに PDF に関してメモしたことと、他のことを結びつけて試したことをメモしてみる。

PDF の検索

まずは、PDF 文書内を検索するに書いたことを応用してアプリケーションで使ってみる。それ以外にもちょっと簡単なことをしている。あと、このサンプルは、Document-based アプリケーションで作ってるので、それに関して細かいことは、このページを参照してください。面倒くさいようなら、普通の Window-based アプリケーションでも応用できます。

まずは、MyDocument.nib の Window にこんな感じでパーツを配置した。

下のは PDF View で、上の方はこんな感じに適当にボタンを置いた。

見ればわかると思うけど、前のページ、次のページ、Drawer、検索、全部検索、全部選択という機能を付けてみる。全部がスクリプトじゃないところが PDFKit のいいところ。

スクリプトの方で準備するのは、とりあえず、Quartz フレームワークを読み込む、OSX を include する、あと、pdfDocView と pdfSearchString いう名前の outlet を加える(これは上の PDFView と Text Field と結びつける)。

OSX.require_framework 'Quartz'

include OSX

ib_outlets :pdfDocView, :pdfSearchString

まずは、スクリプトを書かなくてもできるところから。

PDFView には次のような Received Actions が最初から登録されている。だから、これらをボタンにつなげるだけ。

ここでは、goToLastPage を左向きボタンに、goToNextPage を右向きボタンに、selectAll を SELECT ALL ボタンにつなげる。

さて、これを試そうにも PDF 文書が読み込めないと試せないので、スクリプトの次の部分を変えておく。

def windowControllerDidLoadNib(aController)

super_windowControllerDidLoadNib(aController)

if not self.fileURL.nil?

pdfdoc = PDFDocument.alloc.initWithURL(self.fileURL)

@pdfDocView.setDocument(pdfdoc)

end

end

def readFromURL_ofType_error(url,type,error)

return true

end

これと、あとは PDF を読み込める文書の形式に設定し細かい設定をして(ここを参照)、ビルドと実行をすると、PDF 文書が開けるようになって、実際にボタンが動くのが試せる。

さて、次は、Drawer を追加する。ここには、検索した結果を表示したいので、Table View を置いて、Array Controller で管理する。Drawer の追加については、ここを参照。

Window and Drawer を追加して、追加された Window は消して、もとからある Window に結びつける。そうしたら、toggleDrawer ボタンに結びつけておく。

Drawer Content View には Table View を追加して コラムは2つにする。一つはページ、もう一つは検索結果のテキストの予定。タイトルを付けてもいいかも。

ここには、検索結果とページ番号を表示したい。あと、検索結果は、検索語だけでなく、その前後の文脈語も表示したいので、Drawer はそれなりの幅を取っておく。ここでは思い切って 400 にしてみた。実用的ではないかもしれない。

Array Controller を追加して、page と match という key を追加した。スクリプトの outlet には aryCtl というのを追加して、これと結びつける。

オプションは Select Inserted Objects を外しておく。

そして、この2つを Table View の2つのコラムとバインドする。左の細い方が page 右が match。

さて、ではスクリプト。searchString というメソッドを作り、Search ボタンに結びつける。

def searchString(sender)

end

ib_action :searchString

PDFView の中身が空だと反応させたくないので、その場合は反応しなくする。実際アプリケーションにする場合は、ボタンを使えないようにした方がいいと思うけど、面倒なのでここでは、チェックして空なら戻る、ということにする。

return if @pdfDocView.document.nil?

ここで、PDF 文書内を検索するで扱った PDFDocument の findString_fromSelection_withOptions(string,selection,option) というメソッドを使う。まず、@pdfSearchString から検索語を取り出し、現在の選択位置を読み込んで、次のマッチを見つける。検索語が空なら戻るようにもしておく。オプションは大文字小文字を無視で。

searchString = @pdfSearchString.stringValue

return if searchString == ""

currentSelection = @pdfDocView.currentSelection

selection = @pdfDocView.document.findString_fromSelection_withOptions(searchString,currentSelection,NSCaseInsensitiveSearch)

この結果を、PDFView の setCurrentSelecteion(selection) で強調表示して、scrollSelectionToVisible(sender) でそこまで移動する。

@pdfDocView.setCurrentSelection(selection)

@pdfDocView.scrollSelectionToVisible(nil)

まとめるとこんな感じ。

def searchString(sender)

return if @pdfDocView.document.nil?

searchString = @pdfSearchString.stringValue

return if searchString == ""

currentSelection = @pdfDocView.currentSelection

selection = @pdfDocView.document.findString_fromSelection_withOptions(searchString,currentSelection,NSCaseInsensitiveSearch)

@pdfDocView.setCurrentSelection(selection)

@pdfDocView.scrollSelectionToVisible(nil)

end

ib_action :searchString

最後は、検索にマッチするものを全部探して Table View に表示する。これには、findString_withOptions(string,option) を使う。こっちは searchAll というメソッドにする。

def searchAll(sender)

end

ib_action :searchAll

ここでも、PDF 文書が設定されていないと戻るようにする。検索語を取り入れるのもいっしょ。

return if @pdfDocView.document.nil?

searchString = @pdfSearchString.stringValue

return if searchString == ""

findString_withOptions(string,option) の返り値は配列なので、これをブロックにする。オプションは大文字小文字を無視にする。

@pdfDocView.document.findString_withOptions(searchString,NSCaseInsensitiveSearch).each do |selection|

end

ここで、検索にマッチしたものをただ並べても何の意味もないので、前後の文脈も少し表示して、検索語をボールドにしてみる。

まずは、検索語の長さを得る。PDFSelection の string というメソッドで文字列を取り出せるので、その長さを得る。

matchLength = selection.string.length

次に、選択部分の selection の前後に20文字ずつ追加する。これには、PDFSelection の extendSelectionAtStart(int)extendSelectionAtEnd(int) というメソッドをを使う。int は文字数。これを20にする。あとで、強調表示するときに、左側の文字数がわかる必要があるので、左側を追加した時点で、検索語の長さを引いて、左側の文字数を得る。

selection.extendSelectionAtStart(20)

selectionLeftLength = selection.string.length - matchLength

selection.extendSelectionAtEnd(20)

あとは、ボールドの属性を追加したいので、selection.string を NSMutableAttributedString にする。ただ、前後のところに改行が含まれると表示が途切れるので、改行を半角の空白に変える。全部の pdf ファイルがそうなのかはわからないけど、試したものは改行コードが \r だったので、それを置き換える。実際のアプリケーションでは、もうちょっと柔軟に対応する。\r|\r*\n とか。

matchText = NSMutableAttributedString.alloc.initWithString(selection.string.gsub(/\r/," "))

これでできた NSMutableAttributedString オブジェクトに、addAttribute_value_range(attribute,value,range) で範囲を指定して属性を追加する。ここでは、Table View のテキストの標準のフォントが Lucida Grande の 12 ポイントだったと思うので、それのボールドにする。範囲は、左側の文脈の文字の長さを location、検索語の長さを length で指定する。

matchText.addAttribute_value_range(NSFontAttributeName,NSFont.fontWithName_size("Lucida Grande Bold",12.0),[selectionLeftLength,matchLength])

次に、ページ番号を得る。これには PDFSelection の pages というメソッドを使う。返り値は PDFPage オブジェクトの配列なので、ここでは決めうちで最初の要素にアクセスするだけにする。そして、PDFPage の label でベージ番号を得る。

page = selection.pages[0].label

最後に、aryCtl にテキストとページ番号を入れていく。

@aryCtl.addObject({"match" => matchText, "page" => page})

これらをまとめると、こんな感じ。

def searchAll(sender)

return if @pdfDocView.document.nil?

searchString = @pdfSearchString.stringValue

return if searchString == ""

@pdfDocView.document.findString_withOptions(searchString,NSCaseInsensitiveSearch).each do |selection|

matchLength = selection.string.length

selection.extendSelectionAtStart(20)

selectionLeftLength = selection.string.length - matchLength

selection.extendSelectionAtEnd(20)

matchText = NSMutableAttributedString.alloc.initWithString(selection.string.gsub(/\r/," "))

matchText.addAttribute_value_range(NSFontAttributeName,NSFont.fontWithName_size("Lucida Grande Bold",12.0),[selectionLeftLength,matchLength])

page = selection.pages[0].label

@aryCtl.addObject({"match" => matchText, "page" => page})

end

end

ib_action :searchAll

これで、サンプルアプリの完成。実際は、文脈語の左が20文字より少ないと左詰めになってしまうし、Lucida Grande はプロポーショナルなフォントなので検索語の位置がそろわないが、まあ、その辺りは実際アプリケーションを作ることになったら、好みで考える。

これを実行して表示させてみるとこんな感じに出てくる。スクリーンショットは Drawer だけで取ったので、独立したウィンドウのように見えるけど、実際はメインのウィンドウから出てきてる。

追加で、もうちょっと使えるものに。

せっかく検索語が並んでるので、これをクリックするとそこに移動するようにしたい。これには、PDFSelection の情報を aryCtl に入れておけば何とかなる。ただ、PDFSelection オブジェクトが途中で文脈語が加えられている分長くなっているので、その前に複製を作る。

origSelection = selection.copy

そして、Array Controller に selection という key を追加して、selection という key に origSelection を割り当てる。

@aryCtl.addObject({"match" => matchText, "page" => page, "selection" => origSelection})

Interface Builder に戻って、結果を表示する Table View の delegate を File's Owner にする。

あとは、テーブルの選択が変更になったという notification を tableViewSelectionDidChange(notification) でとらえる。

とらえたら、NSArrayControler の selectionIndex というメソッドで、選択された行のインデックスを得る。ちなみに、Table View はデフォルトのままで、吹く数行が選択できないようになっている。そして、aryCtl の arrangedObjects の選択されたインデックスの要素の selection に入っている PDFSelection の情報を得る。

selection = @aryCtl.arrangedObjects[@aryCtl.selectionIndex]['selection']

あとは、これを上の方にある検索の時と同じように、setCurrentSelection(selection) で選択し、scrollSelectionToVisible(sender) でそこまで移動する。

@pdfDocView.setCurrentSelection(selection)

@pdfDocView.scrollSelectionToVisible(nil)

追加分の書き加えも含めてまとめるとこんな感じ。

def searchAll(sender)

return if @pdfDocView.document.nil?

searchString = @pdfSearchString.stringValue

return if searchString == ""

@pdfDocView.document.findString_withOptions(searchString,NSCaseInsensitiveSearch).each do |selection|

origSelection = selection.copy

matchLength = selection.string.length

selection.extendSelectionAtStart(20)

selectionLeftLength = selection.string.length - matchLength

selection.extendSelectionAtEnd(20)

matchText = NSMutableAttributedString.alloc.initWithString(selection.string.gsub(/\r/," "))

matchText.addAttribute_value_range(NSFontAttributeName,NSFont.fontWithName_size("Lucida Grande Bold",12.0),[selectionLeftLength,matchLength])

page = selection.pages[0].label

@aryCtl.addObject({"match" => matchText, "page" => page, "selection" => origSelection})

end

end

ib_action :searchAll

def tableViewSelectionDidChange(notification)

selection = @aryCtl.arrangedObjects[@aryCtl.selectionIndex]['selection']

@pdfDocView.setCurrentSelection(selection)

@pdfDocView.scrollSelectionToVisible(nil)

end