Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android 15以上でPDFのテキスト検索を爆速開発!

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

Android 15以上でPDFのテキスト検索を爆速開発!

Avatar for tonionagauzzi

tonionagauzzi

July 24, 2025
Tweet

More Decks by tonionagauzzi

Other Decks in Programming

Transcript

  1. 検索対応前の処理 // 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
  2. 検索対応後の処理 // 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
  3. // 検索時に検出数を正確に知るため、全ページを一気に読み込む 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
  4. val density = LocalDensity.current val scrollIndex = targetMatchedIndex + 1

    with(density) { lazyListState.animateScrollToItem( index = scrollIndex, scrollOffset = -searchBarHeight.roundToPx() ) } scrollOffset と with(density) の組み合わせで、拡大率に合わせたスクロール先を 計算可能! Mobile勉強会 #21 ウォンテッドリー × チームラボ × Sansan 19