Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

mokomi-hime👸 Hunachi / ふなち Hana Kondo Androidエンジニア 株式会社タイミー 2024/09- 株式会社ヤプリ 2021/08-2024/08 X: _hunachi Github: Hunachi 2

Slide 3

Slide 3 text

目次(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

Slide 4

Slide 4 text

PDFをAndroidで表示する方法 A way to preview a PDF file on Android 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

PDF関係のライブラリを使わず、簡単なPDF ViewerをComposeで作る Build a simple PDF Viewer in Compose without any PDF library 実装した経験を元にお 話しします! 7

Slide 8

Slide 8 text

簡単な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

Slide 9

Slide 9 text

@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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

色々なデータに対応する(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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

複数のページを持つ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

Slide 17

Slide 17 text

@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

Slide 18

Slide 18 text

java.lang.IllegalStateException: Current page not closed! 🫛 Android 15(API level 35)では起きない! 複数のページを持つPDFを表示する (Display multiple pages of a PDF file) 18

Slide 19

Slide 19 text

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)

Slide 20

Slide 20 text

複数のページを持つPDFを表示する (Display multiple pages of a PDF file) 20

Slide 21

Slide 21 text

ジェスチャーで拡大縮小移動できるようにする (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

Slide 22

Slide 22 text

拡大縮小と移動(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

Slide 23

Slide 23 text

② ジェスチャーから得た情報を元に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

Slide 24

Slide 24 text

🟥 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

Slide 25

Slide 25 text

③ 移動可能な範囲を絞る(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

Slide 26

Slide 26 text

タップ操作(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

Slide 27

Slide 27 text

その他実装すると良いこと (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

Slide 28

Slide 28 text

ジェスチャーで拡大縮小移動できるようにする (Zoom in/out and pan with gestures) 28

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

@HiltViewModel class PdfViewerViewModel @Inject constructor( @ApplicationContext private val context: Context ) : ViewModel() { private val pdfRenderer = MutableStateFlow(null) >/ データ選択時に呼び出す fun updateWithLocalFile(pdfResource: PDFResource) { viewModelScope.launch { pdfRenderer.update { it>.close() generatePdfRenderer(context, pdfResource) } } } override fun onCleared() { pdfRenderer.value>.close() super.onCleared() } } StateFlowで保持 30

Slide 31

Slide 31 text

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?> = pdfRenderer.map { renderer -> renderer >: return@map null (0 until renderer.pageCount).map { PageViewData(renderer, it) } } ページ毎のBitmap 取得用クラスを作成 Rendererの更新に追従する PageViewData一覧 ViewModelに書く 31

Slide 32

Slide 32 text

@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

Slide 33

Slide 33 text

Activity再生成(横画面)対応 (Support configuration changes: landscape mode) PDF PDF P PDF 33

Slide 34

Slide 34 text

PDF Viewerの最適化とUX改善 PDF Viewer Optimization & UX Enhancement 34

Slide 35

Slide 35 text

最適化と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

Slide 36

Slide 36 text

生成する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

Slide 37

Slide 37 text

Bitmapをキャッシュ(Cache Bitmap)① 方法1: 自分でキャッシュするコードを書く方法 Method 1: Manually implement caching logic ViewModel ViewModel PdfViewData PageViewData Bitmap cache PdfViewData instance onClear() clearCache() bitmap() View 37

Slide 38

Slide 38 text

✍ サイズの考慮も一緒にすると良い 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

Slide 39

Slide 39 text

💡 キューでキャッシュ量を操作 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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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 { override fun create( data: PdfPageCoilData, options: Options, imageLoader: ImageLoader ): Fetcher = PdfPageFetcher(data) } } Fetcherを作成する Implement Fetcher class PdfPageCoilData( val resources: Resources, val viewData: PageViewData ) 41

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

UXの改善(Improve UX) 画像のチラチラ表示が改善! Flickering images has been resolved! Before After 44

Slide 45

Slide 45 text

キャッシュ量調整の結果(Result of the cache size) Optimized Cache NonOptimized Cache 512MB Memory 512MB Memory 45

Slide 46

Slide 46 text

Android 15からのPDF Renderer PDF Renderer since Android 15 46

Slide 47

Slide 47 text

今までと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

Slide 48

Slide 48 text

今までと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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

注釈の表示(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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

パスワード付き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

Slide 53

Slide 53 text

PDF内のコンテンツを取得(Extract content from a PDF file) テキスト情報(Text Information) val textContents: List = page.textContents val allText = textContents.joinToString("\n") { it.text } val linkContents: List = page.linkContents val allLinkInfos: List>> = linkContents.map { >/ LinkのURIとそのLinkが描画されている場所 it.uri to it.bounds.toList() } フォーム情報(Forms Infromation) val formWidgetInfos: List = page.formWidgetInfos URI付きのリンク情報(Links Information) 53

Slide 54

Slide 54 text

画像情報(Image Information) PDFの他の場所へのリンク情報 (Link information to other locations within the PDF) val gotoLinks: List = 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 = it.bounds.toList() } val imageContents: List = page.imageContents val allImages = imageContents.joinToString("\n") { it.altText } 54

Slide 55

Slide 55 text

選択範囲のコンテンツを取得(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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

フォーム対応(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

Slide 58

Slide 58 text

フォームの対応タイプ(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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

フォームの変更を反映(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

Slide 61

Slide 61 text

書き込んだPDFを保存(Save an annotated PDF data) ファイルにPdfRenderer内のPDFのデータを書き込む📝 (Write a PDF data from PdfRenderer to a file📝) pdfRenderer.write(fileDescriptor, isRemovePasswordProtection) 61

Slide 62

Slide 62 text

新機能の活用例:AI(Example using a new feature: AI) PDFの内容を要約するアプリ (PDF Summarization App) PdfRenderer × Gemini コンテンツから、テキスト情報を取 得しGeminiに要約依頼 62

Slide 63

Slide 63 text

新機能の活用例:フォーム(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

Slide 64

Slide 64 text

androidx.pdf 64 64

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

androidx.pdf を動かす Run android.pdf 68

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

まとめ Summry 70

Slide 71

Slide 71 text

まとめ(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

Slide 72

Slide 72 text

参考など(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

Slide 73

Slide 73 text

おまけ: 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