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