net.sf.jasperreports.engine.export.JRRtfExporter.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jasperreports.engine.export.JRRtfExporter.java

Source

/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved.
 * http://www.jaspersoft.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Contributors:
 * Matt Thompson - mthomp1234@users.sourceforge.net
 */
package net.sf.jasperreports.engine.export;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.font.TextAttribute;
import java.awt.geom.Dimension2D;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRAbstractExporter;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRFont;
import net.sf.jasperreports.engine.JRGenericElementType;
import net.sf.jasperreports.engine.JRGenericPrintElement;
import net.sf.jasperreports.engine.JRLineBox;
import net.sf.jasperreports.engine.JRPen;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.JRPrintEllipse;
import net.sf.jasperreports.engine.JRPrintFrame;
import net.sf.jasperreports.engine.JRPrintHyperlink;
import net.sf.jasperreports.engine.JRPrintImage;
import net.sf.jasperreports.engine.JRPrintLine;
import net.sf.jasperreports.engine.JRPrintPage;
import net.sf.jasperreports.engine.JRPrintRectangle;
import net.sf.jasperreports.engine.JRPrintText;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.TabStop;
import net.sf.jasperreports.engine.base.JRBaseFont;
import net.sf.jasperreports.engine.base.JRBasePrintText;
import net.sf.jasperreports.engine.type.ImageTypeEnum;
import net.sf.jasperreports.engine.type.LineDirectionEnum;
import net.sf.jasperreports.engine.type.ModeEnum;
import net.sf.jasperreports.engine.type.OrientationEnum;
import net.sf.jasperreports.engine.type.RunDirectionEnum;
import net.sf.jasperreports.engine.type.ScaleImageEnum;
import net.sf.jasperreports.engine.util.FileBufferedWriter;
import net.sf.jasperreports.engine.util.ImageUtil;
import net.sf.jasperreports.engine.util.JRStyledText;
import net.sf.jasperreports.engine.util.JRTypeSniffer;
import net.sf.jasperreports.export.ExportInterruptedException;
import net.sf.jasperreports.export.ExporterInputItem;
import net.sf.jasperreports.export.RtfExporterConfiguration;
import net.sf.jasperreports.export.RtfReportConfiguration;
import net.sf.jasperreports.export.WriterExporterOutput;
import net.sf.jasperreports.renderers.DataRenderable;
import net.sf.jasperreports.renderers.DimensionRenderable;
import net.sf.jasperreports.renderers.Renderable;
import net.sf.jasperreports.renderers.RenderersCache;
import net.sf.jasperreports.renderers.ResourceRenderer;

/**
 * Exports a JasperReports document to RTF format. 
 * <p/>
 * The {@link net.sf.jasperreports.engine.export.JRRtfExporter} implementation helps
 * to export JasperPrint documents in RTF format using RTF Specification 1.6. This
 * means that the RTF files produced by this exporter are compatible with Microsoft Word
 * 6.0, 2003 and XP.
 * <p/>
 * However, users might experience some problems when opening those RTF files with
 * OpenOffice or StarOffice, as these products are not perfectly compatible with the RTF
 * specifications from Microsoft.
 * <p/>
 * RTF is a character-based file format that supports absolute positioning of elements,
 * which means that this exporter produces output very similar to that of the <code>Graphics2D</code>
 * and PDF exporters. The {@link net.sf.jasperreports.export.RtfReportConfiguration} provides special 
 * configuration settings for this exporter.
 * <p/>
 * Almost all the provided samples show how to export to RTF.
 * 
 * @see net.sf.jasperreports.export.RtfReportConfiguration
 * @author Flavius Sana (flavius_sana@users.sourceforge.net)
 */
public class JRRtfExporter extends
        JRAbstractExporter<RtfReportConfiguration, RtfExporterConfiguration, WriterExporterOutput, JRRtfExporterContext> {
    private static final Log log = LogFactory.getLog(JRRtfExporter.class);

    public static final String RTF_EXPORTER_PROPERTIES_PREFIX = JRPropertiesUtil.PROPERTY_PREFIX + "export.rtf.";

    public static final String EXCEPTION_MESSAGE_KEY_INVALID_TEXT_HEIGHT = "export.rtf.invalid.text.height";

    private static final int LINE_SPACING_FACTOR = 240; //(int)(240 * 2/3f);

    /**
     * The exporter key, as used in
     * {@link GenericElementHandlerEnviroment#getElementHandler(JRGenericElementType, String)}.
     */
    public static final String RTF_EXPORTER_KEY = JRPropertiesUtil.PROPERTY_PREFIX + "rtf";

    /**
     *
     */
    protected static final String JR_PAGE_ANCHOR_PREFIX = "JR_PAGE_ANCHOR_";

    protected FileBufferedWriter colorWriter;
    protected FileBufferedWriter fontWriter;
    protected FileBufferedWriter contentWriter;
    protected File destFile;

    protected int reportIndex;

    protected List<Color> colors;
    protected List<String> fonts;

    // z order of the graphical objects in .rtf file
    private int zorder = 1;

    protected RenderersCache renderersCache;

    protected class ExporterContext extends BaseExporterContext implements JRRtfExporterContext {
    }

    /**
     * @see #JRRtfExporter(JasperReportsContext)
     */
    public JRRtfExporter() {
        this(DefaultJasperReportsContext.getInstance());
    }

    /**
     *
     */
    public JRRtfExporter(JasperReportsContext jasperReportsContext) {
        super(jasperReportsContext);

        exporterContext = new ExporterContext();
    }

    @Override
    protected Class<RtfExporterConfiguration> getConfigurationInterface() {
        return RtfExporterConfiguration.class;
    }

    @Override
    protected Class<RtfReportConfiguration> getItemConfigurationInterface() {
        return RtfReportConfiguration.class;
    }

    @Override
    @SuppressWarnings("deprecation")
    protected void ensureOutput() {
        if (exporterOutput == null) {
            exporterOutput = new net.sf.jasperreports.export.parameters.ParametersWriterExporterOutput(
                    getJasperReportsContext(), getParameters(), getCurrentJasperPrint());
        }
    }

    @Override
    public void exportReport() throws JRException {
        /*   */
        ensureJasperReportsContext();
        ensureInput();

        fonts = new ArrayList<String>();
        colors = new ArrayList<Color>();
        colors.add(null);

        initExport();

        ensureOutput();

        Writer writer = getExporterOutput().getWriter();

        try {
            exportReportToWriter(writer);
        } catch (IOException e) {
            throw new JRException(EXCEPTION_MESSAGE_KEY_OUTPUT_WRITER_ERROR, new Object[] { jasperPrint.getName() },
                    e);
        } finally {
            getExporterOutput().close();
            resetExportContext();
        }
    }

    @Override
    protected void initExport() {
        super.initExport();
    }

    @Override
    protected void initReport() {
        super.initReport();

        renderersCache = new RenderersCache(getJasperReportsContext());
    }

    /**
     * Export report in .rtf format to a stream
     * @throws JRException
     * @throws IOException
     */
    protected void exportReportToWriter(Writer writer) throws JRException, IOException {
        colorWriter = new FileBufferedWriter();
        fontWriter = new FileBufferedWriter();
        contentWriter = new FileBufferedWriter();

        List<ExporterInputItem> items = exporterInput.getItems();

        for (reportIndex = 0; reportIndex < items.size(); reportIndex++) {
            ExporterInputItem item = items.get(reportIndex);

            setCurrentExporterInputItem(item);

            List<JRPrintPage> pages = jasperPrint.getPages();
            if (pages != null && pages.size() > 0) {
                PageRange pageRange = getPageRange();
                int startPageIndex = (pageRange == null || pageRange.getStartPageIndex() == null) ? 0
                        : pageRange.getStartPageIndex();
                int endPageIndex = (pageRange == null || pageRange.getEndPageIndex() == null) ? (pages.size() - 1)
                        : pageRange.getEndPageIndex();

                contentWriter.write("{\\info{\\nofpages");
                contentWriter.write(String.valueOf(pages.size()));
                contentWriter.write("}}\n");

                contentWriter.write("\\viewkind1\\paperw");
                contentWriter.write(String.valueOf(LengthUtil.twip(jasperPrint.getPageWidth())));//FIXMEPART rtf does not work in batch mode
                contentWriter.write("\\paperh");
                contentWriter.write(String.valueOf(LengthUtil.twip(jasperPrint.getPageHeight())));

                contentWriter.write("\\marglsxn");
                contentWriter.write(String.valueOf(
                        LengthUtil.twip(jasperPrint.getLeftMargin() == null ? 0 : jasperPrint.getLeftMargin())));
                contentWriter.write("\\margrsxn");
                contentWriter.write(String.valueOf(
                        LengthUtil.twip(jasperPrint.getRightMargin() == null ? 0 : jasperPrint.getRightMargin())));
                contentWriter.write("\\margtsxn");
                contentWriter.write(String.valueOf(
                        LengthUtil.twip(jasperPrint.getTopMargin() == null ? 0 : jasperPrint.getTopMargin())));
                contentWriter.write("\\margbsxn");
                contentWriter.write(String.valueOf(LengthUtil
                        .twip(jasperPrint.getBottomMargin() == null ? 0 : jasperPrint.getBottomMargin())));
                contentWriter.write("\\deftab");
                contentWriter.write(
                        String.valueOf(LengthUtil.twip(new JRBasePrintText(jasperPrint.getDefaultStyleProvider())
                                .getParagraph().getTabStopWidth())));

                if (jasperPrint.getOrientationValue() == OrientationEnum.LANDSCAPE) {
                    contentWriter.write("\\lndscpsxn");
                }

                for (int pageIndex = startPageIndex; pageIndex <= endPageIndex; pageIndex++) {
                    if (Thread.interrupted()) {
                        throw new ExportInterruptedException();
                    }

                    JRPrintPage page = pages.get(pageIndex);

                    contentWriter.write("\n");

                    writeAnchor(JR_PAGE_ANCHOR_PREFIX + reportIndex + "_" + (pageIndex + 1));

                    boolean lastPageFlag = false;
                    if (pageIndex == endPageIndex && reportIndex == (items.size() - 1)) {
                        lastPageFlag = true;
                    }
                    exportPage(page, lastPageFlag);
                }
            }
        }
        contentWriter.write("}\n");

        contentWriter.close();
        colorWriter.close();
        fontWriter.close();

        // create the header of the rtf file
        writer.write("{\\rtf1\\ansi\\deff0\n");
        // create font and color tables
        writer.write("{\\fonttbl ");
        fontWriter.writeData(writer);
        writer.write("}\n");

        writer.write("{\\colortbl ;");
        colorWriter.writeData(writer);
        writer.write("}\n");

        contentWriter.writeData(writer);

        writer.flush();

        contentWriter.dispose();
        colorWriter.dispose();
        fontWriter.dispose();
    }

    /**
     * Return color index from header of the .rtf file. If a color is not
     * found is automatically added to the header of the rtf file. The
     * method is called first when the header of the .rtf file is constructed
     * and when a component needs a color for foreground or background
     * @param color Color for which the index is required.
     * @return index of the color from .rtf file header
     */
    private int getColorIndex(Color color) throws IOException {
        int colorNdx = colors.indexOf(color);
        if (colorNdx < 0) {
            colorNdx = colors.size();
            colors.add(color);
            colorWriter.write(
                    "\\red" + color.getRed() + "\\green" + color.getGreen() + "\\blue" + color.getBlue() + ";");
        }
        return colorNdx;
    }

    /**
     * Return font index from the header of the .rtf file. The method is
     * called first when the header of the .rtf document is constructed and when a
     * text component needs font informations.
     * @param font the font for which the index is required
     * @return index of the font from .rtf file header
     */
    private int getFontIndex(JRFont font, Locale locale) throws IOException {
        String fontName = fontUtil.getExportFontFamily(font.getFontName(), locale, getExporterKey());

        int fontIndex = fonts.indexOf(fontName);

        if (fontIndex < 0) {
            fontIndex = fonts.size();
            fonts.add(fontName);
            fontWriter.write("{\\f" + fontIndex + "\\fnil " + fontName + ";}");
        }

        return fontIndex;
    }

    /**
     * Exports a report page
     * @param page Page that will be exported
     * @throws JRException
     */
    protected void exportPage(JRPrintPage page, boolean lastPage) throws JRException, IOException {
        exportElements(page.getElements());

        if (!lastPage) {
            contentWriter.write("\\page\n");
        }

        JRExportProgressMonitor progressMonitor = getCurrentItemConfiguration().getProgressMonitor();
        if (progressMonitor != null) {
            progressMonitor.afterPageExport();
        }
    }

    /**
     *
     */
    private void startElement(JRPrintElement element) throws IOException {
        contentWriter.write("{\\shp\\shpbxpage\\shpbypage\\shpwr5\\shpfhdr0\\shpfblwtxt0\\shpz");
        contentWriter.write(String.valueOf(zorder++));
        contentWriter.write("\\shpleft");
        contentWriter.write(String.valueOf(LengthUtil.twip(element.getX() + getOffsetX())));
        contentWriter.write("\\shpright");
        contentWriter.write(String.valueOf(LengthUtil.twip(element.getX() + getOffsetX() + element.getWidth())));
        contentWriter.write("\\shptop");
        contentWriter.write(String.valueOf(LengthUtil.twip(element.getY() + getOffsetY())));
        contentWriter.write("\\shpbottom");
        contentWriter.write(String.valueOf(LengthUtil.twip(element.getY() + getOffsetY() + element.getHeight())));

        Color bgcolor = element.getBackcolor();

        if (element.getModeValue() == ModeEnum.OPAQUE) {
            contentWriter.write("{\\sp{\\sn fFilled}{\\sv 1}}");
            contentWriter.write("{\\sp{\\sn fillColor}{\\sv ");
            contentWriter.write(String.valueOf(getColorRGB(bgcolor)));
            contentWriter.write("}}");
        } else {
            contentWriter.write("{\\sp{\\sn fFilled}{\\sv 0}}");
        }

        contentWriter.write("{\\shpinst");
    }

    /**
     *
     */
    private int getColorRGB(Color color) {
        return color.getRed() + 256 * color.getGreen() + 65536 * color.getBlue();
    }

    /**
     *
     */
    private void finishElement() throws IOException {
        contentWriter.write("}}\n");
    }

    /**
     *
     */
    private void exportPen(JRPen pen) throws IOException {
        contentWriter.write("{\\sp{\\sn lineColor}{\\sv ");
        contentWriter.write(String.valueOf(getColorRGB(pen.getLineColor())));
        contentWriter.write("}}");

        float lineWidth = pen.getLineWidth();

        if (lineWidth == 0f) {
            contentWriter.write("{\\sp{\\sn fLine}{\\sv 0}}");
        }

        switch (pen.getLineStyleValue()) {
        case DOUBLE: {
            contentWriter.write("{\\sp{\\sn lineStyle}{\\sv 1}}");
            break;
        }
        case DOTTED: {
            contentWriter.write("{\\sp{\\sn lineDashing}{\\sv 2}}");
            break;
        }
        case DASHED: {
            contentWriter.write("{\\sp{\\sn lineDashing}{\\sv 1}}");
            break;
        }
        }

        contentWriter.write("{\\sp{\\sn lineWidth}{\\sv ");
        contentWriter.write(String.valueOf(LengthUtil.emu(lineWidth)));
        contentWriter.write("}}");
    }

    /**
     *
     */
    private void exportPen(Color color) throws IOException {
        contentWriter.write("{\\sp{\\sn lineColor}{\\sv ");
        contentWriter.write(String.valueOf(getColorRGB(color)));
        contentWriter.write("}}");
        contentWriter.write("{\\sp{\\sn fLine}{\\sv 0}}");
        contentWriter.write("{\\sp{\\sn lineWidth}{\\sv 0}}");
    }

    /**
     * Draw a line object
     * @param line JasperReports line object - JRPrintLine
     * @throws IOException
     */
    protected void exportLine(JRPrintLine line) throws IOException {
        int x = line.getX() + getOffsetX();
        int y = line.getY() + getOffsetY();
        int height = line.getHeight();
        int width = line.getWidth();

        if (width <= 1 || height <= 1) {
            if (width > 1) {
                height = 0;
            } else {
                width = 0;
            }
        }

        contentWriter.write("{\\shp\\shpbxpage\\shpbypage\\shpwr5\\shpfhdr0\\shpz");
        contentWriter.write(String.valueOf(zorder++));
        contentWriter.write("\\shpleft");
        contentWriter.write(String.valueOf(LengthUtil.twip(x)));
        contentWriter.write("\\shpright");
        contentWriter.write(String.valueOf(LengthUtil.twip(x + width)));
        contentWriter.write("\\shptop");
        contentWriter.write(String.valueOf(LengthUtil.twip(y)));
        contentWriter.write("\\shpbottom");
        contentWriter.write(String.valueOf(LengthUtil.twip(y + height)));

        contentWriter.write("{\\shpinst");

        contentWriter.write("{\\sp{\\sn shapeType}{\\sv 20}}");

        exportPen(line.getLinePen());

        if (line.getDirectionValue() == LineDirectionEnum.TOP_DOWN) {
            contentWriter.write("{\\sp{\\sn fFlipV}{\\sv 0}}");
        } else {
            contentWriter.write("{\\sp{\\sn fFlipV}{\\sv 1}}");
        }

        contentWriter.write("}}\n");
    }

    /**
     *
     */
    private void exportBorder(JRPen pen, float x, float y, float width, float height) throws IOException {
        contentWriter.write("{\\shp\\shpbxpage\\shpbypage\\shpwr5\\shpfhdr0\\shpz");
        contentWriter.write(String.valueOf(zorder++));
        contentWriter.write("\\shpleft");
        contentWriter.write(String.valueOf(LengthUtil.twip(x)));//FIXMEBORDER starting point of borders seem to have CAP_SQUARE-like appearence at least for Thin
        contentWriter.write("\\shpright");
        contentWriter.write(String.valueOf(LengthUtil.twip(x + width)));
        contentWriter.write("\\shptop");
        contentWriter.write(String.valueOf(LengthUtil.twip(y)));
        contentWriter.write("\\shpbottom");
        contentWriter.write(String.valueOf(LengthUtil.twip(y + height)));

        contentWriter.write("{\\shpinst");

        contentWriter.write("{\\sp{\\sn shapeType}{\\sv 20}}");

        exportPen(pen);

        contentWriter.write("}}\n");
    }

    /**
     * Draw a rectangle
     * @param rectangle JasperReports rectangle object (JRPrintRectangle)
     */
    protected void exportRectangle(JRPrintRectangle rectangle) throws IOException {
        startElement(rectangle);

        if (rectangle.getRadius() == 0) {
            contentWriter.write("{\\sp{\\sn shapeType}{\\sv 1}}");
        } else {
            contentWriter.write("{\\sp{\\sn shapeType}{\\sv 2}}");
        }

        exportPen(rectangle.getLinePen());

        finishElement();
    }

    /**
     * Draw a ellipse object
     * @param ellipse JasperReports ellipse object (JRPrintElipse)
     */
    protected void exportEllipse(JRPrintEllipse ellipse) throws IOException {
        startElement(ellipse);

        contentWriter.write("{\\sp{\\sn shapeType}{\\sv 3}}");

        exportPen(ellipse.getLinePen());

        finishElement();
    }

    /**
     * Draw a text box
     * @param text JasperReports text object (JRPrintText)
     * @throws JRException
     */
    public void exportText(JRPrintText text) throws IOException, JRException {

        // use styled text
        JRStyledText styledText = getStyledText(text);
        if (styledText == null) {
            return;
        }

        int width = text.getWidth();
        int height = text.getHeight();

        int textHeight = (int) text.getTextHeight();

        if (textHeight <= 0) {
            if (height <= 0) {
                throw new JRException(EXCEPTION_MESSAGE_KEY_INVALID_TEXT_HEIGHT, (Object[]) null);
            }
            textHeight = height;
        }

        /*   */
        startElement(text);

        // padding for the text
        int topPadding = text.getLineBox().getTopPadding();
        int leftPadding = text.getLineBox().getLeftPadding();
        int bottomPadding = text.getLineBox().getBottomPadding();
        int rightPadding = text.getLineBox().getRightPadding();

        String rotation = null;

        switch (text.getRotationValue()) {
        case LEFT: {
            switch (text.getVerticalTextAlign()) {
            case BOTTOM: {
                leftPadding = Math.max(leftPadding, width - rightPadding - textHeight);
                break;
            }
            case MIDDLE: {
                leftPadding = Math.max(leftPadding, (width - rightPadding - textHeight) / 2);
                break;
            }
            case TOP:
            case JUSTIFIED:
            default: {
            }
            }
            rotation = "{\\sp{\\sn txflTextFlow}{\\sv 2}}";
            break;
        }
        case RIGHT: {
            switch (text.getVerticalTextAlign()) {
            case BOTTOM: {
                rightPadding = Math.max(rightPadding, width - leftPadding - textHeight);
                break;
            }
            case MIDDLE: {
                rightPadding = Math.max(rightPadding, (width - leftPadding - textHeight) / 2);
                break;
            }
            case TOP:
            case JUSTIFIED:
            default: {
            }
            }
            rotation = "{\\sp{\\sn txflTextFlow}{\\sv 3}}";
            break;
        }
        case UPSIDE_DOWN: {
            switch (text.getVerticalTextAlign()) {
            case TOP: {
                topPadding = Math.max(topPadding, height - bottomPadding - textHeight);
                break;
            }
            case MIDDLE: {
                topPadding = Math.max(topPadding, (height - bottomPadding - textHeight) / 2);
                break;
            }
            case BOTTOM:
            case JUSTIFIED:
            default: {
            }
            }
            rotation = "";
            break;
        }
        case NONE:
        default: {
            switch (text.getVerticalTextAlign()) {
            case BOTTOM: {
                topPadding = Math.max(topPadding, height - bottomPadding - textHeight);
                break;
            }
            case MIDDLE: {
                topPadding = Math.max(topPadding, (height - bottomPadding - textHeight) / 2);
                break;
            }
            case TOP:
            case JUSTIFIED:
            default: {
            }
            }
            rotation = "";
        }
        }

        contentWriter.write(rotation);
        contentWriter.write("{\\sp{\\sn dyTextTop}{\\sv ");
        contentWriter.write(String.valueOf(LengthUtil.emu(topPadding)));
        contentWriter.write("}}");
        contentWriter.write("{\\sp{\\sn dxTextLeft}{\\sv ");
        contentWriter.write(String.valueOf(LengthUtil.emu(leftPadding)));
        contentWriter.write("}}");
        contentWriter.write("{\\sp{\\sn dyTextBottom}{\\sv ");
        contentWriter.write(String.valueOf(LengthUtil.emu(bottomPadding)));
        contentWriter.write("}}");
        contentWriter.write("{\\sp{\\sn dxTextRight}{\\sv ");
        contentWriter.write(String.valueOf(LengthUtil.emu(rightPadding)));
        contentWriter.write("}}");
        contentWriter.write("{\\sp{\\sn fLine}{\\sv 0}}");
        contentWriter.write("{\\shptxt{\\pard ");

        contentWriter.write("\\fi" + LengthUtil.twip(text.getParagraph().getFirstLineIndent()) + " ");
        contentWriter.write("\\li" + LengthUtil.twip(text.getParagraph().getLeftIndent()) + " ");
        contentWriter.write("\\ri" + LengthUtil.twip(text.getParagraph().getRightIndent()) + " ");
        contentWriter.write("\\sb" + LengthUtil.twip(text.getParagraph().getSpacingBefore()) + " ");
        contentWriter.write("\\sa" + LengthUtil.twip(text.getParagraph().getSpacingAfter()) + " ");

        TabStop[] tabStops = text.getParagraph().getTabStops();
        if (tabStops != null && tabStops.length > 0) {
            for (int i = 0; i < tabStops.length; i++) {
                TabStop tabStop = tabStops[i];

                String tabStopAlign = "";

                switch (tabStop.getAlignment()) {
                case CENTER:
                    tabStopAlign = "\\tqc";
                    break;
                case RIGHT:
                    tabStopAlign = "\\tqr";
                    break;
                case LEFT:
                default:
                    tabStopAlign = "";
                    break;
                }

                contentWriter.write(tabStopAlign + "\\tx" + LengthUtil.twip(tabStop.getPosition()) + " ");
            }
        }

        //      JRFont font = text;
        if (text.getRunDirectionValue() == RunDirectionEnum.RTL) {
            contentWriter.write("\\rtlch");
        }
        //      writer.write("\\f");
        //      writer.write(String.valueOf(getFontIndex(font)));
        //      writer.write("\\cf");
        //      writer.write(String.valueOf(getColorIndex(text.getForecolor())));
        contentWriter.write("\\cb");
        contentWriter.write(String.valueOf(getColorIndex(text.getBackcolor())));
        contentWriter.write(" ");

        //      if (font.isBold())
        //         writer.write("\\b");
        //      if (font.isItalic())
        //         writer.write("\\i");
        //      if (font.isStrikeThrough())
        //         writer.write("\\strike");
        //      if (font.isUnderline())
        //         writer.write("\\ul");
        //      writer.write("\\fs");
        //      writer.write(String.valueOf(font.getFontSize() * 2));

        switch (text.getHorizontalTextAlign()) {
        case LEFT:
            contentWriter.write("\\ql");
            break;
        case CENTER:
            contentWriter.write("\\qc");
            break;
        case RIGHT:
            contentWriter.write("\\qr");
            break;
        case JUSTIFIED:
            contentWriter.write("\\qj");
            break;
        default:
            contentWriter.write("\\ql");
            break;
        }

        switch (text.getParagraph().getLineSpacing()) {
        case AT_LEAST: {
            contentWriter.write("\\sl" + LengthUtil.twip(text.getParagraph().getLineSpacingSize()));
            contentWriter.write(" \\slmult0 ");
            break;
        }
        case FIXED: {
            contentWriter.write("\\sl-" + LengthUtil.twip(text.getParagraph().getLineSpacingSize()));
            contentWriter.write(" \\slmult0 ");
            break;
        }
        case PROPORTIONAL: {
            contentWriter.write("\\sl" + (int) (text.getParagraph().getLineSpacingSize() * LINE_SPACING_FACTOR));
            contentWriter.write(" \\slmult1 ");
            break;
        }
        case DOUBLE: {
            contentWriter.write("\\sl" + (int) (2f * LINE_SPACING_FACTOR));
            contentWriter.write(" \\slmult1 ");
            break;
        }
        case ONE_AND_HALF: {
            contentWriter.write("\\sl" + (int) (1.5f * LINE_SPACING_FACTOR));
            contentWriter.write(" \\slmult1 ");
            break;
        }
        case SINGLE:
        default: {
            contentWriter.write("\\sl" + (int) (1f * LINE_SPACING_FACTOR));
            contentWriter.write(" \\slmult1 ");
            break;
        }
        }

        if (text.getAnchorName() != null) {
            writeAnchor(text.getAnchorName());
        }

        boolean startedHyperlink = exportHyperlink(text);

        // add parameters in case of styled text element
        String plainText = styledText.getText();
        int runLimit = 0;

        AttributedCharacterIterator iterator = styledText.getAttributedString().getIterator();
        while (runLimit < styledText.length() && (runLimit = iterator.getRunLimit()) <= styledText.length()) {

            Map<Attribute, Object> styledTextAttributes = iterator.getAttributes();
            JRFont styleFont = new JRBaseFont(styledTextAttributes);
            Color styleForeground = (Color) styledTextAttributes.get(TextAttribute.FOREGROUND);
            Color styleBackground = (Color) styledTextAttributes.get(TextAttribute.BACKGROUND);

            contentWriter.write("\\f");
            contentWriter.write(String.valueOf(getFontIndex(styleFont, getTextLocale(text))));
            contentWriter.write("\\fs");
            contentWriter.write(String.valueOf((int) (2 * styleFont.getFontsize())));

            if (styleFont.isBold()) {
                contentWriter.write("\\b");
            }
            if (styleFont.isItalic()) {
                contentWriter.write("\\i");
            }
            if (styleFont.isUnderline()) {
                contentWriter.write("\\ul");
            }
            if (styleFont.isStrikeThrough()) {
                contentWriter.write("\\strike");
            }

            if (TextAttribute.SUPERSCRIPT_SUPER.equals(styledTextAttributes.get(TextAttribute.SUPERSCRIPT))) {
                contentWriter.write("\\super");
            } else if (TextAttribute.SUPERSCRIPT_SUB.equals(styledTextAttributes.get(TextAttribute.SUPERSCRIPT))) {
                contentWriter.write("\\sub");
            }

            if (!(null == styleBackground || styleBackground.equals(text.getBackcolor()))) {
                contentWriter.write("\\highlight");
                contentWriter.write(String.valueOf(getColorIndex(styleBackground)));
            }
            contentWriter.write("\\cf");
            contentWriter.write(String.valueOf(getColorIndex(styleForeground)));
            contentWriter.write(" ");

            contentWriter.write(handleUnicodeText(plainText.substring(iterator.getIndex(), runLimit)));

            // reset all styles in the paragraph
            contentWriter.write("\\plain");

            iterator.setIndex(runLimit);
        }

        endHyperlink(startedHyperlink);

        contentWriter.write("\\par}}");

        /*   */
        finishElement();

        exportBox(text.getLineBox(), text.getX() + getOffsetX(), text.getY() + getOffsetY(), width, height);
    }

    /**
     * Replace Unicode characters with RTF Unicode control words
     * @param source source text
     * @return text with Unicode characters replaced
     */
    private String handleUnicodeText(String sourceText) {
        StringBuilder unicodeText = new StringBuilder();

        for (int i = 0; i < sourceText.length(); i++) {
            long ch = sourceText.charAt(i);
            if (ch > 127) {
                unicodeText.append("\\u" + ch + '?');
            } else if (ch == '\n') {
                unicodeText.append("\\line ");
            } else if (ch == '\\' || ch == '{' || ch == '}') {
                unicodeText.append('\\').append((char) ch);
            } else {
                unicodeText.append((char) ch);
            }
        }

        return unicodeText.toString();
    }

    /**
     * Export a image object
     * @param printImage JasperReports image object (JRPrintImage)
     * @throws JRException
     * @throws IOException
     */
    public void exportImage(JRPrintImage printImage) throws JRException, IOException {
        int leftPadding = printImage.getLineBox().getLeftPadding();
        int topPadding = printImage.getLineBox().getTopPadding();
        int rightPadding = printImage.getLineBox().getRightPadding();
        int bottomPadding = printImage.getLineBox().getBottomPadding();

        int availableImageWidth = printImage.getWidth() - leftPadding - rightPadding;
        availableImageWidth = availableImageWidth < 0 ? 0 : availableImageWidth;

        int availableImageHeight = printImage.getHeight() - topPadding - bottomPadding;
        availableImageHeight = availableImageHeight < 0 ? 0 : availableImageHeight;

        Renderable renderer = printImage.getRenderer();

        if (renderer != null && availableImageWidth > 0 && availableImageHeight > 0) {
            InternalImageProcessor imageProcessor = new InternalImageProcessor(printImage,
                    printImage.getScaleImageValue() != ScaleImageEnum.FILL_FRAME, availableImageWidth,
                    availableImageHeight);

            InternalImageProcessorResult imageProcessorResult = null;

            try {
                imageProcessorResult = imageProcessor.process(renderer);
            } catch (Exception e) {
                Renderable onErrorRenderer = getRendererUtil().handleImageError(e,
                        printImage.getOnErrorTypeValue());
                if (onErrorRenderer != null) {
                    imageProcessorResult = imageProcessor.process(onErrorRenderer);
                }
            }

            if (imageProcessorResult != null)//FIXMERTF draw image background for null images, like the other exporters do
            {
                int imageWidth = 0;
                int imageHeight = 0;
                int xoffset = 0;
                int yoffset = 0;
                int cropTop = 0;
                int cropLeft = 0;
                int cropBottom = 0;
                int cropRight = 0;

                switch (printImage.getScaleImageValue()) {
                case CLIP: {
                    int normalWidth = availableImageWidth;
                    int normalHeight = availableImageHeight;

                    Dimension2D dimension = imageProcessorResult.dimension;
                    if (dimension != null) {
                        normalWidth = (int) dimension.getWidth();
                        normalHeight = (int) dimension.getHeight();
                    }

                    switch (printImage.getHorizontalImageAlign()) {
                    case RIGHT: {
                        cropLeft = 65536 * (-availableImageWidth + normalWidth) / availableImageWidth;
                        cropRight = 0;
                        break;
                    }
                    case CENTER: {
                        cropLeft = 65536 * (-availableImageWidth + normalWidth) / availableImageWidth / 2;
                        cropRight = cropLeft;
                        break;
                    }
                    case LEFT:
                    default: {
                        cropLeft = 0;
                        cropRight = 65536 * (-availableImageWidth + normalWidth) / availableImageWidth;
                        break;
                    }
                    }
                    switch (printImage.getVerticalImageAlign()) {
                    case TOP: {
                        cropTop = 0;
                        cropBottom = 65536 * (-availableImageHeight + normalHeight) / normalHeight;
                        break;
                    }
                    case MIDDLE: {
                        cropTop = 65536 * (-availableImageHeight + normalHeight) / normalHeight / 2;
                        cropBottom = cropTop;
                        break;
                    }
                    case BOTTOM:
                    default: {
                        cropTop = 65536 * (-availableImageHeight + normalHeight) / normalHeight;
                        cropBottom = 0;
                        break;
                    }
                    }
                    imageWidth = availableImageWidth;
                    imageHeight = availableImageHeight;
                    break;
                }
                case FILL_FRAME: {
                    imageWidth = availableImageWidth;
                    imageHeight = availableImageHeight;
                    break;
                }
                case RETAIN_SHAPE:
                default: {
                    int normalWidth = availableImageWidth;
                    int normalHeight = availableImageHeight;

                    Dimension2D dimension = imageProcessorResult.dimension;
                    if (dimension != null) {
                        normalWidth = (int) dimension.getWidth();
                        normalHeight = (int) dimension.getHeight();
                    }

                    double ratio = (double) normalWidth / (double) normalHeight;

                    if (ratio > (double) availableImageWidth / (double) availableImageHeight) {
                        normalWidth = availableImageWidth;
                        normalHeight = (int) (availableImageWidth / ratio);
                    } else {
                        normalWidth = (int) (availableImageHeight * ratio);
                        normalHeight = availableImageHeight;
                    }

                    xoffset = (int) (ImageUtil.getXAlignFactor(printImage) * (availableImageWidth - normalWidth));
                    yoffset = (int) (ImageUtil.getYAlignFactor(printImage) * (availableImageHeight - normalHeight));
                    imageWidth = normalWidth;
                    imageHeight = normalHeight;

                    break;
                }
                }

                startElement(printImage);
                exportPen(printImage.getForecolor());//FIXMEBORDER should we have lineColor here, if at all needed?
                finishElement();
                boolean startedHyperlink = exportHyperlink(printImage);

                contentWriter
                        .write("{\\shp{\\*\\shpinst\\shpbxpage\\shpbypage\\shpwr5\\shpfhdr0\\shpfblwtxt0\\shpz");
                contentWriter.write(String.valueOf(zorder++));
                contentWriter.write("\\shpleft");
                contentWriter.write(
                        String.valueOf(LengthUtil.twip(printImage.getX() + leftPadding + xoffset + getOffsetX())));
                contentWriter.write("\\shpright");
                contentWriter.write(String.valueOf(
                        LengthUtil.twip(printImage.getX() + leftPadding + xoffset + getOffsetX() + imageWidth)));
                contentWriter.write("\\shptop");
                contentWriter.write(
                        String.valueOf(LengthUtil.twip(printImage.getY() + topPadding + yoffset + getOffsetY())));
                contentWriter.write("\\shpbottom");
                contentWriter.write(String.valueOf(
                        LengthUtil.twip(printImage.getY() + topPadding + yoffset + getOffsetY() + imageHeight)));
                contentWriter.write("{\\sp{\\sn shapeType}{\\sv 75}}");
                contentWriter.write("{\\sp{\\sn fFilled}{\\sv 0}}");
                contentWriter.write("{\\sp{\\sn fLockAspectRatio}{\\sv 0}}");

                contentWriter.write("{\\sp{\\sn cropFromTop}{\\sv ");
                contentWriter.write(String.valueOf(cropTop));
                contentWriter.write("}}");
                contentWriter.write("{\\sp{\\sn cropFromLeft}{\\sv ");
                contentWriter.write(String.valueOf(cropLeft));
                contentWriter.write("}}");
                contentWriter.write("{\\sp{\\sn cropFromBottom}{\\sv ");
                contentWriter.write(String.valueOf(cropBottom));
                contentWriter.write("}}");
                contentWriter.write("{\\sp{\\sn cropFromRight}{\\sv ");
                contentWriter.write(String.valueOf(cropRight));
                contentWriter.write("}}");

                writeShapeHyperlink(printImage);

                if (printImage.getAnchorName() != null) {
                    writeAnchor(printImage.getAnchorName());
                }

                contentWriter.write("{\\sp{\\sn pib}{\\sv {\\pict");
                if (imageProcessorResult.imageType == ImageTypeEnum.JPEG) {
                    contentWriter.write("\\jpegblip");
                } else {
                    contentWriter.write("\\pngblip");
                }
                contentWriter.write("\n");

                ByteArrayInputStream bais = new ByteArrayInputStream(imageProcessorResult.imageData);

                int count = 0;
                int current = 0;
                while ((current = bais.read()) != -1) {
                    String helperStr = Integer.toHexString(current);
                    if (helperStr.length() < 2) {
                        helperStr = "0" + helperStr;
                    }
                    contentWriter.write(helperStr);
                    count++;
                    if (count == 64) {
                        contentWriter.write("\n");
                        count = 0;
                    }
                }

                contentWriter.write("\n}}}");
                contentWriter.write("}}\n");
                endHyperlink(startedHyperlink);
            }
        }

        int x = printImage.getX() + getOffsetX();
        int y = printImage.getY() + getOffsetY();
        int width = printImage.getWidth();
        int height = printImage.getHeight();

        if (printImage.getLineBox().getTopPen().getLineWidth() <= 0f
                && printImage.getLineBox().getLeftPen().getLineWidth() <= 0f
                && printImage.getLineBox().getBottomPen().getLineWidth() <= 0f
                && printImage.getLineBox().getRightPen().getLineWidth() <= 0f) {
            if (printImage.getLinePen().getLineWidth() > 0f) {
                exportPen(printImage.getLinePen(), x, y, width, height);
            }
        } else {
            exportBox(printImage.getLineBox(), x, y, width, height);
        }
    }

    private class InternalImageProcessor {
        private final JRPrintElement imageElement;
        private final RenderersCache imageRenderersCache;
        private final boolean needDimension;
        private final int availableImageWidth;
        private final int availableImageHeight;

        protected InternalImageProcessor(JRPrintImage imageElement, boolean needDimension, int availableImageWidth,
                int availableImageHeight) {
            this.imageElement = imageElement;
            this.imageRenderersCache = imageElement.isUsingCache() ? renderersCache
                    : new RenderersCache(getJasperReportsContext());
            this.needDimension = needDimension;
            this.availableImageWidth = availableImageWidth;
            this.availableImageHeight = availableImageHeight;
        }

        private InternalImageProcessorResult process(Renderable renderer) throws JRException {
            if (renderer instanceof ResourceRenderer) {
                renderer = imageRenderersCache.getLoadedRenderer((ResourceRenderer) renderer);
            }

            Dimension2D dimension = null;
            if (needDimension) {
                DimensionRenderable dimensionRenderer = imageRenderersCache.getDimensionRenderable(renderer);
                dimension = dimensionRenderer == null ? null : dimensionRenderer.getDimension(jasperReportsContext);
            }

            DataRenderable imageRenderer = getRendererUtil().getImageDataRenderable(imageRenderersCache, renderer,
                    new Dimension(availableImageWidth, availableImageHeight),
                    ModeEnum.OPAQUE == imageElement.getModeValue() ? imageElement.getBackcolor() : null);

            byte[] imageData = imageRenderer.getData(jasperReportsContext);

            return new InternalImageProcessorResult(imageData, dimension,
                    JRTypeSniffer.getImageTypeValue(imageData));
        }
    }

    private class InternalImageProcessorResult {
        protected final byte[] imageData;
        protected final Dimension2D dimension;
        protected final ImageTypeEnum imageType;

        protected InternalImageProcessorResult(byte[] imageData, Dimension2D dimension, ImageTypeEnum imageType) {
            this.imageData = imageData;
            this.dimension = dimension;
            this.imageType = imageType;
        }
    }

    /**
     *
     * @param frame
     * @throws JRException
     */
    public void exportFrame(JRPrintFrame frame) throws JRException, IOException {
        int x = frame.getX() + getOffsetX();
        int y = frame.getY() + getOffsetY();
        int width = frame.getWidth();
        int height = frame.getHeight();

        startElement(frame);

        exportPen(frame.getForecolor());

        finishElement();

        setFrameElementsOffset(frame, false);
        exportElements(frame.getElements());
        restoreElementOffsets();

        exportBox(frame.getLineBox(), x, y, width, height);
    }

    protected void exportElements(Collection<JRPrintElement> elements) throws JRException, IOException {
        if (elements != null && elements.size() > 0) {
            for (Iterator<JRPrintElement> it = elements.iterator(); it.hasNext();) {
                JRPrintElement element = it.next();
                if (filter == null || filter.isToExport(element)) {
                    if (element instanceof JRPrintLine) {
                        exportLine((JRPrintLine) element);
                    } else if (element instanceof JRPrintRectangle) {
                        exportRectangle((JRPrintRectangle) element);
                    } else if (element instanceof JRPrintEllipse) {
                        exportEllipse((JRPrintEllipse) element);
                    } else if (element instanceof JRPrintImage) {
                        exportImage((JRPrintImage) element);
                    } else if (element instanceof JRPrintText) {
                        exportText((JRPrintText) element);
                    } else if (element instanceof JRPrintFrame) {
                        exportFrame((JRPrintFrame) element);
                    } else if (element instanceof JRGenericPrintElement) {
                        exportGenericElement((JRGenericPrintElement) element);
                    }
                }
            }
        }
    }

    /**
     *
     */
    private void exportBox(JRLineBox box, int x, int y, int width, int height) throws IOException {
        exportTopPen(box.getTopPen(), box.getLeftPen(), box.getRightPen(), x, y, width, height);
        exportLeftPen(box.getTopPen(), box.getLeftPen(), box.getBottomPen(), x, y, width, height);
        exportBottomPen(box.getLeftPen(), box.getBottomPen(), box.getRightPen(), x, y, width, height);
        exportRightPen(box.getTopPen(), box.getBottomPen(), box.getRightPen(), x, y, width, height);
    }

    /**
     *
     */
    private void exportPen(JRPen pen, int x, int y, int width, int height) throws IOException {
        exportTopPen(pen, pen, pen, x, y, width, height);
        exportLeftPen(pen, pen, pen, x, y, width, height);
        exportBottomPen(pen, pen, pen, x, y, width, height);
        exportRightPen(pen, pen, pen, x, y, width, height);
    }

    /**
     *
     */
    private void exportTopPen(JRPen topPen, JRPen leftPen, JRPen rightPen, int x, int y, int width, int height)
            throws IOException {
        if (topPen.getLineWidth() > 0f) {
            exportBorder(topPen, x - leftPen.getLineWidth() / 2, y,
                    width + (leftPen.getLineWidth() + rightPen.getLineWidth()) / 2, 0);
            //exportBorder(topPen, x, y + getAdjustment(topPen), width, 0);
        }
    }

    /**
     *
     */
    private void exportLeftPen(JRPen topPen, JRPen leftPen, JRPen bottomPen, int x, int y, int width, int height)
            throws IOException {
        if (leftPen.getLineWidth() > 0f) {
            exportBorder(leftPen, x, y - topPen.getLineWidth() / 2, 0,
                    height + (topPen.getLineWidth() + bottomPen.getLineWidth()) / 2);
            //exportBorder(leftPen, x + getAdjustment(leftPen), y, 0, height);
        }
    }

    /**
     *
     */
    private void exportBottomPen(JRPen leftPen, JRPen bottomPen, JRPen rightPen, int x, int y, int width,
            int height) throws IOException {
        if (bottomPen.getLineWidth() > 0f) {
            exportBorder(bottomPen, x - leftPen.getLineWidth() / 2, y + height,
                    width + (leftPen.getLineWidth() + rightPen.getLineWidth()) / 2, 0);
            //exportBorder(bottomPen, x, y + height - getAdjustment(bottomPen), width, 0);
        }
    }

    /**
     *
     */
    private void exportRightPen(JRPen topPen, JRPen bottomPen, JRPen rightPen, int x, int y, int width, int height)
            throws IOException {
        if (rightPen.getLineWidth() > 0f) {
            exportBorder(rightPen, x + width, y - topPen.getLineWidth() / 2, 0,
                    height + (topPen.getLineWidth() + bottomPen.getLineWidth()) / 2);
            //exportBorder(rightPen, x + width - getAdjustment(rightPen), y, 0, height);
        }
    }

    protected void exportGenericElement(JRGenericPrintElement element) {
        GenericElementRtfHandler handler = (GenericElementRtfHandler) GenericElementHandlerEnviroment
                .getInstance(getJasperReportsContext())
                .getElementHandler(element.getGenericType(), RTF_EXPORTER_KEY);

        if (handler != null) {
            handler.exportElement(exporterContext, element);
        } else {
            if (log.isDebugEnabled()) {
                log.debug("No RTF generic element handler for " + element.getGenericType());
            }
        }
    }

    protected boolean exportHyperlink(JRPrintHyperlink link) throws IOException {
        String hl = null;
        String local = "";
        boolean result = false;

        Boolean ignoreHyperlink = HyperlinkUtil.getIgnoreHyperlink(RtfReportConfiguration.PROPERTY_IGNORE_HYPERLINK,
                link);
        if (ignoreHyperlink == null) {
            ignoreHyperlink = getCurrentItemConfiguration().isIgnoreHyperlink();
        }

        if (!ignoreHyperlink) {
            JRHyperlinkProducer customHandler = getHyperlinkProducer(link);
            if (customHandler == null) {
                switch (link.getHyperlinkTypeValue()) {
                case REFERENCE: {
                    if (link.getHyperlinkReference() != null) {
                        hl = link.getHyperlinkReference();
                    }
                    break;
                }
                case LOCAL_ANCHOR: {
                    if (link.getHyperlinkAnchor() != null) {
                        hl = link.getHyperlinkAnchor();
                        local = "\\\\l ";
                    }
                    break;
                }
                case LOCAL_PAGE: {
                    if (link.getHyperlinkPage() != null) {
                        hl = JR_PAGE_ANCHOR_PREFIX + reportIndex + "_" + link.getHyperlinkPage().toString();
                        local = "\\\\l ";
                    }
                    break;
                }
                case REMOTE_ANCHOR: {
                    if (link.getHyperlinkReference() != null && link.getHyperlinkAnchor() != null) {
                        hl = link.getHyperlinkReference() + "#" + link.getHyperlinkAnchor();
                    }
                    break;
                }
                case REMOTE_PAGE: {
                    if (link.getHyperlinkReference() != null && link.getHyperlinkPage() != null) {
                        hl = link.getHyperlinkReference() + "#" + JR_PAGE_ANCHOR_PREFIX + "0_"
                                + link.getHyperlinkPage().toString();
                    }
                    break;
                }
                case NONE:
                default: {
                    break;
                }
                }
            } else {
                hl = customHandler.getHyperlink(link);
            }
        }

        if (hl != null) {
            contentWriter.write("{\\field{\\*\\fldinst HYPERLINK " + local + "\"" + hl + "\"}{\\fldrslt ");
            result = true;
        }
        return result;
    }

    protected void writeShapeHyperlink(JRPrintHyperlink link) throws IOException {
        String hlloc = null;
        String hlfr = null;
        String hlsrc = null;

        Boolean ignoreHyperlink = HyperlinkUtil.getIgnoreHyperlink(RtfReportConfiguration.PROPERTY_IGNORE_HYPERLINK,
                link);
        if (ignoreHyperlink == null) {
            ignoreHyperlink = getCurrentItemConfiguration().isIgnoreHyperlink();
        }

        if (!ignoreHyperlink) {
            JRHyperlinkProducer customHandler = getHyperlinkProducer(link);
            if (customHandler == null) {
                switch (link.getHyperlinkTypeValue()) {
                case REFERENCE: {
                    if (link.getHyperlinkReference() != null) {
                        hlsrc = link.getHyperlinkReference();
                        hlfr = hlsrc;
                    }
                    break;
                }
                case LOCAL_ANCHOR: {
                    if (link.getHyperlinkAnchor() != null) {
                        hlloc = link.getHyperlinkAnchor();
                        hlfr = hlloc;
                    }
                    break;
                }
                case LOCAL_PAGE: {
                    if (link.getHyperlinkPage() != null) {
                        hlloc = JR_PAGE_ANCHOR_PREFIX + reportIndex + "_" + link.getHyperlinkPage().toString();
                        hlfr = hlloc;
                    }
                    break;
                }
                case REMOTE_ANCHOR: {
                    if (link.getHyperlinkReference() != null && link.getHyperlinkAnchor() != null) {
                        hlsrc = link.getHyperlinkReference() + "#" + link.getHyperlinkAnchor();
                        hlfr = hlsrc;
                    }
                    break;
                }
                case REMOTE_PAGE: {
                    if (link.getHyperlinkReference() != null && link.getHyperlinkPage() != null) {
                        hlsrc = link.getHyperlinkReference() + "#" + JR_PAGE_ANCHOR_PREFIX + "0_"
                                + link.getHyperlinkPage().toString();
                        hlfr = hlsrc;
                    }
                    break;
                }
                case NONE:
                default: {
                    break;
                }
                }
            } else {
                hlsrc = customHandler.getHyperlink(link);
                hlfr = hlsrc;
            }
        }

        if (hlfr != null) {
            contentWriter.write("{\\sp{\\sn fIsButton}{\\sv 1}}");
            contentWriter.write("{\\sp{\\sn pihlShape}{\\sv {\\*\\hl");
            contentWriter.write("{\\hlfr ");
            contentWriter.write(hlfr);
            contentWriter.write(" }");
            if (hlloc != null) {
                contentWriter.write("{\\hlloc ");
                contentWriter.write(hlloc);
                contentWriter.write(" }");
            }
            if (hlsrc != null) {
                contentWriter.write("{\\hlsrc ");
                contentWriter.write(hlsrc);
                contentWriter.write(" }");
            }
            contentWriter.write("}}}");
        }
    }

    protected void endHyperlink(boolean startedHyperlink) throws IOException {
        if (startedHyperlink) {
            contentWriter.write("}}");
        }
    }

    protected void writeAnchor(String anchorName) throws IOException {
        contentWriter.write("{\\*\\bkmkstart ");
        contentWriter.write(anchorName);
        contentWriter.write("}{\\*\\bkmkend ");
        contentWriter.write(anchorName);
        contentWriter.write("}");
    }

    @Override
    public String getExporterKey() {
        return RTF_EXPORTER_KEY;
    }

    @Override
    public String getExporterPropertiesPrefix() {
        return RTF_EXPORTER_PROPERTIES_PREFIX;
    }
}