Проблема с предварительным просмотром удаленного PDF-файла в Android через Glide

avatar
djmm68
8 августа 2021 в 20:25
576
2
1

Я пытаюсь просмотреть (эскизы) PDF-документов удаленно, используя библиотеку Glide от bumptech, версия 4.8.0. Чтобы добиться этого, следуя превосходному руководству Написание пользовательского ModelLoader, я написал собственный ModelLoader, пользовательский DataFetcher для метода buildLoadData; добавил AppGlideModule, реализовал ModelLoaderFactory и зарегистрировал свой ModelLoader. Внутри DataFetcher я добавил некоторую логику для обработки следующих двух случаев:

  1. Содержимое представляет собой изображение. Работает как шарм!
  2. Содержимое представляет собой документ в формате PDF. W/Glide: Ошибка загрузки для https://www.testserver.net/folder/sample.pdf с размером [522x600] класс com.bumptech.glide.load.engine.GlideException: не удалось загрузить ресурс

Один из подходов заключался в том, чтобы загрузить PDF-файл локально, а затем отобразить его (это ДЕЙСТВИТЕЛЬНО работает), но это добавляет значительную задержку, когда приходится загружать файл с URL-адреса и копировать его локально; с другой стороны, он не использует кэш Glide.

Должен ли я добавить еще один дополнительный загрузчик ModelLoader для использования OkHttp3 вместо Volley (по умолчанию)? Любые идеи? Заранее спасибо!

    public final class MyModelLoader implements ModelLoader<File, InputStream> {
    private final Context context;

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

    @NonNull
    @Override
    public ModelLoader.LoadData<InputStream> buildLoadData(@NonNull File model, int width, int height, @NonNull Options options) {
        return new ModelLoader.LoadData<>(new ObjectKey(model), new MyDataFetcher(context, model));
    }

    @Override
    public boolean handles(@NonNull File file) {
        return true;
    }
}

    public class MyDataFetcher implements DataFetcher<InputStream> {
    @SuppressWarnings("FieldCanBeLocal")
    private final Context context;
    private final File file;

    private InputStream inputStream;

    public MyDataFetcher(Context context, File file) {
        this.context = context;
        this.file = file;
    }

    @Override
    public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
        try {
            if (isPdf(file)) {
                //We have a PDF document in "file" -- fail (if document is remote)
                try {
                    //render first page of document PDF to bitmap, and pass to method 'onDataReady' as a InputStream
                    PdfRenderer pdfRenderer = new PdfRenderer(ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY));
                    PdfRenderer.Page page = pdfRenderer.openPage(0);
                    int width = 2048;
                    int height = (page.getHeight() * (width / page.getWidth()));
                    Bitmap pageBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                    page.render(pageBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                    pageBitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream);
                    ByteArrayInputStream stream = new ByteArrayInputStream(outputStream.toByteArray());
                    callback.onDataReady(stream);
                } catch (IOException ignored) {}
            } else {
                //We have an image in "file" -- OK
                FileInputStream fileInputStream = new FileInputStream(file);
                callback.onDataReady(fileInputStream);
            }
        } catch (IOException ignored) {}
    }

    // checks for file content
    public boolean isPdf(File f) throws IOException {
        URLConnection connection = f.toURL().openConnection();
        String mimeType = connection.getContentType();
        return mimeType.equals("application/pdf");
    }

    @Override
    public void cleanup() {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException ignored) {}
        }
    }

    @Override
    public void cancel() {
        //empty
    }

    @NonNull
    @Override
    public Class<InputStream> getDataClass() {
        return InputStream.class;
    }

    @NonNull
    @Override
    public DataSource getDataSource() {
        return DataSource.REMOTE;
    }
}

    public class MyModelLoaderFactory  implements ModelLoaderFactory<File, InputStream> {
    private final Context context;

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

    @NonNull
    @Override
    public ModelLoader<File, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
        return new MyModelLoader(context);
    }

    @Override
    public void teardown() {
        //empty
    }
}


    @GlideModule public class MyAppGlideModule extends AppGlideModule {
    @Override
    public void registerComponents(@NonNull Context context, @NonNull Glide glide, Registry registry) {
        registry.prepend(File.class, InputStream.class, new MyModelLoaderFactory(context));
    }
}

Наконец, после всего вышеперечисленного вызов имеет форму:

GlideApp.with(image.getContext()).load("resource_url").into(image);

Где "resouce_url" может быть: https://www.testserver.net/folder/sample.pdf, например,

Источник
blackapps
8 августа 2021 в 21:34
0

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

djmm68
10 августа 2021 в 18:43
0

Да, собственно, это то, что я имею в виду. Я действительно не хочу просматривать весь документ в формате PDF, просто просмотрите первую страницу в виде эскиза.

Ответы (2)

avatar
djmm68
27 августа 2021 в 20:44
0

Ну, наконец-то я нашел способ решить проблему с другой точки зрения. Вместо предварительной обработки pdf на стороне клиента с помощью Glide ModelLoader я придумал необычную, но эффективную уловку: сделайте это на стороне сервера. С помощью расширения php Imagick я изменил API-интерфейс сервера, чтобы он автоматически генерировал миниатюру на сервере (в модуле «upload.php»), по тому же пути, по которому сохраняется PDF-файл. Таким образом, предполагая, что у нас уже загружен pdf, делаем следующее:

// Create thumbnail, if pdf
if ($ext == 'pdf') {
    $im = new imagick($base_path.$next_id["next"].".".$ext.'[0]');
    $im->setImageFormat('jpg');
    header('Content-Type: image/jpeg');
    file_put_contents($base_path.$next_id["next"]."_thumbnail.jpg", $im);
}

(с помощью этой ссылки на использование Imagick для преобразования pdf в jpg: Как преобразовать pdf в изображение для предварительного просмотра в PHP).

С другой стороны, когда запись удаляется, связанные с ней вложения также должны быть удалены, если таковые имеются. Это приводит к необходимости также удалить миниатюру в том же действии, как показано ниже:

.
// Remove uploaded file from server
unlink($base_path.$id.".".$ext);
        
// If pdf, we also have to remove the thumbnail
if ($ext == 'pdf') {
    unlink($base_path.$id."_thumbnail.jpg");
}

Теперь у нас есть набор файлов, несколько jpg/png и еще один pdf; но это безразлично для Glide, который без проблем будет показывать только изображения jpg/png, даже если они находятся удаленно; и конечно очень быстро. Код на стороне клиента:

/* Have a pdf file, eg. "sample.pdf", Glide will load a file
   with this name: "sample_thumbnail.jpg",
   that contains first page of pdf file (preview)
   (A single tap on one element will download the file
   and launch an intent to display it)
*/
if (item.getType().equals("jpg") || item.getType().equals("jpeg") || item.getType().equals("png")) {
    Glide.with(image.getContext()).load(item.getPath()).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(image);
} else if (item.getType().equals("pdf")) {
    Glide.with(image.getContext()).load(getName(item.getPath()) + "_thumbnail.jpg").diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(image);
} else {
    throw new Exception("File type not supported");
}

Хотя, возможно, не у всех есть Imagick на сервере, в моем случае это решение отлично сработало.

avatar
AtifSayings
9 августа 2021 в 05:57
0

Вы можете отображать миниатюры изображений и видео с помощью библиотеки Glide. Android PdfViewer. Затем вместо использования ImageView используйте PdfView и загружайте только первую страницу вместо всех, например. .pages(0). Вот и все. В вашем xml/layout

<com.github.barteksc.pdfviewer.PDFView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

В вашем классе

pdfView.fromBytes(bytes)
                            .pages(0) //show only first page
                            .spacing(0)
                            .swipeHorizontal(false)
                            .enableSwipe(false)
                            .load();
djmm68
9 августа 2021 в 12:46
0

Я пробовал эту конкретную библиотеку и получил тот же результат (E/PDFView: ошибка загрузки pdf - java.io.FileNotFoundException: нет такого файла или каталога). Как подробно описано в документации самой библиотеки: Почему я не могу открыть PDF с URL? Загрузка файлов — это длительный процесс, который должен учитывать жизненный цикл Activity, должен поддерживать некоторую настройку, очистку данных и кэширование, поэтому создание такого модуля, вероятно, закончится созданием новой библиотеки. Однако пока я продолжу этот путь, пробую другие библиотеки.

AtifSayings
10 августа 2021 в 07:33
0

в моем случае файлы PDF находятся в хранилище firebase, поэтому я получаю pdf в виде байтов, используя встроенный метод хранилища firebase, а затем показываю. Это работает как шарм.

djmm68
10 августа 2021 в 15:54
0

Pdf в байт, это ключ, я думаю - я мог бы использовать метод, который дает мне pdf таким образом, как это делает Firebase Storage, поэтому мне не нужно предварительно обрабатывать его в DataFetcher.