Spool printing with templates and scripting

Green Screens Web Terminal support virtual spool printer with multiple drivers. The most interesting drivers are RAW and PDF. Why RAW driver we will describe it little bit later, for now let's focus on PDF driver.

When virtual printer is started on specific OUTQ and when operator release spool file, PDF driver will convert SCS spool file into PDF document. But here story does not end.

Our PDF printing engine support report overlays (background images) and scripting through powerful custom JavaScript engine.

The basics

Each spool file have attributes, one of them is USER DATA. Usually, this attribute contains program name that generated spool file or some unique value that identifies spool file on OUTQ. When spool file is released, virtual printer will pickup spool attributes and then it will look up for template directory named as USER DATA value.

Another options is to set template name manually from web terminal before releasing spool file.

NOTE: All template folders are located on server at [USER]\io.greenscreens\templates.

TIP

If there are multiple server nodes in cluster, one can create shared template folder and create links under [USER]\io.greenscreens\

Overlays

Clients might require different backgrounds for different spool files. Sometimes spool files have different orientation or different backgrounds for different pages are required. Having this is mind we have enabled declarative configuration mechanism which enables multiple backgrounds for a single spool file.

Declaration is based on report and page properties. Those are (in order of preference):

  • default (0)
  • even (1)
  • odd (1)
  • portrait (2)
  • landscape (2)
  • first (3)
  • last (3)
  • page number (4)

To create multi background support, create background templates named as those properties.

  • Each template has selection weight. Higher number means, that template will apply.
  • If default is defined, all pages without other higher weighted templates will have default background.
  • If page is named as a page number, only that page will have declared background from numbered template.
  • Numbered template with highest weight will override other backgrounds.
  • 1.pdf will override first.pdf, landscape and portrait will override even or odd etc.
  • If image and PDF templates are declared, both will be applied independently
  • Image template will be applied first, PDF overly will be applied last

For example: If we have 10 page report, and declared default.pdf, first.pdf, portrait.pdf and 4.pdf will add background to pages with following rules:

  • 1st page will have first.pdf background
  • 4th page will have 4.pdf background
  • Portrait pages will have portrait.pdf
  • Remaining pages will have default.pdf

If default.png is declared, all pages will also contain default.png background.

NOTE: For HTML reporting, create templates as images in one of available formats: gif, png, jpg and name templates with proper extension.

Scripting

Scripting engine is modern version of JavaScript created in Java through Java Nashorn project. This is next generation of ultrafast JavaScript engine which will compile JavaScript code
into Java Virtual Machine instruction set making script execution fast. Additionally, it is possible to mix Java classes and standard JavaScript which opens very powerful possibilities.

How does it work?

Create script.jsx and save it under templates/[USER_DATA] folder. When spool file is released, virtual printer will determine which template script to use. During spool processing, various callback functions inside script will be called (if function is defined). Those functions are like triggered events. Except functions, there are also some global objects available to current script context like PDF page, PDF Document in process etc...

Here is the list of all available objects and functions.

Global objects
  • IControls controls
  • SpoolAttributes attributes
  • PDDocument document - current document
  • PDPageContentStream stream - current document stream (do no close it)
  • PDPage page - current page in use
  • String outq - name of printer output queue
Event Functions
  • function onInit() {}
  • function onStartReport() {}
  • function onStartPage() {}
  • function onEndPage() {}
  • function onLine(page, line, position, overprint, text) {}
  • function onEndReport(File file) {}
SpoolAttributes methods
  • float getCpi() {}
  • float getLpi() {}
  • float getPageLength() {}
  • float getPageWidth() {}
  • int getOwerflow() {}
  • int getSpoolNumber() {}
  • String getCreateionDate() {}
  • String getCreationTime() {}
  • String getJobName() {}
  • String getJobNumber() {}
  • String getJobUser() {}
  • String getOutq() {}
  • String getSpoolName() {}
  • String getSystem() {}
  • String getUserData() {}
  • String getUserDefinedData() {}
  • String getUserDefinedOptions() {}
  • String getUserDefinedText() {}
IControls methods
  • boolean isNewLine();
  • boolean isPageSet();
  • float getFontSize();
  • float getMarginBottomInPoints();
  • float getMarginLeftInPoints();
  • float getMarginRightInPoints();
  • float getMarginTopInPoints();
  • float getPageHeightInPoints();
  • float getPageWidthInPoints();
  • int cpiToPoint(int cpi);
  • int getCurrentLine();
  • int getLineSize();
  • int pointToCpi(int point);
  • int pointToTwip(int point);
  • int twipToPoint(int twip);
  • short getCharactersPerInch();
  • short getCharactersPerLine();
  • short getLinesPerInch();
  • short getLinesPerPage();

Finally let's show some script

/*
 *
 * Copyright (C) 2015, 2016  Green Screens Ltd.
 *
 *   Barcode generator library - API
 *   https://zxing.github.io/zxing/apidocs/
 *
 *   PDF generator library API
 *   https://pdfbox.apache.org/docs/2.0.0/javadocs/
 *
 *   NOTE: For barcode detection testing use
 *   https://play.google.com/store/apps/details?id=mobi.pdf417
 *
 */

var File = Java.type('java.io.File');
var HashMap = Java.type('java.util.HashMap');

// reference to Java barcode and PDF library classes
var BarcodeFormat = Java.type('com.google.zxing.BarcodeFormat');
var MultiFormatWriter = Java.type('com.google.zxing.MultiFormatWriter');
var MatrixToImageWriter = Java.type('com.google.zxing.client.j2se.MatrixToImageWriter');
var JPEGFactory = Java.type('org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory');
var EncodeHintType = Java.type('com.google.zxing.EncodeHintType');

var PDImageXObject = Java.type('org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject');
var PDFont      = Java.type('org.apache.pdfbox.pdmodel.font.PDFont');
var PDType1Font = Java.type('org.apache.pdfbox.pdmodel.font.PDType1Font');
var PDRectangle = Java.type('org.apache.pdfbox.pdmodel.common.PDRectangle');
var Matrix      = Java.type('org.apache.pdfbox.util.Matrix');


// other local variables
var ADDR = 'This is demo text that will be encoded into PDF417 barcode';
var PAGE_WIDTH = 635; // custom width
var repeat = 0;

/*
 * event function
 * called when new virtual printer is initialized
 */
function onInit() {
   print(outq);
   print(template);
}

/*
 * event function
 * called when new PDF is started
 */
function onStartReport() {
   print(attributes);
   print(controls);
}

/*
 * event function
 * called when PDF is saved to disk
 *
 * @file - java.io.File - location of PDF
 */
function onEndReport(file) {
   print('Location: ' + file.getAbsolutePath());
}

/*
 * event function
 * called when new page is created
 */
function onStartPage() {
   
   var pageSize = page.getMediaBox();
   
   // add logo to the top center 
   createLogo();
   
   // add background overlay
   centerRotate45(stream, pageSize, "SPECIMEN");
   
   // add footer 
   centerFooter(stream, pageSize, "© 2016., Green Screens Ltd.");
   
   // create template frame
   createFrame();
   
   // reset font color to black
   stream.setNonStrokingColor(0);
   
}

/*
 * event function
 * called when page rendering is about to end
 */
function onEndPage() {
   
    // render barcode on page number 2
    if (document.getNumberOfPages() === 2) {
         print('BARCODE VALUE: ' + ADDR);
         create2Dbarcode();
         // magenta color in R=0xff, G=0x00, B=0xff
         stream.setNonStrokingColor(0xff, 0, 0xff);
         writeText(5, 30, PDType1Font.TIMES_BOLD, 16, "DEMO TEXT");
    }
}

/*
 * event function
 * Called on every line before added to PDF. 
 * For performance, remove it if not used.
 *
 * @page int - current page
 * @line int - current line
 * @positionfloat - vertical position in points on PDF page from bottom right
 * @overprint boolean  - true if repeating
 * @text string
 * @return - string or boolean
 *    - if string, line will be replaced with returned value
 *    - if boolean and value is true, line will be skipped
 */
function onLine(page, line, position, overprint, text) {

   if (overprint) {
       repeat++;
   } else {
       repeat = 0;
   }

   // move TITLE to the left to not to print over centered logo
   if (line === 1 && repeat === 1) {
       text = text.replace('                     ', '');
   }
   
   if (line > 8) {
      stream.setNonStrokingColor(0.2);
       if (text.startsWith('      ')) {
           stream.setNonStrokingColor(120, 120, 120);
       }
       
       if (text.contains('CPF')) {
            // blue color 
            stream.setNonStrokingColor(255, 0, 0);
       }         
   } else {
       stream.setNonStrokingColor(0);
   }

   // process text on 2nd page from 15nth line
   if (page === 2 && line > 15) {
       //print(text);

       // remove spaces and dots
       var tmp = text.replace(/[ \.]/g, '');

       // check if it is url address and save it for later
       if (tmp.startsWith('http')) {
         ADDR = tmp;
         // blue color 
         stream.setNonStrokingColor(0, 0, 255);
       }
   }

   // log every line
   //print(page + ':' + line + ':' + text);

   return text.replace('QSYS', 'QDEMO');
}

// internal script function - use PDF reader to get POINT locations
function createFrame() {
       
   var pageSize = page.getMediaBox();
   var h = pageSize.getHeight();
   
   stream.setStrokingColor(32, 32, 32);
 
   stream.setLineWidth(0.5);
   //stream.addRect(2, h - 90, PAGE_WIDTH, 72);
   stream.addRect(2, 25, PAGE_WIDTH, h - 40);
   stream.closeAndStroke();

   stream.setStrokingColor(1);

   // 1st vertical head line
   stream.moveTo(414, h - 34);
   stream.lineTo(414, h - 74);
   stream.closeAndStroke();
   
   // 2nd vertical head line
   stream.moveTo(246, h - 34);
   stream.lineTo(246, h - 74);
   stream.closeAndStroke();
   
   // 1st horizontal line
   stream.moveTo(3, h - 34);
   stream.lineTo(PAGE_WIDTH, h - 34);
   stream.closeAndStroke();

   // 2nd horizontal line   
   stream.moveTo(3, h - 74);
   stream.lineTo(PAGE_WIDTH, h - 74);
   stream.closeAndStroke();
   
   // 3rd horizontal line
   stream.moveTo(3, h - 90);
   stream.lineTo(PAGE_WIDTH, h - 90);
   stream.closeAndStroke();

   // vertical body line
   stream.setStrokingColor(0.25);
   stream.moveTo(168, h - 90);
   stream.lineTo(168, 25);
   stream.closeAndStroke();
}

// internal script function
function createLogo() {
    
    var margin = 5;
    var scale = 0.24;
    var pageSize = page.getMediaBox();
    var logo = new File(template, "logo.png");
    var img = PDImageXObject.createFromFileByExtension(logo, document);

    var width = img.getWidth() * scale; 
    var height =  img.getHeight() * scale;   
    /*
    var posX = pageSize.getWidth() - width - margin;
    var posY = pageSize.getHeight() - height - margin;
    */
    var posX = (PAGE_WIDTH - width - margin) / 2;
    var posY = pageSize.getHeight() - height - margin - 12;
    
    stream.drawImage(img, posX, posY, width, height);
}

// internal script function
function create2Dbarcode() {

  // dimensions in points
  var pageSize = page.getMediaBox();
  	
  // barcode dimensions in pixel
  var bMargin = 2;
  var bWidth = 200;
  var bHeight = 150;
  
  // barcode position in points; 1px = 0.75pt 
  //var bPosX = pageSize.getWidth() - bWidth * 0.75;
  var bPosX = PAGE_WIDTH - bWidth * 0.75;
  var bPosY = 30;
  
  // barcode surrounding white space
  var hints = new HashMap();
  hints.put(EncodeHintType.MARGIN, bMargin);
  
  // create barcode as bitmap matric
  var wtr = new MultiFormatWriter();
  var bitMatrix = wtr.encode(ADDR, BarcodeFormat.PDF_417, bWidth, bHeight, hints);
  var buffImg = MatrixToImageWriter.toBufferedImage(bitMatrix);
  
  // convert bitmap matrix to JPEG 
  var image = JPEGFactory.createFromImage(document, buffImg);
  
  // and draw JPEG image to PDF page
  stream.drawImage(image, bPosX, bPosY);
}

// internal script function
function writeText(x, y, font, size, text) {
  stream.beginText();
  stream.setFont(font, size);
  stream.newLineAtOffset(x, y);
  stream.showText(text);
  stream.endText();
}

// internal script function
function centerFooter(contentStream, pageSize, message) {

	var fontSize = 8.0;
	var font = PDType1Font.HELVETICA;
	
	var textWidth = (font.getStringWidth(message) / 1000) * fontSize;
    var textHeight = (font.getFontDescriptor().getCapHeight() / 1000) * fontSize;

	var centeredXPosition = (PAGE_WIDTH - textWidth) / 2;	
	var centeredYPosition =  (5 + textHeight);
			
	contentStream.beginText();
		contentStream.setNonStrokingColor(0x99, 0x99, 0x99);
		contentStream.setFont(font, fontSize);	
		contentStream.newLineAtOffset(centeredXPosition, centeredYPosition);
		contentStream.showText(message);
	contentStream.endText();
}

// internal script function
function centerRotate45(contentStream, pageSize, message) {
	
	var fontSize = 96.0;
	var font = PDType1Font.HELVETICA;

    var ratio = Math.sqrt(2);
	var textWidth = font.getStringWidth(message) / 1000 * fontSize;
    
    // text is rotated by 45 degres, we need to calculate length of translated x
    // diagonal of square = x * sqrt(2), translated x' = x / sqrt(2)
	
	var centeredXPosition = (pageSize.getWidth() - (textWidth / ratio)) / 2;
	var centeredYPosition = (pageSize.getHeight() - (textWidth / ratio)) / 2;
	var matrix = Matrix.getRotateInstance(-7 * Math.PI * 0.25, centeredXPosition, centeredYPosition);
	
	contentStream.beginText();
		//contentStream.setNonStrokingColor(0xf5, 0xf5, 0xf5);
		contentStream.setNonStrokingColor(0.95);
		contentStream.setFont(font, fontSize);
		contentStream.setTextMatrix(matrix);
		contentStream.showText(message);
	contentStream.endText();
}

And resulting PDF will look like shown on images below. Note frames, colored text, barcode and logo image.

RAW Driver

RAW driver is pass through meaning raw spool data in EBCDIC encoding will be downloaded to the browser. But, what is the use of it? We provide simple scripting and testing desktop application for offline spool scripts testing how they will look like before releasing script to the server. This is much easier that constantly redeploying and reloading templates on the server. Once when script is ready for deployment, we can upload it to the Green Screens Terminal server.