Android 预览 PDF 技术方案总结

一、借助第三方应用

Intent viewIntent = new Intent();

viewIntent.setAction(Intent.ACTION_VIEW);

viewIntent.setDataAndType(pdfUri, "application/pdf");

viewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

startActivity(viewIntent);
  • 优点:借助第三方应用可以让开发者无需做额外的工作,而且让用户选择惯用的软件来打开也是一种传统且合理的方案。

  • 缺点:如果用户没有安装任何可供预览文档的软件,则用户将无法打开该文档,严重影响用户体验。

二、Google 文档服务

webView.loadUrl("http://docs.google.com/gviewembedded=true&url=" + pdfUrl);
  • 优点:使用 WebView 即可。

  • 缺点:这种方式在国内网络环境下是无法访问的,需要翻墙。

三、Android PdfViewer 框架

该框架是目前最流行,最稳定,速度最快的框架,使用起来非常简单,可以访问 GitHub 主页查看使用方法:

https://github.com/barteksc/AndroidPdfViewer

  • 优点:无需联网,使用方便,稳定,速度快,可监听文件打开的各项事件。

  • 缺点:APK 体积增大 16M,且只能打开本地 pdf 文件。因此如果是对 APK 大小有要求的用户不建议使用,如果是专业的阅读功能的 App 则可以使用。注意,如果你对 NDK 比较熟悉,你可以根据自己的需要将对应的库手动放入你的项目里,这样可以有效减少 APK 的体积。

四、PDF.js

PDF.js 官网

PDF.js GitHub

如果说 Google Docs 有高墙限制,那么 PDF.js 则是墙内人的福音。

方式一:使用 mozilla 部署在 github pages 上的 Viewer

pdfWebView.loadUrl("http://mozilla.github.io/pdf.js/web/viewer.html?file=" + pdfUri);
  • 优点:无需翻墙,不增加 APK 体积,界面美观。

  • 缺点:需要联网,不稳定,有时访问速度过慢,有跨域问题。移动端自带的页面有很多功能无法使用,影响用户体验。

方式二:下载 PDF.js 放到 assets 目录下

如果 pdf 文件不能跨域访问的话可以使用这种方式,先把文件下载到本地然后传入本地文件路径:

pdfWebView.loadUrl("file:///android_asset/pdfjs/web/viewer.html?file=" + pdfUri);
  • 优点:无跨域问题。

  • 缺点:导入 PDF.js 库的话 APK 会增大 5MB 左右。可以考虑把 PDF.js 部署到服务端或者使用 CDN 的方式。

方式三:自定义预览界面,PDF.js 使用 CDN 的方式导入

1. 新建一个预览的 index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"/>
    <title>Document</title>
    <style type="text/css">
        canvas {
            width: 100%;
            height: 100%;
            border: 1px solid black;
        }
    </style>
    <script src="https://unpkg.com/pdfjs-dist@1.9.426/build/pdf.min.js"></script>
    <script type="text/javascript" src="index.js"></script>
</head>
<body>
</body>
</html>
2. 实现预览 index.js
var url = location.search.substring(1);

PDFJS.cMapUrl = 'https://unpkg.com/pdfjs-dist@1.9.426/cmaps/';
PDFJS.cMapPacked = true;

var pdfDoc = null;

function createPage() {
    var div = document.createElement("canvas");
    document.body.appendChild(div);
    return div;
}

function renderPage(num) {
    pdfDoc.getPage(num).then(function (page) {
        var viewport = page.getViewport(2.0);
        var canvas = createPage();
        var ctx = canvas.getContext('2d');

        canvas.height = viewport.height;
        canvas.width = viewport.width;

        page.render({
            canvasContext: ctx,
            viewport: viewport
        });
    });
}

PDFJS.getDocument(url).then(function (pdf) {
    pdfDoc = pdf;
    for (var i = 1; i <= pdfDoc.numPages; i++) {
        renderPage(i)
    }
});
3. 示例代码
WebSettings webSettings = pdfWebView.getSettings();

webSettings.setJavaScriptEnabled(true);
webSettings.setAllowFileAccess(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setSupportZoom(true);
webSettings.setDisplayZoomControls(true);

pdfWebView.loadUrl("file:///android_asset/index.html?" + pdfUri);
  • 优点:最终放到 assets 目录下的就只有 index.html 和 index.js 两个文件,可以避免全部导入带来的 APK 体积增大问题。另外如果对预览 UI 和交互有要求的话也可以很方便地通过修改 html 和 js 来实现。

  • 缺点:加载较大的 PDF 文件速度较慢,甚至会直接崩溃(比如 40 MB)。

4.1 遇到的问题

  • 在预览的时候遇到显示模糊

可以在 index.js 文件中设置 scale 系数来解决。

var viewport = page.getViewport(2.0);//设置为2.0
  • pdf 内容显示不完整

可以在 index.js 文件中设置 cMapUrl 和 cMapPacked 来解决。

PDFJS.cMapUrl = 'https://unpkg.com/pdfjs-dist@1.9.426/cmaps/';
PDFJS.cMapPacked = true;

4.2 PDF.js 方案总结

  • 优点:简单稳定,适用于预览小型的 PDF 文件,绝大多数场景可以使用。

  • 缺点:加载速度方面有所欠缺,且加载较大的 PDF 文件会直接导致应用崩溃(比如 40 MB)。

2018-12-27 17:42:53.771 12498-12498/com.example.jerry.mypdfapplication A/chromium: 
[FATAL:aw_browser_terminator.cc(79)] Render process (12623)'s crash 
wasn't handled by all associated  webviews, triggering application crash.

4.3 效果图

mozilla viewer
自定义 Viewer

五、PDFRenderer

PdfRenderer API

这个类可以用来渲染 PDF 文档,它是将 PDF 里的每一页渲染成一张 Bitmap,以此显示出来。该类是线程不安全的。PdfRenderer 中核心代码是用的是 native 方法,所以很难将 PdfRenderer 从 SDK 中抽取出来用。

5.1 简单示例代码

 // 首先创建一个 PdfRenderer
 PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor());

 // 渲染全部页面
 final int pageCount = renderer.getPageCount();
 for (int i = 0; i < pageCount; i++) {
     Page page = renderer.openPage(i);

     // 将一页的 PDF 渲染到一个 Bitmap 上,Bitmap 必须是 ARGB,不可以是 RGB
     Bitmap mBitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
                    Bitmap.Config.ARGB_8888);

     page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY);

     // 显示该 Bitmap
     imageView.setImageBitmap(mBitmap);

     // 注意:每次渲染完成之后都要关闭 page
     page.close();
 }

 // 注意:渲染完成之后要关闭 renderer
 renderer.close();

5.2 Demo 代码

public class PdfRendererActivity extends AppCompatActivity {

    private static final String TAG = "MyPDF";

    private static final int REQUEST_PDF_OPEN = 1;

    private Button getButton;

    private Button openButton;

    private Button previousButton;

    private Button nextButton;

    private ImageView imageView;

    private String pdfUri;

    private PdfRenderer.Page currentPage;

    private PdfRenderer pdfRenderer;

    private ParcelFileDescriptor parcelFileDescriptor;

    private int pageCount;

    private int currentIndex;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pdfrenderer);
        initView();
    }

    private void initView() {

        getButton = findViewById(R.id.button_get);

        openButton = findViewById(R.id.button_open);

        imageView = findViewById(R.id.imageview_pdf_page);

        previousButton = findViewById(R.id.button_previous);

        nextButton = findViewById(R.id.button_next);

        getButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                selectPdf();
            }
        });

        openButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                openPdf();
            }
        });

        previousButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if (currentIndex == 0) {
                    return;
                }
                currentIndex--;

                currentPage = pdfRenderer.openPage(currentIndex);

                Bitmap bitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
                        Bitmap.Config.ARGB_8888);

                // say we render for showing on the screen
                currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);

                // do stuff with the bitmap
                imageView.setImageBitmap(bitmap);
                // close the page
                currentPage.close();
            }
        });

        nextButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if (currentIndex == pageCount - 1) {
                    return;
                }

                currentIndex++;

                currentPage = pdfRenderer.openPage(currentIndex);

                Bitmap bitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
                        Bitmap.Config.ARGB_8888);

                // say we render for showing on the screen
                currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);

                // do stuff with the bitmap
                imageView.setImageBitmap(bitmap);
                // close the page
                currentPage.close();
            }
        });
    }

    private void selectPdf() {

        Intent getIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

        getIntent.setType("*/*");

        getIntent.addCategory(Intent.CATEGORY_OPENABLE);

        PdfRendererActivity.this.startActivityForResult(getIntent, REQUEST_PDF_OPEN);
    }

    private void openPdf() {

        try {
            currentIndex = 0;
            // create a new renderer
            pdfRenderer = new PdfRenderer(getSeekableFileDescriptor());

            // let us just render all pages
            pageCount = pdfRenderer.getPageCount();

            currentPage = pdfRenderer.openPage(currentIndex);

            Bitmap bitmap = Bitmap.createBitmap(currentPage.getWidth(), currentPage.getHeight(),
                    Bitmap.Config.ARGB_8888);

            // say we render for showing on the screen
            currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);

            // do stuff with the bitmap
            imageView.setImageBitmap(bitmap);
            // close the page
            currentPage.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ParcelFileDescriptor getSeekableFileDescriptor() {

        try {
            parcelFileDescriptor = getApplicationContext().getContentResolver().openFileDescriptor(Uri.parse(pdfUri), "r");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return parcelFileDescriptor;
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

        if (requestCode == REQUEST_PDF_OPEN && resultCode == RESULT_OK) {
            pdfUri = data.getDataString();
            Log.d(TAG, "onActivityResult: " + Uri.decode(pdfUri));
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // close the renderer
        if (null != pdfRenderer) {
            pdfRenderer.close();
        }
        if (null != parcelFileDescriptor) {
            try {
                parcelFileDescriptor.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

5.3 效果图

第一页
第二页
  • 优点:原生 API,无需联网,不增加 APK 体积,不借用第三方因此无安全性的风险。权衡利弊,个人认为这是目前最好的技术方案。

  • 缺点:仅 Android 5.0 以上可以使用,但对于 Android 版本已经更新到 9.0 的时代,这个限制是可以接受的。因为它每次只能渲染一个页面,所以想做出上下滚动浏览的效果的话需要自己手动实现,可以使用 RecyclerView + RecyclingImageView + 预加载 的方式。要注意 RecyclerView 的视图复用问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269