One Page Only Please

Got this great question today ... 


Is it possible to check whether a document generated by BI publisher fits 1 page?
If it's more than one page then we want to reduce the font until it fits on a single page.

Its a nice question to which I can quickly answer 'yes - potentially' - but before I could really say 'yes' thou, I needed to put my money where my mouth was and prove it. Finally, I have put together 10mins here and there to get something that works. Its a simple approach and will probably need some more work for production but the solution shows off some of the APIs we have and how they can be used to solve this problem.


From 10,000 feet


Getting the answer to the first part can be done quite simply but only after the final document has been created. Sadly, you can not use a nice 'if' statement in the template to check if you have more than one page and if so then reduce the font size until it fits. Page numbers can not be determined until the rendering engine has done its stuff and laid the data on the page. So it has to be a post generation check.


We need a flow such as

1. Set font size to X
        |
2. Generate Output
        |
3. Test page numbers
        |
4. If page number > 1 
        |
5. Set font size  X=X - 2 or some other number
        |
6. Goto 2

7. else End

Its a nighmarish BASIC program from my distant youth ... arrrrgggghhhh!


From about 12 inches


Lets get step 3 out of the way first cos its the easiest - there is an API we can use to count the number of pages in a PDF document.


Under the FormProcessor API there is a method getPageNumber(). We are going to use it on a completed document, we need to use the setTemplate method and pass it our completed document.


    int numPages;
    FormProcessor fp = new FormProcessor();
    fp.setTemplate("c:\\temp\\1.pdf");
    try {
            numPages = fp.getPageNumber();
            System.out.println("Number of pages: "+ numPages  );
       
        } catch (Exception e) {
            e.printStackTrace();
        }

Straightforward stuff really but as I said thats the easy piece. The other part to this is to change the font size in the template until it fits on a page. I have been playing with a sample template but have put it aside for now. If we had written an XSLFO template by hand we could easily use a variable in the template for the font size and pass that each time. But we are using RTF templates, so we need some logic to update/override the font-size attribute, remember it will have been set when you create the layout in the template. Im not even sure a template can be written that updates itself during processing ... if there are any real XSLT experts out there let me know and I'll post the solution. It's going to be simpler ...


The other steps ...


For now I have dodged the issue and use a parser to look for the relevant font-size attribute and update it to its current value -2 in the generated XSLFO template file from the RTF template.

So our java logic for the whole process will be :
1. RTF -> XSL
2. XSL+XML data - > PDF
3. Count pages
4. If > 1 page then use a parser to find instances of 'font-size' and 'height'. Assign initial value found to a variable, then reduce this by 2 points for all values.
5. With the new XSL+XML -> PDF
6. Retest page numbers and repeat as necessary.


The java class I have written is not perfect but I think you can easily use it as a start for a full solution. Once the intial XSL has been generated and the resulting PDF document tested for page numbers it then parses the XSL template using a DOM parser. This looks for the 'font-size' and 'height' attributes under the 'inline' elements and knocks them down by 2 pts.


There are surely going to be templates where reducing the font-size and height are not going to be enough but if you keep things simple you can do it. You could even get into scaling images as well so everything remains in proportion. My template is simple and heres where you may need to modify the class to handle some of the other 'height' and 'font-size' attributes if present. Here's the code with some annotation:

package xdotestbed;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import java.io.OutputStream;

import oracle.apps.xdo.XDOException;
import oracle.apps.xdo.template.FOProcessor;
import oracle.apps.xdo.template.FormProcessor;
import oracle.apps.xdo.template.RTFProcessor;

import oracle.xml.parser.v2.DOMParser;
import oracle.xml.parser.v2.XMLDocument;
import oracle.xml.parser.v2.XMLElement;


import org.w3c.dom.Document;

import org.w3c.dom.NodeList;

import org.xml.sax.SAXException;

public class FitSinglePage {
    //declare class variables
    String rtfTemplate;
    String xslFile;
    String pdfFile;
    String xmlData;
    XMLDocument newDoc;
   
    public FitSinglePage(String rtfF,String xslF,String xmlF,String pdfF) {
        //Assign program parameters
        rtfTemplate = rtfF;
        xslFile = xslF;
        xmlData = xmlF;
        pdfFile = pdfF;
       
        //Initial RTF -> XSL conversion
        processTemplate(rtfTemplate);
        // Initial PDF generation using XSL above
        generateOutput(xslFile,xmlData,pdfFile);
       
        // Wrapping a loop to stop the process running away
        //if it can never fit on a single page
       
        for (int x=0; x<11; x++){
        //Test the number of pages of the resulting PDF,
        //keep going until it fits 1 page
       
        while (countPages(pdfFile) != 1) {
            // Assign the updated xslFile to an XMLDocument instance
            newDoc = parseTemplate(xslFile);
            // Write out the template to the same file name
            writeTemplate(newDoc);
            // regenerate the PDF document with the adjusted
            //font size and height settings
            generateOutput(xslFile,xmlData,pdfFile);
           }
        System.out.println("Success!");
        break;
    }
   
}
   
public void generateOutput(String xslFileLoc,String xmlData,String pdfFile){
    // This method will generate the PDF output each time
   
    FOProcessor fop = new FOProcessor();
    fop.setData(xmlData);
    fop.setTemplate(xslFileLoc);
    fop.setOutput(pdfFile);
    fop.setOutputFormat(FOProcessor.FORMAT_PDF);
    try {
        fop.generate();
    } catch (XDOException e) {
        e.printStackTrace();
    }

}
public void processTemplate(String rtfFile) {
    // Only called once to create the initial XSLFO template
    // from the RTF template
        try {
            RTFProcessor rtfP = new RTFProcessor(rtfFile);
            rtfP.setOutput(xslFile);
            // this prevents the processor generating attribute sets
            // You could allow it but it would require changes to the
            //the parser code
            rtfP.setExtractAttributeSet(RTFProcessor.EXTRACT_DISABLE);
            rtfP.process();
        }
       
        catch (XDOException e) {
            e.printStackTrace();
        }
        catch (IOException ioe){
            ioe.printStackTrace();
        }
   
    }

    public void writeTemplate(XMLDocument newTemplate){
   
    //Write the updated XMLDocument to the XSLFO template file
   
    OutputStream os;
    try {
        os = new FileOutputStream(xslFile);
        newTemplate.print(os);
        os.close();
         }
    catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    catch (IOException ioe){
        ioe.printStackTrace();
    }
      
}

public static void main(String[] args) {
       
FitSinglePage fitSinglePage = new FitSinglePage(args[0],args[1]
                                                        ,args[2],args[3]);
    }

public XMLDocument parseTemplate (String templFile){
       // Parse the XSLFO template method
        DOMParser dp = new DOMParser();
        try {
            InputStream inp = new FileInputStream(templFile);
            dp.parse(inp);
            inp.close();
        }
        catch (SAXException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
             e.printStackTrace();
        }
        XMLDocument tDoc = dp.getDocument();
        //Grab all instances of the 'inline' element and their children
        NodeList ns = tDoc.getDocumentElement().getElementsByTagNameNS
                             ("http://www.w3.org/1999/XSL/Format","inline");
        XMLElement attrVal;
       //Loop thru the inline elements
        for (int i = 0; i < ns.getLength(); i++)
    {
        attrVal = (XMLElement)ns.item(i);
      //Change the font Sizes
       if (attrVal.getAttribute("font-size").indexOf("pt") != -1)
       {
        //System.out.print("Number: "+i +"::"

//+ attrVal.getAttribute("font-size")+"\n");
        //Get the font size value e.g. 12.0pt

String fontSize = attrVal.getAttribute("font-size");
       
//Strip out the 'pt' part to leave a number e.g. 12.0

String fontVal = fontSize.substring(0,fontSize.indexOf("pt"));
       
//Set the new value and add the 'pt' back in e.g. 10.0pt

attrVal.setAttribute("font-size",
(Double.parseDouble(fontVal)-2) +"pt");
       

//System.out.print("Number: "+i +"::"
+ attrVal.getAttribute("font-size")+"\n");
        }
        // Change the row heights
        if (attrVal.getAttribute("height").indexOf("pt") != -1)
        {
         //System.out.print("Number: "+i +"::" + attrVal.getAttribute("font-size")+"\n");
         String heightSize = attrVal.getAttribute("height");
         String heightVal = heightSize.substring(0,heightSize.indexOf("pt"));
         attrVal.setAttribute("height",(Double.parseDouble(heightVal)-2) +"pt");
         //System.out.print("Number: "+i +"::" + attrVal.getAttribute("height")+"\n");
         }
       
    }   

    return(tDoc);
}

public int countPages (String outDoc) {
   //Count the number of pages in the generated PDF document
    int numPages = 0;
    FormProcessor fp = new FormProcessor();
    fp.setTemplate(outDoc);
    try {
            numPages = fp.getPageNumber();
            System.out.println("Number of pages: "+ numPages  );
       
        } catch (Exception e) {
            e.printStackTrace();
        }
    return(numPages);   
 }
}


You can also get the class here, both compiled and not along with the template and XML data. You can run the class from the command line passing in 4 parameters :


java xdotestbed.FitSinglePage rtfFileName xslFileName xmlFileName pdfFileName


you'll need to set your classpath accordingly, substitute your values for the parameters above e.g.


java xdotestbed.FitSinglePage 1.rtf 1.xsl 1.xml 1.pdf


 and you'll need the following libraries:


aolj.jar - EBS lib, required even for standalone
bicmn.jar - BIBEans for charting
bijdbc14.jar - BIBEans for charting
bipres.jar - BIBEans for charting
collections.jar - Needed in mailing
i18nAPI_v3.jar - internationalization lib
share.jar - general library
versioninfo.jar - anothe EBS lib
xdochartstyles.jar - for charting on the publisher side
xdocore.jar - core library
xdoparser.jar - publisher parser
xmlparserv2-904.jar - XML  lib
xmlpserver.jar - XML  lib


You may not need all of them depending on whats in your template but they are all easily grabbed either from the standalone server install or the Template Builder for MSWord install directory.


Summing Up


It seems quite a niche requirement but if you need this type of functionality then the APIs and an XML parser can help. I could even see the need to manipulate the template repeatedly for other requirements. I chose a DOM paerser because templates are not that big. It would not be a huge task to move this over to SAX.
Overall, I at least had some fun and frustration building the solution  and if nothing else you got to see a few more APIs. 

Comments:

Some executives are VERY picky...especially accountants. At our shop, the balance sheet NEEDS to fit into 2 pages. PERIOD. So, having this option is great.

Posted by Manish on July 18, 2007 at 02:30 AM MDT #

Hi Manish, Glad its useful then ... took so long to get it together, or at least felt like it ... few minutes here, few mins there ... became a monster entry very quickly :o)

Posted by Tim Dexter on July 18, 2007 at 03:57 AM MDT #

hi Tim, You are always the best in giving solutions to BI Publisher related queries. I have always written to you with my queries. I have one trouble. We are trying to generate an excel output with the RTF template. I have defined a few repeating groups in my template. I can see the output in BI Publisher. Though my application needs to run using just the BI Publisher jars. Hence we have used the BI Publisher jars to generate the output and written appropriate methods to call the right functions. Although the excel output is generated, it does not display any information which is a part of the repeating groups. Can you please help me to find what might be the reason? It would be of great help. Thanks, Regards, Divya

Posted by Divyasri on August 08, 2009 at 11:52 PM MDT #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Follow bipublisher on Twitter Find Us on Facebook BI Publisher Youtube ChannelDiscussion Forum

Join our BI Publisher community to get the most and keep updated with the latest news, How-to, Solutions! Share your feedback and let us hear your voice @bipublisher on Twitter, on our official Facebook page, and Youtube!

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today