Tero Heiskanen
Tero Heiskanen

Reputation: 519

Android - Drawing to a PDF canvas from WebView

I've been having troubles getting PDF printing work on Android. What I'm trying to do is render some HTML in WebView, then draw the WebView contents on a PDF canvas and finally write the PDF to a file. The problem I'm having is that when I draw to the PDF canvas the content gets clipped even though there is plenty of canvas left. I've tried resizing the canvas using the .clipRect(Rect rect, Op op) and that kind of worked but not as well as I would've liked.

I also have no idea how I can translate the HTML px measurements to the PDF PostScript 1/72th inch measurements reliably.

Here's the code I'm using:

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

    WebView wv = (WebView) this.findViewById(R.id.webView1);

    wv.loadUrl("file:///android_asset/temp.html");           
}

public void button1onClick(View v)
{
    //Create PDF document
    PdfDocument doc = new PdfDocument();

    //Create A4 sized PDF page
    PageInfo pageInfo = new PageInfo.Builder(595,842,1).create();

    Page page = doc.startPage(pageInfo);

    WebView wv = (WebView) this.findViewById(R.id.webView1);

    page.getCanvas().setDensity(200);

    //Draw the webview to the canvas
    wv.draw(page.getCanvas());

    doc.finishPage(page);

    try
    {
        //Create the PDF file
        File root = Environment.getExternalStorageDirectory();          
        File file = new File(root,"webview.pdf");
        FileOutputStream out = new FileOutputStream(file);
        doc.writeTo(out);
        out.close();
        doc.close();

        //Open the PDF
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.fromFile(file), "application/pdf");
        intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
        startActivity(intent);          
    }
    catch(Exception e)
    {
        throw new RuntimeException("Error generating file", e);
    }
}

Basically the program just loads the temp.html file to webview and renders me a button that I can use to create the PDF.

The temp.html file looks like:

<html>
<head>
    <style>
        div.border
        {
            width:600px;
            height:800px;
            border:1px solid black;
        }
    </style>
</head>
<body>
    <div class="border"></div>
</body>

And here is the result with manually added black border to show the scale:

enter image description here

I would really appreciate some tips on how to convert HTML to PDF reliably on Android without using libraries that require licenses for commercial use.

Upvotes: 16

Views: 8829

Answers (4)

klimat
klimat

Reputation: 24991

I had the same problem.

I solved it using very simple trick.

Just set MediaSize to PrintAttributes.MediaSize.ISO_A1.

The downside of this solution is the pdf size: even one-page-pdf with simple text has roughly 5MB.

Working snippet of code (generates a pdf from a view and export it to a file):

@TargetApi(19)
private void generatePdf() {
    PrintAttributes.Builder builder = new PrintAttributes.Builder();
    builder.setColorMode(PrintAttributes.COLOR_MODE_COLOR);
    builder.setMediaSize(PrintAttributes.MediaSize.ISO_A1); // or ISO_A0
    builder.setMinMargins(PrintAttributes.Margins.NO_MARGINS);
    builder.setResolution(new PrintAttributes.Resolution("1", "label", 300, 300));
    PrintedPdfDocument document = new PrintedPdfDocument(this, builder.build());
    PdfDocument.Page page = document.startPage(1);
    View content = yourView;
    content.draw(page.getCanvas());
    document.finishPage(page);
    try {
        File file = new File(getExternalFilesDir(null).getAbsolutePath(), "document.pdf");
        document.writeTo(new FileOutputStream(file));
    } catch (IOException e) {
        Log.e("cannot generate pdf", e);
    }
    document.close();
}

Upvotes: 0

Jon Goodwin
Jon Goodwin

Reputation: 9153

Here is where it gets interesting. What about those hard coded values for the canvas ?:-

 PageInfo pageInfo = new PageInfo.Builder(595,842,1).create();

You need the width and height of the WebView CONTENTS, after you loaded your HTML. YES you do, but there is no getContentWidth method (only a view port value), AND the getContentHeight() is inaccurate !

Answer: sub-class WebView:

/*
  Jon Goodwin
*/
package com.example.html2pdf;//your package

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.webkit.WebView;

class CustomWebView extends WebView
{
    public int rawContentWidth   = 0;                         //unneeded
    public int rawContentHeight  = 0;                         //unneeded
    Context    mContext          = null;                      //unneeded

    public CustomWebView(Context context)                     //unused constructor
    {
        super(context);
        mContext = this.getContext();
    }   

    public CustomWebView(Context context, AttributeSet attrs) //inflate constructor
    {
        super(context,attrs);
        mContext = context;
    }

    public int getContentWidth()
    {
        int ret = super.computeHorizontalScrollRange();//working after load of page
        rawContentWidth = ret;
        return ret;
    }

    public int getContentHeight()
    {
        int ret = super.computeVerticalScrollRange(); //working after load of page
        rawContentHeight = ret;
        return ret;
    }

    public void onPageFinished(WebView page, String url)
    {
        //never gets called, don't know why, but getContentHeight & getContentWidth function after load of page
        rawContentWidth  =  ((CustomWebView) page).getContentWidth();
        rawContentHeight =  ((CustomWebView) page).getContentHeight();

        Log.e("CustomWebView:onPageFinished","ContentWidth: " + ((CustomWebView) page).getContentWidth());
        Log.e("CustomWebView:onPageFinished","ContentHeight: " + ((CustomWebView) page).getContentHeight());
    }

//=========
}//class
//=========

In my modified code (in the other answer) change:

private CustomWebView wv;
    wv = (CustomWebView) this.findViewById(R.id.webView1);

    int my_width  = wv.getContentWidth();
    int my_height = wv.getContentHeight();

and change your layout class entry from WebView to com.example.html2pdf.CustomWebView.

then you good to go !

Upvotes: 1

Jon Goodwin
Jon Goodwin

Reputation: 9153

Summary: Don't modify the density, (it should be set on your device, probably to medium 160 dpi) instead, use scale. If you Just need Bitmaps of your HTML page in your PDF (No hyper-link function), this works. This is what your code is generating, with the following code:

    //Create PDF document

        PdfDocument doc = new PdfDocument();

        //Create A4 sized PDF page
        int my_width  = 595;
        int my_height = 842;

        PageInfo pageInfo = new PageInfo.Builder(my_width,my_height,1).create();
//      PageInfo pageInfo = new PageInfo.Builder(650,850,1).create();

        Page page = doc.startPage(pageInfo);

        WebView wv = (WebView) this.findViewById(R.id.webView1);

        Canvas canvas = page.getCanvas();
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        final DisplayMetrics displayMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(displayMetrics);
        int    height  = displayMetrics.heightPixels;
        int    width   = displayMetrics.widthPixels;
        float  density = displayMetrics.density;
        int    wvWidth = wv.getWidth();
        int    wvHeight= wv.getHeight();
        float  wvScaleX= wv.getScaleX();
        float  wvScaleY= wv.getScaleY();

//      canvas.setDensity(100);//200 Bitmap.DENSITY_NONE
        int cdensity = canvas.getDensity();
        float scaleWidth = (float)width/(float)my_width;
        float scaleHeight = (float)height/(float)my_height;
        canvas.scale(scaleWidth, scaleHeight);
        Log.e("button1onClick","canvas width:" + canvas.getHeight() + " canvas height:" +  canvas.getWidth());
        Log.e("button1onClick","metrics width:" + width + " metrics height:" +  height + "metrics density:" +  density);
        Log.e("button1onClick"," wvWidth:" + wvWidth + " wvHeight:" +  wvHeight);
        Log.e("button1onClick"," scaleWidth: " + scaleWidth +
                " scaleHeight:" +  scaleHeight +" cdensity:" + cdensity);
        Paint paint = new Paint();
//      paint.setStyle(Style.FILL);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(1);

        //Draw the webview to the canvas
        wv.draw(canvas);
        canvas.scale(1f, 1f);
        canvas.drawRect(0, 0, canvas.getWidth()-1,  canvas.getHeight()-1, paint);
        canvas.drawText("Direct drawn Red Rectangle to fill page canvas 0, 0," +
                canvas.getWidth() + "," + canvas.getHeight(), 100, 100, paint);

        doc.finishPage(page);

webview.pdf

This works well (Hyper-links cannot work of course). More complex example: more complex example

Upvotes: 4

Jon Goodwin
Jon Goodwin

Reputation: 9153

Basically for me, it all comes down to Hyper-Link and external .css support (cascaded style sheets) for (X)HTML to PDF (classes).

        div border is not supported on android in anyway I have found in free to use code.

div color yes, so what. PdfDocument. (API 19 or above). maybe a better lib itextg (API16 maybe less)(itext subset omitting android framework un-allowed classes). (uses XMLWorkerHelper class) (div border == no) but td == yes yippi ! (a border element supported in pdf). Time for itextg, maybe. conformance: http://demo.itextsupport.com/xmlworker/itextdoc/CSS-conformance-list.htm Pretty pathetic. Carry on... Not sure what you want, if it's just a border I can do that on a td element. Pretty sure you want more than that. Carry on... I can do external .css file read with supported elements(see link), quite cool. (flying saucer... does not fly for me "JAVA Library" NOT android supported).

So this kind of thing:

public boolean createPDF(String htmlText, String absoluteFilePath) throws DocumentException, CssResolverException
    {
        try
        {
            // step 1 new doc
            Document document = new Document();

            // step 2 create PdfWriter

            PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(absoluteFilePath));

            writer.setInitialLeading(12.5f);

            // step 3 open doc
            document.open();
            document.add(new Chunk("")); //

            HtmlPipelineContext htmlContext = new HtmlPipelineContext(null);

            htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory());
            CSSResolver cssResolver = null;
        if(true)
        {
            // step 4 CSS
            cssResolver = new StyleAttrCSSResolver();
            java.io.InputStream csspathtest = null;
            try {
                csspathtest =  getResources().getAssets().open("itextweb.css");
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            CssFile cssfiletest = XMLWorkerHelper.getCSS(csspathtest);
            cssResolver.addCss(cssfiletest);  
            Log.i("cssfiletest",cssfiletest.toString());
            }
        else
        {
            cssResolver = XMLWorkerHelper.getInstance().getDefaultCssResolver(false);   
            cssResolver.addCss("td {border-right: white .1px solid;}", true);
            cssResolver.addCss("div {border: green 2px solid;}", true);
        }           

            Pipeline<?> pipeline =  new CssResolverPipeline(cssResolver, new HtmlPipeline(htmlContext, new PdfWriterPipeline(document, writer)));

            XMLWorker worker1 = new XMLWorker(pipeline, true);
            XMLParser p = new XMLParser(worker1);
            ByteArrayInputStream inputRawHTML = new ByteArrayInputStream(htmlText.getBytes());

            Tidy tidy = new Tidy(); // obtain a new Tidy instance
            tidy.setXHTML(true); // set desired config options using tidy setters
            ByteArrayOutputStream output = new ByteArrayOutputStream();
//          tidy.setCharEncoding(Configuration.UTF8);
            tidy.parse(inputRawHTML, output);
            String preparedText = output.toString("UTF-8");

            Log.i("CHECKING", "JTidy Out: " + preparedText);

            ByteArrayInputStream inputPREP = new ByteArrayInputStream(preparedText.getBytes());

            // step 5 parse html
            p.parse(inputPREP); 

So some images:

for HTML:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html >
<head>
    <title>My first web page</title>

    <LINK REL=StyleSheet HREF="itextweb.css" TYPE="text/css" MEDIA=screen>
</head>
<body>
<!-- The <div> tag enables you to group sections of HTML elements together and format them with CSS.-->
    <div>
    <p>helloworld with green border if style worked</p>
    </div>

        <div>
        <h1>helloworld with green border if style worked</h1>
    </div>
<div style="border: 3px yellow solid">
<p>"SHOULD be red text if p style worked, else yellow border from div style" </p>
other text div yellow inline border
</div>
<div style="color: red">red text if div style worked</div>


    <h2>unsorted list</h2>
    <ul>
        <li>To learn HTML</li>
        <li>To show off</li>
    </ul>
<table>
    <tr>
        <td>Row 1, cell 1</td>
        <td>Row 1, cell 2</td>
        <td>Row 1, cell 3</td>
    </tr>
</table>
<textarea rows="5" cols="20">A big load of text</textarea>
<a href="http://www.htmldog.com">blue HTML Dog link</a>
</body>
</html>

input html in browser output pdf file > Note td elements have borders !

Upvotes: 1

Related Questions