在安卓平臺(tái),PDFium 早已開源,第三方閱讀器demo破數(shù)千贊,然而盡管相關(guān)的API已經(jīng)包含在在SDK的頭文件中,這么多年了文本選擇基本處于零開發(fā)狀態(tài)。
我為什么要開啟這個(gè)系列,努力試著從源頭開始,用 PDFium 制作一款閱讀器?有人喜歡問這個(gè)做了有什么用,這個(gè)是唯一的嗎?
當(dāng)然不是唯一的,底層技術(shù)更不是我的。不過我認(rèn)為在維護(hù)者的推動(dòng)下,PDFium 越來越完善,功能越來越多,不真正拿來做些什么實(shí)在是可惜了。另一個(gè)重要原因則是,其他APP要么臃腫或者簡陋,要么用著磕手、滑動(dòng)卡頓、誤觸頻發(fā),而且大多還不免費(fèi)。( 更正,近年來倒是多了好多免費(fèi)的PDF閱讀器 )
目標(biāo)期望:
- 滑動(dòng)不卡。
- 文本選擇媲美Opera瀏覽器。
- 擁有超卓的文本操作體驗(yàn),查詞典、段落翻譯、文本分享等更加方便。
項(xiàng)目地址:https://github.com/KnIfER/PolymPic
一、處理超鏈接
熱身運(yùn)動(dòng):當(dāng)檢測(cè)到單擊( GestureDetector )時(shí),若點(diǎn)擊處存在超鏈接,則打印出超鏈接的對(duì)象。
頭文件:fpdf_doc.h
- 獲取點(diǎn)擊處的超鏈接
JNI_FUNC(jlong, PdfiumCore, nativeGetLinkAtCoord)(JNI_ARGS, jlong pagePtr, jdouble width, jdouble height, jdouble posX, jdouble posY){
double px, py;
FPDF_DeviceToPage((FPDF_PAGE)pagePtr, 0, 0, width, height, 0, posX, posY, &px, &py);
return (jlong)FPDFLink_GetLinkAtPoint((FPDF_PAGE)pagePtr, px, py);
}
需要將屏幕坐標(biāo)轉(zhuǎn)換為頁面坐標(biāo),然后再次在native層轉(zhuǎn)換為所謂的user space、page space。別問我那是啥我也不知道。不過在論壇提問后,有人替我指出了相關(guān)文檔所在,有時(shí)間去看看!
"User space" is defined in section 8.3.2.3 of the PDF 32000-1:2008 specification.
屏幕坐標(biāo):[event.getX(), event.getY()]
頁面坐標(biāo):先前提過將整本PDF當(dāng)作一張超級(jí)大圖,subsampling-scale-imageview 有一系列的 viewToSource 坐標(biāo)轉(zhuǎn)換方法。屏幕轉(zhuǎn)換得到 source 坐標(biāo)后,減去點(diǎn)擊頁面的左上角坐標(biāo),就是頁面坐標(biāo)。
原始頁面坐標(biāo)需用 FPDF_DeviceToPage 再次轉(zhuǎn)換,才能傳給FPDFLink_GetLinkAtPoint,獲取坐標(biāo)處的鏈接指針。
- 鏈接指針不為空時(shí),可以提取超鏈接對(duì)象。
JNI_FUNC(jstring, PdfiumCore, nativeGetLinkTarget)(JNI_ARGS, jlong docPtr, jlong linkPtr){
DocumentFile *doc = reinterpret_cast<DocumentFile*>(docPtr);
FPDF_LINK link = reinterpret_cast<FPDF_LINK>(linkPtr);
FPDF_DEST dest = FPDFLink_GetDest(doc->pdfDocument, link);
if (dest != NULL) {
int pageIdx = FPDFDest_GetDestPageIndex(doc->pdfDocument, dest);
char buffer[16]={0};
buffer[0]='@';
sprintf(buffer+1,"%d",pageIdx);
return env->NewStringUTF(buffer);
}
FPDF_ACTION action = FPDFLink_GetAction(link);
if (action == NULL) {
return NULL;
}
size_t bufferLen = FPDFAction_GetURIPath(doc->pdfDocument, action, NULL, 0);
if (bufferLen <= 0) {
return NULL;
}
std::string uri;
FPDFAction_GetURIPath(doc->pdfDocument, action, WriteInto(&uri, bufferLen), bufferLen);
return env->NewStringUTF(uri.c_str());
}
超鏈接對(duì)象統(tǒng)一返回字符串,可以是Uri地址,也可以是頁碼@頁碼。
二、在單擊處獲取一個(gè)單詞
熱身運(yùn)動(dòng)2:在單擊處獲取一個(gè)英文單詞或者漢語詞組,需要用到安卓的 BreakIterator。
頭文件:fpdf_text.h
首先實(shí)現(xiàn) nativeGetCharIndexAtCoord 方法,獲取單擊附近的文字索引,需進(jìn)行同樣的坐標(biāo)轉(zhuǎn)換。
JNI_FUNC(jint, PdfiumCore, nativeGetCharIndexAtCoord)(JNI_ARGS, jlong pagePtr, jdouble width, jdouble height, jlong textPtr, jdouble posX, jdouble posY, jdouble tolX, jdouble tolY){
double px, py;
FPDF_DeviceToPage((FPDF_PAGE)pagePtr, 0, 0, width, height, 0, posX, posY, &px, &py);
return FPDFText_GetCharIndexAtPos((FPDF_TEXTPAGE)textPtr, px, py, tolX, tolY);
}
若返回的文字index大于等于零,則此 index 指向該頁面全部文本當(dāng)中的一個(gè)字符。全部文本用 FPDFText_GetText 獲?。▽?shí)現(xiàn) nativeGetText):
JNI_FUNC(jstring, PdfiumCore, nativeGetText)(JNI_ARGS, jlong textPtr) {
int len = FPDFText_CountChars((FPDF_TEXTPAGE)textPtr);
//unsigned short* buffer = malloc(len*sizeof(unsigned short));
unsigned short* buffer = new unsigned short[len+1];
FPDFText_GetText((FPDF_TEXTPAGE)textPtr, 0, len, buffer);
jstring ret = env->NewString(buffer, len);
delete []buffer;
return ret;
}
接下來就可以用 BreakIterator 分詞了:
... @@@ public void prepareText()
allText = pdfiumCore.nativeGetText(tid);
if(pageBreakIterator==null) {
pageBreakIterator = new BreakIteratorHelper();
}
pageBreakIterator.setText(allText);
... @@@ public String getWordAtPos(float posX, float posY)
int charIdx = pdfiumCore.nativeGetCharIndexAtCoord(pid.get(), size.getWidth(), size.getHeight(), tid
, posX, posY, 10.0, 10.0);
String ret=null;
if(charIdx>=0) {
int ed=pageBreakIterator.following(charIdx);
int st=pageBreakIterator.previous();
獲得的單詞就是 allText.substring(st, ed)
}
...
三、實(shí)現(xiàn)文本選擇
1. 繪制選框

與繪制PDF本身差不多,不過 bitmap 換成 rect 而已。用到的API依次是FPDFText_CountRects、FPDFText_GetRect。
直接將選框覆蓋繪制在前。若要繪制在后面的背景上,就需要三層透明視圖了,那么加載鋪塊和縮略圖的時(shí)候就要用透明色清空 bitmap,頁面的白色背景等也需要另外繪制(Google PDF Viewer應(yīng)該就是這樣,還給背景加了陰影)。這些較為復(fù)雜,到時(shí)候再說。
有個(gè)問題可能需要解決:同一行的選框,部分沒有合并。

都是小事兒,暫時(shí)不在這上面花時(shí)間。
2. 繪制控點(diǎn)
之前做過類似的事情,將普通 TextView 自帶的文本選擇功能禁用了,然后用API自己做出一個(gè)來,包括單擊選詞,長按托選,放大鏡等等。所以相關(guān)的內(nèi)容還是熟悉的。
繪制 Selection Handle 可以用 AppCompat 支持庫中的圖標(biāo)資源:
handleLeft=getResources().getDrawable(R.drawable.abc_text_select_handle_left_mtrl_dark);
handleRight=getResources().getDrawable(R.drawable.abc_text_select_handle_right_mtrl_dark);

控點(diǎn)的觸控操作也很簡單,在 Action_Down 中檢測(cè)落點(diǎn)是否在其中一個(gè) handle 內(nèi)。若是,則在 Action_Move 中一邊移動(dòng)該 handle,一邊檢測(cè)新的字符索引,作為文本選擇的新邊界。
由于PDF的復(fù)雜性,頁面上的字符索引可能間雜排列,比如頭一段開頭是100,下一段開頭50,再下一段150。這就造成先前簡單的選擇系統(tǒng)“失效”了:

沒什么解決方案,API 就這么點(diǎn)。而且,靜讀天下、Google PDF 查看器都是這樣的,唯有 ezpdfreader 沒有這個(gè)問題。
