Renderizando documentos PDF no Android usando PdfRenderer

Tempo de leitura: 5 minutes

Introdução

Durante o desenvolvimento, a menos que encontremos um desafio ou problema, a maioria de nós não está ciente de muitas coisas no sistema das quais podemos nos beneficiar. Estamos habituados de tal forma que, a menos que algo seja necessário, não exploramos o material disponível.

Portanto, é sempre um bom hábito explorar alguns recursos que tornam nosso trabalho mais fácil quando chegar a hora e, para isso, precisamos continuar explorando as coisas na plataforma que escolhermos. Uma coisa no Android é lidar com documentos PDF. Nesta postagem, veremos diferentes maneiras de abrir documentos PDF.

 

Problema

Lidar com documentos PDF é uma das coisas básicas que a maioria de nós não está ciente porque não tivemos a chance de trabalhar nisso. Não é tão fácil na fase inicial do Android abrir PDFs porque não havia renderizadores ou componentes que pudessem lidar com eles. Então começamos a usar navegadores ou WebViews para lidar com PDFs da seguinte forma

private fun loadPDFWebView(pdfDocUrl: String) {
       webview?.settings?.javaScriptEnabled = true
       webview?.clearHistory()
       webview?.loadUrl("https://docs.google.com/gview?embedded=true&url=$pdfDocUrl")
   }

Mas há um problema de usar isso, não era possível carregar documentos PDF de grande porte. Na minha experiência, digo que mostra um erro ao tentar abrir documentos com mais de 10 MB de tamanho.

Embora existam muitas bibliotecas disponíveis, nem sempre foi uma tarefa fácil personalizar a biblioteca. Em seguida, costumamos disparar a intenção do visualizador de PDF, que leva o usuário a deixar nosso aplicativo e navegar para qualquer outro aplicativo que possa lidar com esse conteúdo PDF.

private fun openDocument(path: Uri) {
     val pdfIntent = Intent(Intent.ACTION_VIEW)
     pdfIntent.setDataAndType(path, "application/pdf")
     pdfIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
     pdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
     try {
         mContext.startActivity(pdfIntent)
     } catch (e: ActivityNotFoundException) {
         mContext.toast(mContext.getString(R.string.no_app_to_view_pdf))
     }
 }

Esta também não era uma solução preferível.

 

Solução

A solução para abrir PDF é resolvida com a introdução da classe PdfRenderer no Android-Lollipop (API 21). Vamos explorar como podemos usar isso em nossos aplicativos.

 

O que é PdfRenderer?

O PdfRenderer nos permite criar um Bitmap a partir de uma página em um documento PDF para que possamos exibi-lo na tela. A classe PdfRenderer não é segura para thread. Se quisermos renderizar um PDF, primeiro precisamos obter um ParcelFileDescriptor do arquivo e, em seguida, criar uma instância do renderizador.

Mais tarde, para cada página que queremos renderizar, abrimos a página, renderizamos e fechamos a página. Depois de terminar a renderização, fechamos o renderizador. Depois que o renderizador é fechado, ele não deve ser mais usado.

Usando este PdfRenderer, somos responsáveis ​​pelo tratamento do fechamento do renderizador e de cada página que abrimos. Aqui, só podemos ter uma página aberta por vez. Antes de fechar, renderizador, precisamos fechar a página aberta no momento. Vejamos passo a passo com um exemplo.

 

Como usar o PdfRenderer?

Inicialmente, vamos verificar as etapas básicas antes de passar pelo código

 

Passo 1:

Obtenha um descritor de arquivo procurável em nosso documento pdf:

val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)

Passo 2:

Agora, vamos criar a instância PDFRenderer usando o ParcleFileDescriptor obtido acima:

val renderer = PdfRenderer(pfd)

Passo 3:

Vamos criar a instância de Bitmap com as dimensões necessárias:

val bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_4444)

Passo 4:

Não fazer com que a página seja renderizada usando PdfRenderer.Page apenas passando o índice da página

val page = renderer.openPage(pageIndex)

Passo 5:

Por último, renderize a página em bitmap criado na etapa 3:

page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);

Passo 6:

Feche a página e o renderizador assim que terminar

page.close()   
renderer.close()

 

Exemplo

Vamos verificar como abrir o documento pdf no fragmento fornecendo Uri como um argumento para ele. É apenas uma parte do exemplo de armazenamento de documentos do Android.

Nosso exemplo é renderizar um documento que é um bitmap em nosso caso, então precisamos de um ImageView, visto que ele exibe uma página por vez, precisamos de dois botões para mover para as páginas seguintes e anteriores, se disponíveis. Vamos projetar o XML

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@android:color/white"
        android:scaleType="fitCenter"
        android:contentDescription="@null"/>

    <LinearLayout
        style="?android:attr/buttonBarStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:measureWithLargestChild="true"
        android:orientation="horizontal">

        <Button
            android:id="@+id/previous"
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/previous" />

        <Button
            android:id="@+id/next"
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/next" />

    </LinearLayout>

</LinearLayout>

Agora que terminamos o design, vamos passar para a parte de codificação – nada mais é do que um fragmento que trata de todas as etapas discutidas acima

package com.example.pdf

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.createBitmap
import android.graphics.pdf.PdfRenderer
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import com.example.pdf.R
import java.io.FileDescriptor
import java.io.IOException


class OpenDocumentFragment : Fragment() {

    private lateinit var pdfRenderer: PdfRenderer
    private lateinit var currentPage: PdfRenderer.Page
    private var currentPageNumber: Int = INITIAL_PAGE_INDEX

    private lateinit var pdfPageView: ImageView
    private lateinit var previousButton: Button
    private lateinit var nextButton: Button

    val pageCount get() = pdfRenderer.pageCount

    companion object {
        private const val DOCUMENT_URI_ARGUMENT =
            "com.example.pdf.opendocument.args.DOCUMENT_URI_ARGUMENT"

        fun newInstance(documentUri: Uri): OpenDocumentFragment {

            return OpenDocumentFragment().apply {
                arguments = Bundle().apply {
                    putString(DOCUMENT_URI_ARGUMENT, documentUri.toString())
                }
            }
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_pdf_renderer_basic, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        previous.setOnClickListener {
            showPage(currentPage.index - 1)
        }
       next.setOnClickListener {
                showPage(currentPage.index + 1)
       }
        
        // If there is a savedInstanceState (screen orientations, etc.), we restore the page index.
        currentPageNumber = savedInstanceState?.getInt(CURRENT_PAGE_INDEX_KEY, INITIAL_PAGE_INDEX)
            ?: INITIAL_PAGE_INDEX
    }

    override fun onStart() {
        super.onStart()

        val documentUri = arguments?.getString(DOCUMENT_URI_ARGUMENT)?.toUri() ?: return
        try {
            openRenderer(activity, documentUri)
            showPage(currentPageNumber)
        } catch (ioException: IOException) {
            Log.d(TAG, "Exception opening document", ioException)
        }
    }

    override fun onStop() {
        super.onStop()
        try {
            closeRenderer()
        } catch (ioException: IOException) {
            Log.d(TAG, "Exception closing document", ioException)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        outState.putInt(CURRENT_PAGE_INDEX_KEY, currentPage.index)
        super.onSaveInstanceState(outState)
    }

    /**
     * Sets up a [PdfRenderer] and related resources.
     */
    @Throws(IOException::class)
    private fun openRenderer(context: Context?, documentUri: Uri) {
        if (context == null) return

        /**
         * It may be tempting to use `use` here, but [PdfRenderer] expects to take ownership
         * of the [FileDescriptor], and, if we did use `use`, it would be auto-closed at the
         * end of the block, preventing us from rendering additional pages.
         */
        val fileDescriptor = context.contentResolver.openFileDescriptor(documentUri, "r") ?: return

        // This is the PdfRenderer we use to render the PDF.
        pdfRenderer = PdfRenderer(fileDescriptor)
        currentPage = pdfRenderer.openPage(currentPageNumber)
    }

    /**
     * Closes the [PdfRenderer] and related resources.
     *
     * @throws IOException When the PDF file cannot be closed.
     */
    @Throws(IOException::class)
    private fun closeRenderer() {
        currentPage.close()
        pdfRenderer.close()
    }

    /**
     * Shows the specified page of PDF to the screen.
     *
     * The way [PdfRenderer] works is that it allows for "opening" a page with the method
     * [PdfRenderer.openPage], which takes a (0 based) page number to open. This returns
     * a [PdfRenderer.Page] object, which represents the content of this page.
     *
     * There are two ways to render the content of a [PdfRenderer.Page].
     * [PdfRenderer.Page.RENDER_MODE_FOR_PRINT] and [PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY].
     * Since we're displaying the data on the screen of the device, we'll use the later.
     *
     * @param index The page index.
     */
    private fun showPage(index: Int) {
        if (index < 0 || index >= pdfRenderer.pageCount) return

        currentPage.close()
        currentPage = pdfRenderer.openPage(index)

        // Important: the destination bitmap must be ARGB (not RGB).
        val bitmap = createBitmap(currentPage.width, currentPage.height, Bitmap.Config.ARGB_8888)

        currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
        pdfPageView.setImageBitmap(bitmap)

        val pageCount = pdfRenderer.pageCount
        previousButton.isEnabled = (0 != index)
        nextButton.isEnabled = (index + 1 < pageCount)
        activity?.title = getString(R.string.app_name_with_index, index + 1, pageCount)
    }
}

/**
 * Key string for saving the state of current page index.
 */
private const val CURRENT_PAGE_INDEX_KEY =
    "com.example.pdf.opendocument.state.CURRENT_PAGE_INDEX_KEY"

private const val TAG = "OpenDocumentFragment"
private const val INITIAL_PAGE_INDEX = 0

 

Resumo

Agora que você deve ter uma ideia básica de como abrir os documentos PDF. Você pode encontrar o código completo do exemplo de abertura de um documento PDF usando o renderizador de PDF no GitHub.