Поиск по этому блогу

среда, 6 декабря 2017 г.

Конвертируем PDF в картинку

Случается такая фигня что бывает нужно создать читалку pdf файлов, на гитхабе есть в принципе читалки которые позволяют отображать pdf файлы в собственных вьюхах, но они очень медленные и как-то очень громоздкие, обычно все те функции что они имеют по сути и не нужны. Вот в моем случае и понадобилось просто отобразить страницы pdf файла, я решил это сделать в картинках что бы это можно было отображать в виде страниц в view pager. 

В этом примере я буду отображать только одну картинку, первую страницу файла, для простоты понимания того как это работает, что бы не нагружать проект лишними элементами.

Как всегда начинаем мы работу с пустого проекта, в который нам нужно добавить библиотеки. Библиотеки мы будем использовать PdfView, немного кастомизированную для корректной работы конвертера, так же будем использовать Dexter для попрошайничества пермишенов на запись и чтение из файловой системы, ну и ButterKnife для удобного биндинга вьюх.

Я для того что бы пофиксить PdfView скачивал его отдельно и подключал как проект, так что у меня он есть уже подключенный, если хотите можете попробовать его подключить через имплементацию чисто либу, не проект, может у вас заработает без фиксов в либе. Что конкретно я фиксил не помню так как это было давно так что проще скачать проект с моего гитхаба :). 

app/build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'

    implementation 'com.karumi:dexter:4.2.0'
    implementation project(':pdfview')

    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

Дальше после того как мы подключили все библиотеки и все прошло нормально, нам нужно создать класс который будет отвечать за работу конвертации пути файлов из непонятного для нас в понятный, что бы проще было находить сохраненные файлы после того как мы будем их получать в onActivityResult().

ImageFilePathUtils.java
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;

import java.lang.reflect.Array;
import java.lang.reflect.Method;

public class ImageFilePathUtils {
    public static String getPath(final Context context, final Uri uri) {
        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            if(uri != null) {
                if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
                    if (isExternalStorageDocument(uri)) {
                        final String docId = DocumentsContract.getDocumentId(uri);
                        final String[] split = docId.split(":");
                        final String type = split[0];
                        if ("primary".equalsIgnoreCase(type)) {
                            return Environment.getExternalStorageDirectory() + "/" + split[1];
                        }else {
                            //Below logic is how External Storage provider build URI for documents
                            StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);

                            try {
                                Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
                                Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
                                Method getUuid = storageVolumeClazz.getMethod("getUuid");
                                Method getState = storageVolumeClazz.getMethod("getState");
                                Method getPath = storageVolumeClazz.getMethod("getPath");
                                Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
                                Method isEmulated = storageVolumeClazz.getMethod("isEmulated");

                                Object result = getVolumeList.invoke(mStorageManager);

                                final int length = Array.getLength(result);
                                for (int i = 0; i < length; i++) {
                                    Object storageVolumeElement = Array.get(result, i);
                                    //String uuid = (String) getUuid.invoke(storageVolumeElement);

                                    final boolean mounted = Environment.MEDIA_MOUNTED.equals( getState.invoke(storageVolumeElement) )
                                            || Environment.MEDIA_MOUNTED_READ_ONLY.equals(getState.invoke(storageVolumeElement));

                                    //if the media is not mounted, we need not get the volume details
                                    if (!mounted) continue;

                                    //Primary storage is already handled.
                                    if ((Boolean)isPrimary.invoke(storageVolumeElement) && (Boolean)isEmulated.invoke(storageVolumeElement)) continue;

                                    String uuid = (String) getUuid.invoke(storageVolumeElement);

                                    if (uuid != null && uuid.equals(type))
                                    {
                                        String res =getPath.invoke(storageVolumeElement) + "/" +split[1];
                                        return res;
                                    }
                                }
                            }
                            catch (Exception ex) {
                            }
                        }
                    } else if (isDownloadsDocument(uri)) {
                        final String id = DocumentsContract.getDocumentId(uri);
                        final Uri contentUri = ContentUris.withAppendedId(
                                Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
                        return getDataColumn(context, contentUri, null, null);
                    } else if (isMediaDocument(uri)) {
                        final String docId = DocumentsContract.getDocumentId(uri);
                        final String[] split = docId.split(":");
                        final String type = split[0];

                        Uri contentUri = null;
                        if ("image".equals(type)) {
                            contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                        } else if ("video".equals(type)) {
                            contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                        } else if ("audio".equals(type)) {
                            contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                        }
                        final String selection = "_id=?";
                        final String[] selectionArgs = new String[]{
                                split[1]
                        };
                        return getDataColumn(context, contentUri, selection, selectionArgs);
                    }
                } else if ("content".equalsIgnoreCase(uri.getScheme())) {
                    if (isGooglePhotosUri(uri))
                        return uri.getLastPathSegment();
                    return getDataColumn(context, uri, null, null);
                } else if ("file".equalsIgnoreCase(uri.getScheme())) {
                    return uri.getPath();
                }
            }
        }
        return null;
    }

    private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };
        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }

    private static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    private static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    private static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    private static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }
}

Особо останавливаться на этом классе не буду, он стырен из stackoverflow очень давно и как-то я его до сих пор использую так как он решает кучу проблем с рисованием ссылок на файлы после получения онных в onActivityResult(). В общем нас интересует только метод getPath(), он нам возвращает реальную ссылку на файл.

Дальше мы создадим AsyncTask который будет в фоне генерировать нам нашу картинку из pdf файла. Я не захотел нагружать проект Rx'ом как я это делал в прошлых статьях так-как хотелось попроще сделать все, но если прям сильно хочется то я думаю те кто его используют, смогут перевести этот AsyncTask в вид RxAndroid'а. Если что вот тут статья про RxAndroid который делает код очень эстетически красивым.

PDFToImageTask.java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;

import org.vudroid.core.DecodeServiceBase;
import org.vudroid.core.codec.CodecPage;
import org.vudroid.pdfdroid.codec.PdfContext;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class PDFToImageTask extends AsyncTask<File, Void, String> {

    private Context context;
    private OnPDFToImageConvertedListener onPDFToImageConvertedListener;
    private String fileName;

    public PDFToImageTask(Context context) {
        this.context = context;
    }

    @Override
    protected String doInBackground(File... files) {
        File originFile = files[0];
        fileName = originFile.getName().toLowerCase().replace(".pdf", ".jpeg");
        File filePath = getCacheDir(context);
        File file = new File (filePath, fileName);
        String path = file.getPath();
        if(!new File(path).exists()) {
            try {
                DecodeServiceBase decodeService = new DecodeServiceBase(new PdfContext());
                decodeService.setContentResolver(context.getContentResolver());
                if (originFile.exists()) {
                    decodeService.open(Uri.fromFile(originFile));
                    int pageCount = decodeService.getPageCount();
                    CodecPage page = decodeService.getPage(0);
                    RectF rectF = new RectF(0, 0, 1, 1);
                    double scaleBy = Math.min(2480 / (double) page.getWidth(), 3508 / (double) page.getHeight());
                    int with = (int) (page.getWidth() * scaleBy);
                    int height = (int) (page.getHeight() * scaleBy);
                    Bitmap bitmap = page.renderBitmap(with, height, rectF);
                    try {
                        OutputStream outputStream = new FileOutputStream(new File(getCacheDir(context), System.currentTimeMillis() + ".JPEG"));
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                        outputStream.close();
                        path = saveImageAndGetURI(bitmap).toString();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return path;
    }

    @Override
    protected void onPostExecute(String uris) {
        onPDFToImageConvertedListener.onPDFToImageConverted(uris);
    }

    private Uri saveImageAndGetURI(Bitmap finalBitmap) {
        File file = new File (getCacheDir(context), fileName);
        if (file.exists ()) file.delete ();
        try {
            FileOutputStream out = new FileOutputStream(file);
            finalBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
            out.flush();
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Uri.parse(file.getPath());
    }

    private File getCacheDir(Context context) {
        return context.getCacheDir();
    }

    public void setOnPDFToImageConvertedListener(OnPDFToImageConvertedListener onPDFToImageConvertedListener) {
        this.onPDFToImageConvertedListener = onPDFToImageConvertedListener;
    }

    public interface OnPDFToImageConvertedListener {
        void onPDFToImageConverted(String imageUri);
    }
}

В этом классе нас интересует метод doInBackground() который у нас принимает адрес файла который мы хотим конвертнуть и дальше делает какую-то магию непонятную которую нам нужно разобрать. После того как мы поменяем разрешение файла в его имени и создании пустого файла в папке кеша программы мы переходим в к самому интересному, к конвертации. У нас в try идет вызов DecodeServiceBase класса который является классом библиотеки PdfView, вот он то и производит разбитие файла на фреймы и дальше мы берем первую страницу файла с помощью метода getPage(0), и делаем ее битмап и дальше сохраняем в файл с форматом jpeg. Дальше возвращаем путь к сохраненной картинке в onPostExecute() в активити для отображения.

Вот собственно и вся магия. Все очень просто, когда ты делал такое и шаришь как это делать. :) Дальше нам нужно в активити вызвать этот класс для конвертации и отобразить картинку в ImageView.

Для начала нарисуем леяут с картинкой и прогрессбаром для визуализации работы конвертера.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical|center_horizontal">

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onImageClick"
            app:srcCompat="@mipmap/ic_launcher" />

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:visibility="gone" />

    </RelativeLayout>

</LinearLayout>

Я это все поместил в RelativeLayout для красоты что бы оно лежало друг на друге, пока мы конвертируем, progressBar будет крутиться пока у нас происходит конвертация и исчезнет как только картинка будет готова к отображению.

Дальше напишем нашу activity что бы она делал все что я описывал выше. Получала pdf файл и конвертировала его в картинку и отображала в ImageView. А еще мы подписали ImageView на onClick событие что бы по нажатию на нее у нас открывался какой-то файловый менеджер для получения списка файлов.

MainActivity.java
import android.Manifest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;

import com.karumi.dexter.Dexter;
import com.karumi.dexter.MultiplePermissionsReport;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;

import java.io.File;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import project.dajver.com.pdftoimage.task.PDFToImageTask;
import project.dajver.com.pdftoimage.task.utils.ImageFilePathUtils;

public class MainActivity extends AppCompatActivity implements PDFToImageTask.OnPDFToImageConvertedListener,
        MultiplePermissionsListener {

    @BindView(R.id.image)
    ImageView imageView;
    @BindView(R.id.progressBar)
    ProgressBar progressBar;

    private int PICKFILE_REQUEST_CODE = 1213;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        Dexter.withActivity(this)
                .withPermissions(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .withListener(this)
                .check();
    }

    @OnClick(R.id.image)
    public void onImageClick() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("file/*");
        startActivityForResult(intent, PICKFILE_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(resultCode == RESULT_OK) {
            progressBar.setVisibility(View.VISIBLE);
            String fPath = ImageFilePathUtils.getPath(this, data.getData());

            PDFToImageTask pdfToImageTask = new PDFToImageTask(this);
            pdfToImageTask.setOnPDFToImageConvertedListener(this);
            pdfToImageTask.execute(new File(fPath));
        }
        super.onActivityResult(requestCode, resultCode, data);

    }

    @Override
    public void onPDFToImageConverted(String imageUri) {
        progressBar.setVisibility(View.GONE);
        Bitmap myBitmap = BitmapFactory.decodeFile(new File(imageUri).getAbsolutePath());
        imageView.setImageBitmap(myBitmap);
    }

    @Override
    public void onPermissionsChecked(MultiplePermissionsReport report) { }

    @Override
    public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) { }
}

Для начала нас интересует метод onCreate(), в котором мы подключили ButterKnife для удобного поиска вьюх в xml и Dexter для удобного запроса на пермишены для чтения и записи файлов. Далее в onImageClick() мы сделали Intent для открытия файлового менеджера, и в onActivityResult() получаем файл который был выбран, показываем ProgressBar и запускаем наш AsyncTask для конвертации. В конце всего этого действия мы отображаем нашу картинку в ImageView в методе onPDFToImageConverted() и прячем ProgressBar.

Ну и не забываем добавить пермишены в AndroidManifest для того что бы приложение знало какие пермишены нам нужны.

AndroidManifest.xml
...

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="replace" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="replace" />

...

Ну и вроде бы все, вроде ничего не забыл. Компилируем проект, выбираем картинку и пробуем конвертировать, должно все работать.

Исходники:
GitHub