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.