Metafile.java

/*
** Module   : Metafile.java
** Abstract : Class to provide GUI metafile service.
**
** Copyright (c) 2019-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 EVL 20190514 Created initial version.
** 002 EVL 20190617 Improving line style/width implementation.
** 003 EVL 20210107 Many bug fixing and improvements.
**     EVL 20210108 Completed approach for page footer generation.  Added more fixes and improvements to
**                  clean up some cosmetic deviations.
**     EVL 20210111 Continue fixup for cosmetic issues.
**     EVL 20210113 Resolution for underlined text issues and improvements for text width calculation.
**                  Several fixes for filled rectangle painting and adding coordinates validity check.
**     EVL 20210114 Fix for text height and space between lines mismatches.  Improved line thick setup.
**     EVL 20211001 Fix for page size bug after changing page orientation.  Also improved startNewPage()
**                  to consider the first page internals are creating in class constructor and new page
**                  start is optional call that never be executed in some conditions.
**     EVL 20211021 Fixed bug with draw text between left and right with right alignment.
**     EVL 20211026 Changing text font size type to float.
**     EVL 20211119 Small improvements for text layout.
**     EVL 20211123 Reworked approach for handling text with 0.0 both position for left and right margins.
**                  Now this case is considering as instruction to pring nothing.
**     EVL 20211129 Small improvements for text layout and underlined text rendering.
**     EVL 20220126 Improvements for rendering text that has more width that rectangle allocated for text
**                  area.  Other minor fixes with footer location.
**     EVL 20220127 Improvements for text region match calculations.  Added method to get underlined flag
**                  value for external classes.
**     EVL 20220225 Fix for bold font width calculation.  Need tointroduce additional scale factor for bold
**                  and probably italic fonts (made place-holder with 1.0 value).
**     EVL 20220302 Moving handling of bold and italic text width to MetafileHelper call to ensure more
**                  granularity and precision.
*/
/*
** 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.util.*;
import com.goldencode.util.*;

import net.sf.jasperreports.engine.type.*;

import java.util.*;

/**
 * The class to provide single GDI metafile service to finally compile and show the PDF document.
 * This replacement is a platform neutral.  The device independent units are centimeters for now.
 */
public class Metafile
implements PdfProvider
{
   /** Left image alignment. */
   public static final int IMAGE_ALIGN_LEFT = 0;
   
   /** Center image alignment. */
   public static final int IMAGE_ALIGN_CENTER = 1;
   
   /** Right image alignment. */
   public static final int IMAGE_ALIGN_RIGHT = 2;
   
   /** Top image alignment. */
   public static final int IMAGE_ALIGN_TOP = 3;
   
   /** Bottom image alignment. */
   public static final int IMAGE_ALIGN_BOTTOM = 4;
   
   /** Left text alignment. */
   public static final int TEXT_ALIGN_LEFT = 0;
   
   /** Right text alignment. */
   public static final int TEXT_ALIGN_RIGHT = 1;
   
   /** Center text alignment. */
   public static final int TEXT_ALIGN_CENTER = 2;
   
   /** Top text alignment. */
   public static final int TEXT_ALIGN_TOP = 3;
   
   /** Baseline text alignment. */
   public static final int TEXT_ALIGN_BASELINE = 4;
   
   /** Bottom text alignment. */
   public static final int TEXT_ALIGN_BOTTOM = 5;
   
   /** Font size default value for metafile. */
   private static final int FONT_SIZE_DEFAULT = 12;
   
   /** Font name default value for metafile. */
   private static final String FONT_NAME_DEFAULT = "Courier";
   
   /** Zoom type default value for metafile. */
   private static final String ZOOM_TYPE_DEFAULT = "page";
   
   /** Optional scaling from PDF coord to metafile. */
   private static final double pdf2MfDpi = 96.0 / (double)Xpr2PdfWorker.DPI_DEFAULT;
   
   /** To convert columns to metafile pixels */
   private static final double pixPerColMf = (double)Xpr2PdfWorker.DPI_DEFAULT /
                                             (double)XprEntity.CPI_DEFAULT;
   
   /** Constant vertical shift for object. Text, line, image and rectangle are involved. */
   private static final double OBJ_Y_SHIFT = 0.45;
   
   /** If X coord is on the lefd edge (0.0), the value must be increased. */
   private static final double EDGE_LINE_X_SHIFT = 0.125;
   
   /** Vertical lines should be shifted with this value. */
   private static final double VERT_LINE_Y_SHIFT = 0.01;
   
   /** 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>();
   
   /** The objects for the current page. */
   private List<XprObjBase> objects = new ArrayList<XprObjBase>();
   
   /** The number of the current page. */
   private int currPageNum = 1;
   
   /** The number of the maximum page. */
   private int maxPageNum = currPageNum;
   
   /** Current page orientation. */
   private int pageOrientation = XprPageOptions.PORTRAIT;
   
   /** Current font name (default is 'Courier New'). */
   private String currFontName = FONT_NAME_DEFAULT;
   
   /** Current zoom type name (default is whole page. */
   private String currZoomType = ZOOM_TYPE_DEFAULT;
   
   /** Current font size (default is 12). */
   private float fontSize = FONT_SIZE_DEFAULT;
   
   /** Current font size in metric units (default is 12). */
   private double fontSizeMetric = MetafileHelper.pix2MfUnits(fontSize);
   
   /** Flag indicating if the font is currently bold or not. */
   private boolean isBold = false;
   
   /** Flag indicating if the font is currently italic or not. */
   private boolean isItalic = false;
   
   /** Flag indicating if the font is currently underlined or not. */
   private boolean isUnderline = false;
   
   /** Foreground color for text.*/
   private int textColor = 0xFF000000;
   
   /** Line color for lines, rectangles and border of the filled rectangles.*/
   private int lineColor = 0xFF000000;
   
   /** Background color for filled rectangles.*/
   private int fillColor = 0xFFFFFFFF;
   
   /** Current X value for page number text. */
   private double xPageNum = 0.0;
   
   /** Current Y value for page number text. */
   private double yPageNum = 0.0;
   
   /** Page number texts. */
   private List<XprObjText> pageNumberTextObj = new ArrayList<XprObjText>();
   
   /** Left margin value. */
   private double leftMargin = 0.5;
   
   /** Top margin value. */
   private double topMargin = 0.0;
   
   /** Footer reserverd size value. */
   private double footerHeight = 0.0;
   
   /** Extra vertical distance between two sequential rows of the text. */
   private double extraRowDistance = 0.06125;
   
   /** Current page width in device independent units. */
   private double diPageWidth = 0.0;
   
   /** Current page height in device independent units. */
   private double diPageHeight = 0.0;
   
   /** Current X value in new entity. */
   private double xCurr = 0.0;
   
   /** Current Y value in new entity. */
   private double yCurr = 0.0;
   
   /** Current line thickness value. */
   private int lineWidth = 1;
   
   /** Current horizontal text alignment. */
   private int textAlignHorizCurr = TEXT_ALIGN_LEFT;
   
   /** Current vertical text alignment. */
   private int textAlignVertCurr = TEXT_ALIGN_TOP;
   
   /** Current horizontal image alignment. */
   private int imageAlignHorizCurr = IMAGE_ALIGN_LEFT;
   
   /** Current vertical image alignment. */
   private int imageAlignVertCurr = IMAGE_ALIGN_TOP;
   
   /** Flag indicating if the footer will be shown or not. */
   private boolean showFooter = false;
   
   /** Current line style value. */
   private LineStyleEnum lineStyle = LineStyleEnum.SOLID;
   
   /** Flag indicating there are possibly records to commit. */
   private boolean recordingStopped = false;
   
   /** Temporary directory to store the intermediate files. */
   private String pdfDir = null;
   
   /** Flag to indicate the first page already created. */
   private boolean firstPageCreated = false;
   
   /**
    * Default metafile object constructor.
    */
   public Metafile()
   {
      // and create new object room for next page objects
      objects = new ArrayList<XprObjBase>();
      // new page options set with current default page size 
      setPageOptions();
      // we have the first empty page now
      firstPageCreated = true;
   }
   
   /**
    * Sets new values for text alignment.
    * 
    * @param    horizAlign
    *           The new String value for horizontal text alignment.  The valid alignment values:
    *           "left", "center", "right".
    * @param    vertAlign
    *           The new String value for vertical text alignment.  The valid alignment values:
    *           "top", "baseline", "bottom".
    */
   public void setTextAlignment(String horizAlign, String vertAlign)
   {
      // horizontal alignment type 
      if (horizAlign != null && !horizAlign.isEmpty())
      {
         horizAlign = horizAlign.toLowerCase();
         if (horizAlign.equals("left"))
         {
            textAlignHorizCurr = TEXT_ALIGN_LEFT;
         }
         else if (horizAlign.equals("center"))
         {
            textAlignHorizCurr = TEXT_ALIGN_CENTER;
         }
         else if (horizAlign.equals("right"))
         {
            textAlignHorizCurr = TEXT_ALIGN_RIGHT;
         }
      }
      // vetical alignment type 
      if (vertAlign != null && !vertAlign.isEmpty())
      {
         vertAlign = vertAlign.toLowerCase();
         if (vertAlign.equals("top"))
         {
            textAlignVertCurr = TEXT_ALIGN_TOP;
         }
         else if (vertAlign.equals("baseline"))
         {
            textAlignVertCurr = TEXT_ALIGN_BASELINE;
         }
         else if (vertAlign.equals("bottom"))
         {
            textAlignVertCurr = TEXT_ALIGN_BOTTOM;
         }
      }
   }

   /**
    * Sets new current color value to be used in text primitives.
    * 
    * @param    rgbColor
    *           The new 32-bit color value packed in int value.
    */
   public void setTextColor(int rgbColor)
   {
      textColor = rgbColor;
   }

   /**
    * Sets new value for text style.
    * 
    * @param    style
    *           The new String value for the current text style.  The valid style values:
    *           "italic", "underline", "bold", "no-italic", "no-underline", "no-bold".
    */
   public void setTextStyle(String style)
   {
      // simple protection from empty style value
      if (style == null || style.isEmpty())
      {
         return;
      }

      // update current text style
      style = style.toLowerCase();
      if (style.equals("italic"))
      {
         isItalic = true;
      }
      else if (style.equals("no-italic"))
      {
         isItalic = false;
      }
      else if (style.equals("bold"))
      {
         isBold = true;
      }
      else if (style.equals("no-bold"))
      {
         isBold = false;
      }
      else if (style.equals("underline"))
      {
         isUnderline = true;
      }
      else if (style.equals("no-underline"))
      {
         isUnderline = false;
      }
   }

   /**
    * Gets flag meaning underlined text should be currently used.
    * 
    * @return   <code>TRUE</code> if text underlined, <code>FALSE</code> otherwise.
    */
   public boolean isUnderline()
   {
      return isUnderline;
   }
   
   /**
    * Starts the new page construction for the document that is currently processing.
    */
   public void startNewPage()
   {
      // add objects of the current page to the page list
      if (currPageNum > 1 || objects.size() > 0)
      {
         pages.add(objects);
      }

      // the first page internals might already been created in constructor
      if (firstPageCreated)
      {
         firstPageCreated = false;
      }
      else
      {
         // start recording new page
         currPageNum++;
         // and create new object room for next page objects
         objects = new ArrayList<XprObjBase>();
         // adjust max page number too
         if (currPageNum > maxPageNum)
         {
            maxPageNum++;
         }
         // new page options set with current default page size
         setPageOptions();
      }
   }

   /**
    * Sets new value for current font.  The font name string can have size and style in addition
    * to the font name itself.  In this case the character "_" is used as delimiter.
    * 
    * @param    fontName
    *           The new String value for the current font name.  Can be "default" for default
    *           font to use.
    */
   public void setFont(String fontName)
   {
      // simple protection from empty font name value
      if (fontName == null || fontName.isEmpty())
      {
         // nothing to change
         return;
      }
      
      // normalize the font names to use with PDF font mapper
      if (fontName.startsWith("Times "))
      {
         currFontName = "Times";
      }
      else if (fontName.startsWith("Courier ") || fontName.equals("default"))
      {
         currFontName = "Courier";
      }
      else
      {
         currFontName = fontName;
      }
   }

   /**
    * Resets new value for text style to be off.
    */
   public void resetTextStyle()
   {
      isItalic = false;
      isBold = false;
      isUnderline = false;
   }
   
   /**
    * Gets the current font value.
    * 
    * @param    fontName
    *           The value for the font name currently in use.
    */
   public void getFont(character fontName)
   {
      fontName.assign(new character(currFontName));
   }

   /**
    * Sets new value for current zoom factor.
    * 
    * @param    zoomType
    *           The new String value for the current zoom type.
    */
   public void setZoomFactor(String zoomType)
   {
      if (zoomType != null && !zoomType.isEmpty())
      {
         zoomType = zoomType.toLowerCase();
      }
      
      currZoomType = zoomType;
   }

   /**
    * Gets the current zoom factor value.
    * 
    * @param    zoomType
    *           The value for the zoom type currently in use.
    */
   public void getZoomFactor(character zoomType)
   {
      zoomType.assign(new character(currZoomType));
   }

   /**
    * Gets the current font height value.
    * 
    * @param    fontHeight
    *           The currently used font height value in metric units.
    */
   public void getFontHeight(decimal fontHeight)
   {
      fontHeight.assign(new decimal(fontSizeMetric));
   }

   /**
    * Gets the current font height value.
    * 
    * @return   The currently used font height value in metric units.
    */
   public double getFontHeight()
   {
      return fontSizeMetric;
   }

   /**
    * Sets the current font height value.
    * 
    * @param    fontHeight
    *           The font height value in pics to use.
    */
   public void setFontHeight(float fontHeight)
   {
      fontSize = (float)((double)fontHeight / pdf2MfDpi);
      fontSizeMetric = MetafileHelper.pix2MfUnits(fontSize);
   }

   /**
    * Gets the given text width value.
    * 
    * @param    textToMeasure
    *           The text to calculate width.
    * @param    textWidth
    *           The width of the text in metric units.
    */
   public void getTextWidth(String textToMeasure, decimal textWidth)
   {
      double res = getTextWidth(textToMeasure);
      
      textWidth.assign(new decimal(res));
   }

   /**
    * Gets the given text width value.
    * 
    * @param    textToMeasure
    *           The text to calculate width.
    *
    * @return   The width of the text in metric units.
    */
   public double getTextWidth(String textToMeasure)
   {
      return getTextWidth(textToMeasure, -1, isBold, isItalic);
   }

   /**
    * Gets the given text width value.  Custom font size is taking into account.
    * 
    * @param    textToMeasure
    *           The text to calculate width.
    * @param    customFontSize
    *           The optional font size to use instead of current one.
    * @param    bold
    *           The bold font in use to measure text.
    * @param    italic
    *           The italic font in use to measure text.
    *
    * @return   The width of the text in metric units.
    */
   public double getTextWidth(String textToMeasure, float customFontSize, boolean bold, boolean italic)
   {
      double res = 0.0;
      boolean customFontUsage = false;
      
      // TODO: this can be inaccurate calculaton, might need to be re-worked
      if (textToMeasure != null && !textToMeasure.isEmpty())
      {
         if (customFontSize > 0)
         {
            customFontSize = customFontSize / (float)pdf2MfDpi;
            customFontUsage = true;
         }
         else
         {
            customFontSize = fontSize;
         }
         
         int length = textToMeasure.length();
         res = MetafileHelper.pix2MfUnits((float)length * (float)pixPerColMf * (float)customFontSize /
                                          (float)(FONT_SIZE_DEFAULT));
      }
      
      return customFontUsage ? res * 0.95
                             : res * MetafileHelper.getTextWeightScale(textToMeasure, currFontName,
                                                                       bold, italic);
   }

   /**
    * Sets the new current position for cursor.  Can be used a starting point for text/line/image
    * draw.
    * 
    * @param    x
    *           The new X position.
    * @param    y
    *           The new Y position.
    */
   public void setXY(double x, double y)
   {
      xCurr = x;
      yCurr = y;
   }

   /**
    * Gets the new current position for cursor.  Can be used a starting point for text/line/image
    * draw.
    * 
    * @param    x
    *           The returned X position.
    * @param    y
    *           The returned Y position.
    */
   public void getXY(decimal x, decimal y)
   {
      x.assign(new decimal(xCurr));
      y.assign(new decimal(yCurr));
   }

   /**
    * Sets the new value for document's left margin.
    * 
    * @param    leftMargin
    *           The new left margin value.
    */
   public void setLeftMargin(double leftMargin)
   {
      this.leftMargin = leftMargin;
   }

   /**
    * Starts new line of the text.  Y coord will be shifted by the text height and interleaving,
    * X coored be set to leftmost point of the text.
    */
   public void startNewTextLine()
   {
      xCurr = leftMargin;
      yCurr += getFontHeight() + extraRowDistance;
   }

   /**
    * Sets the extra vertical space between two sequential rows.
    * 
    * @param    extraYSpace
    *           The new extra Y distance between rows.
    */
   public void setInterleaving(double extraYSpace)
   {
      extraRowDistance = extraYSpace;
   }

   /**
    * Gets the free space below the current Y position.
    * 
    * @param    freeSpaceDown
    *           The remaining vertical free space on the current page.
    */
   public void getFreeSpaceDown(decimal freeSpaceDown)
   {
      double val = diPageHeight - yCurr - footerHeight;
      freeSpaceDown.assign(new decimal(val));
   }

   /**
    * Gets the free space to the right the current X position.
    * 
    * @param    freeSpaceRight
    *           The remaining horizontal free space on the current line(page).
    */
   public void getFreeSpaceRight(decimal freeSpaceRight)
   {
      double val = diPageWidth - xCurr;
      freeSpaceRight.assign(new decimal(val));
   }

   /**
    * Draws the text at current position.
    * 
    * @param    text
    *           The new text to draw at current position.
    */
   public void drawText(String text)
   {
      double x = xCurr;
      // consider current alignment and adjust X position
      switch (textAlignHorizCurr)
      {
         case TEXT_ALIGN_RIGHT:
         {
            x -= getTextWidth(text);
            // shift the current X position to the end of the text
            break;
         }
         case TEXT_ALIGN_CENTER:
         {
            x -= getTextWidth(text) / 2.0;
            // shift the current X position to the end of the text
            break;
         }
      }
      
      if (!text.isEmpty())
      {
         addTextObject(text, x, yCurr);
         // shift the current X position to the end of the text
         xCurr += getTextWidth(text);
      }
   }

   /**
    * Draws the text at current position.
    * 
    * @param    x
    *           The X position to set after text drawing in device independent units.
    * @param    text
    *           The new text to draw at current position.
    */
   public void drawTextAndSetX(double x, String text)
   {
      drawText(text);
      xCurr = Math.min(xCurr, x);
   }

   /**
    * Draws the text at the given X position and current Y position and given alignment.
    * 
    * @param    x
    *           The X position for text to draw in device independent units.
    * @param    text
    *           The new text to draw at the given position.
    * @param    align
    *           The alignment value, either left or right.
    */
   public void drawTextAtX(double x, String text, String align)
   {
      // simple protection from empty style value
      if (align == null || align.isEmpty())
      {
         return;
      }
      
      // currently used text alignment
      int xTextAlign = textAlignHorizCurr;

      // check if we need to override text alignment
      align = align.toLowerCase();
      if (align.equals("left"))
      {
         xTextAlign = TEXT_ALIGN_LEFT;
      }
      else if (align.equals("right"))
      {
         xTextAlign = TEXT_ALIGN_RIGHT;
      }
      
      // consider current alignment and adjust X position
      switch (xTextAlign)
      {
         case TEXT_ALIGN_LEFT:
         {
            // shift the current X position to the end of the text
            xCurr = x + getTextWidth(text);
            break;
         }
         case TEXT_ALIGN_RIGHT:
         {
            // the current X position become now the text right point
            xCurr = x;
            // shift the current X position to the end of the text
            x -= getTextWidth(text);
            break;
         }
         case TEXT_ALIGN_CENTER:
         {
            x -= getTextWidth(text) / 2.0;
            // shift the current X position to the end of the text
            xCurr = x + getTextWidth(text);
            break;
         }
      }
      
      if (!text.isEmpty())
      {
         addTextObject(text, x, yCurr);
      }
   }

   /**
    * Draws the text between left and right X positions and current Y position with given
    * alignment.  If text does not fit the area it will be truncated until match.
    * 
    * @param    left
    *           The left X position for text to draw in device independent units.
    * @param    right
    *           The right X position for text to draw in device independent units.
    * @param    text
    *           The new text to fit and draw in the given positions.
    * @param    align
    *           The alignment value, either left or right.
    */
   public void drawTextBetween(double left, double right, String text, String align)
   {
      // simple protection from empty style value
      if (align == null || align.isEmpty())
      {
         return;
      }
      
      // some special cases that can happen in application
      if (left == right)
      {
         // when both positions are 0.0 we just do not print anything
         if (left > 0.0)
         {
            drawTextAtX(left, text, align);
         }
         return;
      }
      
      // currently used text alignment
      int xTextAlign = textAlignHorizCurr;

      // check if we need to override text alignment
      align = align.toLowerCase();
      if (align.equals("left"))
      {
         xTextAlign = TEXT_ALIGN_LEFT;
      }
      else if (align.equals("right"))
      {
         xTextAlign = TEXT_ALIGN_RIGHT;
      }
      
      // need to truncate part of the text that does not fit the region
      double boundWidth = right - left;
      // omit spaces from trailing part of the string
      int textLength = StringHelper.safeTrimTrailing(text).length();
      for (int i = textLength - 1; i >= 0; i--)
      {
         String textMatch = text.substring(0, i + 1);
         if (getTextWidth(textMatch) <= boundWidth)
         {
            int textMatchLength = textMatch.length();
            if (textMatchLength < textLength)
            {
               int dotLength = Math.min(textMatchLength, 3);
               text = textMatch.substring(0, textMatchLength - dotLength) + "...";
            }
            else
            {
               text = textMatch;
            }
            break;
         }
      }
      
      // consider current alignment and adjust X position
      switch (xTextAlign)
      {
         case TEXT_ALIGN_LEFT:
         {
            // adjust right point to the minimum of current right and text width plus text start
            right = Math.min(left + getTextWidth(text), right);
            break;
         }
         case TEXT_ALIGN_RIGHT:
         {
            left = right - getTextWidth(text);
            break;
         }
         case TEXT_ALIGN_CENTER:
         {
            left -= getTextWidth(text) / 2.0;
            break;
         }
      }
      
      // if even single letter doed not fit, no need to create text object because nothing to draw
      if (!text.isEmpty())
      {
         addTextObject(text, left, yCurr);
      }
      // shift the current X position to the end of the text
      xCurr = right;
   }
   
   /**
    * Sets new values for line attributes.
    * 
    * @param    style
    *           The new String value for the current line style.  The valid style values:
    *           "solid", "dash", "dot", "dashdot", "dashdotdot", "null", "insideframe".
    * @param    thickness
    *           The thick of the line in device independent units.
    * @param    redColor
    *           The new red part of the color value.
    * @param    greenColor
    *           The new green part of the color value.
    * @param    blueColor
    *           The new blue part of the color value.
    */
   public void setLineAttributes(String style, double thickness,
                                 int redColor, int greenColor, int blueColor)
   {
      // line style and thick
      setLineStyle(style, thickness);
      // and color
      lineColor = (redColor & 0x000000FF) | (greenColor & 0x000000FF) << 8 |
                  (blueColor & 0x000000FF) << 16;
   }

   /**
    * Sets new current color value to be used in line based primitives.
    * 
    * @param    rgbColor
    *           The new 32-bit color value packed in int value.
    */
   public void setLineColor(int rgbColor)
   {
      lineColor = rgbColor;
   }

   /**
    * Sets new values for line attributes.
    * 
    * @param    style
    *           The new String value for the current line style.  The valid style values:
    *           "solid", "dash", "dot", "dashdot", "dashdotdot", "null", "insideframe".
    * @param    thickness
    *           The thick of the line in device independent units.
    */
   public void setLineStyle(String style, double thickness)
   {
      // simple protection from empty style value
      if (style != null && !style.isEmpty())
      {
         lineStyle = getLineType(style.toLowerCase());
      }
      
      if (thickness == 0.0)
      {
         lineWidth = 1;
      }
      else
      {
         lineWidth = MetafileHelper.mfUnits2pix(thickness) + 1;
      }
   }

   /**
    * Sets new current color value to be used in fill based primitives.
    * 
    * @param    rgbColor
    *           The new 32-bit color value packed in int value.
    */
   public void setFillColor(int rgbColor)
   {
      fillColor = rgbColor;
   }

   /**
    * Draws the line from left-top point to right-bottom with current line attributes.
    * The coordintes are in device independent units. 
    * 
    * @param    left
    *           The left X position of the line start.
    * @param    top
    *           The top Y position of the line start.
    * @param    right
    *           The right X position of the line start.
    * @param    bottom
    *           The bottom Y position of the line start.
    */
   public void drawLine(double left, double top, double right, double bottom)
   {
      // coordinates check
      if (top > bottom)
      {
         // need to have bottom >= top condition
         double swapY = top;
         top = bottom;
         bottom = swapY;
      }
      if (left > right)
      {
         // need to have bottom >= top condition
         double swapX = left;
         left = right;
         right = swapX;
      }

      // make some adjustments for linex with left edge X coord
      if (left == 0.0)
      {
         left = EDGE_LINE_X_SHIFT;
      }
      if (right == 0.0)
      {
         right = EDGE_LINE_X_SHIFT;
      }
      // and some Y adjust to the all vertical lines
      if (left == right)
      {
         top += VERT_LINE_Y_SHIFT;
         bottom += VERT_LINE_Y_SHIFT;
      }
      
      XprObjLine xprLine = new XprObjLine(lineColor, MetafileHelper.y2Row(top + OBJ_Y_SHIFT),
                                          MetafileHelper.x2Col(left),
                                          MetafileHelper.y2Row(bottom + OBJ_Y_SHIFT),
                                          MetafileHelper.x2Col(right), lineWidth, lineStyle);
      xprLine.setMargins(leftMargin, topMargin);
      objects.add(xprLine);
   }

   /**
    * Draws the framed rectangle between left-top point and right-bottom with current line
    * attributes.  The coordintes are in device independent units. 
    * 
    * @param    left
    *           The left X position of the rectangle.
    * @param    top
    *           The top Y position of the rectangle.
    * @param    right
    *           The right X position of the rectangle.
    * @param    bottom
    *           The bottom Y position of the rectangle.
    */
   public void drawRect(double left, double top, double right, double bottom)
   {
      // coordinates check
      if (top > bottom)
      {
         // need to have bottom >= top condition
         double swapY = top;
         top = bottom;
         bottom = swapY;
      }
      if (left > right)
      {
         // need to have bottom >= top condition
         double swapX = left;
         left = right;
         right = swapX;
      }

      XprObjRectangle xprRect = new XprObjRectangle(lineColor, MetafileHelper.y2Row(top + OBJ_Y_SHIFT),
                                                    MetafileHelper.x2Col(left),
                                                    MetafileHelper.y2Row(bottom + OBJ_Y_SHIFT),
                                                    MetafileHelper.x2Col(right),
                                                    lineWidth, false, lineStyle);
      xprRect.setMargins(leftMargin, topMargin);
      objects.add(xprRect);
   }

   /**
    * Draws the rectangle between left-top point and right-bottom with current line and fill
    * attributes.  The coordintes are in device independent units. 
    * 
    * @param    left
    *           The left X position of the rectangle.
    * @param    top
    *           The top Y position of the rectangle.
    * @param    right
    *           The right X position of the rectangle.
    * @param    bottom
    *           The bottom Y position of the rectangle.
    */
   public void drawFilledRect(double left, double top, double right, double bottom)
   {
      // coordinates check
      if (top > bottom)
      {
         // need to have bottom >= top condition
         double swapY = top;
         top = bottom;
         bottom = swapY;
      }
      if (left > right)
      {
         // need to have bottom >= top condition
         double swapX = left;
         left = right;
         right = swapX;
      }

      // small boxes need additional adjust
      if (bottom - top < 2.0 * (getFontHeight() + extraRowDistance))
      {
         bottom += 0.03;
      }
      // filled rectangle needs special attention because the border should not be painted
      XprObjFilledRectangle xprFillRect =
         new XprObjFilledRectangle(fillColor, fillColor, MetafileHelper.y2Row(top + OBJ_Y_SHIFT + 0.04),
                                   MetafileHelper.x2Col(left + 0.01),
                                   MetafileHelper.y2Row(bottom + OBJ_Y_SHIFT),
                                   MetafileHelper.x2Col(right), lineWidth, false, lineStyle);
      xprFillRect.setMargins(leftMargin, topMargin);
      objects.add(xprFillRect);
   }

   /**
    * Sets new values for image alignment.
    * 
    * @param    horizAlign
    *           The new String value for horizontal text alignment.  The valid alignment values:
    *           "left", "center", "right".
    * @param    vertAlign
    *           The new String value for vertical text alignment.  The valid alignment values:
    *           "top", "baseline", "bottom".
    */
   public void setImageAlign(String horizAlign, String vertAlign)
   {
      // horizontal alignment type 
      if (horizAlign != null && !horizAlign.isEmpty())
      {
         horizAlign = horizAlign.toLowerCase();
         if (horizAlign.equals("left"))
         {
            imageAlignHorizCurr = IMAGE_ALIGN_LEFT;
         }
         else if (horizAlign.equals("center"))
         {
            imageAlignHorizCurr = IMAGE_ALIGN_CENTER;
         }
         else if (horizAlign.equals("right"))
         {
            imageAlignHorizCurr = IMAGE_ALIGN_RIGHT;
         }
      }
      // vetical alignment type 
      if (vertAlign != null && !vertAlign.isEmpty())
      {
         vertAlign = vertAlign.toLowerCase();
         if (vertAlign.equals("top"))
         {
            imageAlignVertCurr = IMAGE_ALIGN_TOP;
         }
         else if (vertAlign.equals("center"))
         {
            imageAlignVertCurr = IMAGE_ALIGN_CENTER;
         }
         else if (vertAlign.equals("bottom"))
         {
            imageAlignVertCurr = IMAGE_ALIGN_BOTTOM;
         }
      }
   }

   /**
    * Draws the image.
    * 
    * @param    option
    *           The image painting option for example "origratio=yes".
    * @param    x
    *           The image X position.
    * @param    y
    *           The image Y position.
    * @param    width
    *           The image width in metric units.
    * @param    height
    *           The image height in metric units.
    * @param    filename
    *           The name of the file to use as image.
    * @param    imgWidth
    *           The final painted image width in metric units(return value).
    * @param    imgHeight
    *           The final painted image height in metric units(return value).
    */
   public void drawImage(String option, double x, double y, double width, double height,
                         String filename, decimal imgWidth, decimal imgHeight)
   {
      // TODO: verify this
      if (FwdJasperExtensionRegistry.getRepositoryService().getInputStream(filename) != null || 
          XprHelper.getInputStream(filename) != null || XprHelper.onErrorImageSilentMode())
      {
         // consider current alignment and adjust X and Y positions
         switch (imageAlignHorizCurr)
         {
            case IMAGE_ALIGN_RIGHT:
            {
               x -= width;
               break;
            }
            case IMAGE_ALIGN_CENTER:
            {
               x -= width / 2.0;
               break;
            }
         }
         switch (imageAlignVertCurr)
         {
            case IMAGE_ALIGN_BOTTOM:
            {
               y -= height;
               break;
            }
            case IMAGE_ALIGN_CENTER:
            {
               x -= height / 2.0;
               break;
            }
         }
         
         // set up image object
         x += 0.15;
         y += 0.15 + OBJ_Y_SHIFT;
         XprObjImage xprImage = new XprObjImage(MetafileHelper.y2Row(y), MetafileHelper.x2Col(x),
                                                MetafileHelper.y2Row(y + height),
                                                MetafileHelper.x2Col(x + width), filename);
         xprImage.setMargins(leftMargin, topMargin);
         objects.add(xprImage);
      }
      
      if (imgWidth != null)
      {
         imgWidth.assign(new decimal(width));
      }
      if (imgHeight != null)
      {
         imgHeight.assign(new decimal(height));
      }
   }

   /**
    * Draws the part of the image.
    * 
    * @param    x
    *           The image X position.
    * @param    y
    *           The image Y position.
    * @param    widthPix
    *           The image width in pixels.
    * @param    heightPix
    *           The image height in pixels.
    * @param    maxWidth
    *           The image maximum right border not to exceed.
    * @param    filename
    *           The name of the file to use as image.
    */
   public void drawImagePart(double x, double y, int widthPix, int heightPix,
                             double maxWidth, String filename)
   {
      double width = MetafileHelper.pix2MfUnits(widthPix);
      double height = MetafileHelper.pix2MfUnits(heightPix);
      // if width > maxWidth adjust height accordingly
      if (width > maxWidth)
      {
         height *= maxWidth / width;
         width = maxWidth;
      }
      // cal generic primitive keeping aspect
      drawImage("origratio=yes", x, y, width, height, filename, null, null);
   }

   /**
    * Sets new value for current page orientation.
    * 
    * @param    mode
    *           The new String value for page orientation.  The valid alignment values:
    *           "standard", "portrait", "landscape".
    */
   public void setPageOrientation(String mode)
   {
      // simple protection from empty mode value
      if (mode == null || mode.isEmpty())
      {
         return;
      }
      
      // normalize oriantation name
      mode = mode.toLowerCase();
      // update page orientation
      if (mode.equals("standard") || mode.equals("portrait"))
      {
         pageOrientation = XprPageOptions.PORTRAIT;
      }
      else if (mode.equals("landscape"))
      {
         pageOrientation = XprPageOptions.LANDSCAPE;
      }

      // Update page size for new orientation
      updatePageOptions();
   }
   
   /**
    * Gets the current page width.
    * 
    * @param    pageWidth
    *           The current page width value in device independent units.
    */
   public void getPageWidth(decimal pageWidth)
   {
      pageWidth.assign(new decimal(diPageWidth));
   }

   /**
    * Gets the current page height.
    * 
    * @param    pageHeight
    *           The current page height value in device independent units.
    */
   public void getPageHeight(decimal pageHeight)
   {
      pageHeight.assign(new decimal(diPageHeight - footerHeight));
   }

   /**
    * Sets new value for current/maximum page number.
    * 
    * @param    mode
    *           The new String value for page mode.  The valid alignment values: "append", "new".
    * @param    newMaxPageNum
    *           The integer max page number value.
    */
   public void resetPage(String mode, int newMaxPageNum)
   {
      // simple protection from empty mode value
      if (mode == null || mode.isEmpty())
      {
         return;
      }

      // check for the mode to continue
      if (mode.equals("append"))
      {
         if (currPageNum == maxPageNum && maxPageNum < newMaxPageNum)
         {
            maxPageNum = newMaxPageNum;
         }
      }
      else if (mode.equals("new"))
      {
         if (maxPageNum < newMaxPageNum)
         {
            startNewPage();
         }
      }
   }
   
   /**
    * Gets the current page number.
    * 
    * @param    pageNum
    *           The current integer page number value.
    */
   public void getPageNumber(integer pageNum)
   {
      pageNum.assign(new integer(currPageNum));
   }

   /**
    * Sets new value for current page number.
    * 
    * @param    newPageNum
    *           The integer page number value.
    */
   public void setPageNumber(int newPageNum)
   {
      currPageNum = newPageNum;
   }
   
   /**
    * Sets the new current position for page number.  Can be used to draw page number text for
    * header and footer.
    * 
    * @param    x
    *           The new X position for page nummber.
    * @param    y
    *           The new Y position for page nummber.
    */
   public void setPageNumberPosition(double x, double y)
   {
      xPageNum = x;
      yPageNum = y + 0.15;
   }

   /**
    * Sets new value for current page number text.
    * 
    * @param    text
    *           The new text for page number.
    * @param    pgNumberNdx
    *           The 1-based index of new text for page number.
    */
   public void setPageNumberText(String text, int pgNumberNdx)
   {
      // the X and Y positions will be set later
      XprObjText xprText = new XprObjText(currFontName, fontSize, isBold, isItalic,
                                          textColor, 0.0, 0.0, text);
      xprText.setMargins(leftMargin, topMargin);
      pageNumberTextObj.add(xprText);
   }

   /**
    * Sets to show page footer or not.
    * 
    * @param    show
    *           The <code>TRUE</code> value means show footer, <code>FALSE</code> means no footer.
    */
   public void showPageFooter(boolean show)
   {
      showFooter = show;
   }

   /**
    * General metafile subsystem init procedure.
    * 
    * @param    footerTextHeight
    *           The height of the footed text in device independent units.
    * @param    tmpDir
    *           The temporary directory to use.
    * @param    ok
    *           The return value, meaning success init or not.
    */
   public void initialize(double footerTextHeight, String tmpDir, integer ok)
   {
      // reserve several lines for page footer
      footerHeight = 1.5 * footerTextHeight;
      pdfDir = tmpDir;
      ok.assign(new integer(1));
   }
   
   /**
    * Stops all operation for the current metafile and commit remaining objects to the current page.
    */
   public void stopRecording()
   {
      // already stopped current recording
      if (recordingStopped)
      {
         return;
      }
      
      // commit remaining objects
      if (objects.size() > 0)
      {
         pages.add(objects);
      }
      
      // now add the optional page number texts
      int pageNumberObjSize = pageNumberTextObj.size();
      if (pageNumberObjSize > 0)
      {
         // prepare total pages string value to put into every page
         int pagesTotalInt = pages.size();
         String pagesTotalStr = String.valueOf(pagesTotalInt);
         // modify total pages number object
         XprObjText xprTextTotal = pageNumberTextObj.get(2);
         if (xprTextTotal != null)
         {
            xprTextTotal.appendText(pagesTotalStr);
         }
         // need to walk through all pages
         for (int i = 0; i < pagesTotalInt; i++)
         {
            List<XprObjBase> pageCurr = getObjectsInPage(i);
            double xPageNumCurrent = xPageNum;
            // now enumerate all page number texts in reverse order and finalize object preparation
            for (int j = pageNumberObjSize - 2; j >= 0; j--)
            {
               XprObjText xprOldText = pageNumberTextObj.get(j);
               float customFontSize = xprOldText.getFontSize();
               XprObjText xprNewText = null;
               String newFooterText = null;
               switch (j)
               {
                  case 1:
                  {
                     // need to crate new text object for every page in the document,
                     // because this part of the text is different for each page
                     newFooterText = xprOldText.getValue() + String.valueOf(i + 1) +
                                     (xprTextTotal != null ? xprTextTotal.getValue() : "");
                     break;
                  }
                  case 0:
                  {
                     newFooterText = xprOldText.getValue();
                     break;
                  }
               }
               if (newFooterText != null)
               {
                  xPageNumCurrent -= getTextWidth(newFooterText, customFontSize,
                                                  xprOldText.isBold(), xprOldText.isItalic());
                  xprNewText = new XprObjText(xprOldText.getFontName(), customFontSize,
                                              xprOldText.isBold(), xprOldText.isItalic(),
                                              xprOldText.getColor(), MetafileHelper.y2Row(yPageNum),
                                              MetafileHelper.x2Col(xPageNumCurrent), newFooterText);
                  pageCurr.add(xprNewText);
               }
            }
         }
      }
      // clean up page number list
      pageNumberTextObj.clear();
      
      // all done
      recordingStopped = true;
   }
   
   /**
    * Gets optionally set temporary directory to store generated PDF temporary files.
    * 
    * @return   The currently used temporary directory.
    */
   public String getTmpDirectory()
   {
      return pdfDir;
   }

   /**
    * Converts line style string into JasperReports compatible line type.
    * 
    * @param    lineType
    *           The line type lower cased string.
    * 
    * @return   The JasperReport compatible line type.
    */
   private LineStyleEnum getLineType(String lineType)
   {
      // default line type is solid
      LineStyleEnum lsRet = LineStyleEnum.SOLID;
      
      if (lineType.equals("dash"))
      {
         lsRet = LineStyleEnum.DASHED;
      }
      else if (lineType.equals("dot"))
      {
         lsRet = LineStyleEnum.DOTTED;
      }
      
      return lsRet;
   }
   
   /**
    * Set up options for new current document page.
    */
   private void setPageOptions()
   {
      pageOptions.add(new XprPageOptions(XprHelper.getPdfPageWidth(), XprHelper.getPdfPageHeight(),
                                         pageOrientation));
      diPageWidth = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getWidthPix());
      diPageHeight = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getHeightPix());
   }
   
   /**
    * Update options for new current document page.
    */
   private void updatePageOptions()
   {
      pageOptions.get(currPageNum - 1).setPageOrientation(pageOrientation);
      diPageWidth = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getWidthPix());
      diPageHeight = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getHeightPix());
   }
   
   /**
    * Internal method to construct and add new text object.
    * 
    * @param    textToAdd
    *           The line of the text to be added..
    * @param    xText
    *           The X text position.
    * @param    yText
    *           The Y text position.
    */
   private void addTextObject(String textToAdd, double xText, double yText)
   {
      // consider current alignment and adjust Y position
      switch (textAlignVertCurr)
      {
         case TEXT_ALIGN_BASELINE:
         {
            yText -= 0.125 + fontSizeMetric / 2.0;
            // shift the current X position to the end of the text
            break;
         }
         case TEXT_ALIGN_BOTTOM:
         {
            yText -= fontSizeMetric;
            // shift the current X position to the end of the text
            break;
         }
      }
      
      XprObjText xprText = new XprObjText(currFontName, fontSize, isBold, isItalic,
                                          textColor, MetafileHelper.y2Row(yText + OBJ_Y_SHIFT),
                                          MetafileHelper.x2Col(xText), textToAdd);
      xprText.setMargins(leftMargin, topMargin);
      objects.add(xprText);
      // simulate underlined text with additional line when necessary
      if (isUnderline)
      {
         yText += getFontHeight();
         // current line width may be other than 1 we need to keep this value untouched
         int savedLineWidth = lineWidth;
         lineWidth = 2;
         drawLine(xText - 0.01, yText, xText + getTextWidth(textToAdd) + 0.085, yText);
         lineWidth = savedLineWidth;
      }
   }

   //-------------------- 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 ---------------------------
}