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

PDF Viewer作成の今までとこれから

Hunachi
September 10, 2024

PDF Viewer作成の今までとこれから

Hunachi

September 10, 2024
Tweet

More Decks by Hunachi

Other Decks in Technology

Transcript

  1. PDF Viewer作成の今までとこれから ~ Android 15で進化したPdfRenderer~ The Past and Future of

    PDF Viewer Development ~ The Enhanced PdfRenderer in Android 15 ~ DroidKaigi 2024 / Hunachi 1 https://speakerdeck.com/hunachi/droidkaigi-2024
  2. 目次(Agenda) • PDFをAndroidで表示する方法 A way to preview a PDF file

    on Android • PDF関係のライブラリを使わず、簡単なPDF ViewerをComposeで作る Build a simple PDF Viewer in Compose without using any PDF library • PDF Viewerの最適化とUX改善 PDF Viewer Optimization & UX Enhancement • Android 15からのPDF Renderer PDF Renderer since Android 15 • androidx.pdf • まとめ Summary 3
  3. Depend on external implementations Launch other Apps Browser / Google

    Drive … PDF.js Use WebView! Only Android Platform API & Jetpack Library Android Platform API Using PDFRenderer Implement UI using Compose or AndroidView androidx.pdf Third-party libraries or SDKs Library SDK DImuthuUpe/AndroidPdfViewer 8k⭐↑ afreakyelf/Pdf-Viewer Compose🆗 A lot of SDKs … 5
  4. Depend on external implementations Launch other Apps Browser / Google

    Drive … PDF.js Use WebView! Only Android Platform API & Jetpack Library Android Platform API Using PDFRenderer Implement UI using Compose or AndroidView androidx.pdf Third-party libraries or SDKs Library SDK DImuthuUpe/AndroidPdfViewer 8k⭐↑ afreakyelf/Pdf-Viewer Compose🆗 A lot of SDKs … Focus on 6
  5. 簡単なPDF Viewer(Simple PDF Viewer) PDF image Display PDF Scroll &

    switch pages PDF image Support Gestures PDF PDF P PDF Support configuration changes PDFのリスト表示 ジェスチャー 画面回転対応 この要件のViewer を実装します! 8
  6. @Composable fun PdfImage( bitmap: Bitmap ) { val image =

    bitmap.asImageBitmap() Image(bitmap = image, >>.) } PDFを表示する(Render PDF) @Composable fun PdfImage(){ Image(pdfUri, >>.) } CAN’T ❌ PDF(Data) Bitmap PDF path CAN ✅ val painter = >/ Coilを使うとか… rememberAsyncImagePainter(pdfUri) PDFRenderer 9
  7. PdfRendererとは?(What is PdfRenderer ?) • Android 5.0-(API level 21-) •

    PDFの情報を提供(Provide PDF information) • PDFをBitmap化する時に使うクラス(Class for rendering PDF) 使う時の注意点( Cautions) • 使わなくなったらcloseする(Close PdfRenderer after the use) • 信用できないファイルを扱う時は権限最小の別プロセスで扱う (Run PdfRenderer on another process with minimal permissions if the file source is not trustable) 10
  8. PdfRendererの生成方法(Generate PdfRenderer) Uri → PdfRenderer fun generatePdfRenderer( context: Context, uri:

    Uri ): PdfRenderer { val fd = context .contentResolver .openFileDescriptor(uri, "r") -: throw Exception("Uri is invalid") return PdfRenderer(fd) } PDF(Data) PDF path 11
  9. 色々なデータに対応する(Support various sources) sealed class PDFResource { class PdfUri(val uri:

    Uri) : PDFResource() class PdfUrl(val url: URL) : PDFResource() } suspend fun generatePdfRenderer( context: Context, pdfResource: PDFResource ): PdfRenderer { return when (pdfResource) { is PDFResource.PdfUri >> generatePdfRenderer(context, pdfResource.uri) is PDFResource.PdfUrl >> generatePdfRenderer(context, pdfResource.url) } } 12
  10. suspend fun generateBitmap(pdfRenderer: PdfRenderer, position: Int): Bitmap = withContext(Dispatchers.IO) {

    val page = pdfRenderer.openPage(position) val bitmap = Bitmap .createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888) page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) page.close() bitmap } Bitmapの生成方法(How to generate Bitmap) 完全版じゃないのでコピペ使用非推奨 Not recommended to use this incomplete code. PDF(Data) Bitmap 🫛 信頼できないファイルを開くかもしれない場合は別プロセスで実行 If there's a possibility of opening untrusted files, open it on another process 13
  11. suspend fun generateBitmap(pdfRenderer: PdfRenderer, position: Int): Bitmap = withContext(Dispatchers.IO) {

    val page = pdfRenderer.openPage(position) val bitmap = Bitmap .createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888) page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) page.close() bitmap } ディスプレイ向けの Modeを設定 ARGB_8888 にする必要がある ③使い終わった らCloseする ①openPageで Pageインスタンスを 取得 ②PageのPDFを Bitmap化して bitmapに書き込む 14
  12. 1ページのPDFを表示する (Render a page of a PDF file) @Composable fun

    PdfImage(pdfResource: PDFResource, position: Int) { val context = LocalContext.current val bitmap by remember(pdfResource) { flow { emit(generatePdfRenderer(context = context, pdfResource))} .map { generateBitmap(it, position) } }.collectAsStateWithLifecycle(initialValue = null) bitmap>.let { val imageBitmap = it.asImageBitmap() Image( modifier = Modifier.fillMaxSize(), bitmap = imageBitmap, contentDescription = null ) } } 先ほどまでの説明で出 てきた関数を使用し Bitmapを生成 15
  13. 複数のページを持つPDFを表示する (Display multiple pages of a PDF file) @Composable fun

    PdfList(pdfResource: PDFResource) { -/ … LazyRow(>>.) { items(it.pageCount) { index -> Box( Modifier .fillMaxHeight() .width(200.dp) ) { PdfImage(renderer, index) } } } } @Composable fun PdfImage( renderer: PdfRenderer, position: Int ) { var bitmap: Bitmap? by remember { mutableStateOf(null) } LaunchedEffect(renderer) { bitmap = generateBitmap( pdfRenderer, position ) } >/ 前のページと同様の実装 } 一般的なリストと同様 LazyListで表示 16 16
  14. @Composable fun PdfList(pdfResource: PDFResource) { val context = LocalContext.current var

    renderer: PdfRenderer? by remember { mutableStateOf(null) } LaunchedEffect(pdfResource) { renderer = generatePdfRenderer(context, pdfResource) } DisposableEffect(pdfResource) { onDispose { renderer>.close() } } >/ … } rendererをonDisposeで closeする☝ 複数のページを持つPDFを表示する (Display multiple pages of a PDF file) 17
  15. java.lang.IllegalStateException: Current page not closed! 🫛 Android 15(API level 35)では起きない!

    複数のページを持つPDFを表示する (Display multiple pages of a PDF file) 18
  16. val mutex = Mutex() suspend fun generateBitmap(pdfRenderer: PdfRenderer, position: Int):

    Bitmap = mutex.withLock(pdfRenderer to position) { withContext(Dispatchers.IO) { val page = pdfRenderer.openPage(position) val bitmap = Bitmap .createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888) page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) page.close() bitmap } } Mutexでposition毎に 排他制御 複数のページを持つPDFを表示する (Display multiple pages of a PDF file) 19 🫛 PdfRenderer管理用のクラスを作ると良い (It's beneficial to create a class for handling PdfRenderer)
  17. ジェスチャーで拡大縮小移動できるようにする (Zoom in/out and pan with gestures) 実装するジェスチャー • 拡大縮小(1倍~5倍)、移動

    Zoom in/out(1x-5x)and pan • 1回タップで元のサイズに戻す Back to the original scale on single tap • 2回タップで拡大 Zoom in when tapping twice PDF image 21
  18. 拡大縮小と移動(Zoom in/out and pan) ① 保持したいデータを定義(Define the data you want

    to retain) >/ 拡大率 var scale by remember { mutableFloatStateOf(1f) } >/ 移動した距離 var offset by remember { mutableStateOf(Offset.Zero) } val minScale = 1f val maxScale = 5f >/ ジェスチャー処理を受け取るためのState val transformableState = rememberTransformableState { zoomChange, panChange, _ -> scale = (scale * zoomChange).coerceIn(minScale, maxScale) offset = panChange + offset } 22
  19. ② ジェスチャーから得た情報を元にViewを拡大縮小、移動させる   (Zoom in/out and move the view based

    on gestures) ImageよりView全体を拡大する方がなめらかな挙動に (Smoother behavior by scaling the view instead of images) Box( modifier = Modifier .fillMaxSize() .transformable(transformableState) .graphicsLayer { translationX = offset.x translationY = offset.y scaleX = scale scaleY = scale transformOrigin = TransformOrigin.Center }, ) { LazyRow( … ){ … } } 🐛 画面から表示部分がズレていってしまう The visible items is scrolling out of screen … 23
  20. 🟥 Offset(0, 0) private fun calculateOffset( scale: Float, >/ 拡大率

    screenSize: IntSize, >/ 画面サイズ offset: Offset >/ 移動したい距離 ): Offset { val maxOffsetX = max(0f, screenSize.width.toFloat() * (scale - 1f) / 2) val maxOffsetY = max(0f, screenSize.height.toFloat() * (scale - 1f) / 2) return Offset( offset.x.coerceIn(-maxOffsetX, maxOffsetX), offset.y.coerceIn(-maxOffsetY, maxOffsetY) ) } Image Screen F D C B A E ③ 移動可能な範囲を絞る(Restrict the movable area) scale = B / A C = scale × screenSize.x D = scale × screenSize.y maxOffsetX = E = (C - screenSize.x) / 2 maxOffsetY = F = (D - screenSize.y) / 2 24
  21. ③ 移動可能な範囲を絞る(Restrict the movable area ) Modifier.graphicsLayer { val usableOffset

    = calculateOffset( zoom = scale, size = IntSize( size.width.toInt(), size.height.toInt() ), offset = offset ) translationX = usableOffset.x translationY = usableOffset.y … }, ) rememberTransformableState{ … offset = calculateOffset( zoom = scale, screenSize = IntSize( screenWidthPx, screenHeightPx ), offset = offset + panChange, ) } offsetを更新時にでき る限り近い値に しておく 移動できるoffset に変化がある ことがある為 25
  22. タップ操作(Tap Action) modifier.pointerInput(Unit) { detectTapGestures( onDoubleTap = { _ ->

    scale *= 2f if (scale > maxScale) { offset = Offset.Zero scale = 1f } }, onTap = { scale = 1f } ) } .transformable(transformableState) .graphicsLayer { … 最大以上になったら1 倍に戻す transformableの 前に適用されるよ うに☝ 26
  23. その他実装すると良いこと (Other Tips) ① 拡大されてない時のみ、ページ遷移できるように The pages should be scrolled

    only when the scale factor is 1x ② 拡大率が1に近い状態で、ジェスチャーが終了された場合にScaleを1に Reset the scale to 1x if the gesture finishes with a zoom level ~1x val scrollEnable by remember { derivedStateOf { scale >= 1f } } LazyRow(userScrollEnabled = scrollEnable) LaunchedEffect(transformableState.isTransformInProgress) { if (!transformableState.isTransformInProgress) { if (abs(scale - 1f) < 0.1f) { scale = 1f } } } 27
  24. Activity再生成(画面回転)対応 (Support configuration changes: landscape mode) • Activityの再生成に関係なく、PDFの情報を保持したい PDF’s data

    instance should be preserved across configuration changes → ViewModel • 表示されているページも保持したい Also the currently visible page should be preserved → rememberLazyListState,   rememberSaveable PDF PDF P PDF 29
  25. @HiltViewModel class PdfViewerViewModel @Inject constructor( @ApplicationContext private val context: Context

    ) : ViewModel() { private val pdfRenderer = MutableStateFlow<PdfRenderer?>(null) >/ データ選択時に呼び出す fun updateWithLocalFile(pdfResource: PDFResource) { viewModelScope.launch { pdfRenderer.update { it>.close() generatePdfRenderer(context, pdfResource) } } } override fun onCleared() { pdfRenderer.value>.close() super.onCleared() } } StateFlowで保持 30
  26. class PageViewData( val pdfRenderer: PdfRenderer, val position: Int ) {

    suspend fun bitmap(): Bitmap? { return try { generateBitmap( pdfRenderer, position ) } catch (e: Exception) { null } } } >/ ViewModelに追加する val pdfPages: Flow<List<PageViewData>?> = pdfRenderer.map { renderer -> renderer >: return@map null (0 until renderer.pageCount).map { PageViewData(renderer, it) } } ページ毎のBitmap 取得用クラスを作成 Rendererの更新に追従する PageViewData一覧 ViewModelに書く 31
  27. @Composable fun PdfImage(pageViewData: PageViewData) { var bitmap: Bitmap? by remember

    { mutableStateOf(null) } LaunchedEffect(pageViewData) { bitmap = pageViewData.bitmap() } -/ … } @Composable fun PdfViewModelListScreen(viewModel: PdfViewerViewModel = viewModel()) { val pdfPages by viewModel.pdfPages.collectAsStateWithLifecycle(initialValue = null) val listState = rememberLazyListState() pdfPages>.let { pages -> LazyRow(state = listState, …) { items(pages) { page -> Box(>>.) { PdfImage(page) } } } } } コードが綺麗になった! The code looks much cleaner ! Listの状態を保持 32
  28. 最適化とUX改善(Optimization & UX Enhancement) 1. 必要以上に大きなBitmapを作成しない Avoid creating unnecessarily huge

    Bitmaps ◦ CPUとメモリの無駄遣いをやめる Stop wasting CPU and memory resources 2. Bitmapをキャッシュ Cache Bitmap ◦ Bitmapが再生成される回数を減らす Reduce the number of times regenerating Bitmaps ◦ UX改善にもなる This also leads to UX improvements 35
  29. 生成するBitmapは View Size で十分な場合も多い It’s enough to generate a bitmap

    based on the view size in some cases Case 1: A / B > C / D E = B / D × C F = B Case 2: C / D >= A / B E = A F = A / C × B 必要以上に大きなBitmapを生成しない Don’t generate huge Bitmap Actual Image Size Screen Size View Size F E D C B A 36
  30. Bitmapをキャッシュ(Cache Bitmap)① 方法1: 自分でキャッシュするコードを書く方法 Method 1: Manually implement caching logic

    ViewModel ViewModel PdfViewData PageViewData Bitmap cache PdfViewData instance onClear() clearCache() bitmap() View 37
  31. ✍ サイズの考慮も一緒にすると良い It is also recommended to consider the size

    of the Bitmap ViewModel ViewModel PdfViewData PageViewData Bitmap cache PdfViewData instance onClear() clearCache() bitmap() View ② キャッシュのサイズが 小さい場合は再生成する ① Sizeを確認 38
  32. 💡 キューでキャッシュ量を操作 Create cache management queue ViewModel ViewModel PdfViewData PageViewData

    Bitmap cache PdfViewData instance onClear() clearCache() bitmap() View Bitmap cache management queue clearCache() addCache() キューのMax サイズはBitmapの 大きさを考慮 キャッシュされているPageのindex を管理するキュー 優先して消すべきキャッシュを 判別するために使う 39
  33. Bitmapをキャッシュ(Cache Bitmap)② 方法2: Coilを使う Method2: Use Coil val painter =

    rememberAsyncImagePainter( model = ImageRequest.Builder(LocalContext.current) .dispatcher(Dispatchers.IO) .data(pageData) .memoryCachePolicy(CachePolicy.ENABLED) .memoryCacheKey("${pdfId}_${pageViewData.position}") .size(displaySize) .build() ) Image(>>. painter = painter, ) こんな感じで書きたい! 40
  34. class PdfPageFetcher( private val data: PdfPageCoilData ) : Fetcher {

    override suspend fun fetch(): FetchResult { val bitmap = data.viewData.bitmap() return DrawableResult( drawable = BitmapDrawable(data.resources, bitmap), isSampled = false, dataSource = DataSource.MEMORY, ) } class Factory : Fetcher.Factory<PdfPageCoilData> { override fun create( data: PdfPageCoilData, options: Options, imageLoader: ImageLoader ): Fetcher = PdfPageFetcher(data) } } Fetcherを作成する Implement Fetcher class PdfPageCoilData( val resources: Resources, val viewData: PageViewData ) 41
  35. Coilの恩恵を受けられるように To leverage the benefits of Coil >/ Application内のコード override

    fun newImageLoader(): ImageLoader { return ImageLoader.Builder(this) .components { add(PdfPageFetcher.Factory()) } .memoryCache { MemoryCache.Builder(this) .maxSizePercent(0.20) .build() } .diskCache { DiskCache.Builder() .directory(...) .build() } .respectCacheHeaders(false) .build() } >/ ViewModel内のコード fun clearPdfRenderer() { >/ … for (i in 0 until pdfPageCount) { val key = … imageLoader.diskCache >.remove(cacheKey) imageLoader.memoryCache >.remove(MemoryCache.Key(key)) } } PDFRendererのcloseの タイミングで キャッシュを消す! PdfRendererのcloseの タイミングで キャッシュを消す! 42
  36. Bitmapをキャッシュ(Cache Bitmap) 自作キャッシュ selfmade Coilを使う Using Coil メリット - 固定のページ数分は確実に

    キャッ シュできる A fixed number of pages can be cached デメリット - アプリ全体のメモリ使用量を考慮す ると実装が大変 に メリット - アプリ全体のメモリ使用量を考慮 Consider the overall memory usage of the app - Coilのキャッシュ機構を使用 - ディスクキャッシュも活用可能! デメリット - PDFに関してのみのキャッシュ量は 考慮できない 43
  37. 今までとAndroid 15以降のPdfRendererを比較 Android 5.0-(API level 21-) • PdfRendererがリリース(Release PdfRenderer) •

    PDFデータをBitmapに変換(Convert PDF Data to bitmap) • ページの大きさ、印刷用かの情報を取得(Get PDF page’s sizes and Information on whether it's for printing) 47
  38. 今までとAndroid 15以降のPdfRendererを比較 Android 15-(API level 35-) • 注釈の表示(Render annotations) •

    PDFの種類の取得(Get type of a PDF) • パスワード付きPDFに対応(Support password-protected PDFs) • PDF内のコンテンツを取得(Extract content from a PDF) • PDF内の文字列検索(Search for text in a PDF) • フォーム対応(Support input forms) • 書き込んだPDFを保存(Save the edited PDF) 48
  39. PdfRendererPrev • PdfRendererのバックポート用のクラス Backport class for PdfRenderer • SDK Extensionsを使い利用可能にしている

    Enabled through the use of SDK Extensions • 現状Android 12(API level 31)以上で使うことが可能 Backward compatibility with API level 31 and above can be maintained for now 49
  40. 注釈の表示(Render annotations) val params = RenderParams .Builder(RenderParams.RENDER_MODE_FOR_DISPLAY) .setRenderFlags( >/ 注釈

    RenderParams.FLAG_RENDER_TEXT_ANNOTATIONS or >/ ハイライト RenderParams.FLAG_RENDER_HIGHLIGHT_ANNOTATIONS ) .build() page.render(bitmap, null, null, params) 50
  41. PDFの種類の取得(Get PDF file types) 線型化PDF(Linearized Type) フォームの種類(Form Type) when(pdfRenderer.documentLinearizationType){ PdfRendererPreV.DOCUMENT_LINEARIZED_TYPE_LINEARIZED

    >> { … } PdfRendererPreV.DOCUMENT_LINEARIZED_TYPE_NON_LINEARIZED >> { … } } when(pdfRenderer.pdfFormType){ PdfRendererPreV.PDF_FORM_TYPE_ACRO_FORM >> { … } PdfRendererPreV.PDF_FORM_TYPE_XFA_FULL >> { … } PdfRendererPreV.PDF_FORM_TYPE_XFA_FOREGROUND >> { … } PdfRendererPreV.PDF_FORM_TYPE_NONE >> { … } } 51
  42. パスワード付きPDF対応(Support for password-protected PDF files) PdfRenderer生成時にLoadParamsを設定することで使用可能 Enabled by setting LoadParams

    when creating PdfRenderer try { val params = LoadParams.Builder() .setPassword("password") .build() val renderer = PdfRenderer(fileDescriptor, params) } catch (e: SecurityException) { >/ パスワードが必要 or パスワードを間違えている } 52
  43. PDF内のコンテンツを取得(Extract content from a PDF file) テキスト情報(Text Information) val textContents:

    List<PdfPageTextContent> = page.textContents val allText = textContents.joinToString("\n") { it.text } val linkContents: List<PdfPageLinkContent> = page.linkContents val allLinkInfos: List<Pair<Uri, List<RectF>>> = linkContents.map { >/ LinkのURIとそのLinkが描画されている場所 it.uri to it.bounds.toList() } フォーム情報(Forms Infromation) val formWidgetInfos: List<FormWidgetInfo> = page.formWidgetInfos URI付きのリンク情報(Links Information) 53
  44. 画像情報(Image Information) PDFの他の場所へのリンク情報 (Link information to other locations within the

    PDF) val gotoLinks: List<PdfPageGotoLinkContent> = page.gotoLinks gotoLinks.forEach { val destination: PdfPageGotoLinkContent.Destination = it.destination val page: Int = destination.pageNumber >/ リンク先のページ番号 val zoom: Float = destination.zoom >/ リンクに紐ずくズーム率 val x: Float = destination.xCoordinate >/ リンク先のX座標 val y: Float = destination.yCoordinate >/ リンク先のY座標 >/ Linkが描画されている場所 val bounds: List<RectF> = it.bounds.toList() } val imageContents: List<PdfPageImageContent> = page.imageContents val allImages = imageContents.joinToString("\n") { it.altText } 54
  45. 選択範囲のコンテンツを取得(Get the content of the selected area) val selectedContent: PageSelection?

    = page.selectContent(startBoundary, endBoundary) >/ 選択範囲のテキスト val selectedTexts = selectedContent>.selectedTextContents >/ 場所から val boundaryFromPoint = SelectionBoundary(point) >/ コンテンツのindexから val boundaryFromIndex = SelectionBoundary(index) 55
  46. PDF内の文字列検索(Text search in a PDF) 画面上への描画は自力で(Rendering on the screen is

    done manually) ) ) page.searchText("検索したい文字列").forEach { result -> result.textStartIndex >/ ヒットした文字が何文字目から始まっているか val bounds = result.bounds.first() >/ ヒットした文字の描画されている場所 } val canvas = Canvas(bitmap) val paint = Paint().apply { color = Color.RED } canvas.drawRect( Rect( bounds.left.toInt(),bounds.top.toInt(), bounds.right.toInt(),bounds.bottom.toInt() ), paint ) 56
  47. フォーム対応(Support input forms) val formWidgetInfo: FormWidgetInfo = … formWidgetInfo.widgetIndex >/

    Formのインデックス formWidgetInfo.widgetRect >/ Formが描画されている場所 formWidgetInfo.widgetType >/ Formの種類 formWidgetInfo.fontSize >/ フォントサイズ formWidgetInfo.accessibilityLabel >/ アクセシビリティラベル formWidgetInfo.isEditableText >/ 編集可能か? formWidgetInfo.isMultiLineText >/ 複数行か? formWidgetInfo.isReadOnly >/ 読み取り専用か? formWidgetInfo.isMultiSelect >/ 複数選択可能か? formWidgetInfo.listItems >/ 選択肢 formWidgetInfo.maxLength >/ 最大文字数 formWidgetInfo.textValue >/ デフォルトテキストの値 57
  48. フォームの対応タイプ(Supported form types) WIDGET_TYPE_PUSHBUTTON >/ ボタン WIDGET_TYPE_CHECKBOX >/ チェックボックス WIDGET_TYPE_RADIOBUTTON

    >/ ラジオボタン WIDGET_TYPE_COMBOBOX >/ コンボボックス WIDGET_TYPE_LISTBOX >/ リストボックス WIDGET_TYPE_TEXTFIELD >/ テキストフィールド WIDGET_TYPE_SIGNATURE >/ サイン WIDGET_TYPE_UNKNOWN >/ 不明 58
  49. indexを指定しフォーム情報を取得(Get a form information from index) x、y座標からフォーム情報を取得 (Get info from

    a position) タイプを指定してフォーム情報を取得(Get info by types) val formWidgetInfoAtPosition = page.getFormWidgetInfoAtPosition(x, y) val formInfo = page.getFormWidgetInfoAtIndex(index) val formWidgetInfosWithTypes = page.getFormWidgetInfos( intArrayOf(WIDGET_TYPE_TEXTFIELD) ) 59
  50. フォームの変更を反映(Reflect the changes made to the form) val formEditRecode =

    FormEditRecord .Builder(type, pagePosition, formIndex) .setText("更新したいフォームの内容").build() >/ Pageに変更を反映 page.applyEdit(formEditRecode) >/ type FormEditRecord.EDIT_TYPE_CLICK >/ チェックボックス系 FormEditRecord.EDIT_TYPE_SET_INDICES >/ 選択肢系 FormEditRecord.EDIT_TYPE_SET_TEXT >/ テキスト >/ 変更が反映されたBitmapを取得 page.render(bitmap, >>.) 60
  51. 書き込んだPDFを保存(Save an annotated PDF data) ファイルにPdfRenderer内のPDFのデータを書き込む📝 (Write a PDF data

    from PdfRenderer to a file📝) pdfRenderer.write(fileDescriptor, isRemovePasswordProtection) 61
  52. 新機能の活用例:AI(Example using a new feature: AI) PDFの内容を要約するアプリ (PDF Summarization App)

    PdfRenderer × Gemini コンテンツから、テキスト情報を取 得しGeminiに要約依頼 62
  53. 新機能の活用例:フォーム(Example using a new feature: Form) page.getFormWidgetInfoAtPosition(x, y) Form Updated

    Form Text page.applyEdit(formEditRecode) Update the pdf data page.render(updatedBitmap, >.) page.write(fd,>.) Select a form Update the bitmap Save edited pdf to a file Input data 63
  54. androidx.pdf PDFの表示機能を提供するJetpackライブラリ PDF Viewer Jetpack Library 特徴(Characteristics) • PdfViewerの機能を持つ PdfViewerFragment

    を提供 Provides a PdfViewerFragment that includes PdfViewer functionality • 別プロセス(isolatedProcess=trueのService)で PdfRenderer を扱ってい るので、信用できないファイルに対しても使える Using PdfRenderer on another process • • 2024 年 9 月 4 日に v1.0.0-alpha02 がリリースされたばかり Release v1.0.0-alpha02 on Sep 4, 2024 現状Android 15(API level 35)の端末からしか使えないAvailable only on API level 35 devices 65
  55. androidx.pdf 機能(Feature) • 注釈やハイライトの表示(Render text and highlight annotations) • PDFをリストで表示(Display

    PDF files in a list style) • ジェスチャー対応(Gesture support) • スライダーでページ遷移(Page navigation with a slider) • パスワード付きPDFにも対応(Support password PDF) • 文字列検索(Text search) 66
  56. androidx.pdf(PdfViewerFragment)を使う Usage dependencies { implementation("androidx.pdf:pdf-viewer-fragment:1.0.0-alpha02") } val documentUri: Uri =

    … val pdfViewerFragment = PdfViewerFragment() >/ Fragmentが少なくともSTARTED状態でセットしないとエラーになる >/ documentUri をセットするとPDFが読み込まれる pdfViewerFragment.documentUri = documentUri >/ Fragmentが少なくともSTARTED状態でセットしないとエラーになる >/ isTextSearchActive をセットすると検索バーが出る pdfViewerFragment.isTextSearchActive = false Composeで実装する時は AndroidViewを使う 67
  57. androidx.pdf まだAlpha版なので( As it's currently an alpha release …) •

    Viewerとしての機能は十分だがちょこちょこバグ挙動あり There are some bugs now. • 私の環境ではビルド時にいくらかエラーが出たが、エラーメッセージの通り に地道に修正していくと動く I encountered some build errors, but by diligently addressing them one by one based on the error messages, I was able to get it to work. PdfRendererの新機能を活用した、さらなる機能追加に期待! I expect to see even more features that leverage the new PdfRenderer functionalities! 69
  58. まとめ(Summary) • PDF Viewerの自作は可能だが、実装時の注意点もある It’s possible to create a custom

    PDF Viewer, but there are some notes on implementation • androidx.pdf:pdf-viewer-fragmentに期待! I’m looking forward to androidx.pdf:pdf-viewer-fragment! • PDFを表示させたい時は、どのAPI level以降を対応するか・重要度・ 工数・リリース日を考慮し適切な方法で! Choose the appropriate method by considering which API level or later is to be supported, priority, workload, and release date, when displaying PDFs! 71
  59. 参考など(Reference) • https://developer.android.com/reference/android/graphics/pdf/package-summary • https://developer.android.com/reference/android/graphics/pdf/LoadParams • https://developer.android.com/reference/android/graphics/pdf/PdfRenderer • https://developer.android.com/reference/android/graphics/pdf/PdfRenderer.Page •

    https://developer.android.com/reference/android/graphics/pdf/PdfRendererPreV • https://developer.android.com/reference/android/graphics/pdf/PdfRendererPreV.Page • https://developer.android.com/reference/android/graphics/pdf/RenderParams • https://developer.android.com/jetpack/androidx/releases/pdf • https://developer.android.com/reference/kotlin/androidx/pdf/viewer/fragment/PdfViewer Fragment#PdfViewerFragment() その他過去に書いた関連ブログ • https://tech.yappli.io/entry/android_compose_transformable_end • https://tech.yappli.io/entry/android_compose_firstVisibleItemScrollOffset • https://tech.yappli.io/entry/android_file_name_conflict 72
  60. おまけ: PDF’s URL → PdfRenderer(OkHttpを使用した場合の例) suspend fun generatePdfRenderer(context: Context, url:

    URL) : PdfRenderer { val file = File(context.cacheDir, "sample.pdf") >/ ファイル管理もする(割愛) val okHttpClient = OkHttpClient() >/ DIするなりしよう! okHttpClient.newCall(Request.Builder().url(url).build()).execute().use { if (it.isSuccessful) { val byteStream = it.body>.byteStream() >: throw Exception("download failed") FileOutputStream(file).use { output -> val buffer = ByteArray(DEFAULT_BUFFER_SIZE) var bytesRead = byteStream.read(buffer) var bytesCopied = 0L while (bytesRead >= 0) { output.write(buffer, 0, bytesRead) bytesCopied += bytesRead bytesRead = byteStream.read(buffer) } bytesCopied } var uri = file.toUri() return generatePdfRenderer(context, uri) } else { throw Exception("download failed") } } } 73