Как реализовать средство просмотра PDF, которое загружает страницы асинхронно

Мы должны разрешить пользователям нашего мобильного приложения просматривать журнал с быстрым, текучим опытом и чувствовать себя родным для платформы (аналогично iBooks/Google Books).

Некоторые из них, которые нам нужны, могут видеть эскизы всего журнала и искать конкретный текст.

Проблема в том, что наши журналы занимают более 140 страниц, и мы не можем заставить наших пользователей полностью загружать всю электронную книгу /PDF заранее. Нам нужны страницы для загрузки асинхронно, то есть чтобы пользователи могли начать чтение без полной загрузки содержимого.

Я изучил PDFKit для iOS, однако я не нашел упоминания в документации по загрузке PDF асинхронно.

Существуют ли какие-либо решения/библиотеки для реализации этой функции на iOS и Android?

Ответ 1

То, что вы ищете, называется linearization и согласно этому ответу.

Первый объект сразу после строки заголовка% PDF-1.x должен содержать ключ словаря, указывающий свойство /Linearized файла.

Эта общая структура позволяет послушному читателю быстро изучить полный список адресов объектов, не загружая полный файл от начала до конца:

  • Зритель может отображать первую страницу очень быстро, до того, как будет загружен полный файл.

  • Пользователь может щелкнуть на предварительном просмотре миниатюр (или ссылку в ToC файла), чтобы перейти на, скажем, страницу 445, сразу после того, как были отображены первая страница (ы), и зритель может запросить все объекты, требуемые для страницы 445, попросив удаленный сервер через запросы диапазона байтов доставить эти "не в порядке", чтобы зритель мог быстрее отображать эту страницу. (Пока пользователь читает страницы не по порядку, загрузка полного документа будет продолжаться в фоновом режиме...)

Вы можете использовать эту родную библиотеку для linearization 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 всякий раз, когда пользователь прокручивает ее. Позвольте мне привести вам пример для ОС Android. Во-первых, вы хотите создать некоторые 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();
    }

Помощник утилит над простой обработкой запросов к серверу для вас, они должны быть понятны. Затем вы просто создаете RecyclerView с помощью WebView viewHolder или еще лучше продвинутого веб-браузера, так как это даст вам больше возможностей с настройкой.

    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;
        }
    }

Это должно быть об этом.

Ответ 2

Мне жаль говорить, но нет никакой library или SDK которая обеспечивает asynchronously загрузку страниц. На мобильном устройстве практически невозможно открыть PDF файл, не загружая полный файл PDF.

Решение:

Я уже сделал R & D для того же и выполнил ваши требования в проекте. Я не уверен, что Google books iBooks и Google books используются ниже механизма или нет. Но работает отлично в соответствии с вашими требованиями.

  • Разделите свой pdf файл на n число частей (например, предположим, что у вас 150 страниц в формате pdf, тогда каждый pdf файл содержит 15 страниц → Это потребует некоторых усилий от веб-конца).
  • Как только первая часть загрузится успешно, затем отобразите ее пользователю и другой части, загружаемой асинхронно.
  • Загрузив всю часть файла pdf, используйте ниже код файла слияния Pdf.

Как слить файл PDF

UIGraphicsBeginPDFContextToFile (oldFile, paperSize, ноль);

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();

Справка: Как объединить файл PDF.

Обновление. Основным преимуществом этого механизма является то, что Logic остается таким же для всех устройств Android и iOS.