Generating PDF from XSL-FO using the HotJVM Render Server

This article is little explanation to a GitGub project.

The HotJVM Render Server is a great way of boosting ZEN Reports performance, when it comes to generating PDF documents. It can spare a second or more even on a single execution of a report, but it becomes unavoidable when you have to generate hundreds or thousands (or maybe hundreds of thousands) of reports.

If you don't want to go through the documentation above: the Render Server comes with InterSystems Caché and Ensemble and makes it possible to use FOP in a multi-threaded and yet a thread-safe way. And thread-safety is a cardinal question, as FOP doesn't claim itself completely thread-safe. Is it possible though to ensure thread-safety under certain circumstances, and this is what the Render Server delivers.

The default (and simplified) way of generating a PDF document with ZEN Reports is this:

  1. From your ReportDefinition XData block a temporary XML file is created, containing your data.
  2. From your ReportDisplay XData block a temporary XSLT file is created, which describes how to transform your data into a document.
  3. ZEN Reports applies the XSLT to the XML file, creating an XSL-FO document, which is basically the XML representation of the PDF, which you want to generate (already containing your data, too).
  4. ZEN Reports calls out to a 3rd party rendering engine (like Apache FOP) to generate the final PDF document from the XSL-FO file.

This is just the default, which you can vary: some parts of or the entire XML can come from different sources (method, other report, stream), and the same is true for the XSLT.

In some cases even the XSL-FO file can be "static" (created outside of the ZEN Reports framework). This can be advantageous when- for example- your XSL-FO contains big chunks of SVG or for whatever other reason it easier to create the FO file on your own, than with ZEN Reports (yes, this can happen wink).

The only problem is that however technically possible to create a PDF from an FO file directly in the ZEN Reports framework, the Render Server option is ignored in this case. Which is a pity!

 

At the same time: it is not too hard to solve this with a little trick and by using a special ZEN Reports class as a wrapper for your XSL-FO document. And before you would run to Studio Ateliér to implement this on your own, please check out this GitGub project first.

  • + 3
  • 0
  • 485
  • 7

Comments

 @Attila Toth I am using your  FOWrapper to pass a stream of XLFO data in my production to generate a pdf and  nothing is happening I am not getting any errors or anything  would you please advice on where I am going wrong I have altered it a bit to use what I need please advice here is my code thanks in advance

Include Ensemble

IncludeGenerator (%occInclude, EnsUtil)

Class ZEN.Report.XLFOToPDFWrapper Extends %ZEN.Report.reportPage
{

Parameter DEFAULTMODE As STRING = "xml";

/// Default value for the CallbackClass property. This can be set in subclasses!
Parameter DEFAULTCALLBACKCLASS;

/// Default value for the CallbackMethod property. This can be set in subclasses!
Parameter DEFAULTCALLBACKMETHOD;

Parameter ENCODING = "Windows-1252";

Property FOStreamString As %Stream.FileCharacter(ZENURL = "FOSTREAMSTRING");

/// If this and the CallbackMethod are not empty and referring a valid class and method name,
///  then the return value of the corresponding classmethod (which should be a character stream) is used as the XSL-FO file.
Property CallbackClass As %String(ZENURL = "CLASS") [ InitialExpression = {..#DEFAULTCALLBACKCLASS} ];

/// If this and the CallbackClass are not empty and referring a valid class and method name,
///  then the return value of the corresponding classmethod (which should be a character stream) is used as the XSL-FO file.
Property CallbackMethod As %String(ZENURL = "METHOD") [ InitialExpression = {..#DEFAULTCALLBACKMETHOD} ];

/// This callback is invoked after this report is instantiated
/// and before it is run.
Method %OnBeforeReport() As %Status [ Internal ]
{
	Set tSC = $$$OK

	If (..CallbackClass '= "") && (..CallbackMethod '= "") {
		TRY {
			Set tSC = $CLASSMETHOD(..CallbackClass, ..CallbackMethod, .fostream)
		}
		CATCH ex {
			Set tSC = ex.AsStatus()
		}
	}
	Else {
		Set fostream = ##class(%Stream.FileCharacter).%New()
		While '..FOStreamString.AtEnd {
		Set tSC = fostream.Write(..FOStreamString.Read(32000))
	}
		
		 $$$TRACE(fostream)
		 $$$LOGWARNING(fostream)
	}
	
	If $$$ISOK(tSC) {
		// TODO: automatically append XSLT frame around the XSL-FO file.
		Set ..toxslfostream = ..TransformFO2XSL(.fostream)
		Do ..toxslfostream.Rewind()
	}
		
	Quit tSC
}

/// This report has a "fake" XML definition. Basically any XML would do, because the XSL-FO file is created outside of the ZEN Report class.
XData ReportDefinition [ XMLNamespace = "http://www.intersystems.com/zen/report/definition" ]
{

}

/// Method, which generates the "fake" XML content of the report.
/// At the moment this contains a single element (), which reflects the parameters of the report.'
ClassMethod FakeXML() [ Internal ]
{
	Write "">"
}

ClassMethod ProducePDFFile(pOutputFile As %String, pXLFOStream As %String, pDisplayLog As %Boolean = 0, pRenderServer As %String = "") As %Status
{
	Set tSC = $$$OK
	
	Set report = ..%New()
	Set report.FOStreamString = pXLFOStream
	
	Set tSC = report.GenerateReport(pOutputFile, 2, pDisplayLog, pRenderServer)
	Quit tSC
}

/// Appends the XSLT frame around the original XSL-FO stream, to make it usable with this report class.
ClassMethod TransformFO2XSL(ByRef pFOStream As %Stream.Object) As %Stream.Object [ Internal ]
{
	Set tSC = $$$OK
	
	Set xslStream = ##class(%Stream.TmpCharacter).%New()
	
	Do pFOStream.Rewind()
	Set first100 = pFOStream.Read(100)
	If $Extract(first100, 1, 2) = "", 1) _ ">"
		Set first100 = $Piece(first100, ">", 2, *)
	}
	Else {
		Set xmlHeader = "" 
	}
	
	Do xslStream.WriteLine($Select(
		xmlHeader '= "": xmlHeader,
		1: ""))
	Do xslStream.WriteLine("")
	Do xslStream.WriteLine("")
	Do xslStream.WriteLine("")

	Quit xslStream
}

}

and on my operation I call it like so

Class FopPDFRenderingEngineOPRN Extends Ens.BusinessOperation
{

Parameter ADAPTER = "EnsLib.File.OutboundAdapter";

Property Adapter As EnsLib.File.OutboundAdapter;

Parameter INVOCATION = "Queue";

Method Render(pRequest As RenderRequestMREQ, Output pResponse As RenderRequestMRES) As %Status
{

	set tSC = $$$OK
		
	set pResponse = ##class(Ecrion.PublishingEngine.RenderRequestMRES).%New()
	set pResponse.FileName = pRequest.FileName
	set pResponse.Status = 0

	

	#Dim oHttpReq3 AS ZEN.Report.XLFOToPDFWrapper= ##class(ZEN.Report.XLFOToPDFWrapper).%New()

	
	// if the input format is xslfo Base64Encode the stream
	if (pRequest.InputFormat = "xslfo")
	{
	
	
	// write the request object 

		set pResponse.Document = ..getStreamData(pRequest.Source)
		set p=oHttpReq3.ProducePDFFile(..Adapter.FilePath_pRequest.FileName,..getStreamData(pRequest.Source))
		If ($$$ISOK(p))
	    {
		    set pResponse.Message="all good"
		    }else
			{
			set pResponse.Message=$$$ISERR(tSC)
			}
	
      	
      	kill oStream
      	 
	}
	
 
    
    kill oStream
        
    quit tSC
}

Method getStreamData(foo As %Stream.TmpCharacter) As %Stream.TmpCharacter
{
	   set oStream = ##class(%Stream.TmpCharacter).%New()
		
		
		while ('foo.AtEnd) { do oStream.Write(foo.Read(1200)) }
		
		
		do oStream.Rewind()
		quit oStream
}

XData MessageMap
{
Render
}

}

Hi Thembelani,

I haven't looked into this project of mine for a while, but I know about people using it actively, so I'm pretty sure that it should work. The very first thing what I would try (maybe already did - it wasn't clear from your comment) to call the ProducePDFFile() method with the third parameter (pDisplayLog) set to 1. If an error happens during the Apache FOP rendering process, usually it doesn't return any error messages, but you can ask to generate a log file instead of the real PDF document. This log file may contain a lot of useful information, if you had any problems.

Please let me know, if it helps. And if not: I'll try to reproduce your issues.

Best Regards,
Attila

@Attila Toth  thanks for getting back at me I have tried with log errors and I get a pdf generated which I can not open as if I have a loop running or the file being corrupt your solution works fine with the reading of files and using the call back method with embedded XLFO  is  it possible to supply the call back method with the XLFO stream and use that instead and if possible please could you give us an example if you have one thanks

I'm gonna try to extend the project with your use case in the next 1-2 days.
Meanwhile: if you could you please send me the log file in an e-mail (Attila.Toth@InterSystems.com), I'd take a look at it.

Regards,
Attila

BTW: I don't know, whether the source code above is really what you are trying to use, or you maybe removed some parts to make it shorter, but: the FakeXML() method has a syntax error and it should return a valid XML document (even if a very simple one). Otherwise it may break the entire process.

Attila

I am using the supplied FakeXML as below 

ClassMethod FakeXML() [ Internal ]
{
    Write "<fofile" _ 
        $Case(%report.FOFilename, "": "", : " name=""" _ ##class(%File).NormalizeFilename(%report.FOFilename) _ """") _
        $Case(%report.FOStreamString, "": "", : " FOStreamString=""" _ %report.FOStreamString _ """") _
        $Case(%report.CallbackClass, "": "", : " callbackClass=""" _ %report.CallbackClass _ """") _
        $Case(%report.CallbackMethod, "": "", : " callbackMethod=""" _ %report.CallbackMethod _ """") _
        "></fofile>"
}