hishidaの開発blog

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

読書尚友で段組に対応

kindle本やPDFを読むのにタブレットがほしくなって、ALLDOCUBE iPlay40 という10インチタブレットを買ってしまった。iPadよりも安いことと、テスト用にAndroid 10 の実機が必要という理由からだ。
なぜかイヤホンジャックがないとか、Amazon Primeの動画がSD画質になってしまう(Widevine のレベルがL3のため)という欠点はあるが、読書端末としてはほとんど不満はない。
しばらく使っているうちに、横向き画面のときに、読書尚友で段組表示ができないことが不満に思えてきた。結構苦労したが、なんとか実現できた。

f:id:hishida:20210330174723p:plain

 

f:id:hishida:20210330174738p:plain

 

f:id:hishida:20210330174752p:plain

 

今のところ横書きで左右二段組だけ対応している。縦書きで上下二段も実現の手間はそれほど変わらないが、ニーズがないような気がしたので、今回は見送った。

 

読書尚友に書き出しと著者画像を追加する

この一ヶ月ほど、読書尚友の改良を重点的に行っている。最近の改良点は:

  • 児童書の分野別検索(NDC)
  • 書籍一覧に書き出しを表示
  • 作家別リストに著者画像を表示(Wikipediaより)
  • 次・前の検索語のボタン
  • 本文に時刻表示

今回の改良の中で、特筆すべきなのは、書き出しと著者画像だと思う。どちらも、PythonでWebスクレイピングを行って情報を取得している。Pythonは初めて使ってみたが、使いやすい。結構古くからある言語なので、EBシリーズ関連のスクリプトRubyではなくPythonを採用しておけばよかったかもしれない。 

書き出しの表示

書店で書籍を選ぶとき、目次と書き出しを見て、まず読むに値するかどうかを決めると思う。書籍一覧に書き出しまで表示すると情報過多な気もするが、意外に便利だ。もちろん、設定で書き出しをオフにすることもできる。

書き出しの例

f:id:hishida:20210321154857p:plain

書き出しは、青空文庫の図書カードのHTMLのMETA要素に含まれている。例えば「坊ちゃん」の図書カードのヘッダは次のようになっている。ここから、 property="og:description"の要素を取り出す。

<html lang="ja" xmlns:og="http://ogp.me/ns#">
<head>
<meta charset="utf-8">
<meta property="og:type" content="book">
<meta property="og:url" content="https://www.aozora.gr.jp/cards/000148/card752.html">
<meta property="og:image" content="https://www.aozora.gr.jp/images/top_logo_300x300.png">
<meta property="og:image:type" content="image/png">
<meta property="og:title" content="坊っちゃん (夏目 漱石)">
<meta property="og:description" content="一 親譲(おやゆず)りの無鉄砲(むてっぽう)で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰(こし)を抜(ぬ)かした事がある。なぜそんな無闇(むやみ)をしたと聞く人…">
<!-- OGP: thanks to @cc4966 https://github.com/aozorahack/ogp -->
<meta name="twitter:card" content="summary" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title>図書カード:坊っちゃん</title>

Pythonで書き出しを取得するスクリプトは次のようになる。requestsとBeautifulSoupを使用する。 

import requests
from bs4 import BeautifulSoup

url = "https://www.aozora.gr.jp/cards/000148/card752.html"
card = requests.get( url )
soup = BeautifulSoup( card.content, "html.parser")
meta = soup.select_one('meta[property="og:description"]')
with open( "kakidasi.txt" ,"w") as file:
	file.write( meta['content'])

 
著者画像の表示

 著者画像もあると興味が湧いて、覗いてみたくなったりする。著者画像は、著作権の関係もあるので、全てWikipediaから取得している。

著者画像の例

f:id:hishida:20210321154915p:plain

著者画像を取得するスクリプトの例を示す。青空文庫夏目漱石の著者カード(person148.html")からWikipediaへのリンクを探し、続いてWikipwdiaから著者画像を取得している。

import requests
from bs4 import BeautifulSoup

# 著者ページを取得
person_url = "https://www.aozora.gr.jp/index_pages/person148.html"
card = requests.get( person_url )
soup0 = BeautifulSoup( card.content, "html.parser")

# wikipedia のリンクを取得
links = [url.get('href') for url in soup0.find_all('a')]
for link in links :
	if not link :
		continue

	if link.find("wikipedia.org/wiki/") >0 :
		# 著者画像を取得
		response = requests. get( link)
		soup = BeautifulSoup( response.content, "html.parser")

		meta = soup.select_one('meta[property="og:image"]')
		if not meta :
			continue
		image_url = meta['content']

		image_data = requests.get( image_url)
		with open( "148.jpg","wb") as file:
			file.write( image_data. content)
		break

 将来構想?

あと実現したいとすれば、端末を横にしたときの段組表示である。スマートフォンだとあまり必要性を感じなかったが、Windows10の検証用に中華タブレット(iPlay40)を買ったところ、横組みの時は段組が必須だと思うようになった。

読書尚友の改良3点

読書尚友に、最近3点の改良を行った。

ファイル一覧でPDFの表紙画像のサムネイルを表示

PdfRendererで先頭ページの画像を取得し、縮小表示している。

f:id:hishida:20210118081857j:plain

読み上げのバックグラウンド再生

text-to-speechでテキストの読み上げを行う場合、Activity上での実行のため、アプリがバックグラウンドに回ったり、端末がスリープすると再生が止まってしまうという問題があった。通勤時間中に青空文庫をイヤホンで聴きたいというニーズがあるはずだが、実際は無理だった。
今回、text-to-speechを Service を用いて別プロセスで再生するようにしたので、音楽アプリのように、端末がスリープしても再生できるようになった。テキストの読み上げ箇所を画面表示するためには、ServiceからActivityへコールバックする必要があるが、Messengerを使った通信で実現している。また、バックグラウンド再生中は通知領域に表示し、タップすることでフォアグラウンドへ復帰するようにしている。

ePubのWebViewによる表示

ePub文書を、これまでは独自ビューアで表示していたが、CSSを活用したレイアウト重視のePubの表示が貧弱で、期待されるレイアウト通りに表示されないという問題があった。そこで、WebViewを用いたブラウザによるePub表示モードを追加した。オーバースクロール(スクロール端で引っ張って離す)による前後の文書への移動、文書内検索、メニューに対応している。

独自ビューだとCSSによるレイアウトが無視されてしまうが。。。

f:id:hishida:20210118082012j:plain

独自ビューアによるePub表示

WebViewだと次のようにきれいにレイアウトされる。

f:id:hishida:20210321161204j:plain

WebViewによるePub表示



ただしテキスト中心の縦書き表示だと、従来の独自ビューアのほうが読みやすい場合もあるので、設定で切り替えができるようにしている。

これでほとんどのePub文書が、一応は読めるようにはなった。ただ個人的には、ePubリーダーとしては、Androidなら楽天koboのビューアかGoogle Play Books、iOSならApple Books がお薦めである。

 

Android Q の外部ストレージアクセスについて

2020年11月以降、Google Play で既存アプリをアップデートするには、targetSDKを29(Android Q)以上にする必要がある。
ところが一つ問題があって、Android Qからは、対象範囲別ストレージ(Scoped storage)が導入され、外部ストレージのアクセスが厳格化された。

developers-jp.googleblog.com


Android P までは、READ_EXTERNAL_STORAGEパーミッションがあれば、外部ストレージのどこでもアクセスができたが、対象範囲別ストレージのもとでは、アクセス可能な領域が限定される。

  1. getFilesDir()で取得できるアプリ専用の内部ストレージ
  2. getExternalFilesDir()で取得できるアプリ専用の外部ストレージ
  3. StorageAccessFrameworkによって選択したファイル
  4. MediaStore APIによるアクセス(画像、動画、音楽、ダウンロードファイル)

ただし、移行期間として、マニフェスト ファイルで requestLegacyExternalStorage の値を true に設定すると、対象範囲別ストレージがオプトアウト(無効化)され、Android Pまでと同様のフルアクセスが可能になる。
現在EBPocket for Androidと読書尚友では、とりあえずrequestLegacyExternalStorageをtrueに設定して、下位互換性を保っている (11月以降のアップデートで一時Android10での外部ストレージアクセスができなくなる問題が出てご迷惑をかけたが、現在は修正済み)。

だが、2021年11月からはGoogle Playに提出するにはtargetSDKを30以上にする必要があり、そうするとrequestLegacyExternalStorageが無効化されてしまう。つまり、対象範囲別ストレージに対応できないと、今後アプリのアップデートができなくなってしまう。

developer.android.com


EBPocketはSDカードに辞書を置いて運用するスタイルなので、対応が難しそうに思う。

実現可能な方法としては、アプリ固有の外部ストレージ領域に辞書を置いてもらう方法があるが、アプリを削除するとデータ領域も削除されるので、アンインストール・インストールすると辞書を再度コピーしなければならない。iOS版のEBPocketは現在でもそうだが、これまでのAndroid版のユーザには受け入れられないだろう。

2021年11月までに対応ができない場合は、アップデートの凍結が最善かもしれない。targetSDKを30以上にしない限り、Android11の端末でも、(requestLegacyExternalStorageによって)対象範囲別ストレージは適用されず、相変わらず自由なアクセスが可能だからだ。

 

Xcode 12 対応

Apple Silicon Macが登場し、今後はIntelからArmへの移行が思ったよりも早く進みそうである。主要なソフトは次々にユニバーサルアプリの対応を発表、あるいはリリースしている。

EBシリーズもユニバーサルアプリ化の準備として、まず開発環境をXcode12に移行することにした。

macOSアプリのユニバーサル化

基本的には、Xcode12でコンパイルするだけで、自動的にユニバーサルアプリになる。Architectures の選択が、デフォルトで次のようになっている。

Build settings → Architectures → Architectures → 

 Standard Architectures(Apple Silicon,Intel)

これで話は終わってしまうのだが、EBMacの場合、これまで対象OSの下限をmacOS X 10.5(Leopard)にしていた関係上、C++ Standard Library に、現在では非推奨の libstdc++ を使用していた。(Xcode9以降はlibstdc++は入っていないため、Xcode8のlibstdc++を無理矢理コピーして、Xcode11まで使い続けていた。)

libstdc++のarm用は提供されないので、ユニバーサルアプリにするためには libc++ に変更する必要がある。

変更箇所は:

Build Settings → Apple Clang-Language-C++ → C++ Standard Library    → libc++

これで晴れてユニバーサルアプリとしてコンパイルすることができるようになったが、対象OSは macOS X 10.9 (Mavericks)以降になってしまった。これは仕方が無い。

ユニバーサルアプリの確認方法

本当にユニバーサルアプリになったかどうかは、file コマンドで確認できる。

$ ls
EBMac.app
$ file EBMac.app/Contents/MacOS/EBMac
EBMac.app/Contents/MacOS/EBMac: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]
EBMac.app/Contents/MacOS/EBMac (for architecture x86_64): Mach-O 64-bit executable x86_64
EBMac.app/Contents/MacOS/EBMac (for architecture arm64): Mach-O 64-bit executable arm64

iOSアプリのXcode12 対応 

次は、EBPocket for iOSの移行である。基本的にそのままコンパイルできるのだが、リリースモジュールは作れるのに、デバッグモードでシミュレータを使用しようとするとエラーになってビルドができない。

色々調べた結果、Xcode11以前に作成したプロジェクトファイルにはVALID_ARCHSの定義があり、これが非推奨になったことが原因らしい。

Build settingsのUser-DefinedからVALID_ARCHSをバッサリ削除すると、デバッグモードでのビルドができるようになり、シミュレータが起動するようになった。

なお、Xcode12でビルドする場合、target OSの下限は9.0になる。

 Apple Silicon Macについて

これでユニバーサルアプリが作れるようになったわけだが、やはり実機で検証する必要がある。だが第一世代機を購入するのはなかなか勇気が要る。

現在の私の開発環境は、

の2台だが、Apple Silicon Mac を入れると3台になって、どうも多すぎる。MacBook Proを置き換えた場合、Apple Silicon MacではVM Wareで仮想環境が使えないのが痛い。

順当に考えると、Apple Silicon の Macbook Air あたりを買い足しするのがいいのだが、どうも時期尚早な気がする。

Apple Silicon MacVMWare が動くようになれば、一台に集約できるかもしれないが、もう少し様子見をして考えたい。

 

読書尚友に OpenSearch-1.1 を実装

拙作の青空文庫ビューアである「読書尚友」にはOPDSクライアントの機能があるが、あるきっかけがあって、OpenSearch対応を行った。

そのきっかけというのは、青空文庫でヘンリー・デイビッド・ソローの『森の生活』が追加されていることに気づいたことである。思い入れのある作品なので読もうと試みたが、翻訳が難解で、少しも頭に入ってこない。ふとオリジナルのテキストを見たくなって*1、読書尚友のOPDSライブラリでProject Gutenbergから探そうとしたが、検索機能がないと全く探せないことが分かった。OpenSearchの存在は以前から知っていたが、なんとなく億劫で対応を避けていたのだが、これがきっかけで調べてみる気になった。

まずOpenSearchの仕様はこちら:

opensearch/opensearch-1-1-draft-6.md at master · dewitt/opensearch · GitHub

翻訳はこちら:

Open Search 仕様書 1.1 ドラフト4版 - Walrus, Googling

OpenSearchは、利用する側にとっては特に難しくはない。Project Gutenbergを例にとって説明してみたい。

まず、Project GutenbergのOPDSのURLを実行すると、ルートカタログがRSSで返される。

https://m.gutenberg.org/ebooks.opds/

このなかに、OpenSearchのエントリがある。

<link rel="search" type="application/opensearchdescription+xml" title="Project Gutenberg Catalog Search" href="https://www.gutenberg.org/catalog/osd-books.xml"/>

 間接参照になっているので、href=のURLを実行すると、OpenSearchxmlが返却される。

<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<LongName>Project Gutenberg</LongName>
<ShortName>Gutenberg</ShortName>
<Description>Search the Project Gutenberg ebook catalog.</Description>
<Tags>free ebooks books public domain</Tags>
<Developer>Marcello Perathoner</Developer>
<Contact>webmaster@gutenberg.org</Contact>
<Url type="text/html" template="http://www.gutenberg.org/ebooks/search/?query={searchTerms}"/>
<Url type="application/atom+xml" template="http://m.gutenberg.org/ebooks/search.opds/?query={searchTerms}"/>
<Url type="application/x-suggestions+json" rel="suggestions" 
template="http://www.gutenberg.org/ebooks/suggest/?query={searchTerms}"/> <!-- <Url type="application/rss+xml" template="http://example.com/?q={searchTerms}&pw={startPage?}&format=rss"/> <Image height="64" width="64" type="image/png">http://example.com/websearch.png <Image height="16" width="16" type="image/vnd.microsoft.icon">http://example.com/websearch.ico --> <Query role="example" searchTerms="shakespeare hamlet"/> <Query role="example" searchTerms="doyle detective"/> <Query role="example" searchTerms="love stories"/> <Attribution>Search Data Copyright 1971-2012, Project Gutenberg, All Rights Reserved.</Attribution> <SyndicationRight>open</SyndicationRight> <Language>en-us</Language> <OutputEncoding>UTF-8</OutputEncoding> <InputEncoding>UTF-8</InputEncoding> </OpenSearchDescription >

 このなかで、

 <Url type="application/atom+xml" template="http://m.gutenberg.org/ebooks/search.opds/?query={searchTerms}"/>

 が検索のテンプレートである。{searchTerms}の部分を検索語に置き換えて実行すれば、検索結果のRSSが得られるので、OPDSと同様に表示すればいい。

Project Gutenbergから "walden" で検索する例:

http://m.gutenberg.org/ebooks/search.opds/?query=walden

連語を検索する場合はURLEndodingにする。”shakespeare hamlet”を検索する例:

 http://m.gutenberg.org/ebooks/search.opds/?query=shakespeare%20hamlet

 
サイトによっては、間接参照のtype="application/opensearchdescription+xml"ではなく、OPDSのカタログの中に直接参照のtype="application/atom+xml" でOpenSearchのテンプレートがある場合がある。

(例:ManyBooks.net)。

<link rel="search" title="Search Catalog" type="application/atom+xml" href="http://manybooks.net/opds/search.php?q={searchTerms}"/>

 

画面の検索イメージ:

f:id:hishida:20201125185720j:plain

f:id:hishida:20201125185737j:plain

f:id:hishida:20201125185746j:plain

*1:もちろん、『森の生活』の英語版を読み通すような英語力は全くない。冒頭だけでも比較してみたくなっただけだ。

Android 10 のクリップボードの仕様変更について

Android 10以降、セキュリティ強化のためにクリップボードの仕様に制限が加えられた。

developer.android.com

クリップボード データへの制限付きアクセス
デフォルト インプット メソッド エディタ(IME)のアプリまたは現在フォーカスのあるアプリでない限り、Android 10 以降ではクリップボード データにアクセスできません。

 EBPocket for Androidにはクリップボードの変更を検知して検索する「クリップボード検索」の機能があったが、Android10以降は機能しなくなった。

代替案として、Android10以降のクリップボード検索では、EBPocket をバックグラウンドからフォアグラウンドに回してフォーカスを得たタイミングでクリップボードのテキストで検索するようにした。

自分でアプリを切り替える手間が必要だが、Android10の仕様なので仕方がない。

またブラウザ等であればコンテキストメニューに EBPocket を追加しているので、文字列を選択してコンテキストメニューからEBPocketを選択して検索する方法もある。