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

Java tutorial

Introduction

Here is the source code for net.sf.jasperreports.engine.export.HtmlExporter.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/>.
 */
package net.sf.jasperreports.engine.export;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.font.TextAttribute;
import java.awt.geom.Dimension2D;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;

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

import net.sf.jasperreports.annotations.properties.Property;
import net.sf.jasperreports.annotations.properties.PropertyScope;
import net.sf.jasperreports.components.headertoolbar.HeaderToolbarElement;
import net.sf.jasperreports.crosstabs.interactive.CrosstabInteractiveJsonHandler;
import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRAnchor;
import net.sf.jasperreports.engine.JRException;
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.JRPrintElementIndex;
import net.sf.jasperreports.engine.JRPrintEllipse;
import net.sf.jasperreports.engine.JRPrintFrame;
import net.sf.jasperreports.engine.JRPrintGraphicElement;
import net.sf.jasperreports.engine.JRPrintHyperlink;
import net.sf.jasperreports.engine.JRPrintHyperlinkParameter;
import net.sf.jasperreports.engine.JRPrintImage;
import net.sf.jasperreports.engine.JRPrintImageArea;
import net.sf.jasperreports.engine.JRPrintImageAreaHyperlink;
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.JRRuntimeException;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.PrintElementId;
import net.sf.jasperreports.engine.PrintElementVisitor;
import net.sf.jasperreports.engine.PrintPageFormat;
import net.sf.jasperreports.engine.ReportContext;
import net.sf.jasperreports.engine.export.tabulator.Cell;
import net.sf.jasperreports.engine.export.tabulator.CellVisitor;
import net.sf.jasperreports.engine.export.tabulator.Column;
import net.sf.jasperreports.engine.export.tabulator.ElementCell;
import net.sf.jasperreports.engine.export.tabulator.FrameCell;
import net.sf.jasperreports.engine.export.tabulator.LayeredCell;
import net.sf.jasperreports.engine.export.tabulator.Row;
import net.sf.jasperreports.engine.export.tabulator.SplitCell;
import net.sf.jasperreports.engine.export.tabulator.Table;
import net.sf.jasperreports.engine.export.tabulator.TableCell;
import net.sf.jasperreports.engine.export.tabulator.TablePosition;
import net.sf.jasperreports.engine.export.tabulator.Tabulator;
import net.sf.jasperreports.engine.type.HorizontalImageAlignEnum;
import net.sf.jasperreports.engine.type.HyperlinkTypeEnum;
import net.sf.jasperreports.engine.type.LineDirectionEnum;
import net.sf.jasperreports.engine.type.LineSpacingEnum;
import net.sf.jasperreports.engine.type.LineStyleEnum;
import net.sf.jasperreports.engine.type.ModeEnum;
import net.sf.jasperreports.engine.type.RotationEnum;
import net.sf.jasperreports.engine.type.RunDirectionEnum;
import net.sf.jasperreports.engine.type.ScaleImageEnum;
import net.sf.jasperreports.engine.type.VerticalImageAlignEnum;
import net.sf.jasperreports.engine.util.HyperlinkData;
import net.sf.jasperreports.engine.util.ImageUtil;
import net.sf.jasperreports.engine.util.JRCloneUtils;
import net.sf.jasperreports.engine.util.JRColorUtil;
import net.sf.jasperreports.engine.util.JRStringUtil;
import net.sf.jasperreports.engine.util.JRStyledText;
import net.sf.jasperreports.engine.util.JRTextAttribute;
import net.sf.jasperreports.engine.util.JRTypeSniffer;
import net.sf.jasperreports.engine.util.Pair;
import net.sf.jasperreports.export.ExportInterruptedException;
import net.sf.jasperreports.export.ExporterInputItem;
import net.sf.jasperreports.export.HtmlExporterConfiguration;
import net.sf.jasperreports.export.HtmlReportConfiguration;
import net.sf.jasperreports.export.type.HtmlBorderCollapseEnum;
import net.sf.jasperreports.properties.PropertyConstants;
import net.sf.jasperreports.renderers.AreaHyperlinksRenderable;
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;
import net.sf.jasperreports.renderers.util.RendererUtil;
import net.sf.jasperreports.renderers.util.SvgDataSniffer;
import net.sf.jasperreports.renderers.util.SvgFontProcessor;
import net.sf.jasperreports.search.HitTermInfo;
import net.sf.jasperreports.search.SpansInfo;
import net.sf.jasperreports.util.Base64Util;

/**
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 */
public class HtmlExporter extends AbstractHtmlExporter<HtmlReportConfiguration, HtmlExporterConfiguration> {
    private static final Log log = LogFactory.getLog(HtmlExporter.class);

    private static final String EXCEPTION_MESSAGE_KEY_INTERNAL_ERROR = "export.html.internal.error";
    private static final String EXCEPTION_MESSAGE_KEY_UNEXPECTED_ROTATION_VALUE = "export.html.unexpected.rotation.value";

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

    /**
     *
     */
    public static final String HTML_EXPORTER_PROPERTIES_PREFIX = JRPropertiesUtil.PROPERTY_PREFIX + "export.html.";

    /**
     * Property that provides the value for the <code>class</code> CSS style property to be applied 
     * to elements in the table generated for the report. The value of this property 
     * will be used as the value for the <code>class</code> attribute of the <code>&lt;td&gt;</code> tag for the element when exported to HTML and/or 
     * the <code>class</code> attribute of the <code>&lt;span&gt;</code> or <code>&lt;div&gt;</code> tag for the element, when exported to XHTML/CSS.
     */
    @Property(category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_4_0_1)
    public static final String PROPERTY_HTML_CLASS = HTML_EXPORTER_PROPERTIES_PREFIX + "class";

    /**
     *
     */
    @Property(category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_3_7_0)
    public static final String PROPERTY_HTML_ID = HTML_EXPORTER_PROPERTIES_PREFIX + "id";

    protected JRHyperlinkTargetProducerFactory targetProducerFactory;

    protected Map<String, String> rendererToImagePathMap;
    protected Map<Pair<String, Rectangle>, String> imageMaps;
    protected RenderersCache renderersCache;

    protected Writer writer;
    protected int reportIndex;
    protected int pageIndex;

    protected LinkedList<Color> backcolorStack = new LinkedList<Color>();

    protected ExporterFilter tableFilter;

    protected int pointerEventsNoneStack = 0;

    private List<HyperlinkData> hyperlinksData = new ArrayList<HyperlinkData>();

    public HtmlExporter() {
        this(DefaultJasperReportsContext.getInstance());
    }

    public HtmlExporter(JasperReportsContext jasperReportsContext) {
        super(jasperReportsContext);

        exporterContext = new ExporterContext();
    }

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

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

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

        rendererToImagePathMap = new HashMap<String, String>();
        imageMaps = new HashMap<Pair<String, Rectangle>, String>();
        renderersCache = new RenderersCache(getJasperReportsContext());

        fontsToProcess = new HashMap<String, HtmlFontFamily>();

        //FIXMENOW check all exporter properties that are supposed to work at report level

        initExport();

        ensureOutput();

        writer = getExporterOutput().getWriter();

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

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

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

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

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

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

        HtmlReportConfiguration configuration = getCurrentItemConfiguration();

        if (configuration.isRemoveEmptySpaceBetweenRows()) {
            log.info("Removing empty space between rows not supported");
        }

        // this is the filter used to create the table, taking in consideration unhandled generic elements
        tableFilter = new GenericElementsFilterDecorator(jasperReportsContext, HTML_EXPORTER_KEY, filter);
    }

    @Override
    protected void setJasperReportsContext(JasperReportsContext jasperReportsContext) {
        super.setJasperReportsContext(jasperReportsContext);

        targetProducerFactory = new DefaultHyperlinkTargetProducerFactory(jasperReportsContext);
    }

    protected void exportReportToWriter() throws JRException, IOException {
        HtmlExporterConfiguration configuration = getCurrentConfiguration();
        String htmlHeader = configuration.getHtmlHeader();
        String betweenPagesHtml = configuration.getBetweenPagesHtml();
        String htmlFooter = configuration.getHtmlFooter();
        boolean flushOutput = configuration.isFlushOutput();//FIXMEEXPORT maybe move flush flag to output

        if (htmlHeader == null) {
            writer.write(
                    "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n");
            writer.write("<html>\n");
            writer.write("<head>\n");
            writer.write("  <title></title>\n");
            writer.write("  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=");
            writer.write(JRStringUtil.encodeXmlAttribute(getExporterOutput().getEncoding()));
            writer.write("\"/>\n");
            writer.write("  <style type=\"text/css\">\n");
            writer.write("    a {text-decoration: none}\n");
            writer.write("  </style>\n");
            writer.write("</head>\n");
            writer.write("<body text=\"#000000\" link=\"#000000\" alink=\"#000000\" vlink=\"#000000\">\n");
            writer.write("<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n");
            writer.write("<tr><td width=\"50%\">&nbsp;</td><td align=\"center\">\n");
            writer.write("\n");
        } else {
            writer.write(htmlHeader);
        }

        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();

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

                    page = pages.get(pageIndex);

                    writer.write("<a name=\"" + JR_PAGE_ANCHOR_PREFIX + reportIndex + "_" + (pageIndex + 1)
                            + "\"></a>\n");

                    /*   */
                    exportPage(page);

                    if (reportIndex < items.size() - 1 || pageIndex < endPageIndex) {
                        if (betweenPagesHtml == null) {
                            writer.write("<br/>\n<br/>\n");
                        } else {
                            writer.write(betweenPagesHtml);
                        }
                    }

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

        ReportContext reportContext = getReportContext();
        if (fontsToProcess != null && fontsToProcess.size() > 0)// when no resourceHandler, fonts are not processed 
        {
            if (reportContext == null) {
                @SuppressWarnings("deprecation")
                HtmlResourceHandler resourceHandler = getExporterOutput().getResourceHandler() == null
                        ? getResourceHandler()
                        : getExporterOutput().getResourceHandler();

                for (HtmlFontFamily htmlFontFamily : fontsToProcess.values()) {
                    writer.write("<link class=\"jrWebFont\" rel=\"stylesheet\" href=\"" + JRStringUtil
                            .encodeXmlAttribute(resourceHandler.getResourcePath(htmlFontFamily.getId())) + "\">\n");
                }

                // generate script tag on static export only
                writer.write("<!--[if IE]>\n");
                writer.write("<script>\n");
                writer.write("var links = document.querySelectorAll('link.jrWebFont');\n");
                writer.write(
                        "setTimeout(function(){ if (links) { for (var i = 0; i < links.length; i++) { links.item(i).href = links.item(i).href; } } }, 0);\n");
                writer.write("</script>\n");
                writer.write("<![endif]-->\n");
            } else {
                reportContext.setParameterValue(JsonExporter.REPORT_CONTEXT_PARAMETER_WEB_FONTS, fontsToProcess);
            }
        }

        // place hyperlinksData on reportContext
        if (hyperlinksData.size() > 0) {
            //for sure reportContext is not null, because otherwise there would be no item in the hyperilnkData
            reportContext.setParameterValue("net.sf.jasperreports.html.hyperlinks", hyperlinksData);
        }

        if (htmlFooter == null) {
            writer.write("</td><td width=\"50%\">&nbsp;</td></tr>\n");
            writer.write("</table>\n");
            writer.write("</body>\n");
            writer.write("</html>\n");
        } else {
            writer.write(htmlFooter);
        }

        if (flushOutput) {
            writer.flush();
        }
    }

    protected void exportPage(JRPrintPage page) throws IOException {
        Tabulator tabulator = new Tabulator(tableFilter, page.getElements());
        tabulator.tabulate();

        HtmlReportConfiguration configuration = getCurrentItemConfiguration();

        boolean isIgnorePageMargins = configuration.isIgnorePageMargins();
        if (!isIgnorePageMargins) {
            PrintPageFormat pageFormat = jasperPrint.getPageFormat(pageIndex);
            tabulator.addMargins(pageFormat.getPageWidth(), pageFormat.getPageHeight());
        }

        Table table = tabulator.getTable();

        boolean isWhitePageBackground = configuration.isWhitePageBackground();
        if (isWhitePageBackground) {
            setBackcolor(Color.white);
        }

        CellElementVisitor elementVisitor = new CellElementVisitor();
        TableVisitor tableVisitor = new TableVisitor(tabulator, elementVisitor);

        exportTable(tableVisitor, table, isWhitePageBackground, true);

        if (isWhitePageBackground) {
            restoreBackcolor();
        }

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

    public void exportElements(List<JRPrintElement> elements) throws IOException {
        Tabulator tabulator = new Tabulator(tableFilter, elements);
        tabulator.tabulate();

        Table table = tabulator.getTable();

        CellElementVisitor elementVisitor = new CellElementVisitor();
        TableVisitor tableVisitor = new TableVisitor(tabulator, elementVisitor);

        exportTable(tableVisitor, table, false, false);
    }

    protected void exportTable(TableVisitor tableVisitor, Table table, boolean whiteBackground,
            boolean isMainReportTable) throws IOException {
        SortedSet<Column> columns = table.getColumns().getUserEntries();
        SortedSet<Row> rows = table.getRows().getUserEntries();
        if (columns.isEmpty() || rows.isEmpty()) {
            // TODO lucianc empty page
            return;
        }

        if (isMainReportTable) {
            int totalWidth = columns.last().getEndCoord() - columns.first().getStartCoord();
            writer.write(
                    "<table class=\"jrPage\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"empty-cells: show; width: ");
            writer.write(toSizeUnit(totalWidth));
            writer.write(";");
        } else {
            writer.write(
                    "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"empty-cells: show; width: 100%;");
        }

        HtmlBorderCollapseEnum borderCollapse = getCurrentItemConfiguration().getBorderCollapseValue();
        if (borderCollapse != null) {
            writer.write(" border-collapse: ");
            writer.write(borderCollapse.getName());
            writer.write(";");
        }

        if (whiteBackground) {
            writer.write(" background-color: white;");
        }
        writer.write("\">\n");

        // TODO lucianc check whether we can use the first row for setting col widths
        writer.write("<tr valign=\"top\" style=\"height:0\">\n");
        for (Column col : columns) {
            writer.write("<td style=\"width:");
            writer.write(toSizeUnit(col.getExtent()));
            writer.write("\"></td>\n");
        }
        writer.write("</tr>\n");

        for (Row row : rows) {
            writer.write("<tr valign=\"top\" style=\"height:");
            writer.write(toSizeUnit(row.getExtent()));
            writer.write("\">\n");

            int emptySpan = 0;
            for (Column col : columns) {
                Cell cell = row.getCell(col);
                if (cell == null) {
                    ++emptySpan;
                } else {
                    if (emptySpan > 0) {
                        writeEmptyCell(emptySpan, 1);
                    }
                    emptySpan = 0;

                    TablePosition position = new TablePosition(table, col, row);
                    cell.accept(tableVisitor, position);
                }
            }
            if (emptySpan > 0) {
                writeEmptyCell(emptySpan, 1);
            }

            writer.write("</tr>\n");
        }

        writer.write("</table>\n");
    }

    protected void writeText(JRPrintText text, TableCell cell) throws IOException {
        JRStyledText styledText = getStyledText(text);
        int textLength = styledText == null ? 0 : styledText.length();

        startCell(text, cell);

        if (text.getRunDirectionValue() == RunDirectionEnum.RTL) {
            writer.write(" dir=\"rtl\"");
        }

        StringBuilder styleBuffer = new StringBuilder();

        String verticalAlignment = HTML_VERTICAL_ALIGN_TOP;

        switch (text.getVerticalTextAlign()) {
        case BOTTOM: {
            verticalAlignment = HTML_VERTICAL_ALIGN_BOTTOM;
            break;
        }
        case MIDDLE: {
            verticalAlignment = HTML_VERTICAL_ALIGN_MIDDLE;
            break;
        }
        case TOP:
        case JUSTIFIED:
        default: {
            verticalAlignment = HTML_VERTICAL_ALIGN_TOP;
        }
        }

        appendElementCellGenericStyle(cell, styleBuffer);
        appendBackcolorStyle(cell, styleBuffer);
        appendBorderStyle(cell.getBox(), styleBuffer);
        appendPaddingStyle(text.getLineBox(), styleBuffer);

        String horizontalAlignment = CSS_TEXT_ALIGN_LEFT;
        if (textLength > 0) {
            switch (text.getHorizontalTextAlign()) {
            case RIGHT: {
                horizontalAlignment = CSS_TEXT_ALIGN_RIGHT;
                break;
            }
            case CENTER: {
                horizontalAlignment = CSS_TEXT_ALIGN_CENTER;
                break;
            }
            case JUSTIFIED: {
                horizontalAlignment = CSS_TEXT_ALIGN_JUSTIFY;
                break;
            }
            case LEFT:
            default: {
                horizontalAlignment = CSS_TEXT_ALIGN_LEFT;
            }
            }
        }

        if (getCurrentItemConfiguration().isWrapBreakWord()) {
            styleBuffer.append("width: " + toSizeUnit(text.getWidth()) + "; ");
            styleBuffer.append("word-wrap: break-word; ");
        }

        if (text.getLineBreakOffsets() != null) {
            //if we have line breaks saved in the text, set nowrap so that
            //the text only wraps at the explicit positions
            styleBuffer.append("white-space: nowrap; ");
        }

        styleBuffer.append("text-indent: " + text.getParagraph().getFirstLineIndent() + "px; ");

        String rotationValue = null;
        StringBuilder spanStyleBuffer = new StringBuilder();
        StringBuilder divStyleBuffer = new StringBuilder();
        if (text.getRotationValue() == RotationEnum.NONE) {
            if (!verticalAlignment.equals(HTML_VERTICAL_ALIGN_TOP)) {
                styleBuffer.append(" vertical-align: ");
                styleBuffer.append(verticalAlignment);
                styleBuffer.append(";");
            }

            //writing text align every time even when it's left
            //because IE8 with transitional defaults to center 
            styleBuffer.append("text-align: ");
            styleBuffer.append(horizontalAlignment);
            styleBuffer.append(";");
        } else {
            rotationValue = setRotationStyles(text, horizontalAlignment, spanStyleBuffer, divStyleBuffer);
        }

        writeStyle(styleBuffer);

        finishStartCell();

        if (text.getAnchorName() != null) {
            writer.write("<a name=\"");
            writer.write(JRStringUtil.encodeXmlAttribute(text.getAnchorName()));
            writer.write("\"/>");
        }

        if (text.getBookmarkLevel() != JRAnchor.NO_BOOKMARK) {
            writer.write("<a name=\"");
            writer.write(
                    JR_BOOKMARK_ANCHOR_PREFIX + reportIndex + "_" + pageIndex + "_" + cell.getElementAddress());
            writer.write("\"/>");
        }

        if (rotationValue != null) {
            writer.write("<div style=\"position: relative; overflow: hidden; ");
            writer.write(divStyleBuffer.toString());
            writer.write("\">\n");
            writer.write("<span class=\"rotated\" data-rotation=\"");
            writer.write(rotationValue);
            writer.write("\" style=\"position: absolute; display: table; ");
            writer.write(spanStyleBuffer.toString());
            writer.write("\">");
            writer.write("<span style=\"display: table-cell; vertical-align:"); //display:table-cell conflicts with overflow: hidden;
            writer.write(verticalAlignment);
            writer.write(";\">");
        }

        boolean hyperlinkStarted = startHyperlink(text);

        if (textLength > 0) {
            //only use text tooltip when no hyperlink present
            //         String textTooltip = hyperlinkStarted ? null : text.getHyperlinkTooltip();
            exportStyledText(text, styledText, text.getHyperlinkTooltip(), hyperlinkStarted);
        }

        if (hyperlinkStarted) {
            endHyperlink();
        }

        if (rotationValue != null) {
            writer.write("</span></span></div>");
        }

        endCell();
    }

    protected String setRotationStyles(JRPrintText text, String horizontalAlignment, StringBuilder spanStyleBuffer,
            StringBuilder divStyleBuffer) {
        String rotationValue;
        int textWidth = text.getWidth() - text.getLineBox().getLeftPadding() - text.getLineBox().getRightPadding();
        int textHeight = text.getHeight() - text.getLineBox().getTopPadding()
                - text.getLineBox().getBottomPadding();
        int rotatedWidth;
        int rotatedHeight;

        int rotationIE;
        int rotationAngle;
        int translateX;
        int translateY;
        switch (text.getRotationValue()) {
        case LEFT: {
            translateX = -(textHeight - textWidth) / 2;
            translateY = (textHeight - textWidth) / 2;
            rotatedWidth = textHeight;
            rotatedHeight = textWidth;
            rotationIE = 3;
            rotationAngle = -90;
            rotationValue = "left";
            break;
        }
        case RIGHT: {
            translateX = -(textHeight - textWidth) / 2;
            translateY = (textHeight - textWidth) / 2;
            rotatedWidth = textHeight;
            rotatedHeight = textWidth;
            rotationIE = 1;
            rotationAngle = 90;
            rotationValue = "right";
            break;
        }
        case UPSIDE_DOWN: {
            translateX = 0;
            translateY = 0;
            rotatedWidth = textWidth;
            rotatedHeight = textHeight;
            rotationIE = 2;
            rotationAngle = 180;
            rotationValue = "upsideDown";
            break;
        }
        case NONE:
        default: {
            throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_UNEXPECTED_ROTATION_VALUE,
                    new Object[] { text.getRotationValue() });
        }
        }

        appendSizeStyle(textWidth, textHeight, divStyleBuffer);
        appendSizeStyle(rotatedWidth, rotatedHeight, spanStyleBuffer);

        spanStyleBuffer.append("text-align: ");
        spanStyleBuffer.append(horizontalAlignment);
        spanStyleBuffer.append(";");

        spanStyleBuffer.append("-webkit-transform: translate(" + translateX + "px," + translateY + "px) ");
        spanStyleBuffer.append("rotate(" + rotationAngle + "deg); ");
        spanStyleBuffer.append("-moz-transform: translate(" + translateX + "px," + translateY + "px) ");
        spanStyleBuffer.append("rotate(" + rotationAngle + "deg); ");
        spanStyleBuffer.append("-ms-transform: translate(" + translateX + "px," + translateY + "px) ");
        spanStyleBuffer.append("rotate(" + rotationAngle + "deg); ");
        spanStyleBuffer.append("-o-transform: translate(" + translateX + "px," + translateY + "px) ");
        spanStyleBuffer.append("rotate(" + rotationAngle + "deg); ");
        spanStyleBuffer
                .append("filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=" + rotationIE + "); ");
        return rotationValue;
    }

    protected void appendSizeStyle(int width, int height, StringBuilder styleBuffer) {
        styleBuffer.append("width:");
        styleBuffer.append(toSizeUnit(width));
        styleBuffer.append(";");

        styleBuffer.append("height:");
        styleBuffer.append(toSizeUnit(height));
        styleBuffer.append(";");
    }

    protected void writeImage(JRPrintImage image, TableCell cell) throws IOException, JRException {
        startCell(image, cell);

        int availableImageWidth = image.getWidth() - image.getLineBox().getLeftPadding()
                - image.getLineBox().getRightPadding();
        if (availableImageWidth < 0) {
            availableImageWidth = 0;
        }

        int availableImageHeight = image.getHeight() - image.getLineBox().getTopPadding()
                - image.getLineBox().getBottomPadding();
        if (availableImageHeight < 0) {
            availableImageHeight = 0;
        }

        String horizontalAlignment = getImageHorizontalAlignmentStyle(image);
        String verticalAlignment = getImageVerticalAlignmentStyle(image);

        StringBuilder styleBuffer = new StringBuilder();
        ScaleImageEnum scaleImage = image.getScaleImageValue();
        if (scaleImage != ScaleImageEnum.CLIP) {
            // clipped images are absolutely positioned within a div
            if (!horizontalAlignment.equals(CSS_TEXT_ALIGN_LEFT)) {
                styleBuffer.append("text-align: ");
                styleBuffer.append(horizontalAlignment);
                styleBuffer.append(";");
            }

            if (!verticalAlignment.equals(HTML_VERTICAL_ALIGN_TOP)) {
                styleBuffer.append(" vertical-align: ");
                styleBuffer.append(verticalAlignment);
                styleBuffer.append(";");
            }
        }

        Renderable renderer = image.getRenderer();

        boolean isLazy = RendererUtil.isLazy(renderer);

        if (isLazy || (scaleImage == ScaleImageEnum.CLIP && availableImageHeight > 0)) {
            // some browsers need td height so that height: 100% works on the div used for clipped images.
            // we're using the height without paddings because that's closest to the HTML size model.
            styleBuffer.append("height: ");
            styleBuffer.append(toSizeUnit(availableImageHeight));
            styleBuffer.append("; ");
        }

        appendElementCellGenericStyle(cell, styleBuffer);
        appendBackcolorStyle(cell, styleBuffer);

        boolean addedToStyle = appendBorderStyle(cell.getBox(), styleBuffer);
        if (!addedToStyle) {
            appendPen(styleBuffer, image.getLinePen(), null);
        }

        appendPaddingStyle(image.getLineBox(), styleBuffer);

        writeStyle(styleBuffer);

        finishStartCell();

        if (image.getAnchorName() != null) {
            writer.write("<a name=\"");
            writer.write(JRStringUtil.encodeXmlAttribute(image.getAnchorName()));
            writer.write("\"/>");
        }

        if (image.getBookmarkLevel() != JRAnchor.NO_BOOKMARK) {
            writer.write("<a name=\"");
            writer.write(
                    JR_BOOKMARK_ANCHOR_PREFIX + reportIndex + "_" + pageIndex + "_" + cell.getElementAddress());
            writer.write("\"/>");
        }

        if (renderer != null) {
            boolean useBackgroundLazyImage = isLazy
                    && ((scaleImage == ScaleImageEnum.RETAIN_SHAPE || scaleImage == ScaleImageEnum.REAL_HEIGHT
                            || scaleImage == ScaleImageEnum.REAL_SIZE)
                            || !(image.getHorizontalImageAlign() == HorizontalImageAlignEnum.LEFT
                                    && image.getVerticalImageAlign() == VerticalImageAlignEnum.TOP))
                    && isUseBackgroundImageToAlign(image);

            boolean useDiv = (scaleImage == ScaleImageEnum.CLIP || useBackgroundLazyImage);
            if (useDiv) {
                writer.write("<div style=\"width: 100%; height: 100%; position: relative; overflow: hidden;\">\n");
            }

            boolean hasAreaHyperlinks = renderer instanceof AreaHyperlinksRenderable
                    && ((AreaHyperlinksRenderable) renderer).hasImageAreaHyperlinks();

            boolean hasHyperlinks = false;

            boolean hyperlinkStarted;
            if (hasAreaHyperlinks) {
                hyperlinkStarted = false;
                hasHyperlinks = true;
            } else {
                hyperlinkStarted = startHyperlink(image);
                hasHyperlinks = hyperlinkStarted;
            }

            String imageMapName = null;
            List<JRPrintImageAreaHyperlink> imageMapAreas = null;

            if (hasAreaHyperlinks) {
                Rectangle renderingArea = new Rectangle(image.getWidth(), image.getHeight());

                if (renderer instanceof DataRenderable) {
                    imageMapName = imageMaps.get(new Pair<String, Rectangle>(renderer.getId(), renderingArea));
                }

                if (imageMapName == null) {
                    Renderable originalRenderer = image.getRenderer();
                    imageMapName = "map_" + getElementIndex(cell).toString() + "-" + originalRenderer.getId();//use renderer.getId()?
                    imageMapAreas = ((AreaHyperlinksRenderable) originalRenderer)
                            .getImageAreaHyperlinks(renderingArea);//FIXMECHART

                    if (renderer instanceof DataRenderable) {
                        imageMaps.put(new Pair<String, Rectangle>(renderer.getId(), renderingArea), imageMapName);
                    }
                }
            }

            InternalImageProcessor imageProcessor = new InternalImageProcessor(image, isLazy,
                    !useBackgroundLazyImage && scaleImage != ScaleImageEnum.FILL_FRAME && !isLazy, cell,
                    availableImageWidth, availableImageHeight);

            InternalImageProcessorResult imageProcessorResult = null;

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

            if (imageProcessorResult != null) {
                if (useBackgroundLazyImage) {
                    writer.write("<div style=\"width: 100%; height: 100%; background-image: url('");
                    String imagePath = imageProcessorResult.imageSource;
                    if (imagePath != null) {
                        writer.write(JRStringUtil.encodeXmlAttribute(imagePath));
                    }
                    writer.write("'); background-repeat: no-repeat; background-position: " + horizontalAlignment
                            + " " + (image.getVerticalImageAlign() == VerticalImageAlignEnum.MIDDLE ? "center"
                                    : verticalAlignment)
                            + ";background-size: ");

                    switch (scaleImage) {
                    case FILL_FRAME: {
                        writer.write("100% 100%");
                        break;
                    }
                    case CLIP: {
                        writer.write("auto");
                        break;
                    }
                    case RETAIN_SHAPE:
                    default: {
                        writer.write("contain");
                    }
                    }
                    writer.write(";\"></div>");
                } else if (imageProcessorResult.isEmbededSvgData) {
                    writer.write("<svg");

                    switch (scaleImage) {
                    case FILL_FRAME: {
                        Dimension2D dimension = imageProcessorResult.dimension;
                        if (dimension != null) {
                            writer.write(" viewBox=\"0 0 ");
                            writer.write(String.valueOf(dimension.getWidth()));
                            writer.write(" ");
                            writer.write(String.valueOf(dimension.getHeight()));
                            writer.write("\"");
                        }

                        writer.write(" width=\"");
                        writer.write(String.valueOf(availableImageWidth));
                        writer.write("\"");
                        writer.write(" height=\"");
                        writer.write(String.valueOf(availableImageHeight));
                        writer.write("\"");
                        writer.write(" preserveAspectRatio=\"none\"");

                        break;
                    }
                    case CLIP: {
                        double normalWidth = availableImageWidth;
                        double normalHeight = availableImageHeight;

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

                            writer.write(" viewBox=\"");
                            writer.write(String.valueOf((int) (ImageUtil.getXAlignFactor(image)
                                    * (normalWidth - availableImageWidth))));
                            writer.write(" ");
                            writer.write(String.valueOf((int) (ImageUtil.getYAlignFactor(image)
                                    * (normalHeight - availableImageHeight))));
                            writer.write(" ");
                            writer.write(String.valueOf(availableImageWidth));
                            writer.write(" ");
                            writer.write(String.valueOf(availableImageHeight));
                            writer.write("\"");
                        }

                        writer.write(" width=\"");
                        writer.write(String.valueOf(availableImageWidth));
                        writer.write("\"");
                        writer.write(" height=\"");
                        writer.write(String.valueOf(availableImageHeight));
                        writer.write("\"");

                        break;
                    }
                    case RETAIN_SHAPE:
                    default: {
                        //considering the IF above, if we get here, then for sure isLazy() is false, so we can ask the renderer for its dimension
                        if (availableImageHeight > 0) {
                            double normalWidth = availableImageWidth;
                            double normalHeight = availableImageHeight;

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

                                writer.write(" viewBox=\"0 0 ");
                                writer.write(String.valueOf(normalWidth));
                                writer.write(" ");
                                writer.write(String.valueOf(normalHeight));
                                writer.write("\"");
                            }

                            double ratio = normalWidth / normalHeight;

                            if (ratio > (double) availableImageWidth / (double) availableImageHeight) {
                                writer.write(" width=\"");
                                writer.write(String.valueOf(availableImageWidth));
                                writer.write("\"");
                            } else {
                                writer.write(" height=\"");
                                writer.write(String.valueOf(availableImageHeight));
                                writer.write("\"");
                            }
                        }
                    }
                    }

                    writer.write("><g>\n");
                    writer.write(imageProcessorResult.imageSource);
                    writer.write("</g></svg>");
                } else {
                    writer.write("<img");
                    writer.write(" src=\"");
                    String imagePath = imageProcessorResult.imageSource;
                    if (imagePath != null) {
                        writer.write(JRStringUtil.encodeXmlAttribute(imagePath));
                    }
                    writer.write("\"");

                    switch (scaleImage) {
                    case FILL_FRAME: {
                        writer.write(" style=\"width: ");
                        writer.write(toSizeUnit(availableImageWidth));
                        writer.write("; height: ");
                        writer.write(toSizeUnit(availableImageHeight));
                        writer.write("\"");

                        break;
                    }
                    case CLIP: {
                        int positionLeft;
                        int positionTop;

                        HorizontalImageAlignEnum horizontalAlign = image.getHorizontalImageAlign();
                        VerticalImageAlignEnum verticalAlign = image.getVerticalImageAlign();
                        if (isLazy || (horizontalAlign == HorizontalImageAlignEnum.LEFT
                                && verticalAlign == VerticalImageAlignEnum.TOP)) {
                            // no need to compute anything
                            positionLeft = 0;
                            positionTop = 0;
                        } else {
                            double normalWidth = availableImageWidth;
                            double normalHeight = availableImageHeight;

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

                            // these calculations assume that the image td does not stretch due to other cells.
                            // when that happens, the image will not be properly aligned.
                            positionLeft = (int) (ImageUtil.getXAlignFactor(horizontalAlign)
                                    * (availableImageWidth - normalWidth));
                            positionTop = (int) (ImageUtil.getYAlignFactor(verticalAlign)
                                    * (availableImageHeight - normalHeight));
                        }

                        writer.write(" style=\"position: absolute; left:");
                        writer.write(toSizeUnit(positionLeft));
                        writer.write("; top: ");
                        writer.write(toSizeUnit(positionTop));
                        // not setting width, height and clip as it doesn't seem needed plus it fixes clip for lazy images
                        writer.write(";\"");

                        break;
                    }
                    case RETAIN_SHAPE:
                    default: {
                        //considering the IF above, if we get here, then for sure isLazy() is false, so we can ask the renderer for its dimension
                        if (availableImageHeight > 0) {
                            double normalWidth = availableImageWidth;
                            double normalHeight = availableImageHeight;

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

                            double ratio = normalWidth / normalHeight;

                            if (ratio > (double) availableImageWidth / (double) availableImageHeight) {
                                writer.write(" style=\"width: ");
                                writer.write(toSizeUnit(availableImageWidth));
                                writer.write("\"");
                            } else {
                                writer.write(" style=\"height: ");
                                writer.write(toSizeUnit(availableImageHeight));
                                writer.write("\"");
                            }
                        }
                    }
                    }

                    if (imageMapName != null) {
                        writer.write(" usemap=\"#" + imageMapName + "\"");
                    }

                    writer.write(" alt=\"\"");

                    if (hasHyperlinks) {
                        writer.write(" border=\"0\"");
                    }

                    if (image.getHyperlinkTooltip() != null) {
                        writer.write(" title=\"");
                        writer.write(JRStringUtil.encodeXmlAttribute(image.getHyperlinkTooltip()));
                        writer.write("\"");
                    }

                    writer.write("/>");
                }
            }

            if (hyperlinkStarted) {
                endHyperlink();
            }

            if (useDiv) {
                writer.write("</div>");
            }

            if (imageMapAreas != null) {
                writer.write("\n");
                writeImageMap(imageMapName, image, imageMapAreas);
            }
        }

        endCell();
    }

    private class InternalImageProcessor {
        private final JRPrintElement imageElement;
        private final RenderersCache imageRenderersCache;
        private final boolean isLazy;
        private final boolean embedImage;
        private final boolean needDimension;
        private final TableCell cell;
        private final int availableImageWidth;
        private final int availableImageHeight;

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

        protected InternalImageProcessorResult process(Renderable renderer) throws JRException, IOException {
            String imageSource = null;
            Dimension2D dimension = null;
            boolean isEmbededSvgData = false;

            if (isLazy) {
                // we do not cache imagePath for lazy images because the short location string is already cached inside the render itself
                imageSource = RendererUtil.getResourceLocation(renderer);
            } else {
                if (renderer instanceof ResourceRenderer) {
                    renderer = imageRenderersCache.getLoadedRenderer((ResourceRenderer) renderer);
                }

                // check dimension first, to avoid caching renderers that might not be used eventually, due to their dimension errors 
                if (needDimension) {
                    DimensionRenderable dimensionRenderer = imageRenderersCache.getDimensionRenderable(renderer);
                    dimension = dimensionRenderer == null ? null
                            : dimensionRenderer.getDimension(jasperReportsContext);
                }

                if (!embedImage //we do not cache imagePath for embedded images because it is too big
                        && renderer instanceof DataRenderable //we do not cache imagePath for non-data renderers because they render width different width/height each time
                        && rendererToImagePathMap.containsKey(renderer.getId())) {
                    imageSource = rendererToImagePathMap.get(renderer.getId());
                } else {
                    if (embedImage) {
                        DataRenderable dataRenderer = null;

                        if (isConvertSvgToImage(imageElement)) {
                            dataRenderer = getRendererUtil().getImageDataRenderable(imageRenderersCache, renderer,
                                    new Dimension(availableImageWidth, availableImageHeight),
                                    ModeEnum.OPAQUE == imageElement.getModeValue() ? imageElement.getBackcolor()
                                            : null);
                        } else {
                            dataRenderer = getRendererUtil().getDataRenderable(renderer,
                                    new Dimension(availableImageWidth, availableImageHeight),
                                    ModeEnum.OPAQUE == imageElement.getModeValue() ? imageElement.getBackcolor()
                                            : null);
                        }

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

                        SvgDataSniffer.SvgInfo svgInfo = getRendererUtil().getSvgInfo(imageData);

                        if (svgInfo != null) {
                            if (isEmbeddedSvgUseFonts(imageElement)) {
                                Locale locale = getLocale();

                                SvgFontProcessor svgFontProcessor = new SvgFontProcessor(jasperReportsContext,
                                        locale) {
                                    @Override
                                    public String getFontFamily(String fontFamily, Locale locale) {
                                        // Here we rely on the ability of FontUtil.getFontInfoIgnoreCase(fontFamily, locale) method to
                                        // find fonts from font extensions based on the java.awt.Font.getFamily() of their font faces.
                                        // This is because the SVG produced by Batik stores the family name of the AWT fonts used to
                                        // render text on the Batik Graphics2D implementation, as it knows nothing about family names from JR extensions.
                                        return HtmlExporter.this.getFontFamily(true, fontFamily, locale);
                                    }
                                };

                                imageData = svgFontProcessor.process(imageData);
                            }

                            isEmbededSvgData = true;

                            String encoding = svgInfo.getEncoding();
                            imageSource = new String(imageData, encoding == null ? "UTF-8" : encoding);

                            // we might have received needDimension false above, as a hint, but if we arrive here, 
                            // we definitely need to attempt getting the dimension of the SVG, regardless of scale image type
                            DimensionRenderable dimensionRenderer = imageRenderersCache
                                    .getDimensionRenderable(renderer);
                            dimension = dimensionRenderer == null ? null
                                    : dimensionRenderer.getDimension(jasperReportsContext);
                        } else {
                            String imageMimeType = JRTypeSniffer.getImageTypeValue(imageData).getMimeType();

                            ByteArrayInputStream bais = new ByteArrayInputStream(imageData);
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();

                            Base64Util.encode(bais, baos);

                            imageSource = "data:" + imageMimeType + ";base64,"
                                    + new String(baos.toByteArray(), "UTF-8"); // UTF-8 is fine as we just need an ASCII compatible encoding for the Base64 array
                        }

                        //don't cache embedded imageSource as they are not image paths
                    } else {
                        @SuppressWarnings("deprecation")
                        HtmlResourceHandler imageHandler = getImageHandler() == null
                                ? getExporterOutput().getImageHandler()
                                : getImageHandler();
                        if (imageHandler != null) {
                            DataRenderable dataRenderer = null;

                            if (isConvertSvgToImage(imageElement)) {
                                dataRenderer = getRendererUtil().getImageDataRenderable(imageRenderersCache,
                                        renderer, new Dimension(availableImageWidth, availableImageHeight),
                                        ModeEnum.OPAQUE == imageElement.getModeValue() ? imageElement.getBackcolor()
                                                : null);
                            } else {
                                dataRenderer = getRendererUtil().getDataRenderable(renderer,
                                        new Dimension(availableImageWidth, availableImageHeight),
                                        ModeEnum.OPAQUE == imageElement.getModeValue() ? imageElement.getBackcolor()
                                                : null);
                            }

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

                            String fileExtension = getRendererUtil().isSvgData(imageData)
                                    ? RendererUtil.SVG_FILE_EXTENSION
                                    : JRTypeSniffer.getImageTypeValue(imageData).getFileExtension();

                            String imageName = getImageName(getElementIndex(cell), fileExtension);

                            imageHandler.handleResource(imageName, imageData);

                            imageSource = imageHandler.getResourcePath(imageName);

                            if (dataRenderer == renderer) {
                                //cache imagePath only for true ImageRenderable instances because the wrapping ones render with different width/height each time
                                rendererToImagePathMap.put(renderer.getId(), imageSource);
                            }
                        }
                        //does not make sense to cache null imagePath, in the absence of an image handler
                    }
                }
            }

            return new InternalImageProcessorResult(imageSource, dimension, isEmbededSvgData);
        }
    }

    private static class InternalImageProcessorResult {
        protected final String imageSource;
        protected final Dimension2D dimension;
        protected final boolean isEmbededSvgData;

        protected InternalImageProcessorResult(String imagePath, Dimension2D dimension, boolean isEmbededSvgData) {
            this.imageSource = imagePath;
            this.dimension = dimension;
            this.isEmbededSvgData = isEmbededSvgData;
        }
    }

    protected String getImageHorizontalAlignmentStyle(JRPrintImage image) {
        String horizontalAlignment = CSS_TEXT_ALIGN_LEFT;
        switch (image.getHorizontalImageAlign()) {
        case RIGHT: {
            horizontalAlignment = CSS_TEXT_ALIGN_RIGHT;
            break;
        }
        case CENTER: {
            horizontalAlignment = CSS_TEXT_ALIGN_CENTER;
            break;
        }
        case LEFT:
        default: {
            horizontalAlignment = CSS_TEXT_ALIGN_LEFT;
        }
        }
        return horizontalAlignment;
    }

    protected String getImageVerticalAlignmentStyle(JRPrintImage image) {
        String verticalAlignment = HTML_VERTICAL_ALIGN_TOP;
        switch (image.getVerticalImageAlign()) {
        case BOTTOM: {
            verticalAlignment = HTML_VERTICAL_ALIGN_BOTTOM;
            break;
        }
        case MIDDLE: {
            verticalAlignment = HTML_VERTICAL_ALIGN_MIDDLE;
            break;
        }
        case TOP:
        default: {
            verticalAlignment = HTML_VERTICAL_ALIGN_TOP;
        }
        }
        return verticalAlignment;
    }

    protected JRPrintElementIndex getElementIndex(TableCell cell) {
        String elementAddress = cell.getElementAddress();
        JRPrintElementIndex elementIndex = new JRPrintElementIndex(reportIndex, pageIndex, elementAddress);
        return elementIndex;
    }

    protected void writeImageMap(String imageMapName, JRPrintImage image,
            List<JRPrintImageAreaHyperlink> imageMapAreas) throws IOException {
        writer.write("<map name=\"" + imageMapName + "\">\n");

        for (ListIterator<JRPrintImageAreaHyperlink> it = imageMapAreas.listIterator(imageMapAreas.size()); it
                .hasPrevious();) {
            JRPrintImageAreaHyperlink areaHyperlink = it.previous();
            JRPrintHyperlink link = areaHyperlink.getHyperlink();
            JRPrintImageArea area = areaHyperlink.getArea();

            writer.write("  <area shape=\"" + JRPrintImageArea.getHtmlShape(area.getShape()) + "\"");

            writeImageAreaCoordinates(area.getCoordinates());
            writeImageAreaHyperlink(link);
            writer.write("/>\n");
        }

        if (image.getHyperlinkTypeValue() != HyperlinkTypeEnum.NONE) {
            writer.write("  <area shape=\"default\"");
            writeImageAreaCoordinates(new int[] { 0, 0, image.getWidth(), image.getHeight() });//for IE
            writeImageAreaHyperlink(image);
            writer.write("/>\n");
        }

        writer.write("</map>\n");
    }

    protected void writeImageAreaCoordinates(int[] coords) throws IOException {
        if (coords != null && coords.length > 0) {
            StringBuilder coordsEnum = new StringBuilder(coords.length * 4);
            coordsEnum.append((int) toZoom(coords[0]));
            for (int i = 1; i < coords.length; i++) {
                coordsEnum.append(',');
                coordsEnum.append((int) toZoom(coords[i]));
            }
            writer.write(" coords=\"" + coordsEnum + "\"");
        }
    }

    protected void writeImageAreaHyperlink(JRPrintHyperlink hyperlink) throws IOException {
        if (getReportContext() != null) {
            if (hyperlink.getLinkType() != null) {
                int id = hyperlink.hashCode() & 0x7FFFFFFF;
                writer.write(" class=\"_jrHyperLink " + JRStringUtil.encodeXmlAttribute(hyperlink.getLinkType())
                        + "\" data-id=\"" + id + "\"");

                HyperlinkData hyperlinkData = new HyperlinkData();
                hyperlinkData.setId(String.valueOf(id));
                hyperlinkData.setHref(getHyperlinkURL(hyperlink));
                hyperlinkData.setSelector("._jrHyperLink." + hyperlink.getLinkType());
                hyperlinkData.setHyperlink(hyperlink);

                hyperlinksData.add(hyperlinkData);
            }
        } else {
            String href = getHyperlinkURL(hyperlink);
            if (href == null) {
                writer.write(" nohref=\"nohref\"");
            } else {
                writer.write(" href=\"" + JRStringUtil.encodeXmlAttribute(href) + "\"");

                String target = getHyperlinkTarget(hyperlink);
                if (target != null) {
                    writer.write(" target=\"");
                    writer.write(JRStringUtil.encodeXmlAttribute(target));
                    writer.write("\"");
                }
            }
        }

        if (hyperlink.getHyperlinkTooltip() != null) {
            writer.write(" title=\"");
            writer.write(JRStringUtil.encodeXmlAttribute(hyperlink.getHyperlinkTooltip()));
            writer.write("\"");
        }
    }

    protected void writeRectangle(JRPrintRectangle rectangle, TableCell cell) throws IOException {
        startCell(rectangle, cell);

        int radius = rectangle.getRadius();
        if (radius == 0) {
            StringBuilder styleBuffer = new StringBuilder();
            appendElementCellGenericStyle(cell, styleBuffer);
            appendBackcolorStyle(cell, styleBuffer);
            appendPen(styleBuffer, rectangle.getLinePen(), null);
            writeStyle(styleBuffer);
        }

        finishStartCell();

        if (radius != 0) {
            float lineDiff = rectangle.getLinePen().getLineWidth() / 2;
            writer.write("<svg height=\"" + rectangle.getHeight() + "\" width=\"" + rectangle.getWidth() + "\">");
            writer.write("<rect x=\"" + lineDiff + "\" y=\"" + lineDiff + "\" rx=\"" + radius + "\" ry=\"" + radius
                    + "\" ");
            writer.write("height=\"" + (rectangle.getHeight() - 2 * lineDiff) + "\" width=\""
                    + (rectangle.getWidth() - 2 * lineDiff) + "\" ");
            writeSvgStyle(rectangle);
            writer.write("\"/></svg>");
        }

        endCell();
    }

    protected void writeEllipse(JRPrintEllipse ellipse, TableCell cell) throws IOException {
        startCell(ellipse, cell);

        finishStartCell();

        float lineDiff = ellipse.getLinePen().getLineWidth() / 2;
        writer.write("<svg height=\"" + ellipse.getHeight() + "\" width=\"" + ellipse.getWidth() + "\">");
        writer.write("<ellipse cx=\"" + (ellipse.getWidth() / 2) + "\" cy=\"" + (ellipse.getHeight() / 2));
        writer.write("\" rx=\"" + (ellipse.getWidth() / 2 - lineDiff) + "\" ry=\""
                + (ellipse.getHeight() / 2 - lineDiff) + "\" ");
        writeSvgStyle(ellipse);
        writer.write("\"/></svg>");

        endCell();
    }

    protected void writeSvgStyle(JRPrintGraphicElement element) throws IOException {
        writer.write("style=\"fill:" + JRColorUtil.getCssColor(element.getBackcolor()) + ";");
        writer.write("stroke:" + JRColorUtil.getCssColor(element.getLinePen().getLineColor()) + ";");
        writer.write("stroke-width:" + element.getLinePen().getLineWidth() + ";");

        switch (element.getLinePen().getLineStyleValue()) {
        case DOTTED: {
            writer.write("stroke-dasharray:" + element.getLinePen().getLineWidth() + ","
                    + element.getLinePen().getLineWidth() + ";");
            break;
        }
        case DASHED: {
            writer.write("stroke-dasharray:" + 5 * element.getLinePen().getLineWidth() + ","
                    + 3 * element.getLinePen().getLineWidth() + ";");
            break;
        }
        case DOUBLE: //FIXME: there is no built-in svg support for double stroke style; strokes could be rendered twice as a workaround
        case SOLID:
        default: {
            break;
        }
        }
    }

    protected void writeLine(JRPrintLine line, TableCell cell) throws IOException {
        startCell(line, cell);
        if (isOblique(line)) {
            finishStartCell();

            int width = line.getWidth();
            int height = line.getHeight();
            LineDirectionEnum lineDirection = line.getDirectionValue();
            int y1 = lineDirection == LineDirectionEnum.BOTTOM_UP ? height : 0;
            int y2 = lineDirection == LineDirectionEnum.BOTTOM_UP ? 0 : height;

            writer.write("<svg height=\"" + height + "\" width=\"" + width + "\">");
            writer.write("<line x1=\"0\" y1=\"" + y1 + "\" x2=\"" + width + "\" y2=\"" + y2 + "\" ");
            writeSvgStyle(line);
            writer.write("\"/></svg>");
        } else {
            StringBuilder styleBuffer = new StringBuilder();

            appendElementCellGenericStyle(cell, styleBuffer);
            appendBackcolorStyle(cell, styleBuffer);

            String side = null;
            float ratio = line.getWidth() / line.getHeight();
            if (ratio > 1) {
                if (line.getDirectionValue() == LineDirectionEnum.TOP_DOWN) {
                    side = "top";
                } else {
                    side = "bottom";
                }
            } else {
                if (line.getDirectionValue() == LineDirectionEnum.TOP_DOWN) {
                    side = "left";
                } else {
                    side = "right";
                }
            }

            appendPen(styleBuffer, line.getLinePen(), side);

            writeStyle(styleBuffer);

            finishStartCell();
        }

        endCell();
    }

    protected boolean isOblique(JRPrintLine line) {
        return line.getWidth() > 1 && line.getHeight() > 1;
    }

    protected void writeGenericElement(JRGenericPrintElement element, TableCell cell) throws IOException {
        GenericElementHtmlHandler handler = (GenericElementHtmlHandler) GenericElementHandlerEnviroment
                .getInstance(getJasperReportsContext())
                .getElementHandler(element.getGenericType(), HTML_EXPORTER_KEY);

        if (handler == null) {
            if (log.isDebugEnabled()) {
                log.debug("No HTML generic element handler for " + element.getGenericType());
            }

            writeEmptyCell(cell.getColumnSpan(), cell.getRowSpan());// TODO lucianc backcolor/borders?
        } else {
            startCell(element, cell);

            StringBuilder styleBuffer = new StringBuilder();
            appendElementCellGenericStyle(cell, styleBuffer);
            appendBackcolorStyle(cell, styleBuffer);
            appendBorderStyle(cell.getBox(), styleBuffer);
            writeStyle(styleBuffer);

            finishStartCell();

            String htmlFragment = handler.getHtmlFragment(exporterContext, element);
            if (htmlFragment != null) {
                writer.write(htmlFragment);
            }

            endCell();
        }
    }

    protected void writeLayers(List<Table> layers, TableVisitor tableVisitor, TableCell cell) throws IOException {
        startCell(cell);

        StringBuilder styleBuffer = new StringBuilder();
        appendElementCellGenericStyle(cell, styleBuffer);
        appendBackcolorStyle(cell, styleBuffer);
        appendBorderStyle(cell.getBox(), styleBuffer);
        writeStyle(styleBuffer);

        finishStartCell();

        // layers need to always specify backcolors
        setBackcolor(null);
        writer.write("<div style=\"width: 100%; height: 100%; position: relative;\">\n");

        for (ListIterator<Table> it = layers.listIterator(); it.hasNext();) {
            Table table = it.next();

            StringBuilder layerStyleBuffer = new StringBuilder();
            if (it.hasNext()) {
                layerStyleBuffer.append("position: absolute; overflow: hidden; ");
            } else {
                layerStyleBuffer.append("position: relative; ");
            }
            layerStyleBuffer.append("width: 100%; height: 100%; ");

            if (it.previousIndex() > 0) {
                layerStyleBuffer.append("pointer-events: none; ");
            }

            writer.write("<div style=\"");
            writer.write(layerStyleBuffer.toString());
            writer.write("\">\n");

            ++pointerEventsNoneStack;
            exportTable(tableVisitor, table, false, false);
            --pointerEventsNoneStack;

            writer.write("</div>\n");
        }

        writer.write("</div>\n");
        restoreBackcolor();

        endCell();
    }

    protected void startCell(JRPrintElement element, TableCell cell) throws IOException {
        startCell(cell.getColumnSpan(), cell.getRowSpan());

        String dataAttr = getDataAttributes(element, cell);
        if (dataAttr != null) {
            writer.write(dataAttr);
        }
    }

    public String getDataAttributes(JRPrintElement element, TableCell cell) {
        StringBuilder sb = new StringBuilder();

        String id = getCellProperty(element, cell, PROPERTY_HTML_ID);
        if (id != null) {
            sb.append(" id=\"" + JRStringUtil.encodeXmlAttribute(id) + "\"");
        }
        String clazz = getCellProperty(element, cell, PROPERTY_HTML_CLASS);
        if (clazz != null) {
            sb.append(" class=\"" + JRStringUtil.encodeXmlAttribute(clazz) + "\"");
        }
        String colUuid = getCellProperty(element, cell, HeaderToolbarElement.PROPERTY_COLUMN_UUID);//FIXMEJIVE register properties like this in a pluggable way; extensions?
        if (colUuid != null) {
            sb.append(" data-coluuid=\"" + JRStringUtil.encodeXmlAttribute(colUuid) + "\"");
        }
        String cellId = getCellProperty(element, cell, HeaderToolbarElement.PROPERTY_CELL_ID);
        if (cellId != null) {
            sb.append(" data-cellid=\"" + JRStringUtil.encodeXmlAttribute(cellId) + "\"");
        }
        String tableUuid = getCellProperty(element, cell, HeaderToolbarElement.PROPERTY_TABLE_UUID);
        if (tableUuid != null) {
            sb.append(" data-tableuuid=\"" + JRStringUtil.encodeXmlAttribute(tableUuid) + "\"");
        }
        String columnIndex = getCellProperty(element, cell, HeaderToolbarElement.PROPERTY_COLUMN_INDEX);
        if (columnIndex != null) {
            sb.append(" data-colidx=\"" + JRStringUtil.encodeXmlAttribute(columnIndex) + "\"");
        }

        String xtabId = getCellProperty(element, cell, CrosstabInteractiveJsonHandler.PROPERTY_CROSSTAB_ID);
        if (xtabId != null) {
            sb.append(" " + CrosstabInteractiveJsonHandler.ATTRIBUTE_CROSSTAB_ID + "=\""
                    + JRStringUtil.encodeXmlAttribute(xtabId) + "\"");
        }

        String xtabColIdx = getCellProperty(element, cell, CrosstabInteractiveJsonHandler.PROPERTY_COLUMN_INDEX);
        if (xtabColIdx != null) {
            sb.append(" " + CrosstabInteractiveJsonHandler.ATTRIBUTE_COLUMN_INDEX + "=\""
                    + JRStringUtil.encodeXmlAttribute(xtabColIdx) + "\"");
        }

        return sb.length() > 0 ? sb.toString() : null;
    }

    protected String getCellProperty(JRPrintElement element, TableCell cell, String key) {
        String property = null;
        if (element != null) {
            property = getPropertiesUtil().getProperty(element, key);
        }

        if (property == null) {
            Tabulator tabulator = cell.getTabulator();
            for (FrameCell parentCell = cell.getCell().getParent(); parentCell != null
                    && property == null; parentCell = parentCell.getParent()) {
                JRPrintElement parentElement = tabulator.getCellElement(parentCell);
                property = getPropertiesUtil().getProperty(parentElement, key);
            }
        }
        return property;
    }

    protected void startCell(TableCell cell) throws IOException {
        startCell(cell.getElement(), cell);
    }

    protected void startCell(int colSpan, int rowSpan) throws IOException {
        writer.write("<td");
        if (colSpan > 1) {
            writer.write(" colspan=\"");
            writer.write(Integer.toString(colSpan));
            writer.write("\"");
        }
        if (rowSpan > 1) {
            writer.write(" rowspan=\"");
            writer.write(Integer.toString(rowSpan));
            writer.write("\"");
        }
    }

    protected void finishStartCell() throws IOException {
        writer.write(">\n");
    }

    protected void endCell() throws IOException {
        writer.write("</td>\n");
    }

    protected void writeEmptyCell(int colSpan, int rowSpan) throws IOException {
        startCell(colSpan, rowSpan);
        finishStartCell();
        endCell();
    }

    protected void writeFrameCell(TableCell cell) throws IOException {
        startCell(cell);

        StringBuilder styleBuffer = new StringBuilder();
        appendElementCellGenericStyle(cell, styleBuffer);
        appendBackcolorStyle(cell, styleBuffer);
        appendBorderStyle(cell.getBox(), styleBuffer);
        writeStyle(styleBuffer);

        finishStartCell();
        endCell();
    }

    protected void writeStyle(StringBuilder styleBuffer) throws IOException {
        if (styleBuffer.length() > 0) {
            writer.write(" style=\"");
            writer.write(styleBuffer.toString());
            writer.write("\"");
        }
    }

    protected void appendElementCellGenericStyle(TableCell cell, StringBuilder styleBuffer) {
        if (pointerEventsNoneStack > 0 && cell.getElement() != null) {
            styleBuffer.append("pointer-events: auto; ");
        }
    }

    protected void setBackcolor(Color color) {
        backcolorStack.addFirst(color);
    }

    protected void restoreBackcolor() {
        backcolorStack.removeFirst();
    }

    protected boolean matchesBackcolor(Color backcolor) {
        if (backcolorStack.isEmpty()) {
            return false;
        }

        Color currentBackcolor = backcolorStack.getFirst();
        return currentBackcolor != null && backcolor.getRGB() == currentBackcolor.getRGB();
    }

    protected Color appendBackcolorStyle(TableCell cell, StringBuilder styleBuffer) {
        Color cellBackcolor = cell.getBackcolor();
        if (cellBackcolor != null && !matchesBackcolor(cellBackcolor)) {
            styleBuffer.append("background-color: ");
            styleBuffer.append(JRColorUtil.getCssColor(cellBackcolor));
            styleBuffer.append("; ");

            return cellBackcolor;
        }

        return null;
    }

    protected boolean appendBorderStyle(JRLineBox box, StringBuilder styleBuffer) {
        boolean addedToStyle = false;

        if (box != null) {
            LineStyleEnum tps = box.getTopPen().getLineStyleValue();
            LineStyleEnum lps = box.getLeftPen().getLineStyleValue();
            LineStyleEnum bps = box.getBottomPen().getLineStyleValue();
            LineStyleEnum rps = box.getRightPen().getLineStyleValue();

            float tpw = box.getTopPen().getLineWidth();
            float lpw = box.getLeftPen().getLineWidth();
            float bpw = box.getBottomPen().getLineWidth();
            float rpw = box.getRightPen().getLineWidth();

            if (0f < tpw && tpw < 1f) {
                tpw = 1f;
            }
            if (0f < lpw && lpw < 1f) {
                lpw = 1f;
            }
            if (0f < bpw && bpw < 1f) {
                bpw = 1f;
            }
            if (0f < rpw && rpw < 1f) {
                rpw = 1f;
            }

            Color tpc = box.getTopPen().getLineColor();

            // try to compact all borders into one css property
            if (tps == lps && // same line style
                    tps == bps && tps == rps && tpw == lpw && // same line width
                    tpw == bpw && tpw == rpw && tpc.equals(box.getLeftPen().getLineColor()) && // same line color
                    tpc.equals(box.getBottomPen().getLineColor()) && tpc.equals(box.getRightPen().getLineColor())) {
                addedToStyle |= appendPen(styleBuffer, box.getTopPen(), null);
            } else {
                addedToStyle |= appendPen(styleBuffer, box.getTopPen(), "top");
                addedToStyle |= appendPen(styleBuffer, box.getLeftPen(), "left");
                addedToStyle |= appendPen(styleBuffer, box.getBottomPen(), "bottom");
                addedToStyle |= appendPen(styleBuffer, box.getRightPen(), "right");
            }
        }

        return addedToStyle;
    }

    protected boolean appendPen(StringBuilder sb, JRPen pen, String side) {
        boolean addedToStyle = false;

        float borderWidth = pen.getLineWidth();
        if (0f < borderWidth && borderWidth < 1f) {
            borderWidth = 1f;
        }

        String borderStyle = null;
        switch (pen.getLineStyleValue()) {
        case DOUBLE: {
            borderStyle = "double";
            break;
        }
        case DOTTED: {
            borderStyle = "dotted";
            break;
        }
        case DASHED: {
            borderStyle = "dashed";
            break;
        }
        case SOLID:
        default: {
            borderStyle = "solid";
            break;
        }
        }

        if (borderWidth > 0f) {
            sb.append("border");
            if (side != null) {
                sb.append("-");
                sb.append(side);
            }

            sb.append(": ");
            sb.append(toSizeUnit(borderWidth));

            sb.append(" ");
            sb.append(borderStyle);

            sb.append(" ");
            sb.append(JRColorUtil.getCssColor(pen.getLineColor()));
            sb.append("; ");

            addedToStyle = true;
        }

        return addedToStyle;
    }

    protected boolean appendPaddingStyle(JRLineBox box, StringBuilder styleBuffer) {
        boolean addedToStyle = false;

        if (box != null) {
            Integer tp = box.getTopPadding();
            Integer lp = box.getLeftPadding();
            Integer bp = box.getBottomPadding();
            Integer rp = box.getRightPadding();

            // try to compact all paddings into one css property
            if (tp == lp && tp == bp && tp == rp) {
                addedToStyle |= appendPadding(styleBuffer, tp, null);
            } else {
                addedToStyle |= appendPadding(styleBuffer, box.getTopPadding(), "top");
                addedToStyle |= appendPadding(styleBuffer, box.getLeftPadding(), "left");
                addedToStyle |= appendPadding(styleBuffer, box.getBottomPadding(), "bottom");
                addedToStyle |= appendPadding(styleBuffer, box.getRightPadding(), "right");
            }
        }

        return addedToStyle;
    }

    protected boolean appendPadding(StringBuilder sb, Integer padding, String side) {
        boolean addedToStyle = false;

        if (padding > 0) {
            sb.append("padding");
            if (side != null) {
                sb.append("-");
                sb.append(side);
            }
            sb.append(": ");
            sb.append(toSizeUnit(padding));
            sb.append("; ");

            addedToStyle = true;
        }

        return addedToStyle;
    }

    protected boolean startHyperlink(JRPrintHyperlink link) throws IOException {
        boolean hyperlinkStarted = false, canWrite = false;

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

            if (!ignoreHyperlink && link.getLinkType() != null) {
                canWrite = true;
                int id = link.hashCode() & 0x7FFFFFFF;

                writer.write("<span class=\"_jrHyperLink " + JRStringUtil.encodeXmlAttribute(link.getLinkType())
                        + "\" data-id=\"" + id + "\"");

                HyperlinkData hyperlinkData = new HyperlinkData();
                hyperlinkData.setId(String.valueOf(id));
                hyperlinkData.setHref(getHyperlinkURL(link));
                hyperlinkData.setSelector("._jrHyperLink." + link.getLinkType());
                hyperlinkData.setHyperlink(link);

                hyperlinksData.add(hyperlinkData);
                hyperlinkStarted = true;
            }
        } else {
            String href = getHyperlinkURL(link);

            if (href != null) {
                canWrite = true;
                writer.write("<a href=\"");
                writer.write(JRStringUtil.encodeXmlAttribute(href));
                writer.write("\"");

                String target = getHyperlinkTarget(link);
                if (target != null) {
                    writer.write(" target=\"");
                    writer.write(JRStringUtil.encodeXmlAttribute(target));
                    writer.write("\"");
                }
            }

            hyperlinkStarted = href != null;
        }

        if (canWrite) {
            if (link.getHyperlinkTooltip() != null) {
                writer.write(" title=\"");
                writer.write(JRStringUtil.encodeXmlAttribute(link.getHyperlinkTooltip()));
                writer.write("\"");
            }

            writer.write(">");
        }

        return hyperlinkStarted;
    }

    protected void endHyperlink() throws IOException {
        if (getReportContext() != null) {
            writer.write("</span>");
        } else {
            writer.write("</a>");
        }
    }

    protected String getHyperlinkURL(JRPrintHyperlink link) {
        return resolveHyperlinkURL(reportIndex, link);
    }

    protected String resolveHyperlinkURL(int reportIndex, JRPrintHyperlink link) {
        String href = null;

        Boolean ignoreHyperlink = HyperlinkUtil
                .getIgnoreHyperlink(HtmlReportConfiguration.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) {
                        href = link.getHyperlinkReference();
                    }
                    break;
                }
                case LOCAL_ANCHOR: {
                    if (link.getHyperlinkAnchor() != null) {
                        href = "#" + link.getHyperlinkAnchor();
                    }
                    break;
                }
                case LOCAL_PAGE: {
                    if (link.getHyperlinkPage() != null) {
                        href = "#" + JR_PAGE_ANCHOR_PREFIX + reportIndex + "_" + link.getHyperlinkPage().toString();
                    }
                    break;
                }
                case REMOTE_ANCHOR: {
                    if (link.getHyperlinkReference() != null && link.getHyperlinkAnchor() != null) {
                        href = link.getHyperlinkReference() + "#" + link.getHyperlinkAnchor();
                    }
                    break;
                }
                case REMOTE_PAGE: {
                    if (link.getHyperlinkReference() != null && link.getHyperlinkPage() != null) {
                        href = link.getHyperlinkReference() + "#" + JR_PAGE_ANCHOR_PREFIX + "0_"
                                + link.getHyperlinkPage().toString();
                    }
                    break;
                }
                case NONE:
                default: {
                    break;
                }
                }
            } else {
                href = customHandler.getHyperlink(link);
            }
        }

        return href;
    }

    protected String getHyperlinkTarget(JRPrintHyperlink link) {
        String target = null;
        JRHyperlinkTargetProducer producer = targetProducerFactory.getHyperlinkTargetProducer(link.getLinkTarget());
        if (producer == null) {
            switch (link.getHyperlinkTargetValue()) {
            case BLANK: {
                target = "_blank";//FIXME make reverse for html markup hyperlinks
                break;
            }
            case PARENT: {
                target = "_parent";
                break;
            }
            case TOP: {
                target = "_top";
                break;
            }
            case CUSTOM: {
                boolean paramFound = false;
                List<JRPrintHyperlinkParameter> parameters = link.getHyperlinkParameters() == null ? null
                        : link.getHyperlinkParameters().getParameters();
                if (parameters != null) {
                    for (Iterator<JRPrintHyperlinkParameter> it = parameters.iterator(); it.hasNext();) {
                        JRPrintHyperlinkParameter parameter = it.next();
                        if (link.getLinkTarget().equals(parameter.getName())) {
                            target = parameter.getValue() == null ? null : parameter.getValue().toString();
                            paramFound = true;
                            break;
                        }
                    }
                }
                if (!paramFound) {
                    target = link.getLinkTarget();
                }
                break;
            }
            case SELF:
            default: {
            }
            }
        } else {
            target = producer.getHyperlinkTarget(link);
        }

        return target;
    }

    public String toSizeUnit(float size) {
        Number number = toZoom(size);
        if (number.intValue() == number.floatValue()) {
            number = number.intValue();
        }

        return String.valueOf(number) + getCurrentItemConfiguration().getSizeUnit().getName();
    }

    protected float toZoom(float size)//FIXMEEXPORT cache this
    {
        float zoom = DEFAULT_ZOOM;

        Float zoomRatio = getCurrentItemConfiguration().getZoomRatio();
        if (zoomRatio != null) {
            zoom = zoomRatio;
            if (zoom <= 0) {
                throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_INVALID_ZOOM_RATIO, new Object[] { zoom });
            }
        }

        return (zoom * size);
    }

    private void addSearchAttributes(JRStyledText styledText, JRPrintText textElement) {
        ReportContext reportContext = getReportContext();
        if (reportContext != null) {
            SpansInfo spansInfo = (SpansInfo) reportContext
                    .getParameterValue("net.sf.jasperreports.search.term.highlighter");
            PrintElementId pei = PrintElementId.forElement(textElement);

            if (spansInfo != null && spansInfo.hasHitTermsInfo(pei.toString())) {
                List<HitTermInfo> hitTermInfos = JRCloneUtils.cloneList(spansInfo.getHitTermsInfo(pei.toString()));

                short[] lineBreakOffsets = textElement.getLineBreakOffsets();
                if (lineBreakOffsets != null && lineBreakOffsets.length > 0) {
                    int sz = lineBreakOffsets.length;
                    for (HitTermInfo ti : hitTermInfos) {
                        for (int i = 0; i < sz; i++) {
                            if (lineBreakOffsets[i] <= ti.getStart()) {
                                ti.setStart(ti.getStart() + 1);
                                ti.setEnd(ti.getEnd() + 1);
                            } else {
                                break;
                            }
                        }
                    }
                }

                AttributedString attributedString = styledText.getAttributedString();
                for (int i = 0, ln = hitTermInfos.size(); i < ln; i = i + spansInfo.getTermsPerQuery()) {
                    attributedString.addAttribute(JRTextAttribute.SEARCH_HIGHLIGHT, Color.yellow,
                            hitTermInfos.get(i).getStart(),
                            hitTermInfos.get(i + spansInfo.getTermsPerQuery() - 1).getEnd());
                }
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("No ReportContext to hold search data!");
            }
        }
    }

    protected void exportStyledText(JRPrintText printText, JRStyledText styledText, String tooltip,
            boolean hyperlinkStarted) throws IOException {
        Locale locale = getTextLocale(printText);
        LineSpacingEnum lineSpacing = printText.getParagraph().getLineSpacing();
        Float lineSpacingSize = printText.getParagraph().getLineSpacingSize();
        float lineSpacingFactor = printText.getLineSpacingFactor();
        Color backcolor = printText.getBackcolor();

        String text = styledText.getText();

        int runLimit = 0;

        addSearchAttributes(styledText, printText);

        AttributedCharacterIterator iterator = styledText.getAttributedString().getIterator();

        boolean first = true;
        boolean startedSpan = false;

        boolean highlightStarted = false;

        while (runLimit < styledText.length() && (runLimit = iterator.getRunLimit()) <= styledText.length()) {
            //if there are several text runs, write the tooltip into a parent <span>
            if (first && runLimit < styledText.length() && tooltip != null) {
                startedSpan = true;
                writer.write("<span title=\"");
                writer.write(JRStringUtil.encodeXmlAttribute(tooltip));
                writer.write("\">");
                //reset the tooltip so that inner <span>s to not use it
                tooltip = null;
            }
            first = false;

            Map<Attribute, Object> attributes = iterator.getAttributes();
            Color highlightColor = (Color) attributes.get(JRTextAttribute.SEARCH_HIGHLIGHT);
            if (highlightColor != null && !highlightStarted) {
                highlightStarted = true;
                writer.write("<span class=\"jr_search_result\">");
            } else if (highlightColor == null && highlightStarted) {
                highlightStarted = false;
                writer.write("</span>");
            }

            exportStyledTextRun(attributes, text.substring(iterator.getIndex(), runLimit), tooltip, locale,
                    lineSpacing, lineSpacingSize, lineSpacingFactor, backcolor, hyperlinkStarted);

            iterator.setIndex(runLimit);
        }

        if (highlightStarted) {
            writer.write("</span>");
        }

        if (startedSpan) {
            writer.write("</span>");
        }
    }

    protected void exportStyledTextRun(Map<Attribute, Object> attributes, String text, String tooltip,
            Locale locale, LineSpacingEnum lineSpacing, Float lineSpacingSize, float lineSpacingFactor,
            Color backcolor, boolean hyperlinkStarted) throws IOException {
        boolean localHyperlink = false;
        JRPrintHyperlink hyperlink = (JRPrintHyperlink) attributes.get(JRTextAttribute.HYPERLINK);
        if (!hyperlinkStarted && hyperlink != null) {
            localHyperlink = startHyperlink(hyperlink);
        }

        boolean isBold = TextAttribute.WEIGHT_BOLD.equals(attributes.get(TextAttribute.WEIGHT));
        boolean isItalic = TextAttribute.POSTURE_OBLIQUE.equals(attributes.get(TextAttribute.POSTURE));

        String fontFamily = resolveFontFamily(attributes, locale);

        // do not put single quotes around family name here because the value might already contain quotes, 
        // especially if it is coming from font extension export configuration
        writer.write("<span style=\"font-family: ");
        // don't encode single quotes as the output would be too verbose and too much of a chance compared to previous releases
        writer.write(JRStringUtil.encodeXmlAttribute(fontFamily, true));
        writer.write("; ");

        Color forecolor = (Color) attributes.get(TextAttribute.FOREGROUND);
        if (!hyperlinkStarted || !Color.black.equals(forecolor)) {
            writer.write("color: ");
            writer.write(JRColorUtil.getCssColor(forecolor));
            writer.write("; ");
        }

        Color runBackcolor = (Color) attributes.get(TextAttribute.BACKGROUND);
        if (runBackcolor != null && !runBackcolor.equals(backcolor)) {
            writer.write("background-color: ");
            writer.write(JRColorUtil.getCssColor(runBackcolor));
            writer.write("; ");
        }

        writer.write("font-size: ");
        writer.write(toSizeUnit((Float) attributes.get(TextAttribute.SIZE)));
        writer.write(";");

        switch (lineSpacing) {
        case SINGLE:
        default: {
            if (lineSpacingFactor == 0) {
                writer.write(" line-height: 1; *line-height: normal;");
            } else {
                writer.write(" line-height: " + lineSpacingFactor + ";");
            }
            break;
        }
        case ONE_AND_HALF: {
            if (lineSpacingFactor == 0) {
                writer.write(" line-height: 1.5;");
            } else {
                writer.write(" line-height: " + lineSpacingFactor + ";");
            }
            break;
        }
        case DOUBLE: {
            if (lineSpacingFactor == 0) {
                writer.write(" line-height: 2.0;");
            } else {
                writer.write(" line-height: " + lineSpacingFactor + ";");
            }
            break;
        }
        case PROPORTIONAL: {
            if (lineSpacingSize != null) {
                writer.write(" line-height: " + lineSpacingSize + ";");
            }
            break;
        }
        case AT_LEAST:
        case FIXED: {
            if (lineSpacingSize != null) {
                writer.write(" line-height: " + lineSpacingSize + "px;");
            }
            break;
        }
        }

        /*
        if (!horizontalAlignment.equals(CSS_TEXT_ALIGN_LEFT))
        {
           writer.write(" text-align: ");
           writer.write(horizontalAlignment);
           writer.write(";");
        }
        */

        if (isBold) {
            writer.write(" font-weight: bold;");
        }
        if (isItalic) {
            writer.write(" font-style: italic;");
        }
        if (TextAttribute.UNDERLINE_ON.equals(attributes.get(TextAttribute.UNDERLINE))) {
            writer.write(" text-decoration: underline;");
        }
        if (TextAttribute.STRIKETHROUGH_ON.equals(attributes.get(TextAttribute.STRIKETHROUGH))) {
            writer.write(" text-decoration: line-through;");
        }

        if (TextAttribute.SUPERSCRIPT_SUPER.equals(attributes.get(TextAttribute.SUPERSCRIPT))) {
            writer.write(" vertical-align: super;");
        } else if (TextAttribute.SUPERSCRIPT_SUB.equals(attributes.get(TextAttribute.SUPERSCRIPT))) {
            writer.write(" vertical-align: sub;");
        }

        writer.write("\"");

        if (tooltip != null) {
            writer.write(" title=\"");
            writer.write(JRStringUtil.encodeXmlAttribute(tooltip));
            writer.write("\"");
        }

        writer.write(">");

        writer.write(JRStringUtil.htmlEncode(text));

        writer.write("</span>");

        if (localHyperlink) {
            endHyperlink();
        }
    }

    protected class TableVisitor implements CellVisitor<TablePosition, Void, IOException> {
        private final Tabulator tabulator;
        private final PrintElementVisitor<TableCell> elementVisitor;

        public TableVisitor(Tabulator tabulator, PrintElementVisitor<TableCell> elementVisitor) {
            this.tabulator = tabulator;
            this.elementVisitor = elementVisitor;
        }

        @Override
        public Void visit(ElementCell cell, TablePosition position) {
            TableCell tableCell = tabulator.getTableCell(position, cell);
            JRPrintElement element = tableCell.getElement();
            element.accept(elementVisitor, tableCell);
            return null;
        }

        @Override
        public Void visit(SplitCell cell, TablePosition position) {
            //NOP
            return null;
        }

        @Override
        public Void visit(FrameCell frameCell, TablePosition position) throws IOException {
            TableCell tableCell = tabulator.getTableCell(position, frameCell);
            HtmlExporter.this.writeFrameCell(tableCell);
            return null;
        }

        @Override
        public Void visit(LayeredCell layeredCell, TablePosition position) throws IOException {
            TableCell tableCell = tabulator.getTableCell(position, layeredCell);
            HtmlExporter.this.writeLayers(layeredCell.getLayers(), this, tableCell);
            return null;
        }
    }

    protected class CellElementVisitor implements PrintElementVisitor<TableCell> {
        @Override
        public void visit(JRPrintText textElement, TableCell cell) {
            try {
                writeText(textElement, cell);
            } catch (IOException e) {
                throw new JRRuntimeException(e);
            }
        }

        @Override
        public void visit(JRPrintImage image, TableCell cell) {
            try {
                writeImage(image, cell);
            } catch (IOException e) {
                throw new JRRuntimeException(e);
            } catch (JRException e) {
                throw new JRRuntimeException(e);
            }
        }

        @Override
        public void visit(JRPrintRectangle rectangle, TableCell cell) {
            try {
                writeRectangle(rectangle, cell);
            } catch (IOException e) {
                throw new JRRuntimeException(e);
            }
        }

        @Override
        public void visit(JRPrintLine line, TableCell cell) {
            try {
                writeLine(line, cell);
            } catch (IOException e) {
                throw new JRRuntimeException(e);
            }
        }

        @Override
        public void visit(JRPrintEllipse ellipse, TableCell cell) {
            try {
                writeEllipse(ellipse, cell);
            } catch (IOException e) {
                throw new JRRuntimeException(e);
            }
        }

        @Override
        public void visit(JRPrintFrame frame, TableCell cell) {
            throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_INTERNAL_ERROR, (Object[]) null);
        }

        @Override
        public void visit(JRGenericPrintElement printElement, TableCell cell) {
            try {
                writeGenericElement(printElement, cell);
            } catch (IOException e) {
                throw new JRRuntimeException(e);
            }
        }
    }

    protected class ExporterContext extends BaseExporterContext implements JRHtmlExporterContext {
        @Override
        public String getHyperlinkURL(JRPrintHyperlink link) {
            return HtmlExporter.this.getHyperlinkURL(link);
        }
    }
}