Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

「Windowsストアアプリ内でPDFを表示するための、ある1つの方法」の補足

こんにちは、間藤です。

前回に続き、中山が書いた記事をネタとして活用したいと思います。
この記事は、pdf.jsをWindowsストアアプリで活用する方法として、1つのアイディアを紹介するものでした。私がこの記事を理解するにあたり、当初いくつかわからないことがありました。その点にフォーカスして、私が今回把握できたことを共有したいと思います。

PdfPageとviewer.html

PDF表示は、PdfPageで行っています。このPdfPageの構造は、前ページ(MainPage)に戻るためのButtonコントロールと、WebViewコントロールが配置されているだけのシンプルなものです。そして、WebViewコントロールがviewer.htmlを表示しているという構成になっています。

<Grid Background="DimGray">
    <Button x:Name="backButton" Click="GoBack" Style="{StaticResource BackButtonStyle}" Foreground="White" BorderBrush="White" Margin="36,6" VerticalAlignment="Top"/>
</Grid>
<Grid Grid.Row="1" Background="LightGray">
    <WebView x:Name="MyWebView" Margin="0" Width="Auto"/>
</Grid>

f:id:IntelligentTechnology:20130829120900p:plain

ですから、PDFビューアの画面構成を変えたい場合は、viewer.htmlをカスタマイズすることになります。

viewer.htmlの配置先

中山の記事でも書かれていたように、viewer.htmlを実行時にコピーする処理を行っています。その理由についても記事で触れていますが、少し説明が足りないところもあるので、それを今回補足したいと思います。(説明が重複しているところもあります。)

Visual Studioのプロジェクト上、viewer.htmlは、Assets/web配下に配置されています。これは、アプリの実行時にはインストールフォルダ(Windows.ApplicationModel.Package.Current.InstalledLocationで取得可能)にも配置されます。これをそのままWebViewコントロールで表示するようにはせず、ローカルデータフォルダ(ApplicationData.Current.LocalFolderで取得可能)にコピーし、WebViewはそれを参照するようにしています。(このコピー処理は、PdfPageに実装されています。)
但し、毎回同じファイルをコピーするのは非効率ですので、Assets/web配下にversion.txtというファイルを用意し、Assets/web配下のファイルを変更したら、このversion.txt中のバージョン番号を大きな値に変更するようにします。このversion.txtの変更を受けて、コピー処理を行うようになっています。

WebViewコントロールでインストールフォルダ配下のhtmlファイルを参照しようとした場合、以下のように記述することができます。
※MyWebViewは、WebViewコントロールのインスタンスを参照しています。

MyWebView.Navigate(new Uri("ms-appx-web:///Assets/web/viewer.html"));

ですが、このようなURL指定では、PDF表示に失敗してしまいます。どうやら、内部でPDF表示処理を担っているpdf.jsは、viewer.htmlを開く際にリクエストされたURL(ms-appx-web:///Assets/web/viewer.html)を加工して、取得するPDFのURLを編集し、XMLHttpRequestを利用してそのURLからPDFのデータを取得するようになっているようです。つまり、viewer.htmlを参照(リクエスト)するためのプロトコルとしてHTTPを選択する必要があるということになります。このような理由から内部にHTTPサーバをたてて、そのサーバを経由してviewer.htmlを取得するようにしたということです。
なお、PDF文書を取得する処理は、pdf.jsの71行目以降に書かれています。この関数を実行しているのは、Web Workersのワーカー側です。また、非同期処理を制御するために、Promiseクラスを実装しています。このあたりのロジックをカスタマイズすることでも、内部にHTTPサーバを用意する方式を回避できるかもしれません。

f:id:IntelligentTechnology:20130829141443p:plain

ここまでの説明だけでは、コピー処理を行う理由になっていませんので、もう少し説明を続けます。

viewer.htmlにクエリパラメータで開きたいPDF文書を指定することも可能です。
内部HTTPサーバのドキュメントルートからのパスを指定します。

viewer.html?file=/pdf/mysample.pdf

内部HTTPサーバのドキュメントルートを、アプリのインストールフォルダにすれば、Assets/web配下のファイルをローカルデータフォルダ配下にコピーする必要はありません。しかしながら、それではAssets/web配下に配置されたPDF文書しか表示できないことになります。例えば、アプリの実行時に別途ネットワーク経由でダウンロードしておいたPDF文書を表示したい場合などに対応できません。※もちろん、そのようなダウンロード機能をアプリに実装する必要があります。
そこで、HTTPサーバのドキュメントルートをローカルデータフォルダ(このフォルダならダウンロードしたPDF文書を保管できる)にしたということです。
これがわざわざコピー処理を行っている理由です。

但し、このあたりは、アプリをどう構成するかで考え方が変わってきます。
ローカルデータフォルダにあらかじめダウンロードしておいたPDF文書を利用する、というのは、オフライン時であってもPDF文書を参照できるようにしたい場合を想定しての工夫です。
もし、ネットワークに接続している環境下で利用することが前提でよいのであれば、viewer.html自体をローカルに持つ必要はなく、どこかのWebサーバ上に配置しておけばよいことになりますし、PDF文書についても、そのWebサーバ上に配置しておけばよいことになります。HTTPサーバを内部に持つ必要もありません。

ツールチップの日本語表記

viewer.htmlでは、上部にツールバーを用意し、PDFを操作するためのボタンを配置しています。ボタンにマウスカーソルを合わせると、ツールチップが表示されます。

f:id:IntelligentTechnology:20130829141844p:plain

私は、これを見て「おや?」と思いました。ペンの機能は、中山が追加した機能なのに、ちゃんとツールチップが出るなと思ったのです。

viewer.htmlは、多言語化に対応しており、利用環境によって表示される言語が切り替わるようになっています。その制御を行っている箇所を探すと、viewer.jsの3112行目に見つけることができます。

var locale = navigator.language;
if ('locale' in hashParams)
  locale = hashParams['locale'];
mozL10n.setLanguage(locale);

setLanguage関数の先では、localeディレクトリ配下のプロパティファイルを読み込みます。よって、表記を変更したい場合は、localeディレクトリ配下のファイルを編集する必要があります。実際、中山もこれらのプロパティファイルを編集したそうです。
navigator.languageは、ブラウザの言語バージョンを表す文字列です。日本語なら"ja"や"ja-JP"のようになります。基本的には、navigator.languageで取得される言語に合わせて表記されれば問題はないと思いますが、もしどうしても変更したい場合は、URLに以下のようにハッシュでロケールを指定すれば良いようです。(上記コードのhashParamsがこれに相当します。)

viewer.html?file=/pdf/mysample.pdf#locale=en-US

「ファイルを開く」ボタン

pdf.jsには、ローカルに保存されたPDF文書を、ユーザが選択して開くための機能も用意しています。もし、これだけで事足りるのであれば、内部HTTPサーバを用意する必要はないということになります。

「ファイルを開く」ボタンを押下すると、ファイル ピッカーが表示され、ファイルを選択できます。実際には、単にtype="file"のinput要素を裏に隠し持っており、「ファイルを開く」ボタンが押されると、そのinput要素のclickイベントを発火するようになっているだけです。

<input id="fileInput" class="fileInput" type="file" oncontextmenu="return false;" style="visibility: hidden; position: fixed; right: 0; top: 0" />

<button id="openFile" class="toolbarButton openFile hiddenSmallView" title="Open File" tabindex="15" data-l10n-id="open_file">
   <span data-l10n-id="open_file_label">Open</span>
</button>
document.getElementById('openFile').addEventListener('click',
  function() {
    document.getElementById('fileInput').click();
  });

次に、ファイル ピッカーでPDF文書が選ばれたら、以下に示すように、changeイベントをハンドリングし、HTML5のFile APIでファイルの内容を読み込んでいます。

window.addEventListener('change', function webViewerChange(evt) {
  var files = evt.target.files;
  if (!files || files.length === 0)
    return;

  // Read the local file into a Uint8Array.
  var fileReader = new FileReader();
  fileReader.onload = function webViewerChangeFileReaderOnload(evt) {
    var buffer = evt.target.result
    var uint8Array = new Uint8Array(buffer);
    PDFView.open(uint8Array, 0);
  };

  var file = files[0];
  fileReader.readAsArrayBuffer(file);
  PDFView.setTitleUsingUrl(file.name);

  // URL does not reflect proper document location - hiding some icons.
//  document.getElementById('viewBookmark').setAttribute('hidden', 'true');
  document.getElementById('download').setAttribute('hidden', 'true');
}, true);

PDFView.open関数の先でPDF文書を表示する処理を行っています。

まとめ

中山の示したサンプルにおけるpdf.jsの活用方法は、要件によっては冗長になります。今回の記事で、もっとシンプルな利用方法もあるということが伝われば幸いです。