【问题标题】:How to implement a PDF viewer that loads pages asynchronously如何实现异步加载页面的 PDF 查看器
【发布时间】:2018-10-16 04:13:22
【问题描述】:

我们需要让我们的移动应用程序的用户浏览杂志,并提供快速、流畅且平台原生的体验(类似于 iBooks/Google 图书)。

我们需要的一些功能是能够查看整个杂志的缩略图,并搜索特定的文本。

问题在于我们的杂志有 140 多页,我们不能强迫我们的用户必须事先完全下载整个电子书/PDF。我们需要异步加载页面,即让用户无需完全下载内容即可开始阅读。

我研究了适用于 iOS 的 PDFKit,但是我在文档中没有发现任何关于异步下载 PDF 的内容。

是否有任何解决方案/库可以在 iOS 和 Android 上实现此功能?

【问题讨论】:

  • 您真的需要在设备上完成这项工作吗?你不能在你的后端索引杂志并让用户异步搜索某种数据库吗?也许你也可以把你的杂志分成章节,根据用户的行为异步下载第一章和后续章节。
  • 这里最好的选择是将杂志转换为 HTML。它将提供更好的用户体验,将被很好地编入索引并将在所有平台上可用。

标签: android ios pdf


【解决方案1】:

很抱歉,但没有任何librarySDK 提供asynchronously 页面加载功能。如果不下载完整的 pdf 文件,在移动设备上打开 PDF 文件几乎是不可能的。

解决方案:

我已经为此进行了研发,并满足了您在项目中的要求。我不确定iBooksGoogle books 是否在机制下使用。但是可以根据您的要求正常工作。

  • 将您的 pdf 分成 n 个部分 (例如,假设您的 pdf 有 150 页,那么每个 pdf 包含 15 页 -> 这需要网络端的一些努力。)
  • 一旦第一部分下载成功,然后将其显示给用户,其他部分异步下载。
  • 下载pdf文件的所有部分后,使用下面的代码合并pdf文件。

如何合并 PDF 文件

UIGraphicsBeginPDFContextToFile(oldFile, paperSize, nil);

for (pageNumber = 1; pageNumber <= count; pageNumber++)
{
    UIGraphicsBeginPDFPageWithInfo(paperSize, nil);

    //Get graphics context to draw the page
    CGContextRef currentContext = UIGraphicsGetCurrentContext();

    //Flip and scale context to draw the pdf correctly
    CGContextTranslateCTM(currentContext, 0, paperSize.size.height);
    CGContextScaleCTM(currentContext, 1.0, -1.0);

    //Get document access of the pdf from which you want a page
    CGPDFDocumentRef newDocument = CGPDFDocumentCreateWithURL ((CFURLRef) newUrl);

    //Get the page you want
    CGPDFPageRef newPage = CGPDFDocumentGetPage (newDocument, pageNumber);

    //Drawing the page
    CGContextDrawPDFPage (currentContext, newPage);

    //Clean up
    newPage = nil;
    CGPDFDocumentRelease(newDocument);
    newDocument = nil;
    newUrl = nil;

}

UIGraphicsEndPDFContext();

参考:How to merge PDF file.

更新: 这种机制的主要优点是逻辑对于所有设备 Android 和 iOS 设备保持相同。

【讨论】:

    【解决方案2】:

    您要查找的内容称为linearizationthis answer

    %PDF-1.x 标题行之后的第一个对象应 包含指示 /Linearized 属性的字典键 文件。

    这种整体结构允许符合要求的读者学习 对象地址的完整列表非常快速,无需 从头到尾下载完整的文件:

    • 查看器可以非常快地显示第一页,在 完整文件已下载。

    • 用户可以单击缩略图页面预览(或 ToC 中的链接) 文件)以便跳转到,比如说,第 445 页,在 已显示第一页,然后查看者可以请求所有 通过字节向远程服务器询问第 445 页所需的对象 范围请求以提供这些“乱序”,以便观众可以 更快地显示此页面。 (当用户乱序阅读页面时, 完整文档的下载仍将在 背景...)

    您可以使用this native librarylinearization PDF。

    但是 我不建议让它渲染 PDF 不会快速、流畅或感觉原生。由于这些原因,据我所知,没有本地移动应用程序可以做到linearization。此外,您必须为 PDF 创建自己的渲染引擎,因为大多数 PDF 查看库不支持 linearization 。您应该做的是在服务器端将 PDF 中的每个单独页面转换为 HTML,并让客户端仅在需要时加载页面并缓存。我们还将单独保存 PDF 计划文本以启用搜索。这样一切都会顺利,因为资源将被延迟加载。为了实现这一点,您可以执行以下操作。

    首先 在服务器端,每当您发布 PDF 时,PDF 的页面都应该被拆分为 HTML 文件,如上所述。页面缩略图也应该从这些页面生成。假设你的服务器在python 上运行,flask microframework 这就是你要做的。

    from flask import Flask,request
    from werkzeug import secure_filename
    import os
    from pyPdf import PdfFileWriter, PdfFileReader
    import imgkit
    from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
    from pdfminer.pdfpage import PDFPage
    from pdfminer.converter import XMLConverter, HTMLConverter, TextConverter
    from pdfminer.layout import LAParams
    import io
    import sqlite3
    import Image
    
    app = Flask(__name__)
    
    
    @app.route('/publish',methods=['GET','POST'])
    def upload_file():
         if request.method == 'POST':
            f = request.files['file']
            filePath = "pdfs/"+secure_filename(f.filename)
            f.save(filePath)
            savePdfText(filePath)
            inputpdf = PdfFileReader(open(filePath, "rb"))
    
            for i in xrange(inputpdf.numPages):
                output = PdfFileWriter()
                output.addPage(inputpdf.getPage(i))
                with open("document-page%s.pdf" % i, "wb") as outputStream:
                    output.write(outputStream)
                    imgkit.from_file("document-page%s.pdf" % i, "document-page%s.jpg" % i)
                    saveThum("document-page%s.jpg" % i)
                    os.system("pdf2htmlEX --zoom 1.3  pdf/"+"document-page%s.pdf" % i) 
    
        def saveThum(infile):
            save = 124,124
            outfile = os.path.splitext(infile)[0] + ".thumbnail"
            if infile != outfile:
                try:
                    im = Image.open(infile)
                    im.thumbnail(size, Image.ANTIALIAS)
                    im.save(outfile, "JPEG")
                except IOError:
                    print("cannot create thumbnail for '%s'" % infile)
    
        def savePdfText(data):
            fp = open(data, 'rb')
            rsrcmgr = PDFResourceManager()
            retstr = io.StringIO()
            codec = 'utf-8'
            laparams = LAParams()
            device = TextConverter(rsrcmgr, retstr, codec=codec, laparams=laparams)
            # Create a PDF interpreter object.
            interpreter = PDFPageInterpreter(rsrcmgr, device)
            # Process each page contained in the document.
            db = sqlite3.connect("pdfText.db")
            cursor = db.cursor()
            cursor.execute('create table if not exists pagesTextTables(id INTEGER PRIMARY KEY,pageNum TEXT,pageText TEXT)')
            db.commit()
            pageNum = 1
            for page in PDFPage.get_pages(fp):
                interpreter.process_page(page)
                data =  retstr.getvalue()
                cursor.execute('INSERT INTO pagesTextTables(pageNum,pageText) values(?,?) ',(str(pageNum),data ))
                db.commit()
                pageNum = pageNum+1
    
        @app.route('/page',methods=['GET','POST'])
        def getPage():
            if request.method == 'GET':
                page_num = request.files['page_num']
                return send_file("document-page%s.html" % page_num, as_attachment=True)
    
        @app.route('/thumb',methods=['GET','POST'])
        def getThum():
            if request.method == 'GET':
                page_num = request.files['page_num']
                return send_file("document-page%s.thumbnail" % page_num, as_attachment=True)
    
        @app.route('/search',methods=['GET','POST'])
        def search():
            if request.method == 'GET':
                query = request.files['query ']       
                db = sqlite3.connect("pdfText.db")
                cursor = db.cursor()
               cursor.execute("SELECT * from pagesTextTables Where pageText LIKE '%"+query +"%'")
               result = cursor.fetchone()
               response = Response()
               response.headers['queryResults'] = result 
               return response
    

    这里解释了烧瓶应用程序正在做什么。

    1. /publish 路由负责杂志的发布、将页面转换为 HTML、将 PDF 文本保存到 SQlite 数据库并为这些页面生成缩略图。我使用 pyPDF 将 PDF 拆分为单个页面,pdfToHtmlEx 将页面转换为 HTML,imgkit 将这些 HTML 生成为图像,PIL 从这些图像生成拇指。此外,一个简单的Sqlite db 可以保存页面的文本。
    2. /page/thumb/search 路由是不言自明的。它们只是返回 HTML、拇指或搜索查询结果。

    其次,在客户端,您只需在用户滚动到它时下载 HTML 页面。让我给你一个安卓操作系统的例子。首先,您需要创建一些 Utils 来处理 GET 请求者

    public static byte[] GetPage(int mPageNum){
    return CallServer("page","page_num",Integer.toString(mPageNum))
    }
    
    public static byte[] GetThum(int mPageNum){
    return CallServer("thumb","page_num",Integer.toString(mPageNum))
    }
    
    private  static byte[] CallServer(String route,String requestName,String requestValue) throws IOException{
    
            OkHttpClient client = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).build();
            MultipartBody.Builder mMultipartBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart(requestName,requestValue);
    
            RequestBody mRequestBody = mMultipartBody.build();
            Request request = new Request.Builder()
                    .url("yourUrl/"+route).post(mRequestBody)
                    .build();
            Response response = client.newCall(request).execute();
            return response.body().bytes();
        }
    

    上面的 helper utils 为您简单地处理对服务器的查询,它们应该是不言自明的。 接下来,您只需创建一个带有 WebView viewHolder 或更好的 advanced webviewRecyclerView,因为它将为您提供更多的自定义功能。

        public static class ViewHolder extends RecyclerView.ViewHolder {
            private AdvancedWebView mWebView;
            public ViewHolder(View itemView) {
                super(itemView);
             mWebView = (AdvancedWebView)itemView;}
        }
        private class ContentAdapter extends RecyclerView.Adapter<YourFrament.ViewHolder>{
            @Override
            public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
    
                return new ViewHolder(new AdvancedWebView(container.getContext()));
            }
    
            @Override
            public int getItemViewType(int position) {
    
                return 0;
            }
    
            @Override
            public void onBindViewHolder( ViewHolder holder, int position) {
    handlePageDownload(holder.mWebView);
            }
           private void handlePageDownload(AdvancedWebView mWebView){....}
    
            @Override
            public int getItemCount() {
                return numberOfPages;
            }
        }
    

    应该是这样的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-02-06
      • 1970-01-01
      • 1970-01-01
      • 2012-12-14
      • 2013-12-04
      • 2015-01-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多