XprEntity.java

/*
** Module   : XprEntity.java
** Abstract : Class to collect and prcess XPR file structures and data.
**
** Copyright (c) 2018-2019, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 EVL 20180509 Created initial version.
** 002 EVL 20180522 Adding multiple pages support.  Tag checking is now case insensitive.
** 003 EVL 20180527 Code optimization and clean ups.  Adding left margin tag support.
** 004 EVL 20180601 Fix for text lines positioning.  Complete filled rectangle support.
** 005 EVL 20180622 Fix for number format error with row or column set tags.  Adding support for
**                  lpi save/adjust/restore tags. Adding page orientation tags support.
** 006 EVL 20180703 Adding support for /P stop font size tag.  Do nothing except message logging
**                  for now until we find the real work to be happen with this tag.
** 007 EVL 20180717 Adding valid integer string to font size tag to separate from other taggs with
**                  same starting substring, like PREVIEW.
** 008 EVL 20180727 Adding abend protection and bookmark handling for column and row setter tags.
** 009 EVL 20181007 Adding ability to get image from application jar file.
** 010 EVL 20190116 Adding simple image resource name validiation check.
** 011 EVL 20190514 Adding interface for all PDF sources to be used in JasperReports.
*/
/*
** 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.*;
import com.goldencode.p2j.util.*;

import java.io.*;
import java.util.*;

/**
 * The object collecting all initial data for given XPR file.  Also it performs tags processing
 * and makes abstraction model to be used by Jasperreport library to make proper report object.
 */
public class XprEntity
implements PdfProvider
{
   /** Default LPI value for screen report. */
   public static final double LPI_DEFAULT = 6.0;
   
   /** Default CPI value for screen report. */
   public static final double CPI_DEFAULT = 10.0;
   
   /** Default DPI value for PDF images rendering. */
   public static final int DPI_PDF_DEFAULT = 300;
   
   /** Base units is inches constant. */
   private static final int UNITS_INCHES = 0;
   
   /** Base units is mm constant. */
   private static final int UNITS_MM = 1;
   
   /** Inch to mm transformation. */
   public static final double INCH_2_MM = 25.4;
   
   /** Font size default value for XPR. */
   private static final int FONT_SIZE_DEFAULT = 12;
   
   /** Font name default value for XPR. */
   private static final String FONT_NAME_DEFAULT = "Courier New";
   
   /** Page separator marker. */
   private static final String PAGE_SEPARATOR = "\f";
   
   /** Page separator marker. */
   private static final String TAG_VALUE_ERROR = "TAG_VALUE_ERROR";
   
   /** This string to be considered as text, not a tag start. */
   private static final String LT_IN_TEXT = "\\<";
   
   /** Constant to replace temporary substitution for lt char */
   private static final String LT_AS_TEXT = "<";
   
   /** Constant to temporary replace lt char in text for the time of tags processing. */
   private static final String LT_IN_TEXT_SUBST = "\\LT";
   
   /** This string to be considered as text, not a tag end. */
   private static final String GT_IN_TEXT = "\\>";
   
   /** Constant to replace temporary substitution for gt char. */
   private static final String GT_AS_TEXT = ">";
   
   /** Constant to temporary replace gt char in text for the time of tags processing. */
   private static final String GT_IN_TEXT_SUBST = "\\GT";
   
   /** Pattern matches string as containing only digits, allowing decimal one. */
   private static final String DECIMAL_DIGIT = "\\d*\\.?\\d*";
   
   /** Pattern matches string as containing only integer digits. */
   private static final String INTEGER_DIGIT = "\\d+";
   
   /** Pattern matches string as containing any char except digit. */
   private static final String ANY_CHAR_BUT_DIGIT = "[^0-9]";
   
   /** LPI identifier as tag value part. */
   private static final String LPI_AS_TEXT = "LPI";
   
   /** Original data from source file as array of pages. */
   private List<List<String>> origPagesBuffer = new ArrayList<List<String>>();
   
   /** The Z-ordered list of the objects recognized inside current file entity, paginated. */
   private List<List<XprObjBase>> pages = new ArrayList<List<XprObjBase>>();
   
   /** New page specific options. */
   private List<XprPageOptions> pageOptions = new ArrayList<XprPageOptions>();
   
   /** Separate list of bookmark objects in use. */
   private Map<String, XprObjBookmark> bkmks = new HashMap<String, XprObjBookmark>();
   
   /** Enhanced vpxPrint color palette. */
   private static Map<String, ColorRgb> xprPalette = new HashMap<String, ColorRgb>();
   
   /** The value of the per document PREVIEW tag. */
   private boolean preview = false;
   
   /** The value of the per document MODAL mode tag. */
   private boolean noModal = false;
   
   /** The value of the per document PROGRESS line style tag. */
   private boolean noProgressStyle = false;
   
   /** Number of copies (single by default). */
   private int numCopies = 1;
   
   /** Current column value in new entity. */
   private double colCurr = 1.0;
   
   /** Current row value in new entity. */
   private double rowCurr = 1.0;
   
   /** Column value for FROM tag. */
   private double colFrom = 1.0;
   
   /** Row value for FROM tag. */
   private double rowFrom = 1.0;
   
   /** Column value for LEFT tag. */
   private double leftMargin = 0.0;
   
   /** Raw value for TOP tag. */
   private double topMargin = 0.0;
   
   /** Current line width value. */
   private int lineWidth = 1;
   
   /** Current font name (default is 'Courier New'). */
   private String currFontName = FONT_NAME_DEFAULT;
   
   /** Current font size (default is 12). */
   private int parSize = FONT_SIZE_DEFAULT;
   
   /** Current lines per inch value (default is 6). */
   private double linesPerInch = LPI_DEFAULT;
   
   /** Previously saved lines per inch value (default is not saved). */
   private double linesPerInchSaved = 0.0;
   
   /** Current characters per inch value (default is 10). */
   private double charsPerInch = 10.0;
   
   /** Flag indicating if the font is currently bold or not. */
   private boolean isBoldFont = false;
   
   /** Flag indicating if the font is currently italic or not. */
   private boolean isItalicFont = false;
   
   /** Background color for filled rectangles.*/
   private int bgColor = 0xFFFFFFFF;
   
   /** Foreground color for text.*/
   private int fgColor = 0xFF000000;
   
   /** Line color for lines, rectangles and border of the filled rectangles.*/
   private int lineColor = 0xFF000000;
   
   /** Base units type, inches or mm. */
   private int units = UNITS_INCHES;
   
   /** Device independent DPI scaling factor */
   private double dpiScalingFactor = 1.0;
   
   /** Current page orientation. */
   private int pageOrientation = XprPageOptions.PORTRAIT;
   
   // one time variables init 
   static
   {
      // init predefined color palette
      xprPalette.put("BLUE",      new ColorRgb(0x00, 0x00, 0xFF));
      xprPalette.put("LTBLUE",    new ColorRgb(0x4D, 0x4D, 0xFF));
      xprPalette.put("DKBLUE",    new ColorRgb(0x00, 0x00, 0xB2));
      xprPalette.put("WHITE",     new ColorRgb(0xFF, 0xFF, 0xFF));
      xprPalette.put("LTWHITE",   new ColorRgb(0xFF, 0xFF, 0xFF));
      xprPalette.put("DKWHITE",   new ColorRgb(0xB2, 0xB2, 0xB2));
      xprPalette.put("BLACK",     new ColorRgb(0x00, 0x00, 0x00));
      xprPalette.put("LTBLACK",   new ColorRgb(0x4D, 0x4D, 0x4D));
      xprPalette.put("DKBLACK",   new ColorRgb(0x00, 0x00, 0x00));
      xprPalette.put("AQUA",      new ColorRgb(0x00, 0xFF, 0xFF));
      xprPalette.put("LTAQUA",    new ColorRgb(0x4D, 0xFF, 0xFF));
      xprPalette.put("DKAQUA",    new ColorRgb(0x00, 0xB2, 0xB2));
      xprPalette.put("DKGRAY",    new ColorRgb(0x80, 0x80, 0x80));
      xprPalette.put("LTDKGRAY",  new ColorRgb(0xA6, 0xA6, 0xA6));
      xprPalette.put("DKDKGRAY",  new ColorRgb(0x5A, 0x5A, 0x5A));
      xprPalette.put("LTGRAY",    new ColorRgb(0xC0, 0xC0, 0xC0));
      xprPalette.put("LTLTGRAY",  new ColorRgb(0xD3, 0xD3, 0xD3));
      xprPalette.put("DKLTGRAY",  new ColorRgb(0x86, 0x86, 0x86));
      xprPalette.put("GRAY",      new ColorRgb(0x80, 0x80, 0x80));
      xprPalette.put("LTGRAY",    new ColorRgb(0xC0, 0xC0, 0xC0));
      xprPalette.put("DKGRAY",    new ColorRgb(0x80, 0x80, 0x80));
      xprPalette.put("FUCHSIA",   new ColorRgb(0xFF, 0x00, 0xFF));
      xprPalette.put("LTFUCHSIA", new ColorRgb(0xFF, 0x4D, 0xFF));
      xprPalette.put("DKFUCHSIA", new ColorRgb(0xB2, 0x00, 0xB2));
      xprPalette.put("GREEN",     new ColorRgb(0x00, 0x80, 0x00));
      xprPalette.put("LTGREEN",   new ColorRgb(0x4D, 0xA6, 0x4D));
      xprPalette.put("DKGREEN",   new ColorRgb(0x00, 0x5A, 0x00));
      xprPalette.put("NAVY",      new ColorRgb(0x00, 0x00, 0x80));
      xprPalette.put("LTNAVY",    new ColorRgb(0x4D, 0x4D, 0xA6));
      xprPalette.put("DKNAVY",    new ColorRgb(0x00, 0x00, 0x5A));
      xprPalette.put("OLIVE",     new ColorRgb(0x80, 0x80, 0x00));
      xprPalette.put("LTOLIVE",   new ColorRgb(0xA6, 0xA6, 0x4D));
      xprPalette.put("DKOLIVE",   new ColorRgb(0x5A, 0x5A, 0x00));
      xprPalette.put("PURPLE",    new ColorRgb(0x80, 0x00, 0x80));
      xprPalette.put("LTPURPLE",  new ColorRgb(0xA6, 0x4D, 0xA6));
      xprPalette.put("DKPURPLE",  new ColorRgb(0x5A, 0x00, 0x5A));
      xprPalette.put("RED",       new ColorRgb(0xFF, 0x00, 0x00));
      xprPalette.put("LTRED",     new ColorRgb(0xFF, 0x4D, 0x4D));
      xprPalette.put("DKRED",     new ColorRgb(0xB2, 0x00, 0x00));
      xprPalette.put("SILVER",    new ColorRgb(0xC0, 0xC0, 0xC0));
      xprPalette.put("LTSILVER",  new ColorRgb(0xD3, 0xD3, 0xD3));
      xprPalette.put("DKSILVER",  new ColorRgb(0x86, 0x86, 0x86));
      xprPalette.put("YELLOW",    new ColorRgb(0xFF, 0xFF, 0x00));
      xprPalette.put("LTYELLOW",  new ColorRgb(0xFF, 0xFF, 0x4D));
      xprPalette.put("DKYELLOW",  new ColorRgb(0xB2, 0xB2, 0x00));
      xprPalette.put("MAROON",    new ColorRgb(0x80, 0x00, 0x00));
      xprPalette.put("LTMAROON",  new ColorRgb(0xA6, 0x4D, 0x4D));
      xprPalette.put("DKMAROON",  new ColorRgb(0x5A, 0x00, 0x00));
   }
   
   /**
    * Creates new instance of the object for given file object.
    *
    * @param   xprFileReader
    *          The XPR file object reader to process.
    */
   public XprEntity(Reader xprFileReader)
   throws IOException
   {
      // reading data from source
      BufferedReader brInput = new BufferedReader(xprFileReader);
      // reading line by line until the end
      String nextLine = brInput.readLine();
      // at least one page we always have
      List<String> origLinesBuffer = new ArrayList<String>();
      while (nextLine != null)
      {
         // next page creation is requied
         if (origLinesBuffer == null)
         {
            origLinesBuffer = new ArrayList<String>();
         }
         // page separation
         int pageSepNdx = nextLine.indexOf(PAGE_SEPARATOR);
         // another original line
         if (pageSepNdx != -1)
         {
            String textBeforePageSep = nextLine.substring(0, pageSepNdx);
            // adding possible text in line before page separator
            if (!textBeforePageSep.isEmpty())
            {
               origLinesBuffer.add(textBeforePageSep);
            }
            // now adding lines buffer as new page
            origPagesBuffer.add(origLinesBuffer);
            // this means we finished current page but not yet started the new one
            origLinesBuffer = null;
            // check if there is something after page separator
            if (pageSepNdx < nextLine.length() - 1)
            {
               // there is a text after page separator
               nextLine = nextLine.substring(pageSepNdx + 1);
               // skip next line reading, consider the rest of line as next one
               continue;
            }
         }
         else
         {
            origLinesBuffer.add(nextLine);
         }
         // read next line
         nextLine = brInput.readLine();
      }
      // close the reader
      brInput.close();
      // check if there is unhandled page buffer
      if (origLinesBuffer != null)
      {
         origPagesBuffer.add(origLinesBuffer);
         origLinesBuffer = null;
      }
   }
   
   /**
    * Setting up internal object of this entity.
    */
   public void init()
   {
      // init global variable for XPR
      initGlobals();
      // scan for objects inside XPR and initialize them
      initObjects();
   }
   
   /**
    * Initializes internal objets for this XPR entity.
    *
    * @return  <code>TRUE</code> if success, <code>FALSE</code> otherwise.
    */
   private boolean initObjects()
   {
      XprObjText xprText = null;
      int totalNumPages = origPagesBuffer.size();
      
      // every page will have separate list
      for (int i = 0; i < totalNumPages; i++)
      {
         // reset internal constants
         resetInitials();
         // new object list for new page
         List<XprObjBase> objects = new ArrayList<XprObjBase>();
         // new page options set with current default page size 
         XprPageOptions pgOpt = new XprPageOptions(XprHelper.getPdfPageWidth(),
                                                   XprHelper.getPdfPageHeight(),
                                                   pageOrientation);
         // go throught all lines from start to end
         List<String> origLinesBuffer = origPagesBuffer.get(i);
         for (int j = 0; j < origLinesBuffer.size(); j++)
         {
            // starting to scan next line
            String currLine = origLinesBuffer.get(j);
            // we need to replace all <> characters to be considered as tags with other
            // string for the time of tags scanning and processing
            // will return back when constructing text object
            currLine = currLine.replace(LT_IN_TEXT, LT_IN_TEXT_SUBST)
                               .replace(GT_IN_TEXT, GT_IN_TEXT_SUBST);
            String nextTag = null;
            // main line loop for not empty initial lines
            while ((nextTag = getNextTagOrTextInLine(currLine)) != null)
            {
               // single case tags one per documents nothing to do here
               // TODO: looks not very optimal check think more to find a better way
               if (!tagMatch(nextTag, XprTag.PREVIEW) && !tagMatch(nextTag, XprTag.MODAL) &&
                   !tagMatch(nextTag, XprTag.NO_PROGRESS) && !tagMatch(nextTag, XprTag.COPIES))
               {
                  // tags processing
                  if (tagMatch(nextTag, XprTag.FROM))
                  {
                     // remember current coords as primitive starting point
                     rowFrom = rowCurr;
                     colFrom = colCurr;
                  }
                  else if (tagMatch(nextTag, XprTag.COL_SET))
                  {
                     // column value change
                     char chOperation = nextTag.charAt(2);
                     // depending on operation type
                     if (chOperation == XprTag.CHR_PLUS)
                     {
                        // getting number from string
                        String numStr = nextTag.substring(3,nextTag.length() - 1);
                        // check if the string has valid digit
                        if (numStr.matches(DECIMAL_DIGIT))
                        {
                           colCurr += Double.valueOf(numStr).doubleValue();
                        }
                        else
                        {
                           // not only digits, the format is invalid
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid tag %s format.",
                                            nextTag));
                        }
                     }
                     else if (chOperation == XprTag.CHR_MINUS)
                     {
                        // getting number from string
                        String numStr = nextTag.substring(3,nextTag.length() - 1);              
                        // check if the string has valid digit
                        if (numStr.matches(DECIMAL_DIGIT))
                        {
                           colCurr -= Double.valueOf(numStr).doubleValue();
                        }
                        else
                        {
                           // not only digits, the format is invalid
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid tag %s format.",
                                            nextTag));
                        }
                     }
                     else if (chOperation == XprTag.CHR_SHARP)
                     {
                        String bkmkName = nextTag.substring(nextTag.indexOf(XprTag.CHR_SHARP) + 1,
                                                            nextTag.indexOf(XprTag.END));
                        // get column from bookmark provided
                        XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                        if (bkmkObj != null)
                        {
                           colCurr = bkmkObj.getColumn();
                        }
                        else
                        {
                           // invalid bookmark name
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid bookmark name in tag %s.",
                                            nextTag));
                        }
                     }
                     else
                     {
                        // getting number from string
                        String numStr = nextTag.substring(2,nextTag.length() - 1);
                        // check if the string has valid digit
                        if (numStr.matches(DECIMAL_DIGIT))
                        {
                           colCurr = Double.valueOf(numStr).doubleValue();
                        }
                        else
                        {
                           // not only digits, the format is invalid
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid tag %s format.", nextTag));
                        }
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.RECT))
                  {
                     double rowBk = 0.0;
                     double colBk = 0.0;
                     int bkmkNdx = nextTag.indexOf(XprTag.CHR_SHARP);
                     // if bookmark embedded - use it as from point
                     if (bkmkNdx != -1)
                     {
                        int bkmkEnd = nextTag.indexOf(XprTag.END);
                        String bkmkName = nextTag.substring(bkmkNdx + 1, bkmkEnd);
                        // get bookmark coords
                        XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                        if (bkmkObj != null)
                        {
                           rowBk = bkmkObj.getRow();
                           colBk = bkmkObj.getColumn();
                        }
                     }
                     else
                     {
                        rowBk = rowFrom;
                        colBk = colFrom;
                     }
                     // check if the rectangle is rounded
                     boolean isRounded = nextTag.indexOf(")") != -1;
                     // new rectangle detected
                     XprObjRectangle xprRect = new XprObjRectangle(lineColor, rowBk, colBk,
                                                                   rowCurr, colCurr, lineWidth,
                                                                   isRounded);
                     xprRect.setMargins(leftMargin, topMargin);
                     objects.add(xprRect);
                  }
                  else if (tagMatch(nextTag, XprTag.FILLRECT))
                  {
                     double rowBk = 0.0;
                     double colBk = 0.0;
                     int bkmkNdx = nextTag.indexOf(XprTag.CHR_SHARP);
                     // if bookmark embedded - use it as from point
                     if (bkmkNdx != -1)
                     {
                        int bkmkEnd = nextTag.indexOf(XprTag.END);
                        String bkmkName = nextTag.substring(bkmkNdx + 1, bkmkEnd);
                        // get bookmark coords
                        XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                        if (bkmkObj != null)
                        {
                           rowBk = bkmkObj.getRow();
                           colBk = bkmkObj.getColumn();
                        }
                     }
                     else
                     {
                        rowBk = rowFrom;
                        colBk = colFrom;
                     }
                     // check if the rectangle is rounded
                     boolean isRounded = nextTag.indexOf(")") != -1;
                     int frLineWidth = 0;
                     if (nextTag.indexOf("+") != -1 || nextTag.indexOf("|") != -1)
                     {
                        frLineWidth = lineWidth;
                     }
                     // new rectangle detected
                     XprObjFilledRectangle xprFillRect =
                        new XprObjFilledRectangle(lineColor, bgColor, rowBk, colBk,
                                                  rowCurr, colCurr, frLineWidth, isRounded);
                     xprFillRect.setMargins(leftMargin, topMargin);
                     objects.add(xprFillRect);
                  }
                  else if (tagMatch(nextTag, XprTag.RESTORE))
                  {
                     String tagVal = getTagValue(nextTag, XprTag.RESTORE);
                     if (tagVal.toUpperCase().indexOf(LPI_AS_TEXT) != -1 &&
                         linesPerInchSaved != 0.0)
                     {
                        linesPerInch = linesPerInchSaved;
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.SAVE))
                  {
                     String tagVal = getTagValue(nextTag, XprTag.SAVE);
                     if (tagVal.toUpperCase().indexOf(LPI_AS_TEXT) != -1)
                     {
                        linesPerInchSaved = linesPerInch;
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.ADJUST))
                  {
                     String tagVal = getTagValue(nextTag, XprTag.ADJUST);
                     if (tagVal.toUpperCase().indexOf(LPI_AS_TEXT) != -1)
                     {
                        // do LPI to match the current font size
                        linesPerInch = LPI_DEFAULT * (double)FONT_SIZE_DEFAULT /
                                                     ((double)parSize + 1.0);
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.ROW_SET))
                  {
                     // row value change
                     char chOperation = nextTag.charAt(2);
                     if (chOperation == XprTag.CHR_PLUS)
                     {
                        // getting number from string
                        String numStr = nextTag.substring(3,nextTag.length() - 1);
                        // check if the string has valid digit
                        if (numStr.matches(DECIMAL_DIGIT))
                        {
                           rowCurr += Double.valueOf(numStr).doubleValue() * LPI_DEFAULT /
                                                                             linesPerInch;
                        }
                        else
                        {
                           // not only digits, the format is invalid
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid tag %s format.",
                                            nextTag));
                        }
                     }
                     else if (chOperation == XprTag.CHR_MINUS)
                     {
                        // getting number from string
                        String numStr = nextTag.substring(3,nextTag.length() - 1);
                        // check if the string has valid digit
                        if (numStr.matches(DECIMAL_DIGIT))
                        {
                           rowCurr -= Double.valueOf(numStr).doubleValue() * LPI_DEFAULT /
                                                                             linesPerInch;
                        }
                        else
                        {
                           // not only digits, the format is invalid
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid tag %s format.",
                                            nextTag));
                        }
                     }
                     else if (chOperation == XprTag.CHR_SHARP)
                     {
                        String bkmkName = nextTag.substring(nextTag.indexOf(XprTag.CHR_SHARP) + 1,
                                                            nextTag.indexOf(XprTag.END));
                        // get column from bookmark provided
                        XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                        if (bkmkObj != null)
                        {
                           rowCurr = bkmkObj.getRow();
                        }
                        else
                        {
                           // invalid bookmark name
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid bookmark name in tag %s.",
                                            nextTag));
                        }
                     }
                     else
                     {
                        // getting number from string
                        String numStr = nextTag.substring(2,nextTag.length() - 1);
                        // check if the string has valid digit
                        if (numStr.matches(DECIMAL_DIGIT))
                        {
                           rowCurr = Double.valueOf(numStr).doubleValue() * LPI_DEFAULT /
                                                                            linesPerInch;
                        }
                        else
                        {
                           // not only digits, the format is invalid
                           XprHelper.displayOrLogError(
                              String.format("XPR Entity has found invalid tag %s format.",
                                            nextTag));
                        }
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.FGCOLOR))
                  {
                     fgColor = getColorFromTag(getTagValue(nextTag, XprTag.FGCOLOR));
                  }
                  else if (tagMatch(nextTag, XprTag.BGCOLOR))
                  {
                     bgColor = getColorFromTag(getTagValue(nextTag, XprTag.BGCOLOR));
                  }
                  // it is important to have LINECOLOR tag before LINE tag to avoid wrong line
                  // creation command
                  else if (tagMatch(nextTag, XprTag.LINECOLOR))
                  {
                     lineColor = getColorFromTag(getTagValue(nextTag, XprTag.LINECOLOR));
                  }
                  // current page orientations
                  else if (tagMatch(nextTag, XprTag.LANDSCAPE) ||
                           tagMatch(nextTag, XprTag.OLANDSCAPE))
                  {
                     pageOrientation = XprPageOptions.LANDSCAPE;
                     // refresh page size
                     pgOpt.setPageOrientation(pageOrientation);
                  }
                  else if (tagMatch(nextTag, XprTag.PORTRAIT) ||
                           tagMatch(nextTag, XprTag.OPORTRAIT))
                  {
                     pageOrientation = XprPageOptions.PORTRAIT;
                     // refresh page size
                     pgOpt.setPageOrientation(pageOrientation);
                  }
                  else if (tagMatch(nextTag, XprTag.LINE))
                  {
                     // new line detected
                     double rowBk = 0.0;
                     double colBk = 0.0;
                     int bkmkNdx = nextTag.indexOf(XprTag.CHR_SHARP);
                     // if bookmark embedded - use it as from point
                     if (bkmkNdx != -1)
                     {
                        int bkmkEnd = nextTag.indexOf(XprTag.END);
                        String bkmkName = nextTag.substring(bkmkNdx + 1, bkmkEnd);
                        // get bookmark coords
                        XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                        if (bkmkObj != null)
                        {
                           rowBk = bkmkObj.getRow();
                           colBk = bkmkObj.getColumn();
                        }
                     }
                     else
                     {
                        rowBk = rowFrom;
                        colBk = colFrom;
                     }
                     XprObjLine xprLine = new XprObjLine(lineColor, rowBk, colBk,
                                                       rowCurr, colCurr, lineWidth);
                     xprLine.setMargins(leftMargin, topMargin);
                     objects.add(xprLine);
                  }
                  // it is important to process #PAGES before bookmark tag
                  // otherwise it will be ignored due to same starting subsequense
                  else if (tagMatch(nextTag, XprTag.NUM_PAGES))
                  {
                     // #PAGES must be substituted by literal with number of pages
                     // this is a text object
                     String numPagesTxt = String.valueOf(totalNumPages);
                     xprText = new XprObjText(currFontName, parSize, isBoldFont, isItalicFont,
                                              fgColor, rowCurr, colCurr, numPagesTxt);
                     xprText.setMargins(leftMargin, topMargin);
                     objects.add(xprText);
                     // current column value shofted to the text length
                     colCurr += (double)numPagesTxt.length() * (double)parSize /
                                (double)FONT_SIZE_DEFAULT;
                  }
                  else if (tagMatch(nextTag, XprTag.BOOKMARK))
                  {
                     // new bookmark with the current coordinates
                     String bkmkName = nextTag.substring(2,nextTag.length() - 1);
                     XprObjBookmark bkmkObj = new XprObjBookmark(bkmkName, rowCurr, colCurr);
                     bkmks.put(bkmkName, bkmkObj);
                  }
                  else if (tagMatch(nextTag, XprTag.GOTO_BOOKMARK))
                  {
                     String bkmkName = null;
                     // optional char in GOTO operation
                     if (nextTag.charAt(2) == XprTag.CHR_SHARP)
                     {
                        bkmkName = nextTag.substring(3,nextTag.length() - 1);
                     }
                     else
                     {
                        bkmkName = nextTag.substring(2,nextTag.length() - 1);
                     }
                     // bookmark object must already be handled
                     XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                     if (bkmkObj != null)
                     {
                        // change the current coordinates
                        rowCurr = bkmkObj.getRow();
                        colCurr = bkmkObj.getColumn();
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.LINE_WIDTH))
                  {
                     // device dependent line width
                     if (nextTag.charAt(2) != '|')
                     {
                        if (nextTag.charAt(2) != '>')
                        {
                           // getting number from string
                           String numStr = nextTag.substring(2,nextTag.length() - 1);
                           if (numStr.matches(INTEGER_DIGIT))
                           {
                              lineWidth = Integer.valueOf(numStr).intValue();
                           }
                           else
                           {
                              // bad digit format
                              XprHelper.displayOrLogError(
                                 String.format("XPR Entity has found unsupported line width tag %s.",
                                               nextTag));
                           }
                        }
                        else
                        {
                           // empty tag, reset line width to default one
                           lineWidth = 1;
                        }
                     }
                     // device independent case, || tag in use
                     else
                     {
                        if (nextTag.charAt(3) != '>')
                        {
                           // getting number from string
                           String numStr = nextTag.substring(3,nextTag.length() - 1);
                           if (numStr.matches(DECIMAL_DIGIT))
                           {
                              double dblLineWidth =
                                        Double.valueOf(numStr).doubleValue() * dpiScalingFactor;
                              lineWidth = dblLineWidth < 1.0 ? 1 : (int)Math.round(dblLineWidth);
                           }
                           else
                           {
                              // bad digit format
                              XprHelper.displayOrLogError(
                                 String.format("XPR Entity has found unsupported line width tag %s.",
                                               nextTag));
                           }
                        }
                        else
                        {
                           // empty tag, reset line width to default one
                           lineWidth = 1;
                        }
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.IMAGE))
                  {
                     double rowBk = 0.0;
                     double colBk = 0.0;
                     int bkmkNdx = nextTag.indexOf(XprTag.CHR_SHARP);
                     String fullImageTag = new String(XprTag.IMAGE); 
                     if (bkmkNdx != -1)
                     {
                        int bkmkEnd = nextTag.indexOf('=');
                        String bkmkName = nextTag.substring(bkmkNdx + 1, bkmkEnd);
                        // get bookmark coords
                        XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                        if (bkmkObj != null)
                        {
                           rowBk = bkmkObj.getRow();
                           colBk = bkmkObj.getColumn();
                        }
                        // append bookmark name to full tag value
                        fullImageTag = fullImageTag.concat(nextTag.substring(bkmkNdx,
                                                                             bkmkEnd + 1));
                     }
                     else
                     {
                        rowBk = rowFrom;
                        colBk = colFrom;
                     }
                     // sometimes image path is null
                     String imgPath = getTagValue(nextTag, fullImageTag);
                     // check if the image is accessible, the server jar will have a preference
                     if (imgPath == null || imgPath.isEmpty())
                     {
                        // the image name is null or empty
                        XprHelper.displayOrLogError(
                           "XPR Entity got null or empty image file name, ignore creation.");
                     }
                     else if (FwdJasperExtensionRegistry.getRepositoryService()
                                                       .getInputStream(imgPath) != null || 
                              XprHelper.getInputStream(imgPath) != null ||
                              XprHelper.onErrorImageSilentMode())
                     {
                        // try to get image object
                        XprObjImage xprImage = new XprObjImage(rowBk, colBk, rowCurr,
                                                               colCurr, imgPath);
                        xprImage.setMargins(leftMargin, topMargin);
                        objects.add(xprImage);
                     }
                     else
                     {
                        XprHelper.displayOrLogError(
                           String.format("XPR Entity can not find the image %s, ignore creation.",
                                         imgPath));
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.FONT))
                  {
                     currFontName = nextTag.substring(2,nextTag.length() - 1);
                  }
                  else if (tagMatch(nextTag, XprTag.FONT_SIZE))
                  {
                     // getting integer value from string
                     String intStr = nextTag.substring(2,nextTag.length() - 1);
                     // check if the string has valid digit
                     if (intStr.matches(INTEGER_DIGIT))
                     {
                        parSize = Integer.valueOf(intStr).intValue();
                     }
                     else
                     {
                        // not only integer digits, unsupported tag yet
                        XprHelper.displayOrLogError(
                           String.format("XPR Entity has found unsupported font size tag %s.",
                                         nextTag));
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.STOP_FONT_SIZE))
                  {
                     // this tag /P does not show any functionality at this time jusy logging
                     XprHelper.logInfo(
                        String.format("XPR Entity ignores the tag %s. The functionality is unknown.",
                                      nextTag));
                  }
                  else if (tagMatch(nextTag, XprTag.BOLD))
                  {
                     isBoldFont = true;
                  }
                  else if (tagMatch(nextTag, XprTag.NO_BOLD))
                  {
                     isBoldFont = false;
                  }
                  else if (tagMatch(nextTag, XprTag.ITALIC))
                  {
                     isItalicFont = true;
                  }
                  else if (tagMatch(nextTag, XprTag.NO_ITALIC))
                  {
                     isItalicFont = false;
                  }
                  else if (tagMatch(nextTag, XprTag.BARCODE))
                  {
                     double rowBk = 0.0;
                     double colBk = 0.0;
                     int bkmkNdx = nextTag.indexOf(XprTag.CHR_SHARP);
                     int bkmkEnd = nextTag.indexOf(',');
                     String fullBCTag = new String(XprTag.BARCODE); 
                     if (bkmkNdx != -1)
                     {
                        String bkmkName = nextTag.substring(bkmkNdx + 1, bkmkEnd);
                        // get bookmark coords
                        XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                        if (bkmkObj != null)
                        {
                           rowBk = bkmkObj.getRow();
                           colBk = bkmkObj.getColumn();
                        }
                        // append bookmark name to full tag value
                        fullBCTag = fullBCTag.concat(nextTag.substring(bkmkNdx, bkmkEnd));
                     }
                     else
                     {
                        rowBk = rowFrom;
                        colBk = colFrom;
                     }
                     fullBCTag = fullBCTag.concat(nextTag.substring(bkmkEnd, bkmkEnd + 1));
                     XprObjBarcode xprBarcode = new XprObjBarcode(rowBk, colBk, rowCurr, colCurr,
                                                                 getTagValue(nextTag, fullBCTag));
                     xprBarcode.setMargins(leftMargin, topMargin);
                     objects.add(xprBarcode);
                  }
                  else if (tagMatch(nextTag, XprTag.AT))
                  {
                     String[] atValue = getTagValue(nextTag, XprTag.AT).split(",");
                     if (atValue != null)
                     {
                        // row change first
                        if (atValue[0] != null && !atValue[0].isEmpty())
                        {
                           char chFirst = atValue[0].charAt(0);
                           // get row from bookmark
                           if (chFirst == XprTag.CHR_SHARP)
                           {
                              String bkmkName = atValue[0].substring(1);
                              // get bookmark coords
                              XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                              if (bkmkObj != null)
                              {
                                 rowCurr = bkmkObj.getRow();
                              }
                           }
                           // adding absolute value to the current one
                           else if (chFirst == XprTag.CHR_PLUS)
                           {
                              // getting number from string
                              String numStr = atValue[0].substring(1);
                              // check if the string has valid digit
                              if (numStr.matches(DECIMAL_DIGIT))
                              {
                                 // take value in inch or mm
                                 double rowInchMM = Double.valueOf(numStr).doubleValue();
                                 // in case ov mm convert to inches
                                 if (units == UNITS_MM)
                                 {
                                    rowInchMM /= INCH_2_MM;
                                 }
                                 // convert inch value to row
                                 rowCurr += rowInchMM * linesPerInch;
                              }
                              else
                              {
                                 // not only digits, the format is invalid
                                 XprHelper.displayOrLogError(
                                    String.format("XPR Entity has found invalid tag %s format.",
                                                  nextTag));
                              }
                           }
                           // subtract absolute value from the current one
                           else if (chFirst == XprTag.CHR_MINUS)
                           {
                              // getting number from string
                              String numStr = atValue[0].substring(1);
                              // check if the string has valid digit
                              if (numStr.matches(DECIMAL_DIGIT))
                              {
                                 // take value in inch or mm
                                 double rowInchMM = Double.valueOf(numStr).doubleValue();
                                 // in case ov mm convert to inches
                                 if (units == UNITS_MM)
                                 {
                                    rowInchMM /= INCH_2_MM;
                                 }
                                 // convert inch value to row
                                 rowCurr -= rowInchMM * linesPerInch;
                              }
                              else
                              {
                                 // not only digits, the format is invalid
                                 XprHelper.displayOrLogError(
                                    String.format("XPR Entity has found invalid tag %s format.",
                                                  nextTag));
                              }
                           }
                           // set absolute value as current one
                           else
                           {
                              // getting number from string
                              String numStr = atValue[0];
                              // check if the string has valid digit
                              if (numStr.matches(DECIMAL_DIGIT))
                              {
                                 // take value in inch or mm
                                 double rowInchMM = Double.valueOf(numStr).doubleValue();
                                 // in case ov mm convert to inches
                                 if (units == UNITS_MM)
                                 {
                                    rowInchMM /= INCH_2_MM;
                                 }
                                 // convert inch value to row
                                 rowCurr = rowInchMM * linesPerInch;
                              }
                              else
                              {
                                 // not only digits, the format is invalid
                                 XprHelper.displayOrLogError(
                                    String.format("XPR Entity has found invalid tag %s format.",
                                                  nextTag));
                              }
                           }
                        }
                        // then column
                        if (atValue.length > 1 && atValue[1] != null && !atValue[1].isEmpty())
                        {
                           char chFirst = atValue[1].charAt(0);
                           // get column from bookmark
                           if (chFirst == XprTag.CHR_SHARP)
                           {
                              String bkmkName = atValue[1].substring(1);
                              // get bookmark coords
                              XprObjBookmark bkmkObj = bkmks.get(bkmkName);
                              if (bkmkObj != null)
                              {
                                 colCurr = bkmkObj.getColumn();
                              }
                           }
                           // adding absolute value to the current one
                           else if (chFirst == XprTag.CHR_PLUS)
                           {
                              // getting number from string
                              String numStr = atValue[1].substring(1);
                              // check if the string has valid digit
                              if (numStr.matches(DECIMAL_DIGIT))
                              {
                                 double colInchMM = Double.valueOf(numStr).doubleValue();
                                 // in case ov mm convert to inches
                                 if (units == UNITS_MM)
                                 {
                                    colInchMM /= INCH_2_MM;
                                 }
                                 // convert inch value to column
                                 colCurr += colInchMM * CPI_DEFAULT;
                              }
                              else
                              {
                                 // not only digits, the format is invalid
                                 XprHelper.displayOrLogError(
                                    String.format("XPR Entity has found invalid tag %s format.",
                                                  nextTag));
                              }
                           }
                           // subtract absolute value from the current one
                           else if (chFirst == XprTag.CHR_MINUS)
                           {
                              // getting number from string
                              String numStr = atValue[1].substring(1);
                              // check if the string has valid digit
                              if (numStr.matches(DECIMAL_DIGIT))
                              {
                                 double colInchMM = Double.valueOf(numStr).doubleValue();
                                 // in case ov mm convert to inches
                                 if (units == UNITS_MM)
                                 {
                                    colInchMM /= INCH_2_MM;
                                 }
                                 // convert inch value to column
                                 colCurr -= colInchMM * CPI_DEFAULT;
                              }
                              else
                              {
                                 // not only digits, the format is invalid
                                 XprHelper.displayOrLogError(
                                    String.format("XPR Entity has found invalid tag %s format.",
                                                  nextTag));
                              }
                           }
                           // set absolute value as current one
                           else
                           {
                              // getting number from string
                              String numStr = atValue[1];
                              // check if the string has valid digit
                              if (numStr.matches(DECIMAL_DIGIT))
                              {
                                 double colInchMM = Double.valueOf(numStr).doubleValue();
                                 // in case ov mm convert to inches
                                 if (units == UNITS_MM)
                                 {
                                    colInchMM /= INCH_2_MM;
                                 }
                                 // convert inch value to column
                                 colCurr = colInchMM * CPI_DEFAULT;
                              }
                              else
                              {
                                 // not only digits, the format is invalid
                                 XprHelper.displayOrLogError(
                                    String.format("XPR Entity has found invalid tag %s format.",
                                                  nextTag));
                              }
                           }
                        }
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.UNITS))
                  {
                     String unitsValue = getTagValue(nextTag, XprTag.UNITS);
                     if (unitsValue.equals("INCHES"))
                     {
                        units = UNITS_INCHES;
                     }
                     else if (unitsValue.equals("MM"))
                     {
                        units = UNITS_MM;
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.LEFT))
                  {
                     // left margin declaration
                     String leftMarginStr = getTagValue(nextTag, XprTag.LEFT).toUpperCase();
                     if (leftMarginStr != null && !leftMarginStr.isEmpty())
                     {
                        double margin = 0.0;
                        if (leftMarginStr.endsWith("MM"))
                        {
                           String numStr = leftMarginStr.substring(0, leftMarginStr.length() - 2);
                           if (numStr.matches(DECIMAL_DIGIT))
                           {
                              margin = Double.valueOf(numStr).doubleValue();
                              // convert mm to inches 
                              margin = margin / INCH_2_MM;
                           }
                           else
                           {
                              // not only digits, unsupported tag yet
                              XprHelper.displayOrLogError(
                                 String.format("XPR Entity has found unsupported LEFT tag %s.",
                                               nextTag));
                           }
                        }
                        else
                        {
                           String numStr = leftMarginStr.substring(0, leftMarginStr.length() - 1);
                           if (numStr.matches(DECIMAL_DIGIT))
                           {
                              margin = Double.valueOf(numStr).doubleValue();
                           }
                           else
                           {
                              // not only digits, unsupported tag yet
                              XprHelper.displayOrLogError(
                                 String.format("XPR Entity has found unsupported LEFT tag %s.",
                                               nextTag));
                           }
                        }
                        leftMargin = margin * CPI_DEFAULT / dpiScalingFactor;
                     }
                  }
                  else if (tagMatch(nextTag, XprTag.TOP))
                  {
                     // left margin declaration
                     String topMarginStr = getTagValue(nextTag, XprTag.TOP).toUpperCase();
                     if (topMarginStr != null && !topMarginStr.isEmpty())
                     {
                        double margin = 0.0;
                        if (topMarginStr.endsWith("MM"))
                        {
                           String numStr = topMarginStr.substring(0, topMarginStr.length() - 2);
                           if (numStr.matches(DECIMAL_DIGIT))
                           {
                              margin = Double.valueOf(numStr).doubleValue();
                              // convert mm to inches 
                              margin = margin / INCH_2_MM;
                           }
                           else
                           {
                              // not only digits, unsupported tag yet
                              XprHelper.displayOrLogError(
                                 String.format("XPR Entity has found unsupported TOP tag %s.",
                                               nextTag));
                           }
                        }
                        else
                        {
                           String numStr = topMarginStr.substring(0, topMarginStr.length() - 1);
                           if (numStr.matches(DECIMAL_DIGIT))
                           {
                              margin = Double.valueOf(numStr).doubleValue();
                           }
                           else
                           {
                              // not only digits, unsupported tag yet
                              XprHelper.displayOrLogError(
                                 String.format("XPR Entity has found unsupported TOP tag %s.",
                                               nextTag));
                           }
                        }
                        topMargin = margin * linesPerInch / dpiScalingFactor;
                     }
                  }
                  else if (!tagMatch(nextTag, XprTag.START))
                  {
                     // anything but the starting as tag can be considered as a text
                     // but first we need to restore possible <> chars to be considered as text
                     String txtLine = nextTag.replace(LT_IN_TEXT_SUBST, LT_AS_TEXT)
                                             .replace(GT_IN_TEXT_SUBST, GT_AS_TEXT);
                     xprText = new XprObjText(currFontName, parSize, isBoldFont, isItalicFont,
                                              fgColor, rowCurr, colCurr, txtLine);
                     xprText.setMargins(leftMargin, topMargin);
                     objects.add(xprText);
                     // current column value shofted to the text length
                     colCurr += (double)txtLine.length() * (double)parSize /
                                (double)FONT_SIZE_DEFAULT;
                  }
                  else
                  {
                     // all unsupported tags should be here
                     XprHelper.displayOrLogError(
                        String.format("XPR Entity has found unsupported tag %s.", nextTag));
                  }
               }
               // need to cut current line to the length of the tag
               int tagStart = currLine.indexOf(nextTag);
               if (tagStart + nextTag.length() <= currLine.length())
               {
                  currLine = currLine.substring(tagStart + nextTag.length());
               }
            }
            // end of the new line
            // reset cursor position
            colCurr = 1.0;
            rowCurr += 1.0 * LPI_DEFAULT / linesPerInch;
         }
         // store next page to container
         pages.add(objects);
         // and new page specific options for current page
         pageOptions.add(pgOpt);
      }

      // if we are here - evrything is OK.
      return true;
   }
   
   //-------------------- Implementation of the PdfProvoder interface ---------------------------
   /**
    * Getting internal object list from XPR object for particular page.
    *
    * @param   pageNum
    *          0 based page number for objects to get.
    *
    * @return  Z-ordered list of the objects recognized inside currently processed XPR file
    *          for given page number.
    */
   @Override
   public List<XprObjBase> getObjectsInPage(int pageNum)
   {
      return pages.get(pageNum);
   }
   
   /**
    * Getting total number of pages in document.
    *
    * @return  The number of pages with objects.
    */
   @Override
   public int getTotalPageNumbers()
   {
      return pages.size();
   }
   
   /**
    * Getting the page width for given page numbers.
    *
    * @param   pageNum
    *          0 based page number for option to get.
    *
    * @return  The page width in pixels or -1 for invalid page num.
    */
   @Override
   public int getPageWidth(int pageNum)
   {
      return pageNum < 0 || pageNum > pageOptions.size() - 1
                ? -1 : pageOptions.get(pageNum).getWidthPix();
   }
   
   /**
    * Getting the page height for given page numbers.
    *
    * @param   pageNum
    *          0 based page number for option to get.
    *
    * @return  The page width in pixels.
    */
   @Override
   public int getPageHeight(int pageNum)
   {
      return pageNum < 0 || pageNum > pageOptions.size() - 1
                ? -1 : pageOptions.get(pageNum).getHeightPix();
   }
   //-------------------- Implementation of the PdfProvoder interface ---------------------------
   
   /**
    * Initializes global variables.
    *
    * @return  <code>TRUE</code> if success, <code>FALSE</code> otherwise.
    */
   private boolean initGlobals()
   {
      // flags that commons for whole document
      preview = findSingleTag(XprTag.PREVIEW);
      String tagValue = getSingleTagValue(XprTag.MODAL);
      noModal = !tagValue.isEmpty() && tagValue.indexOf("=NO") != -1;
      noProgressStyle = findSingleTag(XprTag.NO_PROGRESS);
      tagValue = getSingleTagValue(XprTag.COPIES);
      if (!tagValue.isEmpty() && !tagValue.equals(TAG_VALUE_ERROR) &&
          tagValue.matches(INTEGER_DIGIT))
      {
         numCopies = Integer.valueOf(tagValue).intValue();
      }
      // initialize device independent scaling factor
      dpiScalingFactor = (double)XprHelper.getPdfDPI() / (double)DPI_PDF_DEFAULT;

      // if we are here - evrything is OK.
      return true;
   }
   
   /**
    * Gets the line for the given single tag.
    *
    * @param   tagToSearch
    *          The tag to look for in whole lines buffer.
    *
    * @return  <code>TRUE</code> if tag has been found, <code>FALSE</code> otherwise.
    */
   private boolean findSingleTag(String tagToSearch)
   {
      // the first occurence means tag has found
      for (int i = 0; i < origPagesBuffer.size(); i++)
      {
         // page by page
         List<String> origLinesBuffer = origPagesBuffer.get(i);
         for (int j = 0; j < origLinesBuffer.size(); j++)
         {
            String currLine = origLinesBuffer.get(j);
            if (currLine.indexOf(tagToSearch) != -1)
            {
               return true;
            }
         }
      }
      
      // no tag found so far
      return false;
   }
   
   /**
    * Gets the next tag value or text part for the given text line.
    *
    * @param   lineToScan
    *          The text line to search.
    *
    * @return  The tag value if tag has been found or text substring.
    *          <code>NULL</code> in case of empty string.
    */
   private String getNextTagOrTextInLine(String lineToScan)
   {
      // nothing to scan
      if (lineToScan == null || lineToScan.isEmpty())
      {
         return null;
      }
      
      int tagStart = 0;
      int tagEnd = -1;
      String resStr = null;
      if (lineToScan.startsWith(XprTag.START))
      {
         tagEnd = lineToScan.indexOf(XprTag.END);
         if (tagEnd != -1)
         {
            // extract next tag from string
            resStr = lineToScan.substring(tagStart, tagEnd + 1);
         }
      }
      else
      {
         // the text is considering to be here
         tagEnd = lineToScan.indexOf(XprTag.START);
         if (tagEnd == -1)
         {
            // only text has left
            resStr = lineToScan.substring(tagStart);
         }
         else
         {
            // extract text part from string not including tag start
            resStr = lineToScan.substring(tagStart, tagEnd);
         }
      }
      
      return resStr;
   }
   
   /**
    * Gets the single tag value for the given line.  Works for tags that can only be in single
    * occurence per file.
    *
    * @param   tagToSearch
    *          The tag to search.
    *
    * @return  The tag value if tag has been found or TAG_VALUE_ERROR otherwise.
    */
   private String getSingleTagValue(String tagToSearch)
   {
      // the first occurence means tag has found
      for (int i = 0; i < origPagesBuffer.size(); i++)
      {
         // page by page
         List<String> origLinesBuffer = origPagesBuffer.get(i);
         for (int j = 0; j < origLinesBuffer.size(); j++)
         {
            String currLine = origLinesBuffer.get(j);
            int tagStart = currLine.indexOf(tagToSearch);
            if (tagStart != -1)
            {
               currLine = currLine.substring(tagStart);
               int tagEnd = currLine.indexOf(XprTag.END);
               if (tagEnd != -1)
               {
                  String resStr = currLine.substring(tagToSearch.length(), tagEnd);
                  return resStr;
               }
            }
         }
      }

      // if we are here the tag value does not exist
      return TAG_VALUE_ERROR;
   }
   
   /**
    * Gets the tag value for the given full tag string.
    *
    * @param   fullTag
    *          The full tag text including braces enclosed (&lt;...&gt;).
    * @param   tagMatch
    *          The tag used to locate this entry.
    *
    * @return  The tag value without any extra info, only what inside braces.
    */
   private String getTagValue(String fullTag, String tagMatch)
   {
      return fullTag.substring(tagMatch.length(), fullTag.length() - 1);
   }
   
   /**
    * Extract the color nalue from the given tag value.  Different color specification formats
    * are possible.
    *
    * @param   tagValue
    *          The tag value to check without tag start and end markers.
    *
    * @return  The color value as RGB integer.
    */
   private int getColorFromTag(String tagValue)
   {
      // default color in case of error
      int retColor = 0xFF000000;
      
      // first check if the color is the name in XPR palette
      ColorRgb paletteColor = xprPalette.get(tagValue.toUpperCase());
      if (paletteColor != null)
      {
         // we keep alpha channel as opaque
         retColor |= paletteColor.getGuiRgb();
      }
      // not found in palette, try to consider as "r,g,b" format
      else
      {
         // currently we properly handle "r,g,b" format, any non-digit can be the delimiter
         String [] colors = tagValue.split(ANY_CHAR_BUT_DIGIT);
         // the colors array has valid size and content
         if (colors.length == 3 && colors[0].matches(INTEGER_DIGIT) &&
             colors[1].matches(INTEGER_DIGIT) && colors[2].matches(INTEGER_DIGIT))
         {
            int redValue = Integer.valueOf(colors[0]).intValue();
            int greenValue = Integer.valueOf(colors[1]).intValue();
            int blueValue = Integer.valueOf(colors[2]).intValue();
            // compute the final value to be Java compatible
            // ((blueValue & 0xFF) << 16) | ((greenValue & 0xFF) << 8) | (redValue & 0xFF)
            // we keep alpha channel as opaque
            retColor |= new ColorRgb(redValue, greenValue, blueValue).getGuiRgb(); 
         }
         else
         {
            // badly formatted color value
            XprHelper.displayOrLogError(
               String.format("XPR Entity has found invalid color format in tag %s.", tagValue));
         }
      }
      
      return retColor;
   }
   
   /**
    * Resets current variables to initial values.  Need to run this before processing new page.
    */
   private void resetInitials()
   {
      // reset constans
      colCurr = 1.0;
      rowCurr = 1.0;
      colFrom = 1.0;
      rowFrom = 1.0;
      lineWidth = 1;
      currFontName = FONT_NAME_DEFAULT;
      parSize = FONT_SIZE_DEFAULT;
      // TODO: find out if LPI and CPI values are resetting from page to page
//      linesPerInch = 6.0;
//      charsPerInch = 10.0;
      isBoldFont = false;
      isItalicFont = false;
      bgColor = 0xFFFFFFFF;
      fgColor = 0xFF000000;
      lineColor = 0xFF000000;
      // erase bookmarks
      bkmks.clear();
   }
   
   /**
    * Calculate if the given text match the tag value provided.  The condition is the text must
    * be started with tag value, not necessary to be the same length.  Case sensitivity is not
    * important.
    *
    * @param   textToScan
    *          The text to compute tag matching.
    * @param   tag
    *          The uppercased tag value to find within text.
    *
    * @return  <code>TRUE</code> if tag found, <code>FALSE</code> otherwise.
    */
   private boolean tagMatch(String textToScan, String tag)
   {
      // the tag and text should not be null or empty
      if (textToScan == null || textToScan.isEmpty() || tag == null || tag.isEmpty())
      {
         return false;
      }
      // the text should not be shorted than tag
      int tagLength = tag.length();
      if (textToScan.length() < tagLength)
      {
         return false;
      }
      // now check the text body
      for (int i = 0; i < tagLength; i ++)
      {
         // the code below makes comparison case insensitive
         if (Character.toUpperCase(textToScan.charAt(i)) != tag.charAt(i))
         {
            // first deviation, tag does not match
            return false;
         }
      }
      
      // here means all tag chars matches the text to scan
      return true;
   }
}