This maintenance update to Quartam Reports contains all the enhancements of versions 1.1.1 through 1.1.5, and improves error handling and behavior on MacOSX.
The MacOSX version can be downloaded at: http://downloads.quartam.com/qrtReports_116_MacOSX_Setup.dmg
The Windows version can be downloaded at: http://downloads.quartam.com/qrtReports_116_Windows_Setup.exe
The (expertimental) Linux version can be downloaded at: http://downloads.quartam.com/qrtReports_116_Linux_Archive.zip
Quartam Reports for LiveCode - version 1.1 introduced stretching data fields and relative positioning, added title and summary sections, brought data groups to everyone and extended the Professional edition with barcodes, 2D charts and export to HTML and Excel.
With Quartam Reports, LiveCode and just a little SQL, you can create professional reports - cross-platform and database-independent.
Sunday, February 13, 2011
Sunday, January 16, 2011
ZeroConf/Bonjour in LiveCode with JmDNS
In the last few posts, we examined how LiveCode scripts can delegate work to Java classes using the shell function. Even though this generally works quite fast and reliably, the downside of this approach is that a new Java process is started every time.
In this post, we'll use process communication to start a helper process, interact with it, and finally close it when we're done. Our use case: ZeroConf / Bonjour registration and discovery of services. The JmDNS project offers a pure-Java implementation of Multicast DNS and DNS Service Discovery technologies.
LiveCode process communication revolves around opening a process (a.k.a. command-line interface application), writing data, reading data, and finally closing said process. But we'll get to that later; let's start by writing a LiveCodeJmDNSHelper class in Java. First download a copy of the JmDNS library - I picked version 3.0 as I was building this on an older Mac which can't run Java 6.
In the .zip file, you'll find a file 'jmdns.jar' which is all you need for the rest of this example. Copy it into your Eclipse project, add it to the Build path, and use the following code for the LiveCodeJmDNSHelper.java file:
The start() method goes into an infinite loop, waiting for a line of text as its input, parsing the command and executing it. You can 'list' the ServiceInfo data for a given service type, 'register' your own services, and 'unregister' those services again. All nice and dandy, but how do we talk to it from LiveCode? Let's start by creating a new stack.

So we have two buttons at the top, to Start and Stop the helper process. Then we have a button to Register a service of our own, with fields for the Alias, Type, Name, Port and Text. Next there is a button to Unregister our own service, given an Alias. Finally, there is a button to List all the services of a specific Type in a scrolling field below. With the user interface laid out, we can put the following code into the stack script:
The script for the Start button is easy enough:
And the script of the Stop button is equally trivial:
The script of the Register button isn't too complicated either:
Neither is the script of the Unregister button:
Nor the script of the List button:
Save the stack, and copy jmdns.jar as well as the compiled LiveCodeJmDNSHelper.class into the same directory where you saved the stack. Click the Start button, Register a service of your own, click the List button to make sure it shows up, click the Unregister button to remove it again, and then click the List button to make sure it's gone. If you want, you can launch some of the Java samples that come with JmDNS to see that both Java and LiveCode recognize the same services.
For the purists: no, I didn't do proper error checking or exception handling in this example. Because it is just that: an example. Never promised it was production-ready, did I? ;-)
Download it here.
In this post, we'll use process communication to start a helper process, interact with it, and finally close it when we're done. Our use case: ZeroConf / Bonjour registration and discovery of services. The JmDNS project offers a pure-Java implementation of Multicast DNS and DNS Service Discovery technologies.
LiveCode process communication revolves around opening a process (a.k.a. command-line interface application), writing data, reading data, and finally closing said process. But we'll get to that later; let's start by writing a LiveCodeJmDNSHelper class in Java. First download a copy of the JmDNS library - I picked version 3.0 as I was building this on an older Mac which can't run Java 6.
In the .zip file, you'll find a file 'jmdns.jar' which is all you need for the rest of this example. Copy it into your Eclipse project, add it to the Build path, and use the following code for the LiveCodeJmDNSHelper.java file:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
public class LiveCodeJmDNSHelper {
private final static String EXIT_COMMAND = "exit";
private final static String LIST_COMMAND = "list";
private final static String REGISTER_COMMAND = "register";
private final static String UNREGISTER_COMMAND = "unregister";
private JmDNS jmdns;
private Map<String, ServiceInfo> regMap;
public LiveCodeJmDNSHelper() {
super();
}
public void start() throws IOException {
this.jmdns = JmDNS.create();
this.regMap = new HashMap<String, ServiceInfo>();
String commandLine = ""; // the last received command line
System.out.println("LiveCodeJmDNSHelper started. Type the command you wish to execute (type 'exit' to stop):");
final InputStreamReader isr = new InputStreamReader(System.in);
final BufferedReader bir = new BufferedReader(isr);
while(!EXIT_COMMAND.equals(commandLine)) {
commandLine = bir.readLine();
final String[] commandParts = commandLine.split(" ");
final String commandName = commandParts[0];
if (EXIT_COMMAND.equalsIgnoreCase(commandName)) {
System.out.println("LiveCodeHelper is exiting");
System.exit(0);
} else if (LIST_COMMAND.equalsIgnoreCase(commandName)) {
handleListCommand(commandParts);
} else if (REGISTER_COMMAND.equalsIgnoreCase(commandName)) {
handleRegisterCommand(commandParts);
} else if (UNREGISTER_COMMAND.equalsIgnoreCase(commandName)) {
handleUnregisterCommand(commandParts);
} else {
System.out.println("Unrecognized command: " + commandLine);
}
}
}
private void handleListCommand(final String[] commandParts) {
final String svcType = commandParts[1];
final ServiceInfo[] svcInfos = this.jmdns.list(svcType);
for (ServiceInfo svcInfo : svcInfos) {
System.out.println(svcInfo);
}
System.out.println(".");
}
private void handleRegisterCommand(final String[] commandParts) {
final String svcAlias = commandParts[1];
final String svcType = commandParts[2];
final String svcName = commandParts[3];
final int svcPort = Integer.parseInt(commandParts[4]);
final String svcText = commandParts[5];
ServiceInfo svcInfo = ServiceInfo.create(svcType, svcName, svcPort, svcText);
try {
this.jmdns.registerService(svcInfo);
this.regMap.put(svcAlias, svcInfo);
System.out.println("Registered service '" + svcAlias + "' as: " + svcInfo);
} catch (IOException e) {
System.out.println("Failed to register service '" + svcAlias + "'");
}
}
private void handleUnregisterCommand(final String[] commandParts) {
final String svcAlias = commandParts[1];
final ServiceInfo svcInfo = this.regMap.remove(svcAlias);
if (svcInfo != null) {
this.jmdns.unregisterService(svcInfo);
System.out.println("Unregistered service '" + svcAlias + "'");
} else {
System.out.println("Failed to unregister service '" + svcAlias + "'");
}
}
public static void main(String[] args) throws IOException {
final LiveCodeJmDNSHelper helper = new LiveCodeJmDNSHelper();
helper.start();
}
}
The start() method goes into an infinite loop, waiting for a line of text as its input, parsing the command and executing it. You can 'list' the ServiceInfo data for a given service type, 'register' your own services, and 'unregister' those services again. All nice and dandy, but how do we talk to it from LiveCode? Let's start by creating a new stack.

So we have two buttons at the top, to Start and Stop the helper process. Then we have a button to Register a service of our own, with fields for the Alias, Type, Name, Port and Text. Next there is a button to Unregister our own service, given an Alias. Finally, there is a button to List all the services of a specific Type in a scrolling field below. With the user interface laid out, we can put the following code into the stack script:
local sProcess
on JmDNS_StartHelper
if sProcess is empty then
local tClassPath
if the platform is "Win32" then
put ".;jmdns.jar" into tClassPath
else
put ".:jmdns.jar" into tClassPath
end if
local tDefaultFolder, tStackFolder
put JMDNS_StackFolder() into tStackFolder
put the defaultFolder into tDefaultFolder
set the defaultFolder to tStackFolder
put "java -cp" && tClassPath && "LiveCodeJmDNSHelper" into sProcess
open process sProcess for update
set the defaultFolder to tDefaultFolder
--> make sure we're speaking with the right helper
read from process sProcess for 1 line
if it begins with "LiveCodeJmDNSHelper started." then
--> ready for interaction
else
close process sProcess
put empty into sProcess
end if
end if
end JmDNS_StartHelper
function JmDNS_GetList pServiceType
local tJmDNSList
write ("list" && pServiceType) & return to process sProcess
repeat forever
read from process sProcess for 1 line
if it begins with "." then exit repeat
put it after tJmDNSList
end repeat
delete char -1 of tJmDNSList
return tJmDNSList
end JmDNS_GetList
command JmDNS_RegisterService pAlias, pType, pName, pPort, pText
write ("register" && pAlias && pType && pName && pPort && pText) & return to process sProcess
read from process sProcess for 1 line
return it
end JmDNS_RegisterService
command JmDNS_UnregisterService pAlias
write ("unregister" && pAlias) & return to process sProcess
read from process sProcess for 1 line
return it
end JmDNS_UnregisterService
command JmDNS_StopHelper
write ("exit") & return to process sProcess
close process sProcess
put empty into sProcess
end JmDNS_StopHelper
function JmDNS_StackFolder
local tStackFolder
put the effective filename of this stack into tStackFolder
set the itemDelimiter to slash
delete item -1 of tStackFolder
return tStackFolder
end JmDNS_StackFolder
The script for the Start button is easy enough:
on mouseUp
JmDNS_StartHelper
disable button "Start"
enable button "Stop"
end mouseUp
And the script of the Stop button is equally trivial:
on mouseUp
JmDNS_StopHelper
enable button "Start"
disable button "Stop"
end mouseUp
The script of the Register button isn't too complicated either:
on mouseUp
JmDNS_RegisterService field "Alias", field "Type", field "Name", field "Port", field "Text"
answer the result
end mouseUp
Neither is the script of the Unregister button:
on mouseUp
ask "Unregister which alias?" with field "Alias"
if it is empty then exit mouseUp
JmDNS_UnregisterService it
answer the result
end mouseUp
Nor the script of the List button:
on mouseUp
ask "List services of which type?" with field "Type"
if it is empty then exit mouseUp
put JMDNS_GetList(it) into field "List"
end mouseUp
Save the stack, and copy jmdns.jar as well as the compiled LiveCodeJmDNSHelper.class into the same directory where you saved the stack. Click the Start button, Register a service of your own, click the List button to make sure it shows up, click the Unregister button to remove it again, and then click the List button to make sure it's gone. If you want, you can launch some of the Java samples that come with JmDNS to see that both Java and LiveCode recognize the same services.
For the purists: no, I didn't do proper error checking or exception handling in this example. Because it is just that: an example. Never promised it was production-ready, did I? ;-)
Download it here.
Friday, December 31, 2010
XML validation using a Schema
In earlier posts, I examined how we can use the built-in revXML library in LiveCode to validate XML data against a DTD, later refining it with a version check to match evolving requirements. Unfortunately, a Document Type Definition is quite a limited way of XML validation. So this time, we'll improve our defenses again, by incorporating XML Schemas.
Whereas a DTD is limited to defining the basic structure of the XML in terms of elements and attributes, XML Schemas allow you to define validation on the actual content of the elements and attributes. So you can be sure that an element defined as "xs:date" is actually a valid date, or that an attribute defines as "xs:positiveInteger" is actually a positive integer, etc. A full explanation of XML Schemas is beyond the scope of this post, you'll find plenty of information around the web - a good first stop is this W3Schools XML Schema tutorial.
This all sounds very good, but here's the rub: the revXML library offers no built-in support for XML Schemas. So yet again we turn to Java, with its built-in XML Validation API. We can easily execute Java code using LiveCode's shell function - so let's start by writing the XmlValidateSchema class:
In keeping with earlier Java examples, the code is a bit lazy when it comes to exception handling: if any exception is thrown, it will end up in the output of our shell function call. The only important thing to remember is that the first parameter is the XML file, and the second is the XML Schema Definition (XSD) file.
Let's go to LiveCode and create a new stack for the user interface.

As you can see, there's a field for the Schema text, a field for the XML text, and a button to Validate the XML against the Schema. Since it's perhaps a tad small, here's the content of the Schema field:
And here's the content of the XML field:
After saving the stack, we copy the compiled XmlValidateSchema.class file into the same directory as the stack. Now we can write the script for the 'Validate' button:
This time around, we didn't have to fiddle with the Java classpath, as the XML Validation API is built-in. However, we had to write the XML and Schema to temporary files, to avoid length limitations in the shell command. If we now make a deliberate mistake, say change one of the 'LeafNode' elements into a 'BeafNode' element, we see this error:

Again, the image is a bit small, so here's the content of the error:
Thanks to the combination of LiveCode and Java, we can develop cross-platform solution quickly, without having to give up the power of existing libraries. Unfortunately, loading Java every time for a shell call is not the optimal solution, so in another post, we'll investigate how we can run Java code using LiveCode's 'process' communication. Stay tuned...
Whereas a DTD is limited to defining the basic structure of the XML in terms of elements and attributes, XML Schemas allow you to define validation on the actual content of the elements and attributes. So you can be sure that an element defined as "xs:date" is actually a valid date, or that an attribute defines as "xs:positiveInteger" is actually a positive integer, etc. A full explanation of XML Schemas is beyond the scope of this post, you'll find plenty of information around the web - a good first stop is this W3Schools XML Schema tutorial.
This all sounds very good, but here's the rub: the revXML library offers no built-in support for XML Schemas. So yet again we turn to Java, with its built-in XML Validation API. We can easily execute Java code using LiveCode's shell function - so let's start by writing the XmlValidateSchema class:
import java.io.File;
import java.io.IOException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
public class XmlValidateSchema {
public static void main(String[] args) throws SAXException, ParserConfigurationException, IOException {
final File xmlFile = new File(args[0]);
final File xsdFile = new File(args[1]);
// Load the XML file
final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
final DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
final Document document = docBuilder.parse(xmlFile);
// Load the XSD file
final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
final Schema schema = schemaFactory.newSchema(xsdFile);
final Validator validator = schema.newValidator();
// Validate the XML document against the XSL schema
final Source source = new DOMSource(document);
validator.validate(source);
}
}
In keeping with earlier Java examples, the code is a bit lazy when it comes to exception handling: if any exception is thrown, it will end up in the output of our shell function call. The only important thing to remember is that the first parameter is the XML file, and the second is the XML Schema Definition (XSD) file.
Let's go to LiveCode and create a new stack for the user interface.

As you can see, there's a field for the Schema text, a field for the XML text, and a button to Validate the XML against the Schema. Since it's perhaps a tad small, here's the content of the Schema field:
<?xml version="1.0"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.quartam.com"
xmlns="http://www.quartam.com"
elementFormDefault="qualified">
<xs:element name="RootNode" type="RootNode"/>
<xs:complexType name="RootNode">
<xs:sequence>
<xs:element name="BranchNode" type="BranchNode" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="SpecVersion" type="xs:string"/>
</xs:complexType>
<xs:complexType name="BranchNode">
<xs:sequence>
<xs:element name="LeafNode" type="xs:string" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
And here's the content of the XML field:
<?xml version="1.0"?>
<RootNode
xmlns="http://www.quartam.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.quartam.com schema.xsd"
SpecVersion="1.0">
<BranchNode>
<LeafNode>The first leaf node</LeafNode>
<LeafNode>The second leaf node</LeafNode>
<LeafNode>The third leaf node</LeafNode>
</BranchNode>
<BranchNode>
<LeafNode>The fourth leaf node</LeafNode>
<LeafNode>The fifth leaf node</LeafNode>
</BranchNode>
</RootNode>
After saving the stack, we copy the compiled XmlValidateSchema.class file into the same directory as the stack. Now we can write the script for the 'Validate' button:
on mouseUp
--> write Schema and XML to temporary files
local tSchemaFile, tXmlFile
put the tempName into tSchemaFile
put field "Schema" into URL ("file:" & tSchemaFile)
put the tempName into tXmlFile
put field "XML" into URL ("file:" & tXmlFile)
--> assemble the shell command
local tShellCommand
put "java XmlValidateSchema" && \
ShellPath(tXmlFile) && \
ShellPath(tSchemaFile) \
into tShellCommand
--> execute the shell command
local tHideConsoleWindows, tDefaultFolder, tShellResult
put the hideConsoleWindows into tHideConsoleWindows
set the hideConsoleWindows to true
put the defaultFolder into tDefaultFolder
set the defaultFolder to AbsolutePathFromStack()
put shell(tShellCommand) into tShellResult
set the defaultFolder to tDefaultFolder
set the hideConsoleWindows to tHideConsoleWindows
--> cleanup the temporary files
delete file tSchemaFile
delete file tXmlFile
if tShellResult is not empty then
answer error tShellResult
end if
end mouseUp
function AbsolutePathFromStack pFileName
local tAbsolutePath
put the effective filename of this stack into tAbsolutePath
set the itemDelimiter to slash
if pFileName is not empty then
put pFileName into item -1 of tAbsolutePath
else
delete item -1 of tAbsolutePath
end if
return tAbsolutePath
end AbsolutePathFromStack
function ShellPath pPath
if the platform is "Win32" then
put quote & pPath & quote into pPath
else
replace space with backslash & space in pPath
end if
return pPath
end ShellPath
This time around, we didn't have to fiddle with the Java classpath, as the XML Validation API is built-in. However, we had to write the XML and Schema to temporary files, to avoid length limitations in the shell command. If we now make a deliberate mistake, say change one of the 'LeafNode' elements into a 'BeafNode' element, we see this error:

Again, the image is a bit small, so here's the content of the error:
ERROR: 'cvc-complex-type.2.4.a: Invalid content was found starting with element 'BeafNode'. One of '{"http://www.quartam.com":LeafNode}' is expected.'
Exception in thread "main" org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'BeafNode'. One of '{"http://www.quartam.com":LeafNode}' is expected.
at com.sun.org.apache.xerces.internal.jaxp.validation.Util.toSAXParseException(Util.java:109)
at com.sun.org.apache.xerces.internal.jaxp.validation.ErrorHandlerAdaptor.error(ErrorHandlerAdaptor.java:104)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:382)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:316)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator$XSIErrorReporter.reportError(XMLSchemaValidator.java:429)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.reportSchemaError(XMLSchemaValidator.java:3185)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.handleStartElement(XMLSchemaValidator.java:1831)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.startElement(XMLSchemaValidator.java:705)
at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorHandlerImpl.startElement(ValidatorHandlerImpl.java:335)
at com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.closeStartTag(ToXMLSAXHandler.java:205)
at com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.characters(ToXMLSAXHandler.java:524)
at com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.characters(ToXMLSAXHandler.java:467)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:229)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:215)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:215)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:215)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:121)
at com.sun.org.apache.xalan.internal.xsltc.trax.DOM2TO.parse(DOM2TO.java:85)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transformIdentity(TransformerImpl.java:615)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:661)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:300)
at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorImpl.process(ValidatorImpl.java:220)
at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorImpl.validate(ValidatorImpl.java:141)
at javax.xml.validation.Validator.validate(Validator.java:82)
at XmlValidateSchema.main(XmlValidateSchema.java:31)Thanks to the combination of LiveCode and Java, we can develop cross-platform solution quickly, without having to give up the power of existing libraries. Unfortunately, loading Java every time for a shell call is not the optimal solution, so in another post, we'll investigate how we can run Java code using LiveCode's 'process' communication. Stay tuned...
Thursday, December 30, 2010
Stamping PDF files
In a previous post, I examined how we can use LiveCode and the Java-based iText library to concatenate a series of existing PDF files into a single PDF file. Now we will examine how we can 'stamp' a PDF file with an image using the same technique.
The first thing to code is the Java class that we will call using the shell function. Here's what I came up with:
In a nutshell, the first parameter is the input file, the second the output file, the third the image file, and the fourth parameter is a comma-separated list of coordinates making up the target rectangle. As usual, the code is a tad lazy when it comes to faulty input parameters and exception handling - if there's a mistake you'll simply get the stacktrace as the output of the shell command.
The most important bit is in the loop over the pages, where we use the outputStamper.getOverContent() method to draw our image on top of the existing content. If you'd rather have the image in the back, as a watermark, you would use the outputStamper.getUnderContent() methopd instead. Also note that setting the image position coordinate system works from the bottomLeft of the page, so we have to use the original page height and subtract the bottom coordinate from it.
Now we can proceed with writing a LiveCode button script:
Click the button, and it happily takes the existing PDF files (demo1.pdf), paints the image (Template.png) on top of all pages, and writes a new PDF file (stamp.pdf) in the same folder as our stack. There we have it, another example of using iText from within LiveCode.
The first thing to code is the Java class that we will call using the shell function. Here's what I came up with:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Image;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfStamper;
public class StampPdfFile {
public static void main(String[] args) throws IOException, DocumentException {
final String inputFile = args[0];
final String outputFile = args[1];
final String imageFile = args[2];
final String[] coords = args[3].split(",");
final PdfReader inputReader = new PdfReader(inputFile);
final OutputStream outputStream = new FileOutputStream(outputFile);
final PdfStamper outputStamper = new PdfStamper(inputReader, outputStream);
final int pageCount = inputReader.getNumberOfPages();
final Image image = Image.getInstance(imageFile);
final int left = Integer.parseInt(coords[0]);
final int top = Integer.parseInt(coords[1]);
final int right = Integer.parseInt(coords[2]);
final int bottom = Integer.parseInt(coords[3]);
final int height = bottom - top;
final int width = right - left;
image.scaleToFit(width, height);
for (int pageIndex = 1; pageIndex <= pageCount; pageIndex++) {
final PdfContentByte overContent = outputStamper.getOverContent(pageIndex);
final Rectangle pageSize = inputReader.getPageSize(pageIndex);
image.setAbsolutePosition(left, pageSize.getHeight() - bottom);
overContent.addImage(image);
}
outputStamper.close();
}
}
In a nutshell, the first parameter is the input file, the second the output file, the third the image file, and the fourth parameter is a comma-separated list of coordinates making up the target rectangle. As usual, the code is a tad lazy when it comes to faulty input parameters and exception handling - if there's a mistake you'll simply get the stacktrace as the output of the shell command.
The most important bit is in the loop over the pages, where we use the outputStamper.getOverContent() method to draw our image on top of the existing content. If you'd rather have the image in the back, as a watermark, you would use the outputStamper.getUnderContent() methopd instead. Also note that setting the image position coordinate system works from the bottomLeft of the page, so we have to use the original page height and subtract the bottom coordinate from it.
Now we can proceed with writing a LiveCode button script:
on mouseUp
--> determine the input, output and image files
local tInputFile, tOutputFile, tImageFile
put ShellPath(AbsolutePathFromStack("demo1.pdf")) \
into tInputFile
put ShellPath(AbsolutePathFromStack("stamp.pdf")) \
into tOutputFile
put ShellPath(AbsolutePathFromStack("Template.png")) \
into tImageFile
--> determine the image target rectangle
local tImageRect
put quote & "10,10,103,87" & quote \
into tImageRect
--> determine the class path
local tClassPath
if the platform is "Win32" then
put ".;iText-2.1.7.jar" into tClassPath
else
put ".:iText-2.1.7.jar" into tClassPath
end if
--> assemble the shell command
local tShellCommand
put "java -classpath" && tClassPath && \
"StampPdfFile" && \
tInputFile && \
tOutputFile && \
tImageFile && \
tImageRect \
into tShellCommand
--> execute the shell command
local tHideConsoleWindows, tDefaultFolder, tShellResult
put the hideConsoleWindows into tHideConsoleWindows
set the hideConsoleWindows to true
put the defaultFolder into tDefaultFolder
set the defaultFolder to AbsolutePathFromStack()
put shell(tShellCommand) into tShellResult
set the defaultFolder to tDefaultFolder
set the hideConsoleWindows to tHideConsoleWindows
if tShellResult is not empty then
answer error tShellResult
end if
end mouseUp
function AbsolutePathFromStack pFileName
local tAbsolutePath
put the effective filename of this stack into tAbsolutePath
set the itemDelimiter to slash
if pFileName is not empty then
put pFileName into item -1 of tAbsolutePath
else
delete item -1 of tAbsolutePath
end if
return tAbsolutePath
end AbsolutePathFromStack
function ShellPath pPath
if the platform is "Win32" then
put quote & pPath & quote into pPath
else
replace space with backslash & space in pPath
end if
return pPath
end ShellPath
Click the button, and it happily takes the existing PDF files (demo1.pdf), paints the image (Template.png) on top of all pages, and writes a new PDF file (stamp.pdf) in the same folder as our stack. There we have it, another example of using iText from within LiveCode.
Subscribe to:
Posts (Atom)