Android 4.4 Print to PDF without User Involvement-ThrowExceptions

Exception or error:

I am working on an application (Android 4.4 — API 20) where I am generating a report in HTML format. I use the WebView object to display the report in my app.

What I would like to be able to do is convert this WebView into a pdf document.

I have been able to convert it using PdfDocument, and doing .draw onto the page from the WebView object. I save the file, and this works, except that the result is a single page document. There are no page breaks.

        View content = (View) webView;
        PrintAttributes pdfPrintAttrs = new PrintAttributes.Builder().
                setColorMode(PrintAttributes.COLOR_MODE_MONOCHROME).
                setMediaSize(PrintAttributes.MediaSize.NA_LETTER.asLandscape()).
                setResolution(new Resolution("zooey", PRINT_SERVICE, 300, 300)).
                setMinMargins(PrintAttributes.Margins.NO_MARGINS).
                build();
        PdfDocument document = new PrintedPdfDocument(mContext,pdfPrintAttrs);
        PageInfo pageInfo = new PageInfo.Builder(webView.getMeasuredWidth(), webView.getContentHeight(), 1).create();
        Page page = document.startPage(pageInfo);
        content.draw(page.getCanvas());
        document.finishPage(page);

If I change it so that I use the PrintedPdfDocumet and don’t specify the PageInfo I only get the viewable part of the WebView object.

        View content = (View) webView;
        PrintAttributes pdfPrintAttrs = new PrintAttributes.Builder().
                setColorMode(PrintAttributes.COLOR_MODE_MONOCHROME).
                setMediaSize(PrintAttributes.MediaSize.NA_LETTER.asLandscape()).
                setResolution(new Resolution("zooey", PRINT_SERVICE, 300, 300)).
                setMinMargins(PrintAttributes.Margins.NO_MARGINS).
                build();
        PrintedPdfDocument document = new PrintedPdfDocument(mContext,pdfPrintAttrs);
        Page page = document.startPage(0);
        content.draw(page.getCanvas());
        document.finishPage(page);

If I use the PrintManager and create a print adapter from the WebView object with createPrintDocumentAdapter, I can select the “Save as PDF” option and the resulting pdf file has the page breaks as I specify in the CSS of the original web page.

        PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
        PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter();
        String jobName = getString(R.string.app_name) + " Report "
                + reportName;
        PrintAttributes printAttrs = new PrintAttributes.Builder().
                setColorMode(PrintAttributes.COLOR_MODE_MONOCHROME).
                setMediaSize(PrintAttributes.MediaSize.NA_LETTER.asLandscape()).
                setMinMargins(PrintAttributes.Margins.NO_MARGINS).
                build();
        PrintJob printJob = printManager.print(jobName, printAdapter,
                printAttrs);

My question is: can I specify that I want the PrintManager to perform a “Save as PDF” and provide the name and location of the resulting file so that there is no interaction with the user?

Or: Is there a way I can convert my WebView object into a PDF and allow for page breaks.

How to solve:

It might be a late answer but I was also in need of similar solution with Print Framework so far, and I splitted the Pdf Document into pages with the code below.

As far as I can see, you cannot really make the WebView or Pdf Document splits your pdf file into pages in a smart way (not cutting the text or image). But what we can do is to create Pages in a ratio of A4 or Letter size, so it can fit into print out paper format.

But there is another issue I’m facing. The code below works as expected in Android 4.4 but not in later versions. In Android-L, only the visible part of WebView is drawn into Pdf File, but white blank pages for the rest of the HTML in WebView.

According to documentation,

public static void enableSlowWholeDocumentDraw ()

For apps targeting the L release, WebView has a new default behavior that reduces memory footprint and increases performance by intelligently choosing the portion of the HTML document that needs to be drawn. These optimizations are transparent to the developers. However, under certain circumstances, an App developer may want to disable them:

  • When an app uses onDraw(Canvas) to do own drawing and accesses portions of the page that is way outside the visible portion of the page.
  • When an app uses capturePicture() to capture a very large HTML document. Note that capturePicture is a deprecated API.

Enabling drawing the entire HTML document has a significant performance cost. This method should be called before any WebViews are created.

I’ve created a Bug Report, and commented on a similar bug report HERE, but no response so far. But until then, you can use the code below.

/**
 * Creates a PDF Multi Page Document depending on the Ratio of Letter Size.
 * This method does not close the Document. It should be Closed after writing Pdf Document to a File.
 *
 * @return
 */
private PdfDocument createMultiPagePdfDocument(int webViewWidth, int webViewHeight) {

    /* Find the Letter Size Height depending on the Letter Size Ratio and given Page Width */
    int letterSizeHeight = getLetterSizeHeight(webViewWidth);

    PdfDocument document = new PrintedPdfDocument(getActivity(), getPrintAttributes());

    final int numberOfPages = (webViewHeight/letterSizeHeight) + 1;

    for (int i = 0; i < numberOfPages; i++) {

        int webMarginTop = i*letterSizeHeight;

        PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(webViewWidth, letterSizeHeight, i+1).create();
        PdfDocument.Page page = document.startPage(pageInfo);

        /* Scale Canvas */
        page.getCanvas().translate(0, -webMarginTop);
        mWebView.draw(page.getCanvas());

        document.finishPage(page);
    }

    return document;
}


/**
 * Calculates the Letter Size Paper's Height depending on the LetterSize Dimensions and Given width.
 *
 * @param width
 * @return
 */
private int getLetterSizeHeight(int width) {
    return (int)((float)(11*width)/8.5);
}

###

Not sure if this will solve your page-break issues, but have you considered using the open-source wkHTMLtoPDF library (http://wkhtmltopdf.org/) for the conversion from HTML to PDF? We have used it extensively by creating a micro-service that we pass the HTML code to, then have the service convert it to PDF and return the link to the PDF, or alternatively have it return the PDF (depending on size). I know using an external service for the conversion might be a pain (or maybe you don’t have internet access from the device), but if that’s not an issue, then this could be an option. There may be other APIs available to do this conversion as well. One such API is Neutrino API. There are many others – you can search for APIs using one of these API search engines:

  1. apis.io
  2. Progammable Web
  3. Public APIs

###

After spending enormous time with this problem, I used DexMaker to implement non public abstract callbacks and came up with this:

@Override
protected void onPreExecute() {
    super.onPreExecute();
    printAdapter = webView.createPrintDocumentAdapter();
}

@Override
protected Void doInBackground(Void... voids) {

    File file = new File(pdfPath);
    if (file.exists()) {
        file.delete();
    }
    try {
        file.createNewFile();

        // get file descriptor
        descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);

        // create print attributes
        PrintAttributes attributes = new PrintAttributes.Builder()
                .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
                .setResolution(new PrintAttributes.Resolution("id", PRINT_SERVICE, 300, 300))
                .setColorMode(PrintAttributes.COLOR_MODE_COLOR)
                .setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0))
                .build();
        ranges = new PageRange[]{new PageRange(1, numberPages)};

        // dexmaker cache folder
        cacheFolder =  new File(context.getFilesDir() +"/etemp/");

        printAdapter.onStart();

        printAdapter.onLayout(attributes, attributes, new CancellationSignal(), getLayoutResultCallback(new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {

                if (method.getName().equals("onLayoutFinished")) {
                    onLayoutSuccess();
                } else {
                    Log.e(TAG, "Layout failed");
                    pdfCallback.onPdfFailed();
                }
                return null;
            }
        }, cacheFolder), new Bundle());
    } catch (IOException e) {
        e.printStackTrace();
        Log.e(TAG, e != null ? e.getMessage() : "PrintPdfTask unknown error");
    }
    return null;
}

private void onLayoutSuccess() throws IOException {
    PrintDocumentAdapter.WriteResultCallback callback = getWriteResultCallback(new InvocationHandler() {
        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            if (method.getName().equals("onWriteFinished")) {
                pdfCallback.onPdfCreated();
            } else {
                Log.e(TAG, "Layout failed");
                pdfCallback.onPdfFailed();
            }
            return null;
        }
    }, cacheFolder);
    printAdapter.onWrite(ranges, descriptor, new CancellationSignal(), callback);
}


/**
 * Implementation of non public abstract class LayoutResultCallback obtained via DexMaker
 * @param invocationHandler
 * @param dexCacheDir
 * @return LayoutResultCallback
 * @throws IOException
 */
public static PrintDocumentAdapter.LayoutResultCallback getLayoutResultCallback(InvocationHandler invocationHandler,
                                                                                File dexCacheDir) throws IOException {
    return ProxyBuilder.forClass(PrintDocumentAdapter.LayoutResultCallback.class)
            .dexCache(dexCacheDir)
            .handler(invocationHandler)
            .build();
}

/**
 * Implementation of non public abstract class WriteResultCallback obtained via DexMaker
 * @param invocationHandler
 * @param dexCacheDir
 * @return LayoutResultCallback
 * @throws IOException
 */
public static PrintDocumentAdapter.WriteResultCallback getWriteResultCallback(InvocationHandler invocationHandler,
                                                                              File dexCacheDir) throws IOException {
    return ProxyBuilder.forClass(PrintDocumentAdapter.WriteResultCallback.class)
            .dexCache(dexCacheDir)
            .handler(invocationHandler)
            .build();
}

Leave a Reply

Your email address will not be published. Required fields are marked *