Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Android 15以上でPDFのテキスト検索を爆速開発!
Search
tonionagauzzi
July 24, 2025
Programming
390
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Android 15以上でPDFのテキスト検索を爆速開発!
tonionagauzzi
July 24, 2025
More Decks by tonionagauzzi
See All by tonionagauzzi
Androidエンジニアが大規模プロダクトのWebフロントエンドとバックエンドに越境して感じたギャップと習得したマインド
tonionagauzzi
0
290
Kotlin2.3明示的バッキングフィールド
tonionagauzzi
1
370
【Android】テキスト選択色の問題修正で心がけたこと
tonionagauzzi
0
260
Googleの新しいコーディングAIエージェントJulesを使ってみた
tonionagauzzi
0
840
Compose におけるパスワード自動入力とパスワード保存
tonionagauzzi
0
520
Androidテスト基礎講義
tonionagauzzi
0
400
Android Composeでの自動入力(作成:GPT-4o)
tonionagauzzi
0
170
Jetpack Composeで自動入力(Autofill)を実装しよう(作成:claude-3.7-sonnet)
tonionagauzzi
0
170
Jetpack Composeにおける自動入力の実装と注意点(作成者:Gemini 2.5 Pro Exp 03-25)
tonionagauzzi
0
180
Other Decks in Programming
See All in Programming
型も通る、synthも通る、それでも危ない 〜AIのCDKの権限とコストを機械で検証する〜 / It Passes Type Checks, It Passes Synth Checks, but It’s Still Risky — Automatically Verifying Permissions and Costs in AI’s CDK —
seike460
PRO
1
560
プロポーザルを書いてもらう
pvcresin
0
550
Foundation Models frameworkで画像分析
ryodeveloper
1
620
AWS DevOps AgentのAzure接続機能を検証して見えた活用法/Use Cases Verified for the AWS DevOps Agent's Azure Connectivity Feature
masakiokuda
1
240
そこに3びきプロダクトがいるじゃろう——生成AI時代における“価値が届かない理由”の構造
kosuket
0
510
Flow は今どうなっているか
mizdra
PRO
0
600
Jindong: Introducing Declarative Haptics in Compose Multiplatform
l2hyunwoo
0
120
SlackアプリとLambdaの 連携を構築した話
pawn_4_s
1
120
Japan Community Day at Kubecon + CloudNativeCon Japan 2026: Learning Container Privilege Control by Building My Own Low-Level Container Runtime
ternbusty
1
160
Go 1.27 における memory allocation の高速化
andpad
0
220
わからない話を追いかけたら、プログラミング言語を作る側にいた
ydah
3
520
複数の Claude Code が"放置"されてしまう問題をCLI ダッシュボードを自作して解決した話
sumihiro3
1
700
Featured
See All Featured
Git: the NoSQL Database
bkeepers
PRO
432
67k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
2.1k
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
2.4k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.5k
Visualization
eitanlees
152
17k
Large-scale JavaScript Application Architecture
addyosmani
515
110k
Tips & Tricks on How to Get Your First Job In Tech
honzajavorek
1
700
Making the Leap to Tech Lead
cromwellryan
135
10k
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
240
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
Transcript
Android 15以上でPDFのテキスト検索を爆速開発! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 1
登壇者情報 トニオ(@tonionagauzzi) Androidエンジニア 趣味はブログ https://dribit.hatenablog.com/ Mobile勉強会 #21 ウォンテッドリー × チームラボ
× Sansan 2
アジェンダ 1. PdfRendererの紹介 2. PDFテキスト検索の実装 3. ハマったポイントと解決策 全ページを検索できない問題 スクロール位置の問題 折り返し文字列の検索問題
4. まとめ Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 3
1. PdfRendererの紹介 Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 4
AndroidでPDFを扱うなら"PdfRenderer" Android 5.0以上で使用可能 Android 15では大幅に強化 注釈、パスワード付きPDFへの対応、テキスト検索など 詳細はWebで! https://developer.android.com/about/versions/15/features?hl=ja#pdf Mobile勉強会 #21
ウォンテッドリー × チームラボ × Sansan 5
2. PDFテキスト検索の実装 Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 6
検索対応前の処理 // 1ページを開いて画像を返す private suspend fun PdfRenderer.open(pageNumber: Int): ImageBitmap {
return withContext(Dispatchers.IO) { openPage(pageNumber).use { page -> val bitmap = createBitmap(page.width, page.height) page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) bitmap.asImageBitmap().apply { prepareToDraw() } } } } Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 7
検索対応後の処理 // 1ページを開いて画像を返す。テキストハイライト付き private suspend fun PdfRenderer.open(pageNumber: Int, searchText: String
= ""): OpenedPage { return withContext(Dispatchers.IO) { openPage(pageNumber).use { page -> val scale = 2f val bitmap = createBitmap(page.width * scale.toInt(), page.height * scale.toInt()) val matrix = Matrix().apply { postScale(scale, scale) } page.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) val matchedList = if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && searchText.isNotEmpty() ) { val tmpMatchedList = page.searchText(searchText) tmpMatchedList.map { matched -> matched.bounds.map { bound -> bound.applyScale(scale) val canvas = Canvas(bitmap.asImageBitmap()) val paint = Paint().apply { color = Color(255, 255, 0, 127) } canvas.drawRect(bound.left, bound.top, bound.right, bound.bottom, paint) } } tmpMatchedList } else { emptyList() } OpenedPage( imageBitmap = bitmap.asImageBitmap().apply { prepareToDraw() }, matchedCount = matchedList.size ) } } } private data class OpenedPage( val imageBitmap: ImageBitmap, val matchedCount: Int, ) Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 8
ざっくり言うと、 open を LazyColumn で1ページずつ描画してます 検索バーの文字列が searchText に入ります 最後に renderer.close()
を忘れないように! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 9
拡大してもずれない! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 10
ハマったポイント Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 11
1. 最初に全ページを検索できない! 最初に"1/8"と表示されてても、スクロールしていくと"8/16"になる… どうやら最初のほうのページしか検索できてないようだ。 Mobile勉強会 #21 ウォンテッドリー × チームラボ ×
Sansan 12
原因:LazyColumnで遅延読み込みしているから Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 13
解決策:検索バー表示時にPDFを全部読み込む! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 14
// 検索時に検出数を正確に知るため、全ページを一気に読み込む LaunchedEffect(searchText) { if (searchText.isNotEmpty()) { isSearching = true
matchedPages = emptyList() val allMatchedPages = (0 until pdf.pageCount).map { pageNumber -> async { val openedPage = mutex.withLock(pdf to pageNumber) { pdf.open(pageNumber, searchText) } if (openedPage.matchedCount > 0) { MatchedPage( pageNumber = pageNumber, matchedCount = openedPage.matchedCount ) } else { null } } }.mapNotNull { it.await() } matchedPages = allMatchedPages isSearching = false } else { matchedPages = emptyList() } } Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 15
最初に全ページ開くので、メモリ不足の不安はある… より良いロジックは考えていきたい Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 16
2. 検索ヒット箇所に移動する際、移動先が正しくないことがある Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 17
原因:animateScrollToItemをうまく使えていなかった Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 18
val density = LocalDensity.current val scrollIndex = targetMatchedIndex + 1
with(density) { lazyListState.animateScrollToItem( index = scrollIndex, scrollOffset = -searchBarHeight.roundToPx() ) } scrollOffset と with(density) の組み合わせで、拡大率に合わせたスクロール先を 計算可能! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 19
3. 折り返している文字列がpdf検索でヒットしない Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 20
Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 21
これは許容しました… 文字列検索は認識している矩形範囲で行われるので、そこから切れてしまう文字列は 検索対象にならない。 これはPDFの構造に依存するので、直接的な解決方法はない(自分調べ) 。 Mobile勉強会 #21 ウォンテッドリー × チームラボ
× Sansan 22
まとめ Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 23
Android 15以上ではPDFテキスト検索を爆速開発できる! ハマりどころはいくつかあるのとクローズ忘れに注意しよう! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan
24
ご清聴ありがとうございました! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 25