通过 Glide 在 Android 中预览远程 PDF 时出现问题

Problem previewing remote PDF in Android via Glide

我正在尝试使用 bumptech 的 Glide 库 4.8.0 版远程预览(缩略图)PDF 文档。 为了实现这一点,按照出色的教程 Writing a custom ModelLoader,我编写了一个自定义 ModelLoader,一个用于 buildLoadData 方法的自定义 DataFetcher;添加了 AppGlideModule,实现了 ModelLoaderFactory 并注册了我的 ModelLoader。 在 DataFetcher 内部,我添加了一些逻辑来处理以下两种情况:

  1. 内容是图片。很有魅力!
  2. 内容为PDF文档。 W/Glide:加载大小为 [522x600] 的 https://www.testserver.net/folder/sample.pdf 失败 class 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,例如

您可以使用 Glide 库显示图像和视频缩略图,如果您想显示 pdf 缩略图,则需要使用该库,例如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"/>

在你的class

pdfView.fromBytes(bytes)
                            .pages(0) //show only first page
                            .spacing(0)
                            .swipeHorizontal(false)
                            .enableSwipe(false)
                            .load();

好吧,我终于从另一个角度找到了解决问题的方法。 我没有使用 Glide ModelLoader 在客户端预处理 pdf,而是想出了一个离群但有效的花招:在服务器端进行。 通过 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);
}

(借助此 link 使用 Imagick 将 pdf 转换为 jpg:How to convert pdf to a preview image in 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,但就我而言,这个解决方案非常有效。