Xpr2PdfWorker.java

/*
** Module   : Xpr2PdfWorker.java
** Abstract : Class to generate PDF file from collected XPR objects.
**
** Copyright (c) 2018-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 EVL 20180516 Created initial version.
** 002 EVL 20180522 Adding multi-page support.  Images can use bottom right coort as starting
**                  point.  Now supported.
** 003 EVL 20180527 Clean up and code optimization.  Make rectangle to be transparent.
** 004 EVL 20180531 Fixes for object's X coordinate calculation.  Adding filled rectangle support.
** 005 EVL 20180625 Changed logging level to disable message box for report compile and fill.
** 006 EVL 20181007 Adding ability to get image from application jar file.
**     EVL 20181016 Fix for different font sizing in different OS.  Need to take into account
**                  when computing bounding rectangle.
** 007 EVL 20190514 Changing source type to common interface to be use with different possible
**                  PDF sources.  Typo error fix.
** 008 EVL 20190617 Adding line style attribute support.
** 009 EVL 20211026 Changing text font size type to float.
**     EVL 20220519 For image consistency check we have to test image bytes size in addition to verify the
**                  image byte pointer array is not null.
** 010 GBB 20240419 Ghost4j lib replaced by Apache pdfbox.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.reporting;

import com.goldencode.p2j.ui.client.*;
import com.goldencode.p2j.ui.client.gui.driver.*;
import com.goldencode.p2j.util.*;
import net.sf.jasperreports.components.*;
import net.sf.jasperreports.components.barbecue.*;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.component.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.engine.export.*;
import net.sf.jasperreports.engine.type.*;
import net.sf.jasperreports.export.*;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.graphics.*;

import javax.imageio.*;
import java.awt.image.BufferedImage;
import java.awt.Color;
import java.io.*;
import java.util.*;

/**
 * The object prepares the data structures required to build consistent Jasper report object and
 * initiates the process of generating output PDF file via Jasper API.
 */
public class Xpr2PdfWorker
{
   /** Font style normal. */
   public static final int FONT_STYLE_NORMAL = 0;
   
   /** Font style bold. */
   public static final int FONT_STYLE_BOLD = 1;
   
   /** Font style bold and italic. */
   public static final int FONT_STYLE_BOLD_ITALIC = 2;
   
   /** Font style italic. */
   public static final int FONT_STYLE_ITALIC = 3;
   
   /** Default DPI value for screen report. */
   public static final int DPI_DEFAULT = 72;
   
   /** The XPR base object to get all required info about objects to include in output. */
   private PdfProvider xprBaseInput = null;
   
   /** The output PDF file name to generate.  */
   private String pdfFileName = null;
   
   /** Parameters for Jasper export. */
   private Map<String, Object> jspParameters = new HashMap<String, Object>();
   
   /** Row modifier to convert XPR helper row coords to pixel. */
   private double pixPerRow = XprHelper.getRow2Pix(DPI_DEFAULT, XprEntity.LPI_DEFAULT);
   
   /** Column modifier to convert XPR helper character coords to pixel. */
   private double pixPerCol = XprHelper.getCol2Pix(DPI_DEFAULT, XprEntity.CPI_DEFAULT);
   
   /** Variable to keep tracking the next parameter number. */
   private long parameterCounter = 0;
   
   /** Map to match OS font prefix names to PDF generic font names. */
   private static Map<String, String[]> pdfFontMap = new HashMap<String, String[]>();
   
   /** Extra size for the font cell, depending on OS behind. */
   private static int fntExtra = !XprHelper.isSimpleUseMode() &&
                                 EnvironmentOps.isUnderWindowsFamily() ? 2 : 5;
   
   /** One time font mapping init. */
   static
   {
      // font map
      pdfFontMap.put("Arial", new String[] {"Helvetica", "Helvetica-Bold",
                                            "Helvetica-BoldOblique", "Helvetica-Oblique" });
      pdfFontMap.put("Times", new String[] {"Times-Roman", "Times-Bold",
                                            "Times-BoldItalic", "Times-Italic"});
      pdfFontMap.put("Courier", new String[] {"Courier", "Courier-Bold",
                                              "Courier-BoldOblique", "Courier-Oblique"});
      pdfFontMap.put("Symbol", new String[] {"Symbol"});
      pdfFontMap.put("Dingbats", new String[] {"ZapfDingbats"});
   }
   
   /**
    * Creates new instance of the object with the given file name to export.
    *
    * @param   xprBase
    *          The XPR object to be the base for Jasper report creation.
    * @param   pdfName
    *          The PDF file object to export.
    */
   public Xpr2PdfWorker(PdfProvider xprBase, String pdfName)
   {
      pdfFileName = pdfName;
      xprBaseInput = xprBase;
   }
   
   /**
    * Maps the given OS dependent font name to OS independent PDF font.
    *
    * @param   fntName
    *          The name of the OS dependent font name to be mapped to PDF font.
    * @param   fntAttr
    *          The desired font attributes, normal, bold or italic.
    *
    * @return  The PDF font name to use or <code>NULL</code> if mapping is not possible.
    */
   public static String getMappedPdfFontName(String fntName, int fntAttr)
   {
      // NPE check
      if (fntName == null || fntName.isEmpty())
      {
         return null;
      }
      
      // walk through map to find the first good match
      for (String fntKey : pdfFontMap.keySet())
      {
         // fond close enough match
         if (fntName.indexOf(fntKey) != -1)
         {
            // get available pdf font falily array
            String[] pfdFontArr = pdfFontMap.get(fntKey);
            // if we have fons other than just normal
            if (pfdFontArr.length > fntAttr)
            {
               // return PDF font name to use according requested attributes
               return pfdFontArr[fntAttr];
            }
            else
            {
               // return PDF font name to use, simple normal version
               return pfdFontArr[0];
            }
         }
      }
      // nothing has found
      return null;
   }
   
   /**
    * Creates on the fly Jasper report and exports into PDF.
    *
    * @param   simpleUsage
    *          <code>TRUE</code> means simple using as standalone application for debugging.
    *
    * @return  <code>TRUE</code> if success, <code>FALSE</code> otherwise.
    */
   public boolean export(boolean simpleUsage)
   {
      boolean result = false;
      
      // multi page support
      List<JasperPrint> jspPrintList = new ArrayList<JasperPrint>();
      for (int i = 0; i < xprBaseInput.getTotalPageNumbers(); i++)
      {
         // create report design based on current XPR data
         JasperDesign jspDesign = makeJasperDesign(i);
         if (jspDesign == null)
         {
            return result;
         }
      
         // compile next page report
         JasperReport jspReport = null;
         try
         {
            jspReport = JasperCompileManager.compileReport(jspDesign);
         }
         catch (JRException jre)
         {
            XprHelper.displayOrLogError(String.format("Report compiling error for page %d", i),
                                        jre);
            return result;
         }
      
         // fill next page report with data
         JasperPrint jspPrint = null;
         try
         {
            jspPrint = JasperFillManager.fillReport(jspReport, jspParameters,
                                                    new JREmptyDataSource());
         }
         catch (JRException jre)
         {
            XprHelper.displayOrLogError(String.format("Report fill error for page %d", i), jre);
            return result;
         }
         // collect all filled pages
         jspPrintList.add(jspPrint);
      }
      // export report
      Stream rptStream = null;
      OutputStream outStreamBuff = null;
      if (!simpleUsage)
      {
         try
         {
            rptStream = StreamFactory.openFileStream(pdfFileName, true, false);
         }
         catch (ErrorConditionException ece)
         {
            XprHelper.displayOrLogError(
               String.format("Error opening output stream for PDF file %s", pdfFileName), ece);
            return result;
         }
      
         outStreamBuff = new BufferedOutputStream(new OutputStreamWrapper(rptStream), 16384);
      }
             
      // only PDF exporter is in action for now
      try
      {
         JRPdfExporter exporter = new JRPdfExporter();
         if (simpleUsage)
         {
            exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(pdfFileName));
         }
         else
         {
            exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(outStreamBuff));
         }
         // pass oredered pages as input source
         exporter.setExporterInput(SimpleExporterInput.getInstance(jspPrintList));
         exporter.exportReport();
      }
      catch (JRException jre)
      {
         XprHelper.displayOrLogError(
            String.format("Report export error for file %s", pdfFileName), jre);
         return result;
      }
      finally
      {
         try
         {
            if (!simpleUsage)
            {
               outStreamBuff.flush();
               outStreamBuff.close();
            }
         }
         catch (IOException ioe)
         {
            XprHelper.displayOrLogError(
               String.format("Stream closing error for file %s", pdfFileName), ioe);
            return result;
         }
      }
      
      result = true;
      return result;
   }
   
   /**
    * Creates on the fly Jasper report.
    *
    * @param   pageNum
    *          0 based page number to prepare on the fly report.
    *
    * @return  jasper design object for current XPR data.
    */
   private JasperDesign makeJasperDesign(int pageNum)
   {
      // temporary variables used while design creation
      JRDesignBand band = null;
      // start using new Jasper design
      JasperDesign resDesign = new JasperDesign();
      resDesign.setName(pdfFileName.concat("_InMemoryReport"));
      // set up page properties
      // page size, the page margins to be defined in directory 
      int pageWidth = xprBaseInput.getPageWidth(pageNum);
      // error while getting current page width
      if (pageWidth == -1)
      {
         pageWidth = XprHelper.getPdfPageWidth();
      }   
      int pageHeight = xprBaseInput.getPageHeight(pageNum);
      // error while getting current page height
      if (pageHeight == -1)
      {
         pageHeight = XprHelper.getPdfPageHeight();
      }
      resDesign.setPageWidth(pageWidth);
      resDesign.setPageHeight(pageHeight);
      // 0 for all margins and spacing
      resDesign.setColumnSpacing(0);
      resDesign.setLeftMargin(0);
      resDesign.setRightMargin(0);
      resDesign.setTopMargin(0);
      resDesign.setBottomMargin(0);
      
      // title
      band = new JRDesignBand();
      resDesign.setTitle(band);
      
      // page header
      band = new JRDesignBand();
      resDesign.setPageHeader(band);

      // column header
      band = new JRDesignBand();
      resDesign.setColumnHeader(band);

      // detail
      band = new JRDesignBand();
      band.setHeight(pageHeight);
      // we will use only one band for all objects in page
      setObjectsInBand(band, xprBaseInput.getObjectsInPage(pageNum), resDesign);
      // adding completed band to the report
      ((JRDesignSection)resDesign.getDetailSection()).addBand(band);

      // column footer
      band = new JRDesignBand();
      resDesign.setColumnFooter(band);

      // page footer
      band = new JRDesignBand();
      resDesign.setPageFooter(band);

      // summary
      band = new JRDesignBand();
      resDesign.setSummary(band);
    
      return resDesign;
   }
   
   /**
    * Creates new unique parameter name for Jasper report.
    *
    * @return  jasper design parameter name.
    */
   private String getNewParameterName()
   {
      StringBuilder sb = new StringBuilder();
      // same prefix for all parameters
      sb.append("Xpr2PdfParameter");
      // but different suffix
      sb.append(++parameterCounter);
      
      return sb.toString();
   }
   
   /**
    * Adding different objects to the given Jasper report band keeping Z-order.
    *
    * @param   objBand
    *          The Jasper report band to prepare.
    * @param   objectsToAdd
    *          The list of objects to add.
    * @param   jspDesign
    *          The current design in use.
    */
   private void setObjectsInBand(JRDesignBand objBand, List<XprObjBase> objectsToAdd,
                                 JasperDesign jspDesign)
   {
      // walk through the objects from start to end
      for (int i = 0; i < objectsToAdd.size(); i++)
      {
         XprObjBase xprObj = objectsToAdd.get(i);
         // this is object dependent set up procedure
         switch (xprObj.getType())
         {
            case XprObjBase.TYPE_LINE:
               setLineInBand(objBand, (XprObjLine)xprObj);
               break;
            case XprObjBase.TYPE_RECTANGLE:
               setRectangleInBand(objBand, (XprObjRectangle)xprObj, false);
               break;
            case XprObjBase.TYPE_FILLED_RECTANGLE:
               setRectangleInBand(objBand, (XprObjFilledRectangle)xprObj, true);
               break;
            case XprObjBase.TYPE_TEXT:
               setTextInBand(objBand, (XprObjText)xprObj, jspDesign);
               break;
            case XprObjBase.TYPE_IMAGE:
               setImageInBand(objBand, (XprObjImage)xprObj, jspDesign);
               break;
            case XprObjBase.TYPE_BARCODE:
               setBarcodeInBand(objBand, (XprObjBarcode)xprObj, jspDesign);
               break;
         }
      }
   }

   /**
    * Adding line object to the given Jasper report band as next one.
    *
    * @param   objBand
    *          The Jasper report band to prepare.
    * @param   objLineToAdd
    *          The line object to add.
    */
   private void setLineInBand(JRDesignBand objBand, XprObjLine objLineToAdd)
   {
      // reconstruct the line
      JRDesignLine line = new JRDesignLine();

      // coordinates and size
      double x1 = objLineToAdd.getColumn() - 1.0;
      double x2 = objLineToAdd.getColumnEnd() - 1.0;
      double y1 = objLineToAdd.getRow() - 1.0;
      double y2 = objLineToAdd.getRowEnd() - 1.0;
      // x, y requires floor() to compute, may be dx, dy too 
      int x = (int)Math.floor((x1 + objLineToAdd.getLeftMargin()) * pixPerCol);
      int y = (int)Math.floor((y1 + objLineToAdd.getTopMargin()) * pixPerRow);
      int dx = (int)Math.round((x2-x1) * pixPerCol);
      int dy = (int)Math.round((y2-y1) * pixPerRow);
      line.setX(x);
      line.setY(y);
      line.setWidth(dx);
      line.setHeight(dy);
      line.setForecolor(new Color(objLineToAdd.getColor()));
      // do we really need the line width, ignore for now?
      line.getLinePen().setLineWidth(objLineToAdd.getLineWidth());
      line.getLinePen().setLineStyle(objLineToAdd.getLineStyle());

      // add new line to the band
      objBand.addElement(line);
   }

   /**
    * Adding line object to the given Jasper report band as next one.
    *
    * @param   objBand
    *          The Jasper report band to prepare.
    * @param   objRectangleToAdd
    *          The rectangle object to add.
    * @param   isFilled
    *          <code>TRUE</code> for filled recteangle, <code>FALSE</code> for frame.
    */
   private void setRectangleInBand(JRDesignBand objBand, XprObjRectangle objRectangleToAdd,
                                   boolean isFilled)
   {
      // reconstruct the rect
      JRDesignRectangle rect = new JRDesignRectangle();

      // coordinates and size
      double x1 = objRectangleToAdd.getColumn() - 1.0;
      double x2 = objRectangleToAdd.getColumnEnd() - 1.0;
      double y1 = objRectangleToAdd.getRow() - 1.0;
      double y2 = objRectangleToAdd.getRowEnd() - 1.0;
      // x, y requires floor() to compute, may be dx, dy too 
      int x = (int)Math.floor((x1 + objRectangleToAdd.getLeftMargin()) * pixPerCol);
      int y = (int)Math.floor((y1 + objRectangleToAdd.getTopMargin()) * pixPerRow);
      int dx = (int)Math.round((x2-x1) * pixPerCol);
      int dy = (int)Math.round((y2-y1) * pixPerRow);
      rect.setX(x);
      rect.setY(y);
      rect.setWidth(dx);
      rect.setHeight(dy);
      rect.setForecolor(new Color(objRectangleToAdd.getColor()));
      // line width
      rect.getLinePen().setLineWidth(objRectangleToAdd.getRectLineWidth());
      rect.getLinePen().setLineStyle(objRectangleToAdd.getRectLineStyle());
      // disable fill for framed rerctangle
      if (isFilled)
      {
         rect.setBackcolor(new Color(((XprObjFilledRectangle)objRectangleToAdd).getFillColor()));
      }
      else
      {
         rect.setMode(ModeEnum.TRANSPARENT);
      }
      // set up rounded rectangle
      if (objRectangleToAdd.isRounded())
      {
         // the rounded radius computing approach:
         // - get the minimum af both directions
         // - take 1/4 of the size as radius
         rect.setRadius((int)Math.round((double)Math.min(dx, dy) * XprObjRectangle.RADIUS_SCALE));
      }

      // add new line to the band
      objBand.addElement(rect);
   }

   /**
    * Adding text object to the given Jasper report band as next one.
    *
    * @param   objBand
    *          The Jasper report band to prepare.
    * @param   objTextToAdd
    *          The text object to add.
    * @param   jspDesign
    *          The current design in use.
    */
   private void setTextInBand(JRDesignBand objBand, XprObjText objTextToAdd,
                              JasperDesign jspDesign)
   {
      // reconstruct the text as static one
      double x1 = objTextToAdd.getColumn() - 1.0;
      double y1 = objTextToAdd.getRow() - 1.0;
      // x, y requires floor() to compute, may be dx, dy too 
      int x = (int)(Math.floor((x1 + objTextToAdd.getLeftMargin()) * pixPerCol));
      int y = (int)(Math.floor((y1  + objTextToAdd.getTopMargin()) * pixPerRow));
      // we need extra pixels to make text visible if font size is bigger than row
      // compute them as 1/fntExtra of the font size
      float fontSizeFloat = objTextToAdd.getFontSize();
      int fntSize = (int)Math.round(fontSizeFloat);
      int dy = Math.max((int)Math.round(pixPerRow), fntSize + (fntSize + fntExtra)/fntExtra);
      int dx = (int)Math.max((int)Math.round(pixPerCol),
               (fntSize + fntExtra)/fntExtra + (int)(pixPerCol*(double)fntSize/pixPerRow));
      // text to display
      String textLine = objTextToAdd.getValue();
      // need to exclude text that outside the page area
      if (x > jspDesign.getPageWidth() - 1 || y > jspDesign.getPageHeight() - 1 ||
          y + dy > jspDesign.getPageHeight() - 1)
      {
         XprHelper.displayOrLogError(
            String.format("Text object %s is out of the page area.", textLine));
         return;
      }
      JRDesignStaticText txt = new JRDesignStaticText();
      txt.setX(x);
      txt.setY(y);
      txt.setHeight(dy);
      txt.setWidth(dx * textLine.length());
      // try to use OS independent font name
      boolean isBold = objTextToAdd.isBold();
      boolean isItalic = objTextToAdd.isItalic();
      int fontStyle = isBold && isItalic ? FONT_STYLE_BOLD_ITALIC
                                         : isBold ? FONT_STYLE_BOLD
                                                  : isItalic ? FONT_STYLE_ITALIC
                                                             : FONT_STYLE_NORMAL;  
      String pdfFntName = getMappedPdfFontName(objTextToAdd.getFontName(), fontStyle);
      if (pdfFntName != null)
      {
         txt.setPdfFontName(pdfFntName);
      }
      txt.setFontSize(fontSizeFloat);
      txt.setForecolor(new Color(objTextToAdd.getColor()));
      txt.setText(textLine);
      objBand.addElement(txt);
   }

   /**
    * Adding image object to the given Jasper report band as next one.
    *
    * @param   objBand
    *          The Jasper report band to prepare.
    * @param   objImageToAdd
    *          The image object to add.
    * @param   jspDesign
    *          The current design in use.
    */
   private void setImageInBand(JRDesignBand objBand, XprObjImage objImageToAdd,
                               JasperDesign jspDesign)
   {
      boolean isPDFImage = false;
      // reconstruct the image
      JRDesignImage img = new JRDesignImage(jspDesign);
      
      // coordinates and size
      double x1 = objImageToAdd.getColumn() - 1.0;
      double x2 = objImageToAdd.getColumnEnd() - 1.0;
      double y1 = objImageToAdd.getRow() - 1.0;
      double y2 = objImageToAdd.getRowEnd() - 1.0;
      // need to support possible coord reverse
      // x, y requires floor() to compute, may be dx, dy too 
      int x = (int)Math.floor((Math.min(x1, x2) + objImageToAdd.getLeftMargin()) * pixPerCol);
      int y = (int)Math.floor((Math.min(y1, y2) + objImageToAdd.getTopMargin()) * pixPerRow);
      int dx = (int)Math.round(Math.abs(x2-x1) * pixPerCol);
      int dy = (int)Math.round(Math.abs(y2-y1) * pixPerRow);
      img.setX(x);
      img.setY(y);
      int pageHeight = jspDesign.getPageHeight();
      int pageWidth = jspDesign.getPageWidth();
      // check if the image is not outside the page and compute scaling coeff
      int exDy = y + dy - pageHeight + 1;
      double kYCut = 1.0;
      if (exDy > 0)
      {
         kYCut = (double)(dy - exDy) / (double)(dy);
         dy -= exDy;
      }
      int exDx = x + dx - pageWidth + 1;
      double kXCut = 1.0;
      if (exDx > 0)
      {
         kXCut = (double)(dx - exDx) / (double)(dx);
         dx -= exDx;
      }
      img.setWidth(dx);
      img.setHeight(dy);
      img.setScaleImage(ScaleImageEnum.RETAIN_SHAPE);
      
      // images should be handled via report parameter      
      String imagePath = objImageToAdd.getLocationName();
      // find out if embedded image is pdf
      int extNdx = imagePath.lastIndexOf(".");
      if (extNdx != -1 && extNdx < imagePath.length() - 3)
      {
         String imgExtension = imagePath.substring(extNdx + 1);
         if (imgExtension != null && imgExtension.toUpperCase().equals("PDF"))
         {
            isPDFImage = true;
         }
      }
      String paramName = getNewParameterName();
      // insert parameter into design
      JRDesignParameter param = new JRDesignParameter();
      param.setName(paramName);
      // call is deprecated, no more required according to Jaspersoft
      BufferedImage imgToUse = null;
      if (isPDFImage)
      {
         param.setValueClass(java.awt.Image.class);
      }
      else
      {
         // try to get the image stream from application jar
         InputStream isImg = FwdJasperExtensionRegistry.getRepositoryService()
                                                       .getInputStream(imagePath);
         if (isImg != null)
         {
            byte[] imgBytes = UiUtils.getData(isImg);
            if (imgBytes != null && imgBytes.length > 0)
            {
               imgToUse = AbstractGuiDriver.createImage(imgBytes).getImage();
            }
         }
         
         if (imgToUse != null)
         {
            param.setValueClass(java.awt.Image.class);
         }
         else
         {
            param.setValueClass(java.lang.String.class);
         }
      }
      try
      {
         jspDesign.addParameter(param);
      }
      catch (JRException jre)
      {
         XprHelper.displayOrLogError("Image parameter adding error for JasperDesign", jre);
         return;
      }
      
      // insert expression into design
      JRDesignExpression expr = new JRDesignExpression();
      // call is deprecated, no more required according to Jaspersoft
//      expr.setValueClass(java.lang.String.class);
      expr.setText("$P{".concat(paramName).concat("}"));
      img.setExpression(expr);
      if (XprHelper.onErrorImageSilentMode())
      {
         img.setOnErrorType(OnErrorTypeEnum.ICON);
      }
      // add new line to the band
      objBand.addElement(img);
      // set up new parameter
      if(isPDFImage)
      {
         // get the input stream from helper
         InputStream is = XprHelper.getInputStream(imagePath);
         if (is == null)
         {
            XprHelper.displayOrLogError(
               String.format("Unable to get input stream from image path %s", imagePath));
            return;
         }
         // set up PDF reader
         try
         {
            PDDocument document = PDDocument.load(is);
            Iterator<PDPage> pdPageIterator = document.getDocumentCatalog().getPages().iterator();
            List<BufferedImage> images = new ArrayList<>();
            while (pdPageIterator.hasNext())
            {
               PDPage page = pdPageIterator.next();
               PDResources resources = page.getResources();
               for (COSName nextObjName : resources.getXObjectNames())
               {
                  if (resources.isImageXObject(nextObjName))
                  {
                     PDXObject pdxImage = resources.getXObject(nextObjName);
                     byte[] pdxImageBytes = pdxImage.getStream().toByteArray();
                     BufferedImage image = ImageIO.read(new ByteArrayInputStream(pdxImageBytes));
                     images.add(image);
                  }
               }
            }
            
            // take a first image with truncating
            if ( kXCut != 1.0 || kYCut != 1.0 )
            {
               // we need to truncate the image a bit
               BufferedImage bi = images.get(0);
               imgToUse = bi.getSubimage(0, 0, (int)(kXCut * (double)bi.getWidth()),
                                         (int)(kYCut * (double)bi.getHeight()));
            }
            else
            {
               imgToUse = images.get(0);
            }
            jspParameters.put(paramName, imgToUse);
         }
         catch (Exception e)
         {
            XprHelper.displayOrLogError(String.format("PDF file %s loading error", imagePath), e);
            return;
         }
      }
      else
      {
         if (imgToUse != null)
         {
            jspParameters.put(paramName, imgToUse);
         }
         else
         {
            jspParameters.put(paramName, imagePath);
         }
      }
   }

   /**
    * Adding barcode object to the given Jasper report band as next one.
    *
    * @param   objBand
    *          The Jasper report band to prepare.
    * @param   objBarcodeToAdd
    *          The barcode object to add.
    * @param   jspDesign
    *          The current design in use.
    */
   private void setBarcodeInBand(JRDesignBand objBand, XprObjBarcode objBarcodeToAdd,
                                 JasperDesign jspDesign)
   {
      // check for barcode type
      String type = objBarcodeToAdd.getBarcodeType();
      if (type == null || type.isEmpty())
      {
         // barcode type is invalid, no reasons to continue
         XprHelper.displayOrLogError("Barcode type is invalid");
         return;
      }

      // reconstruct the barcode
      StandardBarbecueComponent bcBBCComp = new StandardBarbecueComponent();
      
      // set type
      bcBBCComp.setType(type);
      // barcodes should be handled via report parameter      
      String barcodeValue = objBarcodeToAdd.getParameterValue("VALUE");
      String paramName = getNewParameterName();
      // insert parameter into design
      JRDesignParameter param = new JRDesignParameter();
      param.setName(paramName);
      param.setValueClass(java.lang.String.class);
      try
      {
         jspDesign.addParameter(param);
      }
      catch (JRException jre)
      {
         XprHelper.displayOrLogError("Barcode parameter adding error for JasperDesign", jre);
         return;
      }
      
      // checksum and show text
      bcBBCComp.setChecksumRequired(objBarcodeToAdd.isChecksum());
      bcBBCComp.setDrawText(objBarcodeToAdd.isShow());
      
      // insert expression into design
      JRDesignExpression expr = new JRDesignExpression();
      // call is deprecated, no more required according to Jaspersoft doc
//      expr.setValueClass(java.lang.String.class);
      expr.setText("$P{".concat(paramName).concat("}"));
      bcBBCComp.setCodeExpression(expr);
      
      // coordinates and size
      double x1 = objBarcodeToAdd.getColumn() - 1.0;
      double x2 = objBarcodeToAdd.getColumnEnd() - 1.0;
      double y1 = objBarcodeToAdd.getRow() - 1.0;
      double y2 = objBarcodeToAdd.getRowEnd() - 1.0;
      // x, y requires floor() to compute, may be dx, dy too 
      int x = (int)Math.floor((x1 + objBarcodeToAdd.getLeftMargin()) * pixPerCol);
      int y = (int)Math.floor((y1 + objBarcodeToAdd.getTopMargin()) * pixPerRow);
      int dx = (int)Math.round((x2-x1) * pixPerCol);
      int dy = (int)Math.round((y2-y1) * pixPerRow);
      // dimensions for barcode
      // the bar width option is the minimum element(single line) width, assume to be 1 pixel
      // or more depending on size ratio and barcode type
      int bWidth = objBarcodeToAdd.getBarWidth(dx, dy);
      if (bWidth > 0)
      {
         bcBBCComp.setBarWidth(bWidth);
      }
      // barcode width means the height might be adjusted too
      if (bWidth > 1)
      {
         bcBBCComp.setBarHeight(objBarcodeToAdd.getBarHeight(dx, dy));
      }
      else
      {
         bcBBCComp.setBarHeight(dy);
      }
      // create design element wrapper
      JRDesignComponentElement jrComp = new JRDesignComponentElement();
      // location and size
      jrComp.setX(x);
      jrComp.setY(y);
      // set up barcode rotation and enclosed element size
      String angle = objBarcodeToAdd.getParameterValue("ANGLE");
      if (!angle.isEmpty())
      {
         if (angle.equals("90"))
         {
            jrComp.setWidth(dy);
            jrComp.setHeight(dx);
            bcBBCComp.setRotation(RotationEnum.LEFT);
         }
         else if (angle.equals("180"))
         {
            jrComp.setWidth(dx);
            jrComp.setHeight(dy);
            bcBBCComp.setRotation(RotationEnum.UPSIDE_DOWN);
         }
         else if (angle.equals("270"))
         {
            jrComp.setWidth(dy);
            jrComp.setHeight(dx);
            bcBBCComp.setRotation(RotationEnum.RIGHT);
         }
         else if (!angle.equals("0") && !angle.equals("360"))
         {
            // unsupported angle here
            XprHelper.displayOrLogError(
               String.format("Barcode angle %s is not supported.", angle));
         }
      }
      else
      {
         jrComp.setWidth(dx);
         jrComp.setHeight(dy);
      }
      // insert barcode into design component to be added
      jrComp.setComponent(bcBBCComp);
      jrComp.setComponentKey(
         new ComponentKey(ComponentsExtensionsRegistryFactory.NAMESPACE, "c",
                          ComponentsExtensionsRegistryFactory.BARBECUE_COMPONENT_NAME));
      // add new line to the band
      objBand.addElement(jrComp);
      // set up new parameter
      jspParameters.put(paramName, barcodeValue);
   }
}