Printing a Form from ODK Collect using XSLT

Hi there, I'm attempting to modify ODK Collect so that I can print out Form Instances directly from a mobile device to a Wi-Fi printer.

I'm new to programming and so it's been a bit of a jump in the deep end but I have got somewhere at last. However I need some pointers!

I've managed to write an app that will take a hardcoded XML file in res/raw and use XSLT to transform it into HTML which Google's WebView then prints out successfully.

What I want to do now is replace the hardcoded XML file with the desired XML of the Form Instance that the user wants to print out.

But I'm stuck!

I know there are XML files for each Form Instance but I can't work out how they are linked back to the Form Instance that you see when using the UI.

When looking at a particular Form Instance in the UI would it be possible to retrieve a URI for its XML file?

If so then I could simply add this print button to my Form UI and have my print code fetch the relevant XML file and print it upon pressing the 'Print' button.

See screenshot:

52b75a741bccdac5d6f8d890ed58bad4a7a02204_2_281x500

Any help much appreciated!

Cheers

1 Like

The hierarchy activity has access to the global FormController through Collect.getInstance().getFormController(). You can call getInstanceFile to get a reference to the XML document that represents the filled instance.

2 Likes

Fantastic, thanks @LN!

Can't wait to have a go at it tomorrow! :grinning:

I'll be so chuffed if I can get this to work...

1 Like

Ok, so I'm having a bit of trouble here, I keep getting an 'Unhandled exception: java.io.IOExceptionยด with whichever method I attempt to convert the File to a String.

At the moment I'm trying:

    File InstanceXmlFile = Collect.getInstance().getFormController().getInstanceFile();

    String strXML = FileUtils.readFileToString(InstanceXmlFile, "utf-8");

@LN What am I doing wrong?

Ah ok, I think I had to add a catch statement?

Seem to be sorted now, although I can't quite get the end result to work. Hmm, back to it...

Wow, I have success! :grin:

1 Like

Just some proof :grin:

Screenshot_20200214-164902 (1)

Not a very elegant solution as we still need to update the XSLT for each new Form Template that we want to print out, but it serves our needs at the minute!

If anyone is interested here is the main PrintFormInstanceActivity code:

package org.odk.collect.android.activities;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintJob;
import android.print.PrintManager;
import android.util.Log;
import android.view.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;


import org.apache.commons.io.FileUtils;
import org.odk.collect.android.R;
import org.odk.collect.android.application.Collect;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

public class PrintFormInstanceActivity extends Activity {
    public static final String TAG = "YOUR-TAG-NAME";
    private WebView mWebView;
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getWindow().requestFeature(Window.FEATURE_PROGRESS);

        WebView webview = new WebView(this);
        setContentView(webview);

        //Reading XSLT
        String strXSLT = GetStyleSheet(R.raw.xsltfile);

        //Reading XML

        File InstanceXmlFile = Collect.getInstance().getFormController().getInstanceFile();

        String strXML = null;
        try {
            strXML = FileUtils.readFileToString(InstanceXmlFile, "utf-8");
        } catch (IOException e) {
            e.printStackTrace();
        }


        //Transform ...
        String html = StaticTransform(strXSLT, strXML);


        //Loading the above transformed XSLT in to Webview...
        webview.loadData(html, "text/html", null);


        WebView webView = new WebView(this);
        webView.setWebViewClient(new WebViewClient() {

            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                return false;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                Log.i(TAG, "page finished loading " + url);
                createWebPrintJob(view);
                mWebView = null;
            }
        });

        webView.loadDataWithBaseURL(null, html, "text/HTML", "UTF-8", null);

        mWebView = webView;


    }

    /** Google's WebView print code **/

    private void createWebPrintJob(WebView webView) {

        // Get a PrintManager instance
        PrintManager printManager = (PrintManager) this
                .getSystemService(Context.PRINT_SERVICE);

        String jobName = getString(R.string.app_name) + " Document";

        // Get a print adapter instance
        PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter(jobName);

        // Set default page size to A4
        PrintAttributes.Builder builder = new PrintAttributes.Builder();
        builder.setMediaSize( PrintAttributes.MediaSize.ISO_A4);

        // Create a print job with name and adapter instance
        PrintJob printJob = printManager.print(jobName, printAdapter, builder.build());


        // Save the job object for later status checking
        // printJobs.add(printJob);
    }


    /**
     * Transform XML and XSLT to HTML string
     **/
    public static String StaticTransform(String strXsl, String strXml) {
        String html = "";

        try {

            InputStream ds = null;
            ds = new ByteArrayInputStream(strXml.getBytes("UTF-8"));

            Source xmlSource = new StreamSource(ds);

            InputStream xs = new ByteArrayInputStream(strXsl.getBytes("UTF-8"));
            Source xsltSource = new StreamSource(xs);

            StringWriter writer = new StringWriter();
            Result result = new StreamResult(writer);
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer(xsltSource);
            transformer.transform(xmlSource, result);

            html = writer.toString();

            ds.close();
            xs.close();

            xmlSource = null;
            xsltSource = null;

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (TransformerConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerFactoryConfigurationError e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return html;
    }

    /**
     * Read XSLT file from res/raw...
     **/
    private String GetStyleSheet(int fileId) {
        String strXsl = null;

        InputStream raw = getResources().openRawResource(fileId);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        int size = 0;
        // Read the entire resource into a local byte buffer.
        byte[] buffer = new byte[1024];
        try {
            while ((size = raw.read(buffer, 0, 1024)) >= 0) {
                outputStream.write(buffer, 0, size);
            }
            raw.close();

            strXsl = outputStream.toString();

            Log.v("Log", "xsl ==> " + strXsl);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return strXsl;

    }



}

Was a bit hesitant about sharing the code as I know it's pretty horrendous, but it's my first shot at coding so I guess I don't even know how bad it really is :sweat_smile:

P.s. there maaay be some memory leak issues. I tried to narrow it down with the profiler but all I could see was something related to the GNSS stuff, so not sure if I did something or it's always like that?

2 Likes

Well done!

Yes, creating a generic XSLT script that'll handle an arbitrary XForm is a bit trickier, since you have then to extract the question info from the XML form def (or pull it out of Collects's internal data structures). But this is still a great accomplishment, and may inspire others to also take up the challenge... :wink:

3 Likes

Thanks!

Yeah I thought I'd leave the catch-all scenario for another time haha.

It will definitely be harder to design something that makes all those possible nested groups look half-decent in all cases.

Would be an interesting challenge though!

Hello. I have an urgent need to leave a copy of your answers to my respondents. This version allows you to obtain a PDF document of the questionnaire answered? In case it is so, how do I have this "version" of collect on my device? I have no knowledge of code. I'm from Paraguay. Best regards

Hi @marceloscarone, I have the version that allows PDF printing stored in a repository on my GitHub. I'm happy to share it but need to remove some personal data first.

However, unfortunately it will require some coding knowledge at your end so that you can tailor it it to suit your needs.

For each Form Template that you want to print the code needs to be altered to update the XSLT file so that it can format the printing correctly. This requires Android Studio to be installed and to clone the repository from GitHub so that you can then make changes to your own version of the app.

You'll need some understanding of XSLT and XML / HTML to be able to get your Form Templates to print out successfully. Also you'll need to be able to build the modified Android code into an installable apk file.

Depending on how urgent your need is and how much time you have available, it should be possible to learn how to do this - but it's definitely not as simple as just downloading the correct version of ODK Collect to be able to print to PDF I'm afraid :confused:

Could you please post an example of an XSLT?

A simple example of an XSLT might look like this:

<?xml version="1.0" encoding="UTF-8"?> 
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 
<xsl:template match="/"> 

<html> 
<head>
<style>
#table_id {
font-family: Arial;
border-collapse: collapse;
width: 100%;
margin-bottom: 40px;
font-size: 110%;
}

</style>
</head>
<body> 

<xsl:choose>
    <!-- 'Name of Form Template' -->
	<xsl:when test="/*/@id = 'template_id'">
	
		<h1>Heading</h1> 
			<table id="table_id">
				<tr> 
					<td colspan="2">Category 1 heading</td> 
				</tr>
				<tr> 
					<td>Question 1</td> 
					<td><xsl:value-of select="xml_template_id/category_1/subcategory_1"/></td> 
				</tr> 
				<tr>
					<td>Question 2</td> 
					<td><xsl:value-of select="xml_template_id/category_1/subcategory_2"/></td> 
				</tr>
				<tr>
					<td>Question 3</td> 
					<td><xsl:value-of select="xml_template_id/category_1/subcategory_3"/></td> 
				</tr>
			</table>
			
			<table id="table_id">
				<tr>
					<td colspan="2">Category 2 heading</td> 
				</tr>
				<tr> 
					<td>Question 1</td> 
					<td><xsl:value-of select="xml_template_id/category_2/subcategory_1"/></td> 
				</tr> 
				<tr>
					<td>Question 2</td> 
					<td><xsl:value-of select="xml_template_id/category_2/subcategory_2"/></td> 
				</tr>
				<tr>
					<td>Question 3</td> 
					<td><xsl:value-of select="xml_template_id/category_2/subcategory_3"/></td> 
				</tr>
			</table>	
			
	</xsl:when>
	
	<xsl:otherwise>
		<h1 align="center">This Form Template cannot be printed. Please contact a developer to ask them to add the template to the XSLT.</h1>
	</xsl:otherwise>
	
</xsl:choose>

</body> 
</html> 
</xsl:template> 
</xsl:stylesheet>
1 Like

Hi @chewDK , any chance you can share your GitHub repository? Thanks very much.

1 Like