hishidaの開発blog

EBシリーズ(EBPocket,EBWin,EBMac,EBStudio),KWIC Finder,xdoc2txt,読書尚友の開発者ブログ

EBStudio リニューアル計画(1)

EPWING辞書作成アプリであるEBStudioの最終バージョンのリリースは2009年で、もう8年も開発を中断している。その頃からEBPocketのiOS版やAndroid版の開発を始め、さらに青空文庫ビューアまで手を出したため、手が回らなかったというのが正直なところだ。リニューアルの構想自体はもう何年も前からあったが、様々なプロジェクトが一段落してきたので、いよいよ手をつけることにした。

EBStudioの開発の着手は1999年に遡る。
EPWINGソニーの電子ブックは1990年代には興隆を誇ったが、その後電子辞書の主力はCD-ROMから電子辞書端末に移り、さらにスマートフォンの辞書アプリやネット辞書に移行した。気がつけば電子辞書端末の雄だったセイコーも市場から去り、隔世の感がある。EPWINGは新たな製品が全く出なくなったが、規格がJIS化されているために、オープンな辞書規格として生き残ることが可能になった。翻訳者の間ではEPWINGは現在でも現役で使われていると言われており、今後も細々と生き続けるのではないかと思っている。

作者が考えるEBStudioの問題点

  • 開発言語がVisual C++6.0 であり、C++の仕様が古いため、最近のVisual Studioではコンパイルができない。
  • MFCライブラリを多用しているために他のプラットフォームに移植が困難
  • 1999年当時はHTMLパーサーのデファクトがないために独自パーサーを開発したため、その後のXMLテクノロジーの進化に対応できていない
  • 入力エンコーディングSJISのみでUTF-8に未対応
  • EPWING外字の作成が難しい
  • 変換用の内部バッファサイズを細かく指定しないといけないので利用しづらい
  • HONMON のサイズの上限が4GB
  • EPWING Ver2までのサポートのため、マルチメディアデータもHONMONに全て収納する必要がある(EPWING Ver4から画像をHONMONG,音声をHONMONSに分けることができるようになった)

-

EBStudio 2.0の目標

  • モダンなC++コンパイルできるようにソースのリファクタリングを行う。MFCは全てATLで置き換える。
  • Unicodeのサポート。JIS外の文字はEPWING外字を自動割当し、EBWin用の外字マップを自動作成する
  • XMLパーサーのサポート
  • バッファサイズの指定を不要とすること
  • マルメディアファイルをHONMONG,HONMONSに分割できるようにする
  • EBStudioのファイル形式との互換性の確保(各種EPWING化Toolkitの資産がそのまま使用できること)

-

現時点で達成できていること

  • VisualStudio 2010と、Mac OS XXcodeでのコンパイル
  • EBStudioのファイル形式との互換性の確保
  • 4GB超のHONMON
  • マルメディアファイルをHONMONG,HONMONSに分割可能
  • EBStudio用HTMLのutf-8のサポート
  • unicode外字の自動割り当て。外字パターンはefontオープンラボのhexファイルを使用する。もう外字を作字しなくてもいい!
  • CSVからの変換のサポート。EXCELのテキスト変換ウィザード風に、列の意味を指定する。
  • PDIC Unicodeからの直接変換。
  • libxml2パーサーによるHTML4.0/XHTML1.0からの変換(こちらのパーサーは従来のEBStudio用HTMLと互換性はない)

これからの予定

順調にいけば年内にWindows版をリリース。ただし根本的に書き直しているので、従来のライセンスとは別になるかもしれない。その場合価格面で配慮したい。
Mac OS X はまだ変換コアエンジンが動いただけでUIを書いていないので、来年になるとおもう。

Visual C++2010ランタイムをインストーラに含める方法

EBWin4はVisual Studio 2010 のC#C++で開発しているため、実行には.Net Framework 4.0とVisual C++ 2010 ランタイムが要る。
.Net Framework 4はWindows Vista以降は標準でWindowsに含まれているため、通常はインストールの必要はない。
だがVisual C++ 2010 ランタイムは Windows に含まれていないため、別途ユーザに「Visual C++ 2010 再配布可能パッケージ(x86)」をインストールしてもらわないといけない。やっかいなことに、Visual C++ はバージョンごとにランタイムが違い、さらに32bit(x86)版と64bit版(x64)が存在する。EBWin4のREADMEには対応するバージョンの再配布可能パッケージのリンクが書いてあるが、利用者にとって敷居が高いことは事実である。

これまでインストーラにはフリーの Inno Setup を使っていたが、Visual Studioインストーラを使用すれば、必須コンポーネントをインターネットから自動インストールするようにできるので、今回からインストーラを変更することにした。

Visual Studioインストーラの作り方

  • セットアップの追加→新しいプロジェクト→その他のプロジェクトの種類→セットアップと配置
  • ここでInstallShield LEとVisual Studio インストーラーが選択できるので、Visual Studio インストーラーを選択
  • セットアップウィザードで各プロジェクトの「プライマリ出力」「ローカライズされたリソース」「サテライトDLL」を全て選択。これでセットアッププロジェクトが生成される。
  • インストーラに含める追加ファイルやアイコンを追加してセットアッププロジェクトを完成させる。
  • セットアップのプロパティ→構成プロパティ→必須コンポーネント

ここでインストールする必須コンポーネントVisual Studio 2010ランタイムライブラリ(x86)を追加する。(なお、必須コンポーネントインストーラに含めるか、インターネットからダウンロードするかを選ぶことができる。)

  • セットアップをビルドすると、setup.exeと<プロジェクト名>.msiの2つのファイルができる。

インストーラの本体は.msiだが、setup.exeはコンピュータに必須コンポーネントがあるかどうかを調べ、インストールされていなければ次のような画面が出て、インターネットからダウンロードしてインストールまで行う。

これで起動トラブルは減ると思う。

ここで一つ問題があって、Visual Studio 2010にはSP1が提供されており、2010無印と 2010 SP1とでランタイムライブラリが異なる。再配布可能パッケージも2010無印と2010 SP1の2種類が存在する。
Download Microsoft Visual C++ 2010 再頒布可能パッケージ (x86) from Official Microsoft Download Center
Download Microsoft Visual C++ 2010 SP1 再頒布可能パッケージ (x86) from Official Microsoft Download Center
ユーザのコンピューターにVisual C++ 2010 SP1のランタイムが先に入っていた場合、 「より新しいバージョンのMicrosoft Visual C++ Redistributableがコンピューター上で検出されました」 と表示されてインストールが止まってしまう。
この場合は.msiを実行していただければ、EBWin4の本体だけインストールされる。

Visual Studio 2015 Installer Projects

Visual Studioインストーラはその後、Visual Studio 2012 で削除されてInstallShield LEだけになったが、 Visual Studio 2013から復活した。ただしVisual Studio Marketplaceから別途インストールする必要がある。現在のVisual Studio Community 2015でも使用できる。

Microsoft Visual Studio 2013 Installer Projects の設定方法について - マコーの日記
Microsoft Visual Studio 2015 Installer Projects をインストールする (Visual Studioの使い方 Tips)

補足:Inno setup でVisual C++ ランタイムを自動インストールする方法

なお、Inno setup でもスクリプトを書けば、Visual C++ Redistribuableのインストール有無をチェックして自動インストールすることができる。
inno setup - How to make vcredist_x86 reinstall only if not yet installed? - Stack Overflow
ただし、vcredist_x86.exeはあらかじめダウンロードしてパッケージに含めなければならない。

xdoc2txt の64bit版

ユーザからの要望があって、xdoc2txtおよびdllの64bit版を作成したので、忘備録を書いておく。

x64コンパイラおよびツールのインストール

Visual Studio 2010を標準でインストールすると、64ビット用コンパイラはインストールされないので、インストール時のオプションで「x64コンパイラおよびツール」を指定してインストールする。

既存のプロジェクトの64bit化

32bit版の既存のプロジェクトを64bit化する場合の手順。

  • 32bit版の既存のプロジェクトを読み込んだ後、メニュー→ビルド(B)→構成マネージャー(O)を起動
  • アクティブソリュージョン構成(C)→新規作成...を選び、64bit用の新しいソリュージョン構成を作成する。名称は例えば"Release x64"とする。この時設定のコピー元に(32bitの)既存のソリュージョンを選択すると、ソリュージョンがコピーされる。
  • アクティブソリュージョンプラットフォーム(P)→新規作成

新しいプラットフォームを入力または選択してください(P) : x64
設定のコピー元 : Win32

  • 構成マネージャーで、プラットフォームをWin32からx64に変更する。基本的にはこれだけで64bit版になる。
  • プロパティを開き、ライブラリアン→全般→対象コンピュータが MachineX64(/MACHINE:X64)になっていることを確認する(なっていなければ変更する)。

EXE、LIB、DLLも全て同様の手順で64bit化される。
注意点は、32bitのDLLと64bitのDLLを混ぜて使うことはできないので、関連するDLLやライブラリは全て64bitで揃える必要がある。xdoc2txtは内部でzlib1.2.3を使用しているので、zlib1.2.3もソースから64bitで再コンパイルした。
またVisual C++の64bit版を使用してコンパイルしたモジュールを実行する場合、Visual C++ 再頒布可能パッケージもx64用をインストールする必要がある。

32bit版 Download Microsoft Visual C++ 2010 再頒布可能パッケージ (x86) from Official Microsoft Download Center
64bit版 Download Microsoft Visual C++ 2010 再頒布可能パッケージ (x64) from Official Microsoft Download Center

一般的な注意

Visual C++ の64bit版の一般的な注意は、

  • 64 ビット Windows OS上ではポインタは64bitだが、int と long は相変わらず32bit ( 64bit整数は__int64 または long long ! )。したがってポインタをintやlongに代入してはいけない。
  • size_t、time_t、 ptrdiff_t は、64 ビット Windows OS上では 64 ビット。したがってtime_tをlongに代入してはいけない。

C# で64bit版のdllを使用する場合の注意点

C#のプロジェクトのプラットフォームターゲットはデフォルトではx86になっているため、デフォルトのままだと32ビット版のdllしか読み込まない。C#から64bitのdllを呼び出す場合は、プラットフォームターゲットをAnyCPUに変える。

読書尚友 Text-to-speech対応

家電量販店で現在発売中のAndroidスマホ最新機種を調べたところ、どうやら現行の機種は全て標準で日本語読み上げエンジンに対応しているようだ。
(設定→「言語と入力」→「テキスト読み上げの出力」→「優先するエンジン」→「Googleテキスト読み上げエンジン」→「言語」 に日本語があり、音声データがインストール済みになっている。)
そこで安心して読書尚友もText-to-speech対応することにした。

  • 青空文庫形式のルビ《》に対応。
  • 。、の句読点の単位で読み上げを行い、ポーズボタンで中断/再開 ができる。現在読み上げ中の位置は下線で表示する。
  • 頁をまたがると自動的に頁送りを行う。
  • CJKなら日本語読み上げエンジン、英語なら英語のエンジンを使う。青空文庫の「アーサー王物語」のように英語と日本語が混じる文章でもそれぞれのエンジンで読み上げを行う。

機能的にはほぼこれでいいと思うが、肝心の日本語読み上げエンジンの抑揚のない機械音が不自然で、とても長く聞いていられない。日本語読み上げエンジンが進化しないと、実用性は今ひとつかもしれない。

その他仕掛り中のプロジェクト

  1. xdoc2txt の64bit 版。(→できました。)
  2. EBStudioのUnicodeベースでの再開発。EBStudioのソースはVisual Studio6.0時代のもので、今日のVisual Studioではコンパイル自体が不可能。こちらは大きな作業になるので、1年以上かかりそうだし、途中で投げ出すかもしれない。

EBPocket for Android pro Text-to-speech サポート

Text-to-speechはテキストの読み上げ機能で、Androidでは1.6から早くもAPIが提供されていた。だがAndroidの標準のテキスト読み上げエンジンは、英語、イタリア語、スペイン語、ドイツ語、フランス語しかサポートしておらず、日本語の読み上げを行うためにはサードパーティの日本語読み上げエンジンを導入する必要があった。このため、EBPocketも読書尚友も、Text-to-speechをサポートしてこなかった。
ところが、昨年購入したASUS Zenfone 3 laserは、標準で日本語読み上げエンジンを搭載していることがわかり、Text-to-speechをサポートする意欲が湧いてきた。
とりあえずEBPocket for Android proで対応してみた。

  • テキストの範囲を選択してコンテキストメニューでTTSを選択すると、範囲指定したテキストを読み上げる
  • 範囲指定せずにメニューからTTSを選択すると、本文全てを読み上げる
  • 画面にタッチすると読み上げを止める
  • 文字列中にCJK統合漢字、ひらがな、カタカナを含んでいた場合は、日本語読み上げエンジンを使う。含まれていない場合は英語の読み上げエンジンを使う。
  • CJK統合漢字に使用する読み上げエンジンは、日本語と中国語から選択できる。

日本語読み上げエンジンが使えない機種でも、少なくとも英語の読み上げはできるので、英和や和英で発音を確かめたい場合には、それなりに有用だと思う。

次は当然、読書尚友でサポートしなければいけないと思っている。

EBPocket for iOS のApp Extension対応について

iOSのアプリケーション間でテキストを受け渡したい場合、以前はURL Schemeクリップボード、OpenInぐらいしか方法がなかった。
iOS8からは App Extension という新たなアプリケーション連携の方法が提供されており、Androidのintentのように、任意のアプリケーション間でテキストやイメージが受け渡せるようになった。
iOS8の登場からもう3年も経っているが、遅ればせながらEBPocket Pro for iOSにもApp Extensionを実装してみた。
App Extensionには種類があるが、EBPocketではShare Extensionに対応した。

共有の画面例

Safariやメモで文字列を範囲選択し、[共有...]を実行すると、App Extensionに対応したアプリの一覧が表示される。


ここでEBPocketを選択し、Postを押すとEBPoketが起動して受け渡された文字列で検索を実行する。


左上の戻るボタンを押すと呼び出し元のアプリに戻る。
スプリットビューが使用できるiPadでは、SafariとEBPocketを両方開いたままにして、Safariで文字選択した文字列でEBPocketで検索できる。

実装の方法

App Extensionのプログラミングについては、Appleから日本語訳が出ている.
日本語ドキュメント - Apple Developer
App Extensionは単独で配布することはできず、必ず収容アプリと同時に配布しないといけない。今回はEBPocketが収容アプリということになる。
App Exensionと収容アプリとは別のバイナリであり、開発言語が異なっていてもいい。今回は勉強のため、Swift3で書くことにした。
ただし、実行できるiOSのバージョンの条件がiOS7.0以上になったが、現在では困る人はほとんどいないと思う。

Xcodeで 既存のアプリにApp Extensionを追加するのは簡単で、
[File]->[New]->[Target...]を押してApp Extensionの種類からShare Extensionを選択し、Product Nameを入力する。
収容アプリの Bundle Identifire にProduct Nameを連結したものが、App ExtensionのBundle Identifireになる。

収容アプリのBundle Identifire : 
        info.ebstudio.EBPocketPro
App ExtensionのBundle Identifire : 
        info.ebstudio.EBPocketPro.shareExtension

ここで次の3つのファイルが生成されるので、これを雛形としてコードを追加していく。

  • ShareViewController.swift
  • Maininterface.storyboard
  • info.plist

info.plistでは:

  1. Bundle display name にextensionの表示名を設定(この場合EBPocket)
  2. 共有でテキストのみ選択できるようにNSExtensionActivationRuleを修正する
    NSExtension
        NSExtensionAttributes
            NSExtensionActivationRule
                NSExtensionActivationSupportsText	Boolean	YES

収容アプリと同じアイコンを使用するには、Targetに収容アプリの .xcassets のアイコン名を設定する:

Build Phases
    Copy Bundle Resources
        + 収容アプリの .xcassets   を追加する
Build Settings
    Asset Catalog App Icon Set Name     ->  AppIcon (.xcassetsのアイコン名)

ここで一つ困ったことは、Share Extension から収容アプリを起動する方法が URL Scheme しかないのだが、公式の方法である openURL:completionHandler: を実行できるExtensionの種類がToday Extensionだけという問題である。
これは世界中でみんな困っていて、ネットで検索するとさまざまな回避方法が出ている。
最終的に、iOS10でも実行できるSwift3の最終的なソースは次のようになった。

//
//  ShareViewController.swift
//  shareExtension
//
//  Created by hishida on 2017/06/14.
//
//

import UIKit
import Social

class ShareViewController: SLComposeServiceViewController {
    
    override func isContentValid() -> Bool {
        // Do validation of contentText and/or NSExtensionContext attachments here

        let canPost: Bool = self.contentText.characters.count > 0
        if canPost {
            return true
        }
        
        return false
    }

    override func didSelectPost() {
        // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
        
        let inputItem: NSExtensionItem = self.extensionContext?.inputItems[0] as! NSExtensionItem
        let itemProvider = inputItem.attachments![0] as! NSItemProvider
        
        if (itemProvider.hasItemConformingToTypeIdentifier("public.plain-text")) {
            itemProvider.loadItem(forTypeIdentifier: "public.plain-text", options: nil, completionHandler: {
                (item, error) in
                
                let URLSCHEME:String = "ebpocket://search?text="

                let encodedString:String = self.contentText.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!
                
                let myUrlStr:String = URLSCHEME + encodedString

                let url = NSURL( string:myUrlStr )
                let context = NSExtensionContext()
                context.open(url! as URL, completionHandler: nil)
                
                var responder = self as UIResponder?
                
                while (responder != nil){
                    if responder?.responds(to: Selector("openURL:")) == true{
                        responder?.perform(Selector("openURL:"), with: url)
                    }
                    responder = responder!.next
                }
                
            })
        }

        // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
        self.extensionContext!.completeRequest(returningItems: , completionHandler: nil)
    }

    override func configurationItems() -> [Any]! {
        // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
        return 
    }

}


開発作業以上に手間取ったのが、App Storeに提出するための証明書の問題だった。
わかってみるとなんでもないが、App Extensionと本体の収容アプリは別のバイナリのため、Application Identifier、Provisioning profileは全て収容アプリとは別にする必要がある。

  1. iOS Dev Centerで Extension用の Application IdentifierとProvisioning profileを作成する
  2. 収容アプリのEntitlements.plistとは別に、App Extension用のEntitlements.plistを作成し、ここでApp ExtensionのApplication Identifierを定義する

ちなみApplication Identifierとは、開発者を表す10桁のprefix + Bundle Identifierのこと:

 XXXXXXXXXX.info.ebstudio.EBPocketPro.shareExtension 

あと、収容アプリと App Extensionのバージョン番号が食い違っていると提出時にワーニングが出るが、これは無視していいらしい。