Search code examples
androidpdfappglidemodule

Problem previewing remote PDF in Android via Glide


I'm trying to preview (thumbnail) PDF documents that are remotely, using the Glide library from bumptech, version 4.8.0. To achieve this, following the excellent tutorial Writing a custom ModelLoader, I've written a custom ModelLoader, a custom DataFetcher for the buildLoadData method; added the AppGlideModule, implemented ModelLoaderFactory and registered my ModelLoader. Inside the DataFetcher I've added some logic to process the following two cases:

  1. The content is an image. Works like a charm!
  2. The content is a PDF document. W/Glide: Load failed for https://www.testserver.net/folder/sample.pdf with size [522x600] class com.bumptech.glide.load.engine.GlideException: Failed to load resource

One approach has been to download the PDF file locally, and then render it (this DOES work), but it adds a considerable delay when having to download a file from a url and copy it locally; on the other hand, it doesn't take advantage of Glide's use of the cache.

Should I add another extra ModelLoader to use OkHttp3 instead of Volley (default)? Any ideas? Thanks in advance!

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

Finally, after all of the above, the call is of the form:

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

Where "resouce_url" could be: https://www.testserver.net/folder/sample.pdf, eg.


Solution

  • Well, I've finally found a method to solve the problem, from another perspective. Instead of preprocessing the pdf on client-side, using a Glide ModelLoader, I've come up with an outlier but effective subterfuge: do it on server-side. By means of the php Imagick extension, I've modified the server api so that it automatically generates a thumbnail on the server (in the "upload.php" module), same path where the pdf is saved. Thus, assuming that we have the pdf already loaded, we do the following:

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

    (with the help of this link on using Imagick to convert pdf to jpg: How to convert pdf to a preview image in PHP).

    On the other hand, when a record is deleted, the attachments that it may have associated must also be deleted, if any. This makes it necessary to also delete the thumbnail, in the same act, as shown below:

    // 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");
    }
    

    Now we have a set of files, some jpg/png and another pdf; but this is indifferent for Glide, which will only show jpg/png images, without any problem, even if they are remotely; and of course very quickly. The code on client-side is:

    /* 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");
    }
    

    Although maybe not everyone can have Imagick on the server, in my case this solution has worked wonderfully.